DEV Community

Python-T Point
Python-T Point

Posted on • Originally published at pythontpoint.in

☁️ Configure AWS VPC subnet routing with Terraform made easy

Approximately 30 % of newly created AWS VPCs lack a default route to an Internet Gateway, according to the AWS Well‑Architected Review of . To configure AWS VPC subnet routing with Terraform , define an aws_route resource that links a route table to a subnet and sets the destination CIDR and target (Internet Gateway, NAT Gateway, or VPC peering).

📑 Table of Contents

  • 💡 Overview — Understanding Routing
  • 🛠 Core Terraform Resources — Building Routes
  • 🛠 aws_vpc — Creating the VPC
  • 🛠 aws_route_table — Defining the Table
  • 🛠 aws_route — Adding a Route
  • 🔧 Connecting Subnets — Attaching Route Tables
  • 🔧 aws_route_table_association — Linking the Subnet
  • 📦 Advanced Targets — Using NAT and VPC Peering
  • 📦 aws_nat_gateway — Routing Private Subnet Egress
  • 📦 aws_vpc_peering_connection — Enabling Cross‑VPC Traffic
  • 🟩 Final Thoughts
  • ❓ Frequently Asked Questions
  • How do I reference an existing route table that was created outside of Terraform?
  • Can I use a single route table for both public and private subnets?
  • What is the recommended way to handle IPv6 traffic?
  • 📚 References & Further Reading

💡 Overview — Understanding Routing

Routing determines how traffic leaves a subnet and reaches other networks. A route table is the data structure AWS consults for each packet.

Route table is a collection of routes that specifies where network traffic from a subnet is directed—the fundamental AWS construct for VPC routing.

When a packet originates in a subnet, the hypervisor retrieves the subnet’s associated route‑table ID. Each route entry contains a destination CIDR block and a target identifier (for example, an Internet Gateway ID). The control plane evaluates the most specific match (longest‑prefix) and forwards the packet accordingly. Because the lookup is performed per packet, the added latency is negligible; the primary cost is the additional state stored in the VPC control plane.

In Terraform, you declare this state. The provider translates the configuration into API calls that create or modify the underlying route‑table objects.

Key point: A route table is the single source of truth for subnet egress, so managing it with Terraform guarantees reproducible network behavior across environments.


🛠 Core Terraform Resources — Building Routes

Terraform provides three primary resources for VPC routing: aws_vpc, aws_route_table, and aws_route.

🛠 aws_vpc — Creating the VPC

# main.tf
resource "aws_vpc" "example" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true tags = { Name = "example-vpc" }
}
Enter fullscreen mode Exit fullscreen mode

What this does: (Also read: ☁️ aws cloudformation vs terraform for python deployments — which one should you use?)

  • cidr_block: Defines the primary address space for the VPC.
  • enable_dns_hostnames: Allows instances to resolve public DNS names, required for Internet‑gateway traffic.
  • tags.Name: Human‑readable identifier used by the console and many AWS services.

🛠 aws_route_table — Defining the Table

# route_table.tf
resource "aws_route_table" "public" { vpc_id = aws_vpc.example.id tags = { Name = "public-rt" }
}
Enter fullscreen mode Exit fullscreen mode

What this does: (More onPythonTPoint tutorials)

  • vpc_id: Associates the table with the VPC created above.
  • tags.Name: Makes the table discoverable in the console.

🛠 aws_route — Adding a Route

# route.tf
resource "aws_route" "igw_route" { route_table_id = aws_route_table.public.id destination_cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • route_table_id: Specifies which table receives the new entry.
  • destination_cidr_block: The catch‑all IPv4 range, meaning “any address not matched by a more specific route”.
  • gateway_id: Points the traffic to the Internet Gateway, enabling outbound Internet access.

According to the official Terraform documentation, the aws_route resource abstracts the CreateRoute API call, handling idempotency and state tracking automatically.

Managing routes as code eliminates drift between environments and makes network changes auditable.

Key point: The trio of aws_vpc, aws_route_table, and aws_route forms the minimal set required to configure AWS VPC subnet routing with Terraform.


🔧 Connecting Subnets — Attaching Route Tables

A subnet uses the aws_route_table_association resource to bind a route table, and the association determines which routes apply to the subnet’s traffic.

🔧 aws_route_table_association — Linking the Subnet

# subnet_association.tf
resource "aws_subnet" "public_a" { vpc_id = aws_vpc.example.id cidr_block = "10.0.1.0/24" map_public_ip_on_launch = true availability_zone = "us-east-1a" tags = { Name = "public-a" }
} resource "aws_route_table_association" "public_a" { subnet_id = aws_subnet.public_a.id route_table_id = aws_route_table.public.id
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • aws_subnet.cidr_block: Defines the subnet's address range within the VPC.
  • map_public_ip_on_launch: Ensures instances receive a public IP, required for direct Internet access.
  • aws_route_table_association: Explicitly attaches the previously created public route table to the subnet.

Running Terraform validates the association: (Also read: ⚙️ Terraform create AWS EC2 instance with Python environment)

$ terraform init
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v5.13.0...
...



Terraform has been successfully initialized!



$ terraform plan
...
aws_route_table_association.public_a will be created + id = (known after apply) + route_table_id = "rtb-0a1b2c3d4e5f6g7h8" + subnet_id = "subnet-1234abcd"
Enter fullscreen mode Exit fullscreen mode

Applying the plan creates the association, and the subnet now inherits the default route to the Internet Gateway.

Why not rely on the VPC's default route table? The default table is shared by all subnets that lack an explicit association, which makes it difficult to enforce least‑privilege networking in multi‑tier architectures. Explicit associations give you granular control.

Key point: By associating a dedicated route table, you can tailor egress paths per subnet while keeping the Terraform state source‑of‑truth.


📦 Advanced Targets — Using NAT and VPC Peering

Beyond an Internet Gateway, you can point routes to a NAT Gateway or a VPC peering connection to enable private subnet egress or cross‑VPC communication.

📦 aws_nat_gateway — Routing Private Subnet Egress

# nat_gateway.tf
resource "aws_eip" "nat_eip" { vpc = true tags = { Name = "nat-eip" }
} resource "aws_nat_gateway" "nat" { allocation_id = aws_eip.nat_eip.id subnet_id = aws_subnet.public_a.id tags = { Name = "nat-gateway" }
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • aws_eip.allocation_id: Allocates a static public IP for the NAT gateway.
  • aws_nat_gateway.subnet_id: Places the gateway in a public subnet so it can reach the Internet.

    private_route.tf

    resource "aws_route" "private_nat" { route_table_id = aws_route_table.private.id destination_cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.nat.id
    }

This route sends all outbound traffic from the private route table to the NAT gateway, preserving inbound security. The NAT gateway incurs an hourly charge (e.g., $0.045 per hour) plus data‑processing fees ($0.045 per GB).

📦 aws_vpc_peering_connection — Enabling Cross‑VPC Traffic

# vpc_peering.tf
resource "aws_vpc_peering_connection" "peer" { vpc_id = aws_vpc.example.id peer_vpc_id = var.peer_vpc_id tags = { Name = "example-to-peer" }
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • vpc_id / peer_vpc_id: Identifies the two VPCs that will be linked.
  • aws_vpc_peering_connection: Creates a low‑latency, private network tunnel between them.

    peering_route.tf

    resource "aws_route" "peer_route" { route_table_id = aws_route_table.private.id destination_cidr_block = var.peer_vpc_cidr vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
    }

This route directs traffic destined for the peer VPC’s CIDR block through the peering connection.

Target Typical Use‑Case Cost Consideration
Internet Gateway Public subnet egress Free (per‑hour charge is $0)
NAT Gateway Private subnet egress Hourly + data‑processing fees
VPC Peering Cross‑VPC communication Data transfer per GB, no hourly charge

Choosing the right target depends on security posture and cost. NAT gateways add a layer of address translation, protecting private subnets, while VPC peering avoids the public internet altogether.

Key point: By adding aws_route entries that reference NAT gateways or VPC peering connections, you can configure AWS VPC subnet routing with Terraform for a wide range of architectural patterns.


🟩 Final Thoughts

Declarative routing via Terraform eliminates the manual steps that traditionally caused drift between development, staging, and production VPCs. The provider handles ordering, idempotency, and state reconciliation, so the same configuration can be applied across accounts without hidden side effects.

For a developer, the practical benefit is a single source of truth: any change to a route—whether adding an Internet Gateway, swapping a NAT gateway, or establishing a peering link—requires only a code edit and a terraform apply. This approach scales with the size of the network and integrates cleanly into CI/CD pipelines, delivering repeatable, auditable infrastructure.

❓ Frequently Asked Questions

How do I reference an existing route table that was created outside of Terraform?

Import the resource using terraform import aws_route_table.my_table rtb-0123456789abcdef, then manage additional routes with aws_route resources.

Can I use a single route table for both public and private subnets?

Technically yes, but it mixes egress policies. Separate tables let you enforce principle‑of‑least‑privilege by routing private subnets through a NAT gateway while keeping public subnets directly attached to an Internet Gateway.

What is the recommended way to handle IPv6 traffic?

Define an aws_route with destination_ipv6_cidr_block and point it to an aws_internet_gateway or an IPv6‑enabled NAT solution, then associate the route table with the subnet.

💡 Want to practise this hands-on? DigitalOcean gives new accounts $200 free credit for 60 days — enough to spin up a full Linux/Docker/Kubernetes environment at no cost.

📚 Recommended reading: Best DevOps & cloud books on Amazon — from Linux fundamentals to Kubernetes in production, curated for working engineers.

📚 References & Further Reading

Top comments (0)