Skip to content

Transit gateway multi-account implementation guide

A centrally managed transit gateway shared across an AWS Organization, with member VPCs and their attachments created by CloudFormation StackSets. One account owns the network; workload accounts attach to it and cannot change it.

Management account. Owns AWS Organizations itself: account creation, service control policies, consolidated billing. It should not own the network.

Network (infrastructure) account. Owns the transit gateway, the resource shares, the StackSets that deploy member VPCs, network monitoring and network security controls. Delegated by the management account.

Member accounts. Host workloads in their own VPCs, each attached to the shared transit gateway.

Keeping the transit gateway out of the management account matters: it means day-to-day network changes do not require credentials in the account that can create and close accounts.

flowchart TB subgraph Management["Management Account"] OrgMgmt[Organizations] end subgraph Infrastructure["Network Account"] TG[Transit Gateway] RAM[Resource Access Manager] CFN[CloudFormation StackSet] Monitoring[Network Monitoring] end subgraph MemberAccounts["Member Accounts"] subgraph Account1[Account 1] VPC1[VPC] TGA1[TGW Attachment] end subgraph Account2[Account 2] VPC2[VPC] TGA2[TGW Attachment] end subgraph AccountN[Account N] VPCN[VPC] TGAN[TGW Attachment] end end Management -->|Delegate network control| Infrastructure RAM -->|Share transit gateway| MemberAccounts CFN -->|Deploy| VPC1 CFN -->|Deploy| VPC2 CFN -->|Deploy| VPCN CFN -->|Create| TGA1 CFN -->|Create| TGA2 CFN -->|Create| TGAN TGA1 -->|Associate| TG TGA2 -->|Associate| TG TGAN -->|Associate| TG classDef aws fill:#FF9900,stroke:#232F3E,stroke-width:2px,color:black; class TG,RAM,CFN,VPC1,VPC2,VPCN,TGA1,TGA2,TGAN,Monitoring,OrgMgmt aws;
  • An AWS Organization with all features enabled.
  • A dedicated network account, and at least one member account.
  • Trusted access enabled for AWS RAM and for CloudFormation StackSets.
  • An address plan that assigns each account a non-overlapping CIDR block. This is a prerequisite, not a detail — see private IPv4 address planning.
Terminal window
aws organizations create-account \
--email network-infra@example.com \
--account-name "Network Infrastructure"

Give it a role that CloudFormation can assume for network administration:

network-account-roles.yaml
Resources:
NetworkAdminRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSNetworkAdministrator
transit-gateway.yaml
Resources:
TransitGateway:
Type: AWS::EC2::TransitGateway
Properties:
AmazonSideAsn: 64512
AutoAcceptSharedAttachments: enable
DefaultRouteTableAssociation: disable
DefaultRouteTablePropagation: disable
Description: Central transit gateway for the organization
Tags:
- Key: Name
Value: central-tgw

Default association and propagation are disabled deliberately. With them enabled every attachment can reach every other attachment as soon as it is created, and there is no way to segment environments later without unpicking it.

Step 3: share the transit gateway with AWS RAM

Section titled “Step 3: share the transit gateway with AWS RAM”

Enable sharing with the organization, from the management account:

Terminal window
aws ram enable-sharing-with-aws-organization

Then create the share in the network account:

ram-share.yaml
Parameters:
OrganizationArn:
Type: String
Description: >-
ARN of the organization or organizational unit to share with, for example
arn:aws:organizations::111122223333:organization/o-exampleorgid
Resources:
TransitGatewayShare:
Type: AWS::RAM::ResourceShare
Properties:
Name: tgw-share
AllowExternalPrincipals: false
Principals:
- !Ref OrganizationArn
ResourceArns:
- !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/${TransitGateway}'

A RAM principal must be an account ID, an organization ARN, an organizational unit ARN, or an IAM role or user ARN. '*' is not shorthand for “my organization” and will not share the gateway with it. Sharing with an organizational unit ARN rather than the whole organization is usually the better choice, because it lets you onboard accounts by moving them between OUs.

member-vpc-tgw.yaml
Parameters:
TransitGatewayId:
Type: String
Description: ID of the shared transit gateway
VpcCidr:
Type: String
Description: Non-overlapping /16 for this account, for example 10.20.0.0/16
AllowedPattern: '^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
AttachmentSubnet1Cidr:
Type: String
Description: /28 in the first Availability Zone for the transit gateway attachment
AttachmentSubnet2Cidr:
Type: String
Description: /28 in the second Availability Zone for the transit gateway attachment
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${AWS::AccountId}-vpc'
AttachmentSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref AttachmentSubnet1Cidr
AvailabilityZone: !Select [0, !GetAZs '']
AttachmentSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref AttachmentSubnet2Cidr
AvailabilityZone: !Select [1, !GetAZs '']
TransitGatewayAttachment:
Type: AWS::EC2::TransitGatewayAttachment
Properties:
TransitGatewayId: !Ref TransitGatewayId
VpcId: !Ref VPC
SubnetIds:
- !Ref AttachmentSubnet1
- !Ref AttachmentSubnet2
Tags:
- Key: Name
Value: !Sub '${AWS::AccountId}-tgw-attachment'

Two things this template does that a shorter one gets wrong:

  • The CIDR block is a parameter. Deriving it from ${AWS::AccountId} produces 10.123456789012.0.0/16 — an account ID is twelve digits, not one octet — and the stack fails immediately.
  • The subnets the attachment references are defined in the template. SubnetIds: !Ref PrivateSubnets referring to a resource that does not exist is a validation error.

Workload subnets, route tables and NAT are deliberately left out; add them here or in a separate stack, and add a route for the other accounts’ CIDR blocks pointing at the transit gateway.

Terminal window
aws cloudformation create-stack-set \
--stack-set-name member-vpc-tgw \
--template-body file://member-vpc-tgw.yaml \
--parameters ParameterKey=TransitGatewayId,ParameterValue=tgw-0123456789abcdef0 \
--permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false
Terminal window
aws cloudformation create-stack-instances \
--stack-set-name member-vpc-tgw \
--regions eu-west-2 \
--deployment-targets OrganizationalUnitIds='["ou-xxxx-xxxxxxxx"]'

With SERVICE_MANAGED permissions, CloudFormation uses the organization’s service-managed roles and you do not create administration and execution roles yourself. If you use SELF_MANAGED instead, scope the administration role to the actions the StackSet needs — ec2:*TransitGateway*, ec2:*Vpc*, ec2:*Subnet*, cloudformation:* — rather than attaching AdministratorAccess. A least-privilege policy in the same document as an AdministratorAccess role is not a least-privilege design.

Because each account needs its own CIDR block, either pass per-account parameter overrides on create-stack-instances, or have the template look the block up from a parameter store the network account maintains.

Terminal window
aws ec2 describe-transit-gateway-attachments \
--filters Name=transit-gateway-id,Values=tgw-0123456789abcdef0
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=*-vpc"
aws ec2 describe-transit-gateway-route-tables

Then confirm end to end: an instance in one member VPC should reach an instance in another, and the transit gateway route table should show a route for each attached CIDR block with no blackhole entries.

Monitoring. Turn on VPC flow logs in each member account and on the attachment subnets. Use Network Manager for a topology view across the organization, and CloudWatch alarms on transit gateway packet-drop metrics.

Routing. Associate attachments with the route table that matches their environment rather than a single shared table. Review route tables for blackhole entries after any account is closed.

Security. Routes make traffic possible; security groups and network ACLs decide whether it is allowed. Enable AWS Config rules for both. Audit the RAM share periodically — it is the control that decides which accounts can attach at all.

Cost. Transit gateway charges are per attachment-hour plus per gigabyte processed, and cross-Availability-Zone traffic is charged separately. An attachment left behind by a decommissioned workload keeps billing.

SymptomLook at
StackSet deployment fails in some accountsService-managed permissions enabled? Trusted access for CloudFormation? Region opted in?
Attachment stays pendingAcceptanceAutoAcceptSharedAttachments is disable, or the account is not in the RAM share
Instances cannot reach another VPCRoute in the VPC route table pointing at tgw-…; route in the transit gateway route table pointing at the attachment; security groups; network ACLs
Traffic works from one subnet but not anotherNo attachment subnet in that Availability Zone
Stack fails with an invalid CIDRThe CIDR parameter overlaps an existing VPC, or was derived rather than passed in