Regional NAT Gateway Now Supports Transit Gateway (TGW), So I Tried Out a Consolidated Outbound Traffic Configuration

Regional NAT Gateway Now Supports Transit Gateway (TGW), So I Tried Out a Consolidated Outbound Traffic Configuration

Specifying a Transit Gateway directly in an edge route table, not possible at the November 2025 GA, was available by May 2026. I built the setup with CloudFormation and verified AZ affinity and single-AZ failure fallback.
2026.05.08

This page has been translated by machine translation. View original

In May 2026, I confirmed that the official AWS documentation had been updated with the following addition:

Regional NAT gateways support AWS Transit Gateway as a valid route in the regional NAT gateway route table

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

In this article, I actually built an outbound traffic aggregation (Centralized Egress) configuration and verified its behavior.

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

I confirmed that the limitation that Nonpi's article (2025/11/22) concluded with, "let's wait until a Transit Gateway can be specified as the route target in the edge route table of the Regional NAT Gateway," has been resolved.

Background: Limitations at GA and Earlier Verification

What Is Centralized Egress (Outbound Traffic Aggregation)?

This is a configuration that consolidates the internet egress of multiple VPCs into a single Egress VPC. A Transit Gateway forwards traffic from Spoke VPCs to the Egress VPC, and it goes out to the internet via a NAT Gateway. You no longer need to place a NAT Gateway in each VPC, and it becomes possible to consolidate EIP management and security policies.

Why a TGW Is Needed for the "Return Path"

In an outbound traffic aggregation configuration, the outbound path (EC2 → Internet) flows as Spoke → TGW → Egress VPC → NAT GW → IGW. The problem is the return path (Internet → EC2). Return packets from the internet arrive at the IGW → NAT GW (DNAT), and then need to be returned to the Spoke VPC. The edge route table controls this "return from the NAT GW to the Spoke VPC," and whether a TGW can be specified as the target here is the key to making this configuration work.

Problem at GA

Item At GA (2025/11) Now (2026/05)
TGW specified in edge RT ❌ Removed 2 days after GA ✅
Workaround Direct ENI specification (SPOF) Not needed
Outbound aggregation procedure in AWS Blog Removed Explicitly documented

At GA, a TGW could not be specified in the edge RT, so a workaround of directly specifying the ENI of the TGW Attachment was used. However, because an ENI exists in a single AZ, if that AZ failed, return traffic for all AZs would be cut off = there was a SPOF risk.

Verification Environment Configuration

  • The diagram is simplified. In reality, traffic passes through EC2 → Spoke TGW Subnet → TGW → Egress TGW Subnet → NAT GW in that order.

Key points of the configuration:

  • The NAT GW is in manual mode (AvailabilityZoneAddresses), with EIPs explicitly specified for 2 AZs
    • It runs in 2 AZs immediately after deployment (no need to wait for AZ expansion as in auto mode)
    • By removing the EIP of a specific AZ with disassociate-nat-gateway-address, you can reproduce 1-AZ operation, making it possible to simulate a single-AZ failure
    • This operation cannot be done with the automatically assigned EIPs of auto mode, so I adopted manual mode for verification purposes
  • Appliance Mode is disabled (to maintain AZ affinity)

CloudFormation Templates

infra.yaml (click to expand)
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 (click to expand)
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

TGW Route Definition for the Edge RT

I defined it in the template as follows. A Custom Resource is not required.

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

You can obtain the ID of the edge route table with !GetAtt RegionalNATGateway.RouteTableId. Since the NAT GW and TGW are defined in the same stack (infra.yaml), they can be referenced with !Ref. I confirmed that a complete environment is built with only a CFn deployment, without any manual CLI operations.

Deployment Procedure

# インフラスタック
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

Template implementation points:

  • Obtain the edge RT ID with !GetAtt RegionalNATGateway.RouteTableId and define the TGW route without needing a Custom Resource
  • Explicitly specify AZs and EIPs with AvailabilityZoneAddresses to avoid waiting for auto mode expansion
  • AZs are explicitly specified via parameters (because !GetAZs does not return AZs where a default subnet is missing)

AZ Affinity and Traffic Flow

I connected to each EC2 instance with SSM Session Manager and ran curl http://checkip.amazonaws.com multiple times. The EC2 instances are placed in a Private Subnet, and the SSM connection also uses the public endpoint via the 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 Used NAT GW AZ Match
EC2-1 ap-northeast-1a xx.xx.xx.1 ap-northeast-1a ✅
EC2-2 ap-northeast-1d yy.yy.yy.2 ap-northeast-1d ✅

Internet access succeeded from EC2 instances in both AZs, and AZ affinity was fully maintained with direct TGW specification.

Outbound Traffic Flow

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

Return Traffic Flow (This Is the New Feature)

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

At the point when the NAT GW evaluates the edge RT after DNAT, the packet is being processed by the NAT GW instance in AZ-a. When it is handed from the edge RT to the TGW, the TGW receives the packet at the AZ-a side ENI of the Egress VPC Attachment. A TGW with Appliance Mode disabled forwards traffic to the ENI of the egress Attachment (on the Spoke VPC side) in the same AZ as the AZ where it was received at the ingress Attachment. As a result, the return path also did not cross AZs.

Cost Benefits

Because AZ affinity is maintained, cross-AZ data transfer charges ($0.01/GB per direction, $0.02/GB round trip) do not occur during normal operation. With the ENI-specification workaround, all traffic concentrated in 1 AZ, so cross-AZ charges always applied to traffic from other AZs.

Verifying Fallback During a Single-AZ Failure

I released the AZ-a EIP from the NAT GW to cause a simulated failure.

# 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

Results:

State EC2-1 (AZ-a) EC2-2 (AZ-d)
Normal AZ-a EIP AZ-d EIP
After AZ-a EIP release AZ-d EIP ← fallback AZ-d EIP (unaffected)

After the EIP of one AZ was released, communication fell back to the NAT GW in the remaining AZ and continued.

Fallback Mechanism (Inferred from Verification Results)

From the verification results, it is thought that when the NAT GW resource (EIP) in AZ-a disappears, the Regional NAT GW internally forwards traffic cross-AZ to the resource in the healthy AZ-d. Because the Regional NAT GW is a VPC-level resource not tied to a subnet, it is presumed that such behavior is possible.

I also confirmed that after running disassociate-nat-gateway-address, the behavior switched to fallback after passing through the disassociating state, so a graceful drain was being performed.

Comparison with the ENI-Specification Workaround

Item ENI specification (2025/11) Direct TGW specification (2026/05)
Normal All traffic concentrated in 1 AZ Processed independently in each AZ
Single-AZ failure ❌ Communication lost in all AZs (SPOF) ✅ Falls back to remaining AZ
Cross-AZ charges ⚠️ Always incurred ($0.02/GB) ✅ Not incurred during normal operation

Summary

It is now possible to directly specify a Transit Gateway in the edge route table of the Regional NAT Gateway, making it possible to build a configuration that avoids the SPOF risk pointed out in Nonpi's article. The environment can be built with just a CFn template, AZ affinity is maintained during normal operation, and although it was a simple verification via EIP release, I was also able to confirm fallback during a single-AZ failure.

The combination of Regional NAT Gateway + TGW is now a configuration you can adopt with confidence. If you are considering outbound traffic aggregation, please give it a try.

Share this article

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