The misconfiguration that does not appear in the API output
A subnet with no explicit route table association is returned by nothing. DescribeRouteTables says so in one line of its own reference, and it means "check the route tables" structurally cannot find the problem.
· Venerable Networks
Here is a line from the DescribeRouteTables reference that costs people entire afternoons:
If a subnet is not explicitly associated with any route table, it is implicitly associated with the main route table. This command does not return the subnet ID for implicit associations.
Read the second sentence again. A subnet in that state appears in no association list anywhere in the output. Not flagged as implicit. Not listed against the main route table. Absent.
Which means a whole diagnostic habit fails silently. If your mental model of "check the route tables" is "read the associations and see which table each subnet uses", a subnet with no explicit association is not something you overlooked. It is something the output does not contain.
What it looks like from the inside
A team adds a subnet for a new tier. Same Availability Zone, adjacent CIDR, same security group, same instance profile, same AMI as the working tier. They forget the association resource.
The subnet falls back to the main route table, which in a well-built VPC holds only the local route. Instances there reach the rest of the VPC and nothing else.
The first symptom is usually that the instances never register with Systems Manager, because the agent is the first thing on a fresh host that needs to talk to the outside world. So the ticket says "instances not appearing in SSM", the SSM troubleshooting documentation walks you through agent versions and IAM policies and endpoint configuration, and all of it is fine.
Meanwhile the team reads all three route tables, confirms every route is correct — which is true — and cannot find anything wrong.
Two compounding details
The public IP address makes it worse. If map_public_ip_on_launch is set, the instances get real public IPv4 addresses that do absolutely nothing. A public address is only usable if the subnet's route table sends traffic to an internet gateway, and the two settings are independent. One is visible in the console next to the instance; the other is two clicks away in a different view. "It has a public IP, so networking is configured" is a durable wrong conclusion for exactly this reason.
The console hides it differently rather than better. The main route table's subnet associations tab shows implicitly associated subnets under a separate heading that is easy to skim past. The subnet's own route table tab shows the main route table with no indication that the association is implicit rather than chosen. Neither view lies; both are easy to read as confirmation.
The check worth having
The absence is enumerable even though it is not returned. Compare the set of subnets against the set of explicitly associated subnets:
VPC=vpc-xxxxxxxx
comm -23 \
<(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC" \
--query 'Subnets[].SubnetId' --output text | tr '\t' '\n' | sort) \
<(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC" \
--query 'RouteTables[].Associations[?SubnetId].SubnetId' --output text | tr '\t' '\n' | sort)Anything that prints is on the main route table by default. In a well-run account that list should be empty or explicitly justified.
There is also a direct per-subnet form, which is useful in a runbook because the empty result is the answer:
aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=subnet-xxxxxxxx" \
--query 'RouteTables[].RouteTableId' --output textAn explicitly associated subnet returns a route table ID. An implicitly associated one returns nothing at all.
Decide what the main route table is for
Every VPC has one and every unassociated subnet uses it, so its contents are a policy decision about what happens to subnets nobody configured. Two defensible choices:
Leave it with only the local route. New subnets fail closed — they reach nothing outside the VPC until someone associates them deliberately. Confusing, contained, and it never sends traffic somewhere unintended.
Make it the private route table, so a forgotten subnet gets NAT egress instead of nothing. Less confusing, but a subnet you did not know about now has an egress path you did not choose, which is worse in a regulated environment.
What you should not do is put the internet gateway route in the main route table. That makes every forgotten subnet a public subnet.
Terraform will not warn you
aws_subnet is complete and valid without an association. aws_route_table_association is a separate resource with no required back-reference. There is no plan-time error, no provider lint rule, and nothing in terraform validate.
The specific way this happens in practice: a module takes a list of subnet CIDRs, creates subnets in one loop, and creates associations in a second loop keyed off a different map. Any subnet missing from the second map is silently skipped. Which is the argument for the two loops being one loop.
If you want to watch it happen — including the SSM red herring and the useless public IP — there is a lab: the subnet that belonged to no route table. The broader model of subnets being defined entirely by their associated route table is in the VPC guide.