Regional NAT Gateway Now Supports Transit Gateway (TGW), So I Tried Out a Consolidated Outbound Traffic Configuration
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
In this article, I actually built an outbound traffic aggregation (Centralized Egress) configuration and verified its behavior.
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.RouteTableIdand define the TGW route without needing a Custom Resource - Explicitly specify AZs and EIPs with
AvailabilityZoneAddressesto avoid waiting for auto mode expansion - AZs are explicitly specified via parameters (because
!GetAZsdoes 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.
