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.
Account structure
Section titled “Account structure”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.
Prerequisites
Section titled “Prerequisites”- 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.
Step 1: create the network account
Section titled “Step 1: create the network account”aws organizations create-account \ --email network-infra@example.com \ --account-name "Network Infrastructure"Give it a role that CloudFormation can assume for network administration:
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/AWSNetworkAdministratorStep 2: create the transit gateway
Section titled “Step 2: create the transit gateway”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-tgwDefault 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:
aws ram enable-sharing-with-aws-organizationThen create the share in the network account:
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.
Step 4: the member VPC template
Section titled “Step 4: the member VPC template”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}produces10.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 PrivateSubnetsreferring 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.
Step 5: deploy the StackSet
Section titled “Step 5: deploy the StackSet”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=falseaws 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.
Step 6: validate
Section titled “Step 6: validate”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-tablesThen 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.
Operating it
Section titled “Operating it”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.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Look at |
|---|---|
| StackSet deployment fails in some accounts | Service-managed permissions enabled? Trusted access for CloudFormation? Region opted in? |
Attachment stays pendingAcceptance | AutoAcceptSharedAttachments is disable, or the account is not in the RAM share |
| Instances cannot reach another VPC | Route 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 another | No attachment subnet in that Availability Zone |
| Stack fails with an invalid CIDR | The CIDR parameter overlaps an existing VPC, or was derived rather than passed in |