Route tables and the local route
A subnet does not decide where packets go. Its route table does. Every packet leaving an instance is matched against that table, and the most specific matching row wins. Inside the VPC, one row is always present and you do not write it: the local route. This lesson is that row, the rule that picks among rows, and what happens when nothing matches.
One subnet, one table
Each subnet is associated with exactly one route table. Many subnets may share a table. An instance cannot pick a different table for itself. If the application and the database need different paths, they need different subnets.
When you create a VPC, AWS creates a main route table and puts the local route in it. Any subnet you do not explicitly associate uses the main table. That default is the usual accident: someone adds an internet route to the main table “just to test,” and every subnet they forgot to associate, including the database, starts using it.
Create a table per tier and associate every subnet with one of them. Leave the main table alone, with only the local route AWS put there. A subnet with no explicit association is then still private.
The local route
For a VPC of 10.0.0.0/16, the local route is:
- Destination
10.0.0.0/16 - Target
local
The VPC router delivers any packet whose address falls in that range. An instance in 10.0.1.0/24 reaches an instance in 10.0.12.0/24 because both addresses are inside 10.0.0.0/16, not because you added a route for each subnet. The return packet uses the destination subnet’s table, which has the same local route. Both directions have to match. One table is not enough if the other subnet’s table cannot route back.
You cannot delete this route, and you cannot replace it with a more specific route that sends VPC addresses out a gateway. Addresses inside the CIDR stay on the VPC router. Terraform will not show the local route in the resource. An empty aws_route_table still has it.
A route is not permission. The local route means the packet can be delivered. The security group on the destination still has to allow it. Two instances that “should see each other” and do not are often allowed by the route and denied by the group. That distinction comes back in a later lesson. Do not “fix” it by adding routes.
The most specific row wins
When several rows match, AWS uses the longest prefix. A /24 beats a /16, and a /16 beats 0.0.0.0/0. 0.0.0.0/0 matches every address, so it is only used when no narrower row matches.
| Packet to | Rows that exist | Row that wins |
|---|---|---|
10.0.12.20 |
10.0.0.0/16 → local, and maybe 0.0.0.0/0 |
local. The address is inside the VPC, and that route cannot be overridden. |
10.1.4.8 |
10.1.0.0/16 → peering, and 0.0.0.0/0 → igw |
The /16. It is narrower than the default route, so this packet does not go to the internet. |
1.1.1.1 |
Only 10.0.0.0/16 → local |
No row matches. The packet is dropped. |
1.1.1.1 |
local and 0.0.0.0/0 → igw |
The default route. Nothing more specific claimed this address. |
The peering row is here only as a prefix-length example. Connecting two VPCs is a later lesson. The internet gateway row is the next lesson after this one. What matters now is the third case: a table with only the local route drops everything else. That is a private subnet. It is not broken.
A table with no extra routes
This is the private tier from the address plan. The resource has no route block. Association is a separate resource. Forgetting the association leaves the subnet on the main table, which is why the main table should stay empty of extra routes.
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = { Name = "private-rt" }
}
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
After apply, the table in the console shows 10.0.0.0/16 → local even though the file does not. An instance in private-a can reach private-b and the public subnets. It cannot reach 1.1.1.1. Adding that path is a NAT gateway or a public address, and both are later lessons.
When the main table is the wrong place for a route
- You added
0.0.0.0/0to the main table. Every subnet without an explicit association inherited the internet. Move that route to a table named for the public tier, and associate only the public subnets with it. - Two tiers share one table. You cannot give the database a different default route without moving it to another subnet.
- A connection inside the VPC fails and the route table is blamed. Check the local route is there (it is), then check the security group. Adding a more specific route for
10.0.12.0/24will be rejected or will not change delivery.
A private subnet is a subnet whose table has only the local route. Traffic to other addresses in the VPC is delivered. Traffic to anywhere else is dropped until you add a row.