A platform team starts with two VPCs — production and a shared services VPC for the bastion, Prometheus, and the package mirrors — peered together. Six months later they have a separate VPC per business unit, plus dev, staging, and a sandbox account for the data team. That is six VPCs, and full-mesh peering would be 6 × 5 ÷ 2 = 15 peering connections, each with its own route table entry on every side. The team has been creating peerings one at a time and the routing tables now have 35 entries each, none of them clearly named. The latest peering had a CIDR overlap and silently failed. The right move was a Transit Gateway six months ago. Here is the migration that gets there without another quarter of accreted plumbing.
What’s happening
A Transit Gateway (TGW) is a regional networking hub. VPCs attach to it once, and TGW handles routing between every attached VPC, on-prem network, and Direct Connect gateway. The peering full-mesh problem goes away — every VPC has a single attachment to the TGW, and the TGW has a route table that decides which VPCs can reach which other VPCs. The cost model is hourly per attachment plus per-GB data processed; for an organization with 5+ VPCs, the per-attachment cost is well below what you would pay for the cross-AZ data transfer and operational overhead of running a peering mesh.
The single thing TGW gives you that peering does not is transitive routing. With peering, if VPC A is peered to VPC B and B is peered to C, traffic from A cannot reach C — peering is non-transitive by design. With TGW, A can reach C if the TGW route table allows it. This is the property that makes hub-and-spoke designs possible: the shared services VPC holds the bastion, the package mirror, the centralized log forwarder, and every spoke VPC routes to it through the hub.
The other thing TGW gives you is multi-account sharing through AWS Resource Access Manager (RAM). A central networking account owns the TGW. Other accounts in the organization receive the TGW as a shared resource and can attach their own VPCs to it. The route table sits in the networking account, which means the network team controls who can reach whom — application accounts get to deploy their VPCs but cannot accidentally route into another team’s VPC.
Direct Connect attaches to TGW through a Direct Connect Gateway. The on-prem network appears as another spoke in the TGW route table, and the rule for which on-prem CIDRs are reachable from which VPCs lives in the same TGW route table you already manage. The alternative — VPN to each VPC, or a Direct Connect virtual interface per VPC — is the operational mess that TGW was designed to replace.
How to verify
The decision point: does TGW earn its keep for this organization? The rule of thumb:
- 2–3 VPCs that need full mesh: VPC peering is fine. 3 peerings, manageable route tables.
- 4 VPCs in a hub-and-spoke (one shared services VPC + 3 spokes): peering still works, 3 peerings, but transitive routing pain starts.
- 5+ VPCs OR cross-account requirement OR Direct Connect / VPN involvement: TGW.
Inspect existing peering count and overlap:
# Count active peerings in the region
aws ec2 describe-vpc-peering-connections \
--filters Name=status-code,Values=active \
--query 'length(VpcPeeringConnections)'
# List CIDRs per VPC to spot overlap before TGW
aws ec2 describe-vpcs \
--query 'Vpcs[].[VpcId,CidrBlock,Tags[?Key==`Name`]|[0].Value]' \
--output table
Any two VPCs sharing a CIDR block cannot both attach to a TGW — that has to be fixed before the migration, either by re-IP-ing one VPC or by using TGW’s static route capability with CIDR translation (a hack, not a long-term plan).
The fix
The procedure to deploy TGW for a 6-VPC organization spanning 3 accounts:
-
Decide which account owns the TGW. Put it in the networking account (or the shared services account if there is no dedicated networking account). That account owns the TGW, the route tables, and the RAM share. Application accounts only own their attachments and their own VPC route tables.
-
Create the TGW with sensible defaults disabled. The default behavior is to auto-associate every new attachment with the default route table and auto-propagate routes. That is convenient for a single-environment setup and dangerous for a multi-environment one — a new sandbox VPC attaches and immediately has routes to production.
aws ec2 create-transit-gateway \ --description "Primary regional TGW" \ --options "AmazonSideAsn=64512,AutoAcceptSharedAttachments=disable,DefaultRouteTableAssociation=disable,DefaultRouteTablePropagation=disable,VpnEcmpSupport=enable,DnsSupport=enable" -
Create per-environment route tables on the TGW. The pattern is one route table per security zone. For this org:
tgw-rt-shared-services,tgw-rt-production,tgw-rt-non-production,tgw-rt-on-prem. Each route table holds the rules for which attachments can route to which other attachments.aws ec2 create-transit-gateway-route-table \ --transit-gateway-id tgw-0123456789abcdef0 \ --tag-specifications 'ResourceType=transit-gateway-route-table,Tags=[{Key=Name,Value=tgw-rt-production}]' -
Share the TGW via RAM with the rest of the org. The application accounts receive the TGW resource and can attach their VPCs:
aws ram create-resource-share \ --name tgw-share \ --resource-arns arn:aws:ec2:us-east-1:111122223333:transit-gateway/tgw-0123456789abcdef0 \ --principals arn:aws:organizations::111122223333:organization/o-aaaaaaaaaa \ --allow-external-principals -
Attach each VPC. In each VPC’s account, create a TGW attachment:
aws ec2 create-transit-gateway-vpc-attachment \ --transit-gateway-id tgw-0123456789abcdef0 \ --vpc-id vpc-prodaaaaaaaaaaa \ --subnet-ids subnet-prod-1a subnet-prod-1b subnet-prod-1cThe subnet selection is one subnet per AZ. AWS deploys a TGW ENI in each chosen subnet — use a dedicated
/28per AZ for these (carve from the reserved range in the VPC). The TGW ENI does not route customer traffic by itself; it is the path between the VPC’s local routing and the TGW’s regional routing. -
Associate and propagate per environment. Each attachment associates with one route table (the one that decides what this attachment sees) and propagates into the route tables that should learn about this attachment’s VPC CIDR. For production:
aws ec2 associate-transit-gateway-route-table \ --transit-gateway-attachment-id tgw-attach-prodaaaaaaaa \ --transit-gateway-route-table-id tgw-rtb-production # Production sees shared services aws ec2 enable-transit-gateway-route-table-propagation \ --transit-gateway-route-table-id tgw-rtb-production \ --transit-gateway-attachment-id tgw-attach-sharedservicesa # Shared services sees production (and the other spokes) aws ec2 enable-transit-gateway-route-table-propagation \ --transit-gateway-route-table-id tgw-rtb-shared-services \ --transit-gateway-attachment-id tgw-attach-prodaaaaaaaaThe pattern: shared services is reachable from every environment; environments are NOT reachable from each other. Production cannot reach non-production; staging cannot reach sandbox; sandbox cannot reach anything except shared services for the package mirror and the bastion.
-
Update VPC route tables to send remote traffic to the TGW. In each VPC, the private subnet route table needs a route for the other VPCs’ CIDRs (or a summary route) pointing at the TGW attachment:
aws ec2 create-route \ --route-table-id rtb-prod-private-1a \ --destination-cidr-block 10.0.0.0/8 \ --transit-gateway-id tgw-0123456789abcdef0The TGW’s own route table then decides which destination is reachable from this attachment.
-
Attach on-prem (when applicable). A Direct Connect Gateway associates with the TGW; the on-prem CIDRs propagate via BGP. The shared services route table receives the on-prem propagation; the production route table receives it if production needs on-prem reachability.
The migration from peering to TGW happens one VPC at a time. The order: attach all VPCs to the TGW first, with route tables wired but VPC-level routes still pointing at the old peering. Then flip one VPC’s route table to send remote CIDRs at the TGW, watch for two days, and delete the peerings to that VPC. Repeat. The peering mesh dissolves spoke by spoke.
The mistake to avoid: enabling the default-association and default-propagation flags on the TGW. They look convenient on day one and end with the sandbox VPC having a route to production by accident. Disable them, and require every association to be explicit.
The other rule we hold: TGW route tables are infrastructure-as-code, period. The “who can route to whom” decision matters as much as IAM policies, and aws ec2 create-route from a console session is the same mistake as editing an IAM policy by hand.
For the alternative when you only need service-level access (not VPC-level reachability), see VPC peering vs PrivateLink. For the underlying subnet plan, see Multi-AZ subnet design.
Stack Harbor designs and audits multi-VPC TGW topologies as part of Managed AWS and ongoing Managed Cloud Ops. If your peering mesh has crossed the readability threshold, start with an infrastructure consult.