リージョナル NAT Gateway がトランジットゲートウェイ(TGW)をサポートしたのでアウトバウンド通信の集約構成を試してみた

リージョナル NAT Gateway がトランジットゲートウェイ(TGW)をサポートしたのでアウトバウンド通信の集約構成を試してみた

2025年11月の GA 時には不可だったエッジルートテーブルへの トランジットゲートウェイの 直接指定が、2026年5月時点で可能になっていました。CloudFormation テンプレートで環境を構築し、AZ アフィニティの維持と片 AZ 障害時のフォールバックを確認しました。
2026.05.08

2026年5月、AWS 公式ドキュメントが更新され、以下の記載が追加されていることを確認しました。

リージョン NAT ゲートウェイは、リージョン NAT ゲートウェイルートテーブルで有効なルートとして AWS Transit Gateway をサポートします

https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/nat-gateways-regional.html

本記事では実際にアウトバウンド通信の集約(Centralized Egress)構成を構築し、動作を確認しました。

https://dev.classmethod.jp/articles/regional-nat-gateway-centralized-egress/

のんピさんの記事(2025/11/22)で「リージョナル NAT Gateway のエッジルートテーブルのルートのターゲットとして Transit Gateway を指定できるようになるのを待ちましょう」と結論づけられていた制限が解消されたことを確認できました。

背景:GA 時の制限と先行検証

Centralized Egress(アウトバウンド通信の集約)とは

複数の VPC のインターネット出口を 1 つの Egress VPC に集約する構成です。Transit Gateway で Spoke VPC のトラフィックを Egress VPC に転送し、NAT Gateway 経由でインターネットに出ます。VPC ごとに NAT Gateway を配置する必要がなくなり、EIP の管理やセキュリティポリシーの集約が可能になります。

なぜ「復路」で TGW が必要か

アウトバウンド通信の集約構成では、往路(EC2 → Internet)は Spoke → TGW → Egress VPC → NAT GW → IGW と流れます。問題は復路(Internet → EC2)です。インターネットからの戻りパケットは IGW → NAT GW(DNAT)まで来た後、Spoke VPC に返す必要があります。この「NAT GW から Spoke VPC への戻り」を制御するのがエッジルートテーブルであり、ここに TGW をターゲットとして指定できるかがこの構成成立の鍵です。

GA 時の問題

項目 GA 時(2025/11) 現在(2026/05)
エッジ RT に TGW 指定 ❌ GA 後2日で削除
ワークアラウンド ENI 直接指定(SPOF) 不要
AWS Blog のアウトバウンド集約手順 削除された ドキュメントに明記

GA 時はエッジ RT に TGW を指定できなかったため、TGW Attachment の ENI を直接指定するワークアラウンドが使われていました。しかし ENI は単一 AZ に存在するため、その AZ が障害を起こすと全 AZ の戻りトラフィックが不通になる = SPOF リスクがありました。

検証環境の構成

※図は簡略化しています。実際は EC2 → Spoke TGW Subnet → TGW → Egress TGW Subnet → NAT GW の順に経由します。

構成のポイント:

  • NAT GW は手動モード(AvailabilityZoneAddresses)で 2AZ に EIP を明示指定しています
    • デプロイ直後から 2AZ で稼働します(auto mode の AZ 展開待ちが不要)
    • disassociate-nat-gateway-address で特定 AZ の EIP を外すことで 1AZ 稼働を再現でき、片 AZ 障害のシミュレーションが可能になります
    • auto mode の自動割当 EIP ではこの操作ができないため、検証目的で手動モードを採用しました
  • Appliance Mode は disable です(AZ アフィニティ維持のため)

CloudFormation テンプレート

infra.yaml(クリックで展開)
AWSTemplateFormatVersion: '2010-09-09'
Description: >
  Regional NAT Gateway + Transit Gateway - Infrastructure Stack.
  Creates VPCs, TGW, Regional NAT GW, and routing.

Parameters:
  ProjectName:
    Type: String
    Default: rnat-tgw
  AZ1:
    Type: AWS::EC2::AvailabilityZone::Name
    Description: First Availability Zone
  AZ2:
    Type: AWS::EC2::AvailabilityZone::Name
    Description: Second Availability Zone

Resources:
  # Egress VPC
  EgressVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-egress-vpc

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-igw

  IGWAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref EgressVPC
      InternetGatewayId: !Ref InternetGateway

  EgressTGWSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref EgressVPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Ref AZ1
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-egress-tgw-az1

  EgressTGWSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref EgressVPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Ref AZ2
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-egress-tgw-az2

  EgressTGWRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EgressVPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-egress-tgw-rt

  EgressTGWRouteToNAT:
    Type: AWS::EC2::Route
    DependsOn: TGWAttachmentEgress
    Properties:
      RouteTableId: !Ref EgressTGWRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref RegionalNATGateway

  EgressTGWSubnet1Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref EgressTGWSubnet1
      RouteTableId: !Ref EgressTGWRouteTable

  EgressTGWSubnet2Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref EgressTGWSubnet2
      RouteTableId: !Ref EgressTGWRouteTable

  # Regional NAT Gateway (manual mode, 2AZ with explicit EIPs)
  EIP1:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-nat-eip-az1

  EIP2:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-nat-eip-az2

  RegionalNATGateway:
    Type: AWS::EC2::NatGateway
    DependsOn: IGWAttachment
    Properties:
      VpcId: !Ref EgressVPC
      AvailabilityMode: regional
      ConnectivityType: public
      AvailabilityZoneAddresses:
        - AvailabilityZone: !Ref AZ1
          AllocationIds:
            - !GetAtt EIP1.AllocationId
        - AvailabilityZone: !Ref AZ2
          AllocationIds:
            - !GetAtt EIP2.AllocationId
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-regional-nat

  # Edge Route: Spoke CIDR → TGW (CFn native, no Custom Resource needed)
  EdgeRouteToSpoke:
    Type: AWS::EC2::Route
    DependsOn: TGWAttachmentEgress
    Properties:
      RouteTableId: !GetAtt RegionalNATGateway.RouteTableId
      DestinationCidrBlock: 10.1.0.0/16
      TransitGatewayId: !Ref TransitGateway

  # Spoke VPC
  SpokeVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.1.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-spoke-vpc

  SpokePrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SpokeVPC
      CidrBlock: 10.1.1.0/24
      AvailabilityZone: !Ref AZ1
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-spoke-private-az1

  SpokePrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SpokeVPC
      CidrBlock: 10.1.2.0/24
      AvailabilityZone: !Ref AZ2
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-spoke-private-az2

  SpokeTGWSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SpokeVPC
      CidrBlock: 10.1.11.0/24
      AvailabilityZone: !Ref AZ1
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-spoke-tgw-az1

  SpokeTGWSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SpokeVPC
      CidrBlock: 10.1.12.0/24
      AvailabilityZone: !Ref AZ2
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-spoke-tgw-az2

  SpokePrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SpokeVPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-spoke-private-rt

  SpokeDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: TGWAttachmentSpoke
    Properties:
      RouteTableId: !Ref SpokePrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      TransitGatewayId: !Ref TransitGateway

  SpokePrivateSubnet1Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SpokePrivateSubnet1
      RouteTableId: !Ref SpokePrivateRouteTable

  SpokePrivateSubnet2Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SpokePrivateSubnet2
      RouteTableId: !Ref SpokePrivateRouteTable

  SpokeTGWRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SpokeVPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-spoke-tgw-rt

  SpokeTGWSubnet1Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SpokeTGWSubnet1
      RouteTableId: !Ref SpokeTGWRouteTable

  SpokeTGWSubnet2Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SpokeTGWSubnet2
      RouteTableId: !Ref SpokeTGWRouteTable

  # Transit Gateway
  TransitGateway:
    Type: AWS::EC2::TransitGateway
    Properties:
      Description: !Sub ${ProjectName} TGW
      DefaultRouteTableAssociation: disable
      DefaultRouteTablePropagation: disable
      DnsSupport: enable
      VpnEcmpSupport: enable
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-tgw

  TGWAttachmentEgress:
    Type: AWS::EC2::TransitGatewayVpcAttachment
    Properties:
      TransitGatewayId: !Ref TransitGateway
      VpcId: !Ref EgressVPC
      SubnetIds:
        - !Ref EgressTGWSubnet1
        - !Ref EgressTGWSubnet2
      Options:
        ApplianceModeSupport: disable
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-tgw-att-egress

  TGWAttachmentSpoke:
    Type: AWS::EC2::TransitGatewayVpcAttachment
    Properties:
      TransitGatewayId: !Ref TransitGateway
      VpcId: !Ref SpokeVPC
      SubnetIds:
        - !Ref SpokeTGWSubnet1
        - !Ref SpokeTGWSubnet2
      Options:
        ApplianceModeSupport: disable
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-tgw-att-spoke

  TGWRouteTableApp:
    Type: AWS::EC2::TransitGatewayRouteTable
    Properties:
      TransitGatewayId: !Ref TransitGateway
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-tgw-rt-app

  TGWRouteTableEgress:
    Type: AWS::EC2::TransitGatewayRouteTable
    Properties:
      TransitGatewayId: !Ref TransitGateway
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-tgw-rt-egress

  TGWRTAssocSpoke:
    Type: AWS::EC2::TransitGatewayRouteTableAssociation
    Properties:
      TransitGatewayAttachmentId: !Ref TGWAttachmentSpoke
      TransitGatewayRouteTableId: !Ref TGWRouteTableApp

  TGWRTAssocEgress:
    Type: AWS::EC2::TransitGatewayRouteTableAssociation
    Properties:
      TransitGatewayAttachmentId: !Ref TGWAttachmentEgress
      TransitGatewayRouteTableId: !Ref TGWRouteTableEgress

  TGWRouteAppDefault:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      TransitGatewayRouteTableId: !Ref TGWRouteTableApp
      DestinationCidrBlock: 0.0.0.0/0
      TransitGatewayAttachmentId: !Ref TGWAttachmentEgress

  TGWRouteEgressToSpoke:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      TransitGatewayRouteTableId: !Ref TGWRouteTableEgress
      DestinationCidrBlock: 10.1.0.0/16
      TransitGatewayAttachmentId: !Ref TGWAttachmentSpoke

Outputs:
  SpokeVPCId:
    Value: !Ref SpokeVPC
    Export:
      Name: !Sub ${AWS::StackName}-SpokeVPCId
  SpokePrivateSubnet1Id:
    Value: !Ref SpokePrivateSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-SpokePrivateSubnet1Id
  SpokePrivateSubnet2Id:
    Value: !Ref SpokePrivateSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-SpokePrivateSubnet2Id
  TransitGatewayId:
    Value: !Ref TransitGateway
    Export:
      Name: !Sub ${AWS::StackName}-TransitGatewayId
  RegionalNATGatewayId:
    Value: !Ref RegionalNATGateway
    Export:
      Name: !Sub ${AWS::StackName}-RegionalNATGatewayId
ec2.yaml(クリックで展開)
AWSTemplateFormatVersion: '2010-09-09'
Description: >
  Regional NAT Gateway + Transit Gateway - EC2 Stack.
  Deploys verification EC2 instances into Spoke VPC.

Parameters:
  InfraStackName:
    Type: String
    Description: Name of the infrastructure stack

Resources:
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow outbound only (SSM Session Manager)
      VpcId: !ImportValue
        Fn::Sub: ${InfraStackName}-SpokeVPCId
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${InfraStackName}-ec2-sg

  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref EC2Role

  EC2Instance1:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
      SubnetId: !ImportValue
        Fn::Sub: ${InfraStackName}-SpokePrivateSubnet1Id
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      Tags:
        - Key: Name
          Value: !Sub ${InfraStackName}-ec2-az1

  EC2Instance2:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
      SubnetId: !ImportValue
        Fn::Sub: ${InfraStackName}-SpokePrivateSubnet2Id
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      Tags:
        - Key: Name
          Value: !Sub ${InfraStackName}-ec2-az2

Outputs:
  EC2Instance1Id:
    Description: EC2 in AZ1
    Value: !Ref EC2Instance1
  EC2Instance2Id:
    Description: EC2 in AZ2
    Value: !Ref EC2Instance2

エッジ RT への TGW ルート定義

テンプレート内で以下のように定義しました。Custom Resource は不要です。

EdgeRouteToSpoke:
  Type: AWS::EC2::Route
  DependsOn: TGWAttachmentEgress
  Properties:
    RouteTableId: !GetAtt RegionalNATGateway.RouteTableId
    DestinationCidrBlock: 10.1.0.0/16
    TransitGatewayId: !Ref TransitGateway

!GetAtt RegionalNATGateway.RouteTableId でエッジルートテーブルの ID を取得できます。NAT GW と TGW は同一スタック(infra.yaml)内で定義しているため !Ref で参照可能です。CFn デプロイのみで手動 CLI 操作なしに完全な環境が構築されることを確認しました。

デプロイ手順

# インフラスタック
aws cloudformation create-stack --stack-name rnat-tgw-infra \
  --template-body file://cfn/infra.yaml --region ap-northeast-1 \
  --parameters ParameterKey=AZ1,ParameterValue=ap-northeast-1a \
               ParameterKey=AZ2,ParameterValue=ap-northeast-1d

# EC2スタック(infraスタック完了後)
aws cloudformation create-stack --stack-name rnat-tgw-ec2 \
  --template-body file://cfn/ec2.yaml --region ap-northeast-1 \
  --capabilities CAPABILITY_IAM \
  --parameters ParameterKey=InfraStackName,ParameterValue=rnat-tgw-infra

テンプレートの実装ポイント:

  • !GetAtt RegionalNATGateway.RouteTableId でエッジ RT の ID を取得し、Custom Resource 不要で TGW ルートを定義
  • AvailabilityZoneAddresses で AZ と EIP を明示指定し、auto mode の展開待ちを回避
  • AZ はパラメータで明示指定(!GetAZs はデフォルトサブネットが欠落した AZ を返さないため)

AZ アフィニティとトラフィックフロー

SSM Session Manager で各 EC2 に接続し、curl http://checkip.amazonaws.com を複数回実行しました。EC2 は Private Subnet に配置しており、SSM 接続も NAT GW 経由のパブリックエンドポイントを利用しています。

=== EC2-1 (ap-northeast-1a) ===
xx.xx.xx.1  ← AZ-a の EIP

=== EC2-2 (ap-northeast-1d) ===
yy.yy.yy.2  ← AZ-d の EIP
EC2 AZ 使用 EIP NAT GW AZ 一致
EC2-1 ap-northeast-1a xx.xx.xx.1 ap-northeast-1a
EC2-2 ap-northeast-1d yy.yy.yy.2 ap-northeast-1d

両 AZ の EC2 からインターネットアクセスが成功し、TGW 直接指定で AZ アフィニティが完全に維持されていました。

往路のトラフィックフロー

EC2-1 (AZ-a) → Spoke TGW Subnet (AZ-a) → TGW → Egress TGW Subnet (AZ-a) → NAT GW (AZ-a EIP) → IGW → Internet

復路のトラフィックフロー(ここが新機能)

Internet → IGW → NAT GW (DNAT: EIP → 10.1.1.x)
  → エッジ RT: 10.1.0.0/16 → TGW  ← ここが今回サポートされたルート
  → TGW Egress RT: 10.1.0.0/16 → Spoke Attachment
  → Spoke VPC TGW Subnet (AZ-a) → EC2-1

NAT GW が DNAT 後にエッジ RT を評価する時点で、パケットは AZ-a の NAT GW インスタンスが処理しています。エッジ RT から TGW に渡す際、TGW は Egress VPC Attachment の AZ-a 側 ENI でパケットを受け取ります。Appliance Mode が無効の TGW は、入口 Attachment で受け取った AZ と同じ AZ にある出口 Attachment(Spoke VPC 側)の ENI へトラフィックを転送します。そのため、復路でも AZ を跨ぎませんでした。

コスト面のメリット

AZ アフィニティが維持されることで、クロス AZ データ転送料金($0.01/GB per direction、往復で $0.02/GB)が正常時は発生しません。ENI 指定ワークアラウンドでは全トラフィックが 1AZ に集中するため、他 AZ からのトラフィックには常にクロス AZ 料金が発生していました。

片 AZ 障害時のフォールバック確認

AZ-a の EIP を NAT GW から解除して擬似障害を発生させました。

# association-id は describe-nat-gateways の NatGatewayAddresses[].AssociationId で確認
aws ec2 disassociate-nat-gateway-address \
  --nat-gateway-id <NAT-GW-ID> \
  --association-ids <ASSOCIATION-ID> \
  --region ap-northeast-1

結果:

状態 EC2-1 (AZ-a) EC2-2 (AZ-d)
正常時 AZ-a の EIP AZ-d の EIP
AZ-a EIP 解除後 AZ-d の EIP ← フォールバック AZ-d の EIP(影響なし)

片 AZ の EIP 解除後、残存 AZ の NAT GW にフォールバックして通信が継続しました。

フォールバックの仕組み(検証結果からの推測)

検証結果から、AZ-a の NAT GW リソース(EIP)が消失すると、リージョナル NAT GW が内部的に正常な AZ-d のリソースへクロス AZ でトラフィックを転送していると考えられます。リージョナル NAT GW はサブネットに紐付かない VPC レベルのリソースであるため、このような挙動が可能と推測されます。

なお、disassociate-nat-gateway-address 実行後に disassociating 状態を経てからフォールバックに切り替わる挙動も確認しており、グレースフルドレインが行われていました。

ENI 指定ワークアラウンドとの比較

項目 ENI 指定(2025/11) TGW 直接指定(2026/05)
正常時 全トラフィックが 1AZ に集中 各 AZ で独立処理
片 AZ 障害時 ❌ 全 AZ 通信不能(SPOF) ✅ 残存 AZ にフォールバック
クロス AZ 料金 ⚠️ 常時発生($0.02/GB) ✅ 正常時は発生しない

まとめ

リージョナル NAT Gateway のエッジルートテーブルに Transit Gateway を直接指定できるようになり、のんピさんの記事で指摘されていた SPOF リスクを回避できる構成が組めるようになりました。CFn テンプレートだけで環境構築が完結し、正常時は AZ アフィニティが維持され、EIP 解除による簡易検証ながら片 AZ 障害時のフォールバックも確認できました。

リージョナル NAT Gateway + TGW の組み合わせを安心して採用できる構成になっています。アウトバウンド通信の集約を検討中の方はぜひ試してみてください。

参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事