Contents


What is Transit Gateway

AWS Transit Gateway (TGW) is a network hub that connects multiple VPCs, AWS accounts, and on-premises networks through a single routing point. Instead of creating mesh connections between each pair of VPCs (VPC Peering), TGW implements a hub-and-spoke topology, simplifying network management.


Architecture

                    ┌──────────────┐
                    │  Transit GW  │
                    │   (Hub)      │
                    └──────┬───────┘
              ┌────────────┼────────────┐
              │            │            │
        ┌─────┴─────┐ ┌───┴─────┐ ┌───┴─────┐
        │  VPC A    │ │  VPC B  │ │  VPC C  │
        │ (Spoke)   │ │ (Spoke) │ │ (Spoke) │
        └───────────┘ └─────────┘ └─────────┘

Core Components

ComponentDescription
Transit GatewayCentral router between VPCs
TGW AttachmentConnection of a VPC to the Transit Gateway
TGW Route TableRouting table inside the TGW
RAM Resource ShareMechanism for sharing TGW across AWS accounts

How It Works

  1. The hub account creates the Transit Gateway and shares it via AWS RAM (Resource Access Manager)
  2. Spoke accounts accept the share and create VPC Attachments
  3. Routing between VPCs is handled through route table propagation

Terraform Setup

Module

Uses the official Terraform module:

terraform-aws-modules/transit-gateway/aws

Step 1: Create the Transit Gateway (Hub Account)

The hub account creates the Transit Gateway itself and configures RAM sharing.

module "tgw" {
  source  = "terraform-aws-modules/transit-gateway/aws"
  version = "~> 3.0"
 
  name = "${var.name}-tgw"
 
  amazon_side_asn                        = var.amazon_side_asn
  enable_auto_accept_shared_attachments  = true
  enable_default_route_table_association = true
  enable_default_route_table_propagation = true
 
  # RAM sharing
  share_tgw                       = true
  ram_allow_external_principals   = true
  ram_principals                  = var.ram_principals
 
  # VPC attachment for the hub account
  vpc_attachments = {
    vpc = {
      vpc_id                                           = var.vpc_id
      subnet_ids                                       = var.private_subnets
      dns_support                                      = var.dns_support
      ipv6_support                                     = var.ipv6_support
      transit_gateway_default_route_table_association  = true
      transit_gateway_default_route_table_propagation  = true
      tgw_routes = [
        {
          destination_cidr_block = var.tgw_destination_cidr
        }
      ]
    }
  }
}

Hub account outputs:

output "transit_gateway_id" {
  value = module.tgw.ec2_transit_gateway_id
}
 
output "ram_resource_share_id" {
  value = module.tgw.ram_resource_share_id
}

Step 2: Attach Spoke Accounts

Each spoke account needs to:

  1. Accept the RAM share
  2. Create the VPC attachment
# Accept RAM Resource Share
resource "aws_ram_resource_share_accepter" "tgw" {
  share_arn = var.ram_share_arn
}
 
# Create VPC Attachment
module "tgw_attachment" {
  source  = "terraform-aws-modules/transit-gateway/aws"
  version = "~> 3.0"
 
  name       = "${var.name}-tgw"
  create_tgw = false  # TGW already exists
 
  share_tgw          = false
  transit_gateway_id = var.tgw_id
 
  vpc_attachments = {
    vpc = {
      vpc_id                                           = var.vpc_id
      subnet_ids                                       = var.private_subnets
      dns_support                                      = var.dns_support
      ipv6_support                                     = var.ipv6_support
      transit_gateway_default_route_table_association  = true
      transit_gateway_default_route_table_propagation  = true
      tgw_routes = [
        {
          destination_cidr_block = var.tgw_destination_cidr
        }
      ]
    }
  }
 
  depends_on = [aws_ram_resource_share_accepter.tgw]
}

Important

The depends_on on aws_ram_resource_share_accepter is mandatory — without it, Terraform may attempt to create the attachment before the share has been accepted.


Step 3: VPC Routing

After creating the attachment, routes need to be added to VPC route tables so that traffic to other VPCs goes through the TGW.

The module automatically adds routes to the specified route tables based on tgw_destination_cidr. If done manually:

resource "aws_route" "tgw_route" {
  for_each = toset(var.private_rtb_ids)
 
  route_table_id         = each.value
  destination_cidr_block = var.tgw_destination_cidr
  transit_gateway_id     = var.tgw_id
}

Variables

Hub Account

VariableTypeDefaultDescription
namestringResource naming prefix
vpc_idstringVPC ID for the attachment
private_subnetslist(string)Private subnets for the attachment
private_rtb_idslist(string)Route table IDs for routing
amazon_side_asnnumber64512ASN for the Transit Gateway
ram_principalslist(string)List of AWS Account IDs to share with
tgw_destination_cidrstring10.160.0.0/12Destination CIDR for routing
dns_supportbooltrueEnable DNS support
ipv6_supportboolfalseEnable IPv6

Spoke Accounts

VariableTypeDefaultDescription
namestringResource naming prefix
vpc_idstringVPC ID for the attachment
private_subnetslist(string)Private subnets for the attachment
private_rtb_idslist(string)Route table IDs for routing
ram_share_arnstringARN of the RAM Resource Share
tgw_idstringTransit Gateway ID
tgw_destination_cidrstring10.160.0.0/12Destination CIDR for routing
dns_supportbooltrueEnable DNS support
ipv6_supportboolfalseEnable IPv6

Terragrunt Integration

The TGW module depends on the VPC module to obtain vpc_id, private_subnets, and private_rtb_ids:

dependency "vpc" {
  config_path = "../vpc"
}
 
inputs = {
  vpc_id          = dependency.vpc.outputs.vpc_id
  private_subnets = dependency.vpc.outputs.private_subnets
  private_rtb_ids = dependency.vpc.outputs.private_route_table_ids
}

CIDR Planning

Best Practice

Use a single supernet CIDR (e.g., 10.160.0.0/12) for the destination route that covers all VPCs in your organization. This avoids having to add individual routes when new VPCs are created.

Example allocation:

AccountVPC CIDRDescription
Infra10.160.0.0/16Shared services
Dev10.161.0.0/16Development
Prod10.162.0.0/16Production

Supernet: 10.160.0.0/12 covers the range 10.160.0.010.175.255.255.


Route Table Propagation vs Association

  • Association — binds an attachment to a specific TGW route table
  • Propagation — automatically adds routes from an attachment into a route table

With enable_default_route_table_association = true and enable_default_route_table_propagation = true, all attachments automatically:

  1. Associate with the default route table
  2. Propagate their routes into the default route table

This means all VPCs can automatically see each other.

Isolation

If isolation between VPCs is needed (e.g., dev should not see prod), create separate route tables and manage association/propagation manually.