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
| Component | Description |
|---|---|
| Transit Gateway | Central router between VPCs |
| TGW Attachment | Connection of a VPC to the Transit Gateway |
| TGW Route Table | Routing table inside the TGW |
| RAM Resource Share | Mechanism for sharing TGW across AWS accounts |
How It Works
- The hub account creates the Transit Gateway and shares it via AWS RAM (Resource Access Manager)
- Spoke accounts accept the share and create VPC Attachments
- 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:
- Accept the RAM share
- 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_ononaws_ram_resource_share_accepteris 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
| Variable | Type | Default | Description |
|---|---|---|---|
name | string | — | Resource naming prefix |
vpc_id | string | — | VPC ID for the attachment |
private_subnets | list(string) | — | Private subnets for the attachment |
private_rtb_ids | list(string) | — | Route table IDs for routing |
amazon_side_asn | number | 64512 | ASN for the Transit Gateway |
ram_principals | list(string) | — | List of AWS Account IDs to share with |
tgw_destination_cidr | string | 10.160.0.0/12 | Destination CIDR for routing |
dns_support | bool | true | Enable DNS support |
ipv6_support | bool | false | Enable IPv6 |
Spoke Accounts
| Variable | Type | Default | Description |
|---|---|---|---|
name | string | — | Resource naming prefix |
vpc_id | string | — | VPC ID for the attachment |
private_subnets | list(string) | — | Private subnets for the attachment |
private_rtb_ids | list(string) | — | Route table IDs for routing |
ram_share_arn | string | — | ARN of the RAM Resource Share |
tgw_id | string | — | Transit Gateway ID |
tgw_destination_cidr | string | 10.160.0.0/12 | Destination CIDR for routing |
dns_support | bool | true | Enable DNS support |
ipv6_support | bool | false | Enable 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:
| Account | VPC CIDR | Description |
|---|---|---|
| Infra | 10.160.0.0/16 | Shared services |
| Dev | 10.161.0.0/16 | Development |
| Prod | 10.162.0.0/16 | Production |
Supernet: 10.160.0.0/12 covers the range 10.160.0.0 — 10.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:
- Associate with the default route table
- 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.