Transit Gateway as the Hub
When you're running more than three or four AWS accounts, VPC peering turns into a mess pretty fast. I've watched teams maintain 30+ peering connections and lose track of which account can talk to which. Transit Gateway solves this by acting as a central router — every VPC connects to the TGW, and route tables handle the rest.
The setup isn't complicated in isolation. You create the Transit Gateway in a shared networking account, then use AWS Resource Access Manager (RAM) to share it with your org's accounts. Each account attaches its VPC to the shared TGW. That's the basic topology.
Route Table Segmentation
Here's where it gets interesting. A single TGW route table means every VPC can reach every other VPC. That's rarely what you want. Production shouldn't talk to development. Shared services (DNS, logging, CI/CD) need to reach everything, but sandbox accounts shouldn't reach production databases.
You'll want at least three route tables: one for production, one for non-production, and one for shared services. The shared services table has routes to both prod and non-prod. The prod table has routes to shared services but not non-prod. And non-prod routes to shared services only.
# Terraform snippet for TGW route table association
resource "aws_ec2_transit_gateway_route_table" "production" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = { Name = "production-rt" }
}
resource "aws_ec2_transit_gateway_route_table_association" "prod_vpc" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.prod.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}
resource "aws_ec2_transit_gateway_route" "prod_to_shared" {
destination_cidr_block = "10.0.0.0/16"
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.shared.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}
CIDR Planning That Won't Bite You Later
This is the part most teams underestimate. If your VPCs have overlapping CIDRs, Transit Gateway can't route between them. Period. I've seen organizations paint themselves into a corner because every account used the default 172.31.0.0/16 range.
Allocate a master CIDR block for your entire organization — something like 10.0.0.0/8 — and carve it into non-overlapping chunks. Give each account type a /16 or /12 range. Individual VPCs get /20 or /22 blocks within that allocation. Document it somewhere central and don't let teams pick their own ranges.
A simple scheme I've used successfully:
- 10.0.0.0/12 for production accounts
- 10.16.0.0/12 for staging
- 10.32.0.0/12 for development and sandbox
- 10.128.0.0/12 for shared services (DNS, CI/CD, logging)
RAM Sharing Mechanics
Resource Access Manager lets you share the TGW across your AWS Organization without individual account IDs. You share to the entire org or to specific OUs. This matters because new accounts added to those OUs automatically get access — no manual step required.
resource "aws_ram_resource_share" "tgw_share" {
name = "transit-gateway-share"
allow_external_principals = false
tags = { ManagedBy = "terraform" }
}
resource "aws_ram_resource_association" "tgw" {
resource_arn = aws_ec2_transit_gateway.main.arn
resource_share_arn = aws_ram_resource_share.tgw_share.arn
}
resource "aws_ram_principal_association" "org" {
principal = data.aws_organizations_organization.current.arn
resource_share_arn = aws_ram_resource_share.tgw_share.arn
}
Cross-Region Considerations
Transit Gateway is regional. If you've got VPCs in us-east-1 and eu-west-1, you need a TGW in each region and a peering connection between them. The inter-region TGW peering uses AWS's backbone, so latency is consistent and traffic doesn't traverse the public internet.
That said, inter-region peering adds cost. Data transfer between TGW peering attachments runs $0.02/GB on top of the standard TGW processing charge. For high-bandwidth cross-region communication, you'll want to evaluate whether the traffic actually needs to flow between regions or if you can restructure your architecture to keep things local.
Monitoring and Troubleshooting
VPC Flow Logs won't show you TGW-level routing decisions. For that, you need Transit Gateway Flow Logs — a separate feature that logs traffic at the TGW attachment level. Enable these in the networking account and ship them to a centralized logging bucket.
The most common issue I've debugged? Missing return routes. Traffic goes from VPC A to VPC B through the TGW just fine, but VPC B's route table doesn't have a return route through the TGW for VPC A's CIDR. The connection times out. Always check both directions.
Another gotcha: security groups don't reference TGW attachments. When a packet arrives at an instance in VPC B from VPC A via the TGW, the source IP is the original instance's private IP in VPC A. Your security group rules need to allow the source VPC's CIDR range, not the TGW's ENI.
Cost Structure You Should Know
TGW charges two things: an hourly rate per attachment ($0.05/hour in us-east-1) and a per-GB data processing fee ($0.02/GB). With 20 VPCs attached, that's $720/month just for attachments before any data moves. It adds up.
For small environments (under five VPCs), peering might actually be cheaper. The break-even depends on the number of connections you'd need — peering is free for the connection itself, you only pay data transfer. But once you pass the threshold where N*(N-1)/2 peering connections become unmanageable, TGW's centralized model wins on operational cost even if the AWS bill is higher.
DNS Resolution and Hybrid Connectivity
If you're running a hybrid setup with on-premises data centers, the TGW becomes your bridge. Attach a Site-to-Site VPN or Direct Connect gateway to the Transit Gateway, and on-prem traffic routes through the same hub as your VPC-to-VPC traffic. The route tables work the same way for on-prem routes.
DNS resolution across this setup needs Route53 Resolver endpoints. Inbound endpoints let on-prem DNS queries resolve AWS private hosted zones. Outbound endpoints let VPCs resolve on-prem DNS names. Without these, your hybrid network has connectivity but services can't find each other by name. I've spent days troubleshooting "connectivity issues" that turned out to be DNS resolution failures across the TGW boundary.
One gotcha with DNS and Transit Gateway: if you're using Route53 private hosted zones, they're associated with specific VPCs. VPCs in other accounts need cross-account authorization to resolve those zones. Combine this with RAM sharing of the TGW, and you've got two parallel sharing mechanisms that both need to be configured correctly.
Terraform Module Structure
For managing TGW infrastructure as code, I've found that a dedicated networking module works best. The module lives in the shared networking account's Terraform state and exposes the TGW ID and route table IDs as outputs. Account-level Terraform configurations then reference those outputs to create their VPC attachments.
# networking-account/tgw/outputs.tf
output "transit_gateway_id" {
value = aws_ec2_transit_gateway.main.id
}
output "production_route_table_id" {
value = aws_ec2_transit_gateway_route_table.production.id
}
# app-account/vpc/main.tf
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "networking-tf-state"
key = "tgw/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_ec2_transit_gateway_vpc_attachment" "main" {
subnet_ids = module.vpc.private_subnet_ids
transit_gateway_id = data.terraform_remote_state.networking.outputs.transit_gateway_id
vpc_id = module.vpc.vpc_id
}
This separation means application teams can manage their own VPC attachments without touching the networking account's configuration. The platform team controls route tables and propagation rules centrally.
Operational Runbook Items
A few things to document for your operations team. First, TGW attachments have a maximum bandwidth of 50 Gbps per VPC attachment. If you're pushing more than that between two VPCs, you'll need to split traffic across multiple attachments or rethink the architecture.
Second, when you delete a TGW attachment, existing connections drain gracefully but new connections are rejected immediately. Plan maintenance windows accordingly.
Third, keep your route table documentation current. The TGW console shows routes, but it doesn't show the intent behind them. Why can the analytics VPC reach the payments VPC? Who approved that? A simple spreadsheet tracking approved routes, their business justification, and their review date saves enormous time during security audits and incident response.
Migration from VPC Peering to Transit Gateway
If you've got an existing mesh of VPC peering connections and want to move to TGW, don't try to do it all at once. The approach that's worked best for me is a parallel-run migration. Stand up the TGW and create attachments for all VPCs while keeping the peering connections active. Update route tables to prefer TGW routes for new connections while existing connections continue over peering.
The tricky part is asymmetric routing. During the migration, traffic from VPC A might go to VPC B via TGW, but the return traffic from B to A might still use the peering connection. This can cause issues with stateful firewalls and security groups that expect symmetric paths. Plan for a cutover window where you switch all routes simultaneously for a group of related VPCs.
After the cutover, monitor for 48 hours before deleting the old peering connections. Look at VPC Flow Logs for any traffic still trying to use the peering routes. Leftover application configurations with hardcoded CIDR routes can quietly fail when peering connections are removed.
Budget-wise, expect the TGW migration to increase your monthly networking cost by 15-30% compared to peering, depending on your traffic patterns. The operational savings from centralized management usually justify the premium, but present both numbers to leadership so there are no surprises on the next bill.