VPC Networking: Subnets, Route Tables, and Why Your Instance Can't Reach the Internet
A VPC is just a CIDR block, a subnet is an AZ plus a slice of it, and "public" is a fact about a route table — not a checkbox. Here is the mental model that makes connectivity bugs obvious.
Almost every "my EC2 instance can't reach the internet" ticket comes down to one of four things: no route, no public IP, the wrong gateway, or a firewall. The fix is fast once you stop treating "public subnet" as a setting and start reading it as a consequence of a route table. Here is the model, bottom-up.
The VPC is a CIDR block
A VPC is a private IPv4 range you carve out, e.g. 10.0.0.0/16 — 65,536 addresses that exist only inside AWS. Nothing in it is reachable from the internet by default; the VPC is an island. Everything below is about building bridges off that island, deliberately.
A subnet is one AZ plus a slice of the CIDR
You divide the VPC range into subnets, and each subnet lives in exactly one Availability Zone. That AZ binding is why production layouts duplicate every tier across two or three subnets — it is the unit of failure isolation.
10.0.0.0/16 VPC
10.0.1.0/24 subnet A (AZ us-east-1a)
10.0.2.0/24 subnet B (AZ us-east-1b)
AWS silently reserves 5 addresses per subnet (network, VPC router, DNS, future use, broadcast), so a /24 gives you 251 usable hosts, not 256.
"Public" is a route-table fact, not a checkbox
Every subnet is associated with exactly one route table. A subnet is public if and only if its route table has a default route pointing at an Internet Gateway (IGW):
Destination Target
10.0.0.0/16 local # intra-VPC, always present
0.0.0.0/0 igw-0abc123 # THIS line makes the subnet public
Remove that second line and the identical instance becomes unreachable — same subnet, same security group, same everything. There is no "public" flag on the subnet itself; the route is the whole story.
IGW vs NAT: outbound is not symmetric
An Internet Gateway allows two-way traffic, but only for instances that have a public IP. That is the second gotcha: a public route table is necessary but not sufficient — the instance also needs a public IPv4 (auto-assigned by the subnet, or an Elastic IP).
Private subnets that need outbound-only access (to pull packages, call APIs) use a NAT Gateway instead. The NAT lives in a public subnet; the private subnet's route table sends 0.0.0.0/0 to the NAT, which has the public IP and proxies the connection. Inbound from the internet stays impossible. This is the standard "private app tier, public load balancer" shape.
Security Groups vs NACLs: stateful vs stateless
Two firewalls sit in front of traffic and people conflate them:
- Security Group — attached to the ENI (instance), stateful: allow inbound 443 and the response is automatically allowed out. Allow rules only; default-deny.
- Network ACL — attached to the subnet, stateless: you must allow the return traffic explicitly (typically the ephemeral port range 1024–65535 outbound), and it supports deny rules. Evaluated in rule-number order.
The classic NACL bug: you open inbound 443 but forget the outbound ephemeral range, so the TCP handshake completes inbound and the response is silently dropped. Security groups never have this failure mode because they are stateful.
Rules of thumb
- Can't reach the internet? Check in order: route table has
0.0.0.0/0 → igw, instance has a public IP, security group allows it, NACL allows the return ports. - "Public subnet" is shorthand for "its route table points at an IGW" — read the route table, not the name someone gave the subnet.
- Private tier that needs to pull updates → NAT Gateway, not an IGW route. Inbound stays closed.
- One subnet = one AZ. Spread each tier across ≥2 subnets in different AZs or you have no HA.
- Prefer security groups (stateful, simpler) for almost everything; reach for NACLs only for coarse subnet-wide denies.