Esc
Type to search posts, tags, and more...
Skip to content

Managing AWS Transit Gateway with Terraform

Applying network engineering discipline to cloud networking — using Terraform to manage Transit Gateway route tables, attachments, and peering with the same rigor as an MPLS backbone.

Contents

The Cloud Networking Gap

Network engineers moving to AWS face a cultural mismatch. In the data center, you configure routers through a CLI with immediate feedback. In AWS, networking is API-driven, asynchronous, and spread across a console UI that was clearly designed by someone who has never troubleshot a routing loop at 3am.

Transit Gateway (TGW) is AWS’s answer to hub-and-spoke connectivity. It is conceptually similar to an MPLS P router — VPCs attach to it like CE routers, route tables control traffic flow, and peering extends reach across regions. The difference is that you manage it through API calls instead of configure terminal.

Terraform bridges this gap. You describe your desired network state in HCL, and Terraform handles the API calls.

The Base TGW Setup

resource "aws_ec2_transit_gateway" "main" {
  description                     = "Production Transit Gateway"
  default_route_table_association = "disable"
  default_route_table_propagation = "disable"
  dns_support                     = "enable"
  vpn_ecmp_support                = "enable"

  tags = {
    Name        = "tgw-prod-us-east-1"
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

Disabling the default route table association is critical. Without this, every new attachment automatically joins the default route table, which means full mesh connectivity. In a segmented network, that is exactly what you do not want.

Route Table Segmentation

Just like VRFs on a physical router, TGW route tables let you isolate traffic domains:

A typical enterprise pattern:

resource "aws_ec2_transit_gateway_route_table" "production" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  tags               = { Name = "rt-production" }
}

resource "aws_ec2_transit_gateway_route_table" "development" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  tags               = { Name = "rt-development" }
}

resource "aws_ec2_transit_gateway_route_table" "shared_services" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  tags               = { Name = "rt-shared-services" }
}

Production VPCs associate with the production route table and propagate routes only to shared services. Development VPCs do the same with their own table. The two environments cannot reach each other, but both can reach DNS, logging, and monitoring in shared services.

VPC Attachments with Explicit Associations

resource "aws_ec2_transit_gateway_vpc_attachment" "prod_app" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  vpc_id             = aws_vpc.prod_app.id
  subnet_ids         = aws_subnet.prod_app_tgw[*].id

  transit_gateway_default_route_table_association = false
  transit_gateway_default_route_table_propagation = false

  tags = { Name = "tgw-attach-prod-app" }
}

resource "aws_ec2_transit_gateway_route_table_association" "prod_app" {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.prod_app.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}

resource "aws_ec2_transit_gateway_route_table_propagation" "prod_app_to_shared" {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.prod_app.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.shared_services.id
}

This is the TGW equivalent of setting route targets on a VRF. The association says “this attachment uses this route table for forwarding.” The propagation says “this attachment’s routes should appear in that route table.”

Static Routes for Default Paths

For outbound internet access through a centralized NAT gateway or firewall:

resource "aws_ec2_transit_gateway_route" "prod_default" {
  destination_cidr_block         = "0.0.0.0/0"
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.egress.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}

This is your ip route 0.0.0.0 0.0.0.0 equivalent. All production traffic with no more specific match goes to the egress VPC.

Cross-Region Peering

For multi-region deployments, TGW peering extends the fabric:

resource "aws_ec2_transit_gateway_peering_attachment" "us_to_eu" {
  transit_gateway_id      = aws_ec2_transit_gateway.main.id
  peer_transit_gateway_id = aws_ec2_transit_gateway.eu_west.id
  peer_region             = "eu-west-1"

  tags = { Name = "tgw-peer-us-east-eu-west" }
}

Think of it as an eBGP peering session between two P routers. Routes do not propagate automatically across the peering — you need explicit static routes or careful propagation configuration on both sides.

Lessons from the Migration

After moving a 40-VPC environment from VPC peering mesh to Transit Gateway:

  • Plan your route tables first. Changing segmentation after attachments are live requires detaching and reattaching VPCs, which causes brief connectivity loss.
  • Use dedicated TGW subnets. Create small /28 subnets in each AZ specifically for TGW attachments. Do not reuse application subnets.
  • Watch the bandwidth. TGW has a 50 Gbps per-AZ per-attachment limit. If you are moving serious data between VPCs, you will hit it sooner than you think.
  • Terraform state is your routing table. Treat terraform plan like show ip route — review it before every change.
! Was this useful?