22.4 Route Tables: Associating Subnets and Adding Routes
Right, let’s talk about the GPS of your VPC: route tables. If subnets are the neighborhoods of your cloud city, route tables are the street signs telling traffic where to go. And just like in a real city, if the signs are wrong, your packets end up in a ditch. Or worse, in a competitor’s data center. We don’t want that.
Every subnet you create must be associated with a route table. AWS plays a fun little trick here by giving you a “main” route table for your VPC. It’s not special, it’s just the one they automatically associate with any new subnet you create that doesn’t get explicitly assigned to another. This is a classic “convenience” feature that will absolutely bite you if you forget about it. I’ve seen more than one junior dev accidentally expose a private subnet because they tweaked the main route table thinking it only affected one thing. Nope. It’s a default, and defaults are landmines. We’ll defuse them in a bit.
The Anatomy of a Route Entry
Open up a route table and you’ll see a list of, well, routes. Each one is a simple instruction: “If you’re trying to go to this network, send the packet to that target.” It’s a matching game. The most specific match wins (hello, CIDR blocks). Let’s look at the two you’ll see in every single route table out of the box.
[
{
"Destination": "10.0.0.0/16",
"Target": "local"
},
{
"Destination": "0.0.0.0/0",
"Target": "igw-0a1b2c3d4e5f67890"
}
]
The first route, the local route, is non-negotiable and magic. It’s how every resource within your VPC finds each other. AWS adds it and you can’t remove it, which is good because if you could, you’d break everything instantly. It says, “Any traffic destined for the VPC’s CIDR block (in this case 10.0.0.0/16) should stay right here and be routed internally.” This is why your EC2 instance in subnet A can talk to your RDS instance in subnet B without needing a gateway. It’s the foundational rule.
The second route is your classic internet escape hatch. 0.0.0.0/0 means “all IPv4 traffic.” It’s the default. The target is an Internet Gateway (IGW), which means this route table is making its associated subnets public. Any instance with a public IP in a subnet using this table can directly access the internet and, crucially, be accessed from the internet. This is the “you sure about that?” moment for most architectures.
The Public/Private Subnet Dance
This is where most people’s brains short-circuit. A “public subnet” isn’t a special type of subnet AWS creates. It’s just a regular subnet that happens to be associated with a route table that has a route to an Internet Gateway. That’s it. A “private subnet” is just a subnet whose route table does not have that route. The subnet itself is agnostic; its personality is defined entirely by its route table.
So, for a proper, resilient setup, you don’t want all your subnets associated with the default main route table that has the IGW route. You want to be explicit. Here’s the best practice drill:
- Create a custom “Public-RT” route table and explicitly add the
0.0.0.0/0 -> igw-...route. - Create a custom “Private-RT” route table that lacks the IGW route. Its default route will point to a NAT Gateway (which we’ll get to) or simply have no way out.
- Go back to the main VPC route table and DELETE the IGW route. Yes, do it. This is the defusing step. Now it’s just a boring, isolated route table with only the local route. Any subnet that accidentally inherits it is private by default, which is the safer failure mode.
- Associate your public subnets explicitly with “Public-RT” and your private ones with “Private-RT”.
You can do this in Terraform or the AWS CLI to make it repeatable and sane.
# Create the public route table and add the internet gateway route
aws ec2 create-route-table --vpc-id vpc-123abc
aws ec2 create-route --route-table-id rtbl-789xyz --destination-cidr-block 0.0.0.0/0 --gateway-id igw-0a1b2c3d4e5f67890
# Associate a specific subnet with it (making it public)
aws ec2 associate-route-table --route-table-id rtbl-789xyz --subnet-id subnet-456def
When Routes Collide: The Order of Precedence
Remember I said the most specific match wins? This is crucial. A route to 10.0.1.0/24 is more specific than a route to 10.0.0.0/16, which is more specific than 0.0.0.0/0. AWS isn’t just checking for any match; it’s doing a longest prefix match. This is how you can create exceptions, like sending traffic for a specific corporate network (172.16.1.0/24) to a Virtual Private Gateway (VGW) for your VPN, while letting everything else (0.0.0.0/0) go out the IGW for general internet access. The order of the routes in the table itself doesn’t matter—the CIDR mask length is what dictates the winner.
The pitfall here is obvious: if you add a route that’s too broad, it can accidentally override a more specific route you wanted to use. Always double-check your CIDR blocks. A typo like 10.0.0.0/8 instead of 10.0.0.0/16 will swallow a huge amount of traffic and send it to the wrong place, with predictably hilarious and career-limiting results.