亚马逊AWS官方博客

EC2 NAT 实例选型与部署实践

摘要:本文对比了 NAT 网关与 NAT 实例的费用结构,基于 EC2 网络带宽机制和多流流量规则,分析了中国宁夏区域(cn-northwest-1)中所有当前代实例类型的可用带宽与成本,筛选出各流量档位性价比最优的实例类型,并提供了基于 Auto Scaling Group 和 CloudFormation 的高可用一键部署方案。


1. 背景

在亚马逊云科技中,私有子网中的实例可以通过 NAT 访问互联网。亚马逊云科技提供了托管的 NAT 网关和自建的 NAT 实例两种方案,两者在可用性、运维成本和费用结构上各有差异。除出站流量收取数据传输费外,NAT 网关还额外对双向流量收取数据处理费。选择 NAT 实例时,需要了解不同实例类型的实际可用带宽,才能进行合理选型。

以某客户在亚马逊云科技部署的开发测试环境为例,其 VPC 中的私有子网需要访问互联网以拉取软件包、下载容器镜像和调用外部 API。以入站流量为主,前期预估每月约 10 TB,并持续增加。希望在满足带宽需求的前提下尽可能降低成本。

本文分析了亚马逊云科技中国(宁夏)区域(cn-northwest-1)中所有当前代 EC2 实例类型的网络带宽数据,计算了作为 NAT 实例时的实际可用带宽和成本,并提供了基于 CloudFormation 的一键部署方案。

NAT 网关 vs NAT 实例

参考:比较 NAT 网关和 NAT 实例

选择建议

  • 选择 NAT 网关:追求免运维、对可用性要求高。例如生产环境中运行关键业务,无法接受任何中断时间,NAT 网关提供单 AZ 内自动冗余,无需关注实例健康状态、OS 补丁和故障恢复逻辑
  • 选择 NAT 实例:成本敏感,适用于开发测试环境、非关键生产环境,或业务关键路径不依赖出站互联网访问的场景。能接受分钟级故障恢复时间。需要安全组控制流量、需要端口转发或堡垒机功能
属性 NAT 网关 NAT 实例
可用性 高度可用,单 AZ 内冗余实施 无内置高可用,需自行实现故障转移
带宽 自动扩展到 100 Gbps 取决于实例类型
维护 亚马逊云科技托管,无需维护 自行管理 OS 更新、补丁、NAT 配置
性能 软件经过 NAT 优化 通用 AMI,需自行配置
费用 按小时 + 双向数据处理费 + 出站流量费 实例费用 + 出站流量费
安全组 不支持关联安全组 支持安全组控制流量
端口转发 不支持 支持自定义配置
堡垒服务器 不支持 可兼做堡垒机
IP 分段 仅支持 UDP 分段转发 支持 UDP/TCP/ICMP 分段重组
超时行为 返回 RST 数据包 发送 FIN 数据包关闭连接

2. 名词解释

  • 基准带宽(BaselineBandwidthInGbps):实例可持续稳定使用的网络带宽
  • 峰值带宽(PeakBandwidthInGbps):通过网络 I/O 积分机制可短暂突增到的最大带宽(持续 5-60 分钟,取决于实例大小)
  • NAT 可用带宽:NAT 实例的流量经过互联网网关(IGW),受多流流量规则限制:
    • vCPUs < 32:带宽限制为基准带宽或 5 Gbps,以较小者为准
    • vCPUs ≥ 32:带宽限制为通过互联网网关的流量的 50%,或者限制为 5 Gbps,以较大者为准

3. 实例类型建议

以下数据表列出了亚马逊云科技中国(宁夏)区域(cn-northwest-1)筛选后的 NAT 实例类型,按流量从小到大排列。根据实际月流量需求,在表中选择满足流量需求且价格最低的实例类型即可。

数据说明

  • 价格单位为人民币(CNY)。NAT 实例价格为 Linux 按需价格。NAT 网关费用:¥0.325/小时 + ¥0.325/GB 数据处理费
  • 入站/出站流量费:本文场景以入站流量为主(入站免费),出站流量相对较小(¥0.933/GB,可忽略不计)
  • 流量为理论最大值:按 730 小时满载计算,实际使用中不可能 24/7 跑满带宽
  • 突增带宽不可持续:小规格实例(≤ 16 vCPUs)的峰值带宽依赖网络 I/O 积分,持续高流量时会回落到基准带宽
  • 实例购买选项:NAT 实例价格基于查询时的亚马逊云科技Pricing API 数据,可能随时变动。使用 Savings Plans 或 Reserved Instances 可再降 30-60%
  • 数据时效:截止至 2026 年 5 月,仅包含当前代(current-generation)实例类型,主要包括:
    • 通用型:m5a、m5d、m6g、m7g、m8g
    • 计算优化型:c5a、c5d、c6g、c6gd、c6i、c6in、c7g、c8g、c8gd、c8gn
    • 内存优化型:r5a、r5d、r6g、r6gd、r7g、r8g、r8gd、x1、x2idn、x2iedn、z1d、u-6tb1
    • 存储优化型:i3、i3en、i4i、i7ie
    • 加速计算型:g4dn、g5、inf1
    • 突增型:t2、t3、t3a、t4g
实例类型 vCPUs 网络性能 峰值带宽 (Gbps) 基准带宽 (Gbps) NAT 可用带宽 (Gbps) 入站流量 (TB/月) NAT 实例价格 (¥/月) NAT 网关价格 (¥/月)
t4g.nano 2 Up to 5 Gigabit 5.0 0.032 0.032 10.5 13.5 3,650
t4g.micro 2 Up to 5 Gigabit 5.0 0.064 0.064 21.0 27.0 7,062
t4g.small 2 Up to 5 Gigabit 5.0 0.128 0.128 42.0 53.9 13,887
t4g.medium 2 Up to 5 Gigabit 5.0 0.256 0.256 84.1 108.0 27,570
c8g.medium 1 Up to 12.5 Gigabit 12.5 0.52 0.52 170.8 142.9 55,747
c8g.large 2 Up to 12.5 Gigabit 12.5 0.937 0.937 307.8 285.9 100,272
c8gn.medium 1 Up to 25 Gigabit 25.0 3.125 3.125 1,026.6 338.4 333,882
c8gn.large 2 Up to 30 Gigabit 30.0 6.25 5.0 1,642.5 676.9 534,050
c8g.8xlarge 32 15 Gigabit 15.0 15.0 7.5 2,463.8 4,573.8 800,972
c8g.12xlarge 48 22.5 Gigabit 22.5 22.5 11.25 3,695.6 6,860.7 1,201,307
c8gn.8xlarge 32 100 Gigabit 100.0 100.0 50.0 16,425.0 10,830.0 5,338,362
c8gn.12xlarge 48 150 Gigabit 150.0 150.0 75.0 24,637.5 16,244.9 8,007,425
c8gn.16xlarge 64 200 Gigabit 200.0 200.0 100.0 32,850.0 21,659.9 10,676,487
c8gn.24xlarge 96 300 Gigabit 300.0 300.0 150.0 49,275.0 32,489.9 16,014,612

4. 数据分析过程

NAT 转发的核心操作是修改 IP 报文头,主要消耗 CPU,并不依赖大内存或高速存储。因此整体来看计算优化型(c 系列)性价比最高。以 medium 规格为例,c/m/r 三者网络带宽相同,但 c8g 在大流量场景下性价比反而最优,t4g 系列则更适合小流量场景。

实例类型 系列 基准带宽 (Gbps) 月价 (¥)
c8g.medium 计算优化 0.52 142.9
m8g.medium 通用 0.52 196.4
r8g.medium 内存优化 0.52 255.9

具体分析过程如下:

步骤一:查询所有实例类型的网络带宽信息

参考:Amazon EC2 实例网络带宽 – 介绍实例网络带宽规格、基准/突增机制、describe-instance-types 查询方法

使用 describe-instance-types API 查询目标区域中所有实例类型的网络性能数据(含 vCPUs、网络性能、峰值带宽、基准带宽)。

aws ec2 describe-instance-types \
    --filters "Name=current-generation,Values=true" \
    --query "InstanceTypes[].[InstanceType, VCpuInfo.DefaultVCpus, NetworkInfo.NetworkPerformance, NetworkInfo.NetworkCards[0].PeakBandwidthInGbps, NetworkInfo.NetworkCards[0].BaselineBandwidthInGbps]" \
    --output json

步骤二:计算 NAT 实例可用带宽

参考:Amazon EC2 实例网络带宽 – 【多流流量】及【可用实例带宽】部分

NAT 实例流量经过互联网网关(IGW),根据文档【多流流量】规则计算:

  • vCPUs < 32:限制为 5 Gbps,即 min(基准带宽, 5 Gbps)
  • vCPUs ≥ 32:限制为基准带宽的 50%,或 5 Gbps,以较大者为准,即 max(基准带宽 × 50%, 5 Gbps)

步骤三:计算月流量

基于 NAT 可用带宽,按每月 730 小时满载计算理论最大月流量,新增 入站流量 (TB/月) 列。

公式:流量 (TB) = NAT 可用带宽 (Gbps) × 730 × 3600 / 8 / 1000

步骤四:查询 NAT 实例价格

使用 AWS Pricing API 查询 Linux 共享租赁按需实例价格,按每月 730 小时换算为月费,新增 NAT 实例价格 列。

aws pricing get-products \
    --service-code AmazonEC2 \
    --filters \
        'Type=TERM_MATCH,Field=location,Value=China (Ningxia)' \
        'Type=TERM_MATCH,Field=operatingSystem,Value=Linux' \
        'Type=TERM_MATCH,Field=tenancy,Value=Shared' \
        'Type=TERM_MATCH,Field=capacitystatus,Value=Used' \
        'Type=TERM_MATCH,Field=preInstalledSw,Value=NA' \
    --output json

步骤五:筛选实例类型

在当前客户场景下,流量以入站方向为主,因此筛选的核心指标是:以最低的实例价格获得最大的可用带宽(即入站流量吞吐能力)。

1)规则筛选

将数据按流量升序、价格升序排列,从最后一行(流量最大)向前扫描,记录已见过的最低价格。如果当前行价格大于已见最低价,说明后面存在流量更大且价格更低的实例类型,当前行予以删除。如果价格相同,保留流量更大的,删除流量更小的。

此外,部分实例类型在 NAT 场景下可用带宽相同(如被 IGW 5 Gbps 封顶)且价格相近,仅保留流量更大或价格更低的一个。

2)手动筛选

自动筛选后仍存在价格几乎相同但流量差距明显的情况,手动去除:

  • c7g.16xlarge(¥9147.6/月,4928 TB)vs c8gn.8xlarge(¥10830/月,16425 TB):差 ¥1682,流量多 233%
  • c6in.8xlarge(¥10363/月,8212 TB)vs c8gn.8xlarge(¥10830/月,16425 TB):差 ¥467,流量多 100%

5. 基于 Auto Scaling Group 的高可用方案

参考:NAT 实例教程 – 介绍 NAT 实例的创建、配置 iptables、禁用源/目标检查、更新路由表等完整流程

NAT 实例为单实例部署,需要自行实现故障恢复。以下基于 Amazon EC2 Auto Scaling Group(ASG)实现高可用方案,并使用 CloudFormation 模板实现一键部署。

5.1 架构概览

[图1]

核心服务

  • Auto Scaling Group(min=max=1):始终维持一台 NAT 实例运行,故障时自动替换
  • Elastic IP:固定出口 IP,新实例启动后自动接管
  • Launch Template:定义实例配置和 UserData 自动化脚本
  • IAM Role:授予实例调用 EC2 API 的权限(接管 EIP、修改路由表等)
  • Lambda Custom Resource:自动获取 VPC ID 和配置安全组规则(仅在部署时调用一次)

5.2 部署方案

输入参数

参数 说明
InstanceType NAT 实例类型
KeyPairName EC2 密钥对,用于 SSH 访问
PublicSubnetId NAT 实例部署的公有子网
PrivateSubnetIds 需要 NAT 的私有子网,支持多选

核心资源

资源 类型 说明
NatLaunchTemplate Launch Template 定义实例配置(AMI、实例类型、安全组、UserData)
NatAutoScalingGroup Auto Scaling Group 管理实例生命周期,min=max=1 确保始终运行一台实例
NatEip Elastic IP 固定出口 IP,便于下游白名单配置
NatSecurityGroup Security Group 控制 NAT 实例的入站和出站流量
NatInstanceRole IAM Role 授予实例调用 EC2 API 的权限

辅助资源(Lambda Custom Resource)

资源 说明
GetVpcIdFunction 从公有子网自动获取 VPC ID,用于创建安全组
AddSecurityGroupRulesFunction 为每个私有子网 CIDR 动态添加安全组入站规则

安全组规则

方向 协议 端口 来源/目标 说明
入站 TCP 80 私有子网 CIDR 允许 HTTP 流量
入站 TCP 443 私有子网 CIDR 允许 HTTPS 流量
出站 TCP 80 0.0.0.0/0 允许 HTTP 到互联网
出站 TCP 443 0.0.0.0/0 允许 HTTPS 到互联网

故障恢复流程

  1. ASG 检测到实例终止(硬件故障、手动终止、状态检查失败)
  2. ASG 自动启动新实例
  3. 新实例执行 UserData 脚本:
    • 配置 iptables NAT 转发
    • 接管 EIP(--allow-reassociation
    • 禁用源/目标检查
    • 更新私有子网路由表指向新实例
  4. 私有子网恢复互联网访问(中断时间约 2-3 分钟)

5.2.1 UserData 脚本

以下脚本已嵌入 CloudFormation 模板的 Launch Template 中,部署时无需单独执行。每次 ASG 启动新实例时自动运行,并按以下脚本执行顺序完成 NAT 接管。

配置 NAT 转发

安装 iptables,启用 IP 转发,配置 MASQUERADE 规则。这是 NAT 的核心功能,必须最先完成。

# 安装 iptables
sudo yum install iptables-services -y
sudo systemctl enable iptables
sudo systemctl start iptables
# 启用 IP 转发
sudo tee /etc/sysctl.d/custom-ip-forwarding.conf << EOF
net.ipv4.ip_forward=1
EOF
sudo sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf
# 获取默认路由网卡,配置 MASQUERADE
IFACE=$(ip route show default | awk '{print $5}' | head -1)
sudo /sbin/iptables -t nat -A POSTROUTING -o "$IFACE" -j MASQUERADE
sudo /sbin/iptables -F FORWARD
sudo service iptables save

获取实例元数据

通过 IMDSv2(实例元数据服务)获取当前实例 ID,后续步骤均依赖此变量。

# 获取 IMDSv2 Token
TOKEN=$(curl -s -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" -X PUT "http://169.254.169.254/latest/api/token")
# 通过 Token 获取当前实例 ID
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)

接管 EIP

将预分配的 EIP 绑定到当前实例。--allow-reassociation 确保即使 EIP 仍关联在旧实例上也能接管。

# 接管 EIP,--allow-reassociation 允许从旧实例接管
aws ec2 associate-address --allocation-id ${NatEip.AllocationId} --instance-id $INSTANCE_ID --allow-reassociation

禁用 SourceDestCheck

NAT 实例需要转发非自身的流量,必须禁用源/目标检查。Launch Template 不支持此属性,只能通过 API 设置。

# 禁用源/目标检查,允许转发非自身流量
aws ec2 modify-instance-attribute --no-source-dest-check --instance-id $INSTANCE_ID

更新路由表

遍历所有私有子网,将其路由表的 0.0.0.0/0 指向当前实例。此步骤放在最后,确保 NAT 功能就绪后再切换流量,避免路由黑洞。

for SUBNET in ${PrivateSubnets}; do
  # 查找子网显式关联的路由表
  RT=$(aws ec2 describe-route-tables --filters "Name=association.subnet-id,Values=$SUBNET" --query 'RouteTables[0].RouteTableId' --output text)

  # 未找到则说明子网使用主路由表,通过 VPC ID 查找
  if [ "$RT" = "None" ] || [ -z "$RT" ]; then
    VPC_ID=$(aws ec2 describe-subnets --subnet-ids $SUBNET --query 'Subnets[0].VpcId' --output text)
    RT=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=association.main,Values=true" --query 'RouteTables[0].RouteTableId' --output text)
  fi
  # 替换已有路由,若不存在则创建
  aws ec2 replace-route --route-table-id $RT --destination-cidr-block 0.0.0.0/0 --instance-id $INSTANCE_ID || \
  aws ec2 create-route --route-table-id $RT --destination-cidr-block 0.0.0.0/0 --instance-id $INSTANCE_ID
done

5.3 CloudFormation 部署模版

AWSTemplateFormatVersion: "2010-09-09"
Description: NAT Instance
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Instance Configuration
        Parameters:
          - InstanceType
          - KeyPairName
          - PublicSubnetId
      - Label:
          default: Network Configuration
        Parameters:
          - PrivateSubnetIds
    ParameterLabels:
      InstanceType:
        default: Instance Type
      KeyPairName:
        default: Key Pair
      PublicSubnetId:
        default: Public Subnet
      PrivateSubnetIds:
        default: Private Subnets
Parameters:
  InstanceType:
    Type: String
    Default: t4g.nano
    AllowedValues:
      - t4g.nano
      - t4g.micro
      - t4g.small
      - t4g.medium
      - c8g.medium
      - c8g.large
      - c8gn.medium
      - c8gn.large
      - c8g.8xlarge
      - c8g.12xlarge
      - c8gn.8xlarge
      - c8gn.12xlarge
      - c8gn.16xlarge
      - c8gn.24xlarge
    Description: Select a recommended NAT instance type in cn-northwest-1
  KeyPairName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: Select a key pair for SSH access
  PublicSubnetId:
    Type: AWS::EC2::Subnet::Id
    Description: Select a public subnet to deploy the NAT instance
  PrivateSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Select private subnets that need NAT
Resources:
  GetVpcIdRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        - PolicyName: DescribeVpcSubnets
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeSubnets
                Resource: "*"
  GetVpcIdFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.14
      Handler: index.handler
      Role: !GetAtt GetVpcIdRole.Arn
      Timeout: 60
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          def handler(event, context):
              try:
                  # Skip processing on stack deletion
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                      return

                  ec2 = boto3.client('ec2')
                  public_subnet_id = event['ResourceProperties']['PublicSubnetId']

                  # Get VPC ID from subnet
                  response = ec2.describe_subnets(SubnetIds=[public_subnet_id])
                  vpc_id = response['Subnets'][0]['VpcId']

                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {
                      'VpcId': vpc_id
                  })
              except Exception as e:
                  print(f"Error: {str(e)}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
  GetVpcId:
    Type: Custom::GetVpcId
    Properties:
      ServiceToken: !GetAtt GetVpcIdFunction.Arn
      PublicSubnetId: !Ref PublicSubnetId
  NatSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for NAT instance
      VpcId: !GetAtt GetVpcId.VpcId
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: Allow HTTP to internet
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: Allow HTTPS to internet
  AddSecurityGroupRulesRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        - PolicyName: SecurityGroupOperations
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeSubnets
                  - ec2:AuthorizeSecurityGroupIngress
                Resource: "*"
  AddSecurityGroupRulesFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.14
      Handler: index.handler
      Role: !GetAtt AddSecurityGroupRulesRole.Arn
      Timeout: 60
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          def handler(event, context):
              try:
                  ec2 = boto3.client('ec2')

                  # Skip processing on stack deletion
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                      return

                  sg_id = event['ResourceProperties']['SecurityGroupId']
                  private_subnet_ids = event['ResourceProperties']['PrivateSubnetIds']

                  # Get CIDR blocks for all private subnets
                  response = ec2.describe_subnets(SubnetIds=private_subnet_ids)

                  # Add ingress rules for each private subnet CIDR
                  for subnet in response['Subnets']:
                      cidr = subnet['CidrBlock']

                      ec2.authorize_security_group_ingress(
                          GroupId=sg_id,
                          IpPermissions=[
                              {
                                  'IpProtocol': 'tcp',
                                  'FromPort': 80,
                                  'ToPort': 80,
                                  'IpRanges': [{'CidrIp': cidr, 'Description': f'HTTP from {cidr}'}]
                              },
                              {
                                  'IpProtocol': 'tcp',
                                  'FromPort': 443,
                                  'ToPort': 443,
                                  'IpRanges': [{'CidrIp': cidr, 'Description': f'HTTPS from {cidr}'}]
                              }
                          ]
                      )

                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              except Exception as e:
                  print(f"Error: {str(e)}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
  AddNatSecurityGroupRules:
    Type: Custom::SecurityGroupRules
    Properties:
      ServiceToken: !GetAtt AddSecurityGroupRulesFunction.Arn
      SecurityGroupId: !Ref NatSecurityGroup
      PrivateSubnetIds: !Ref PrivateSubnetIds
  NatInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: NatInstancePolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:ModifyInstanceCreditSpecification
                  - ec2:AssociateAddress
                  - ec2:ModifyInstanceAttribute
                  - ec2:DescribeRouteTables
                  - ec2:DescribeSubnets
                  - ec2:ReplaceRoute
                  - ec2:CreateRoute
                Resource: "*"
  NatInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref NatInstanceRole
  NatLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        ImageId: "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64}}"
        InstanceType: !Ref InstanceType
        KeyName: !Ref KeyPairName
        NetworkInterfaces:
          - DeviceIndex: "0"
            AssociatePublicIpAddress: true
            Groups:
              - !Ref NatSecurityGroup
        IamInstanceProfile:
          Arn: !GetAtt NatInstanceProfile.Arn
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash
              set -ex
              # Install and enable iptables
              sudo yum install iptables-services -y
              sudo systemctl enable iptables
              sudo systemctl start iptables
              # Enable IP forwarding
              sudo tee /etc/sysctl.d/custom-ip-forwarding.conf << EOF
              net.ipv4.ip_forward=1
              EOF
              sudo sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf
              # Configure NAT masquerade on default interface
              IFACE=$(ip route show default | awk '{print $5}' | head -1)
              sudo /sbin/iptables -t nat -A POSTROUTING -o "$IFACE" -j MASQUERADE
              sudo /sbin/iptables -F FORWARD
              sudo service iptables save
              # Get instance ID using IMDSv2
              TOKEN=$(curl -s -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" -X PUT "http://169.254.169.254/latest/api/token")
              INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
              # Set CPU credit specification to standard
              aws ec2 modify-instance-credit-specification --instance-credit-specifications InstanceId=$INSTANCE_ID,CpuCredits=standard
              # Associate EIP with this instance
              aws ec2 associate-address --allocation-id ${NatEip.AllocationId} --instance-id $INSTANCE_ID --allow-reassociation
              # Disable source/destination check for NAT
              aws ec2 modify-instance-attribute --no-source-dest-check --instance-id $INSTANCE_ID
              # Update route tables for private subnets
              for SUBNET in ${PrivateSubnets}; do
                # Find route table explicitly associated with subnet
                RT=$(aws ec2 describe-route-tables --filters "Name=association.subnet-id,Values=$SUBNET" --query 'RouteTables[0].RouteTableId' --output text)

                # If not found, use main route table
                if [ "$RT" = "None" ] || [ -z "$RT" ]; then
                  VPC_ID=$(aws ec2 describe-subnets --subnet-ids $SUBNET --query 'Subnets[0].VpcId' --output text)
                  RT=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=association.main,Values=true" --query 'RouteTables[0].RouteTableId' --output text)
                fi
                # Replace or create default route pointing to this instance
                aws ec2 replace-route --route-table-id $RT --destination-cidr-block 0.0.0.0/0 --instance-id $INSTANCE_ID || \
                aws ec2 create-route --route-table-id $RT --destination-cidr-block 0.0.0.0/0 --instance-id $INSTANCE_ID
              done
            - PrivateSubnets: !Join [ " ", !Ref PrivateSubnetIds ]
  NatAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref NatLaunchTemplate
        Version: !GetAtt NatLaunchTemplate.LatestVersionNumber
      VPCZoneIdentifier:
        - !Ref PublicSubnetId
      DesiredCapacity: "1"
      MinSize: "1"
      MaxSize: "1"
  NatEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

5.4 使用说明

进入 CloudFormation 控制台,选择创建堆栈,然后选择 CloudFormation 部署脚本。根据输入参数提示,依次选择要使用的实例类型、实例密钥、要部署的目标公网,要转发的私有子网列表等信息。

[图2]

提交并等待当前堆栈创建完成后,在堆栈的详情中,查看已自动化部署的资源。

[图3]

6. 总结

本文从带宽机制、成本对比和高可用三个维度分析了 EC2 NAT 实例方案:

  • 带宽机制:EC2 实例的实际可用带宽受多种因素影响,如多流流量规则、vCPU 数量、网络 I/O 积分等。本文梳理了这些因素如何共同作用于 NAT 实例场景,明确了基准带宽、峰值带宽与可用带宽三者之间的关系
  • 成本与选型:以亚马逊云科技中国(宁夏)区域(cn-northwest-1)为例,基于亚马逊云科技 API 获取所有当前代实例的网络带宽和价格数据,结合 NAT 带宽规则计算实际可用流量,筛选出每个流量档位的实例类型
  • 高可用部署:Auto Scaling Group 是实现 NAT 实例故障恢复的方案之一,本文提供了基于 ASG 的 CloudFormation 模板部署示例。此外还可通过 Lambda 监听 CloudWatch 告警、多 AZ 主备切换等方式实现

NAT 网关和 NAT 实例各有所长,适合的方案取决于业务对成本、可用性和运维复杂度的权衡。希望本文对带宽机制的梳理、成本数据的分析以及高可用方案的实践,能为您的选型决策提供参考。

➡️ 下一步行动:

相关产品:

相关文章:

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

张克鹏

西云数据解决方案架构师。10年+软件研发和架构经验,在研发效能提升、生成式AI等方向拥有丰富经验。


2026 亚马逊云科技中国峰会

探索多 Agent 架构、MCP 与 A2A 协议,掌握安全治理与评测体系,全面解锁生产级 Agent 的关键能力与前沿趋势。