# VPC, Subnets, and Route Tables

## 1. What a VPC Is

A **VPC (Virtual Private Cloud)** is an isolated, software-defined network inside a Region — your own private IP address space that nothing else in AWS can reach unless you explicitly wire it up. You define it with a CIDR block (a range of IP addresses):

```bash
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=app-vpc}]'
```

A `/16` gives ~65,000 addresses. Nothing runs directly "in" the VPC — the VPC is just the address space; you then carve it into subnets.

## 2. Subnets Are Tied to One Availability Zone

A **subnet** is a slice of the VPC's CIDR range, and critically, it lives in exactly **one** Availability Zone — never spread across AZs. This is why high-availability designs always create at least one subnet per AZ per tier (public, private) rather than one giant subnet.

```bash
aws ec2 create-subnet --vpc-id vpc-0123456789abcdef0 --cidr-block 10.0.1.0/24 --availability-zone us-east-1a
aws ec2 create-subnet --vpc-id vpc-0123456789abcdef0 --cidr-block 10.0.2.0/24 --availability-zone us-east-1b
```

## 3. Route Tables Define "Public" vs. "Private" — Not a Setting

There is no checkbox that makes a subnet "public." A subnet is public purely because its associated **route table** sends `0.0.0.0/0` (all internet-bound traffic) to an **Internet Gateway (IGW)**. A subnet is private if that same route instead points nowhere useful for the open internet (or to a NAT Gateway for outbound-only access — see the Internet/NAT Gateway topic).

```bash
aws ec2 create-route-table --vpc-id vpc-0123456789abcdef0
aws ec2 create-route --route-table-id rtb-0abc123 --destination-cidr-block 0.0.0.0/0 --gateway-id igw-0def456
aws ec2 associate-route-table --route-table-id rtb-0abc123 --subnet-id subnet-0111
```

```
Terraform equivalent:

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
}

resource "aws_route_table_association" "public_a" {
  subnet_id      = aws_subnet.public_a.id
  route_table_id = aws_route_table.public.id
}
```

## 4. The Main Route Table and Local Routes

Every VPC gets an implicit **local route** (traffic within the VPC's own CIDR range routes directly, always, and cannot be removed) plus a **main route table** that any subnet uses by default until you explicitly associate it with a different one. Forgetting to associate a subnet with your custom route table means it silently falls back to the main table's rules — a common source of "why can't this subnet reach the internet" confusion.

## 5. CIDR Sizing and Non-Overlap

Subnets within a VPC cannot overlap in address range, and if you ever plan to VPC-peer or connect via Transit Gateway, the two VPCs' CIDR ranges cannot overlap either — this has to be planned up front because it can't be fixed by reconfiguring later without re-addressing everything.

```
VPC:              10.0.0.0/16   (65,536 addresses)
  public-a:       10.0.1.0/24   (256 addresses, ~251 usable — AWS reserves 5 per subnet)
  public-b:       10.0.2.0/24
  private-a:      10.0.11.0/24
  private-b:      10.0.12.0/24
```

AWS reserves the first four and the last address in every subnet (network address, VPC router, DNS, future use, broadcast) — a `/24` "256 addresses" subnet really gives you 251 usable ones.

## 6. Security Groups vs. NACLs (Preview)

Route tables control *where* traffic goes; they say nothing about whether it's *allowed*. That's the job of **Security Groups** (stateful, attached to instances/ENIs — covered in its own topic) and **Network ACLs** (stateless, attached to the subnet itself, evaluated in numbered rule order, and the only construct that can explicitly Deny traffic at the network layer). A packet must pass the subnet's NACL AND the resource's security group to be delivered.

## 7. Under the Hood

**A route table lookup is a longest-prefix-match, not a first-match or "any matching rule fires."** When traffic leaves an instance, AWS looks at every route in the table for the destination subnet and picks the single most specific (longest) matching CIDR — the local `10.0.0.0/16` route always loses to a more specific `10.0.1.0/24` route if the destination falls inside it, and both lose to a `/32` route to one specific host if one exists. This is exactly how VPC peering and VPN routes get inserted safely alongside the default `0.0.0.0/0` internet route without conflicting: a peering route to `172.31.0.0/16` is more specific than `0.0.0.0/0`, so peered traffic takes the peering connection while everything else still falls through to the internet gateway.

The local route (traffic to the VPC's own CIDR) is implicit, unremovable, and *always* the most specific applicable route for in-VPC traffic — which is why two subnets in the same VPC can always reach each other by default regardless of what else is in their route tables, unless a NACL or security group explicitly blocks it. Route tables have no concept of "deny" at all; they are purely a next-hop lookup. Blocking traffic is NACL/security-group territory, not routing territory — a completely separate layer of the stack from "where does this packet's next hop go."

## 8. Common Pitfalls

1. **Forgetting to associate a subnet with a custom route table**, leaving it silently on the main table — the subnet behaves unexpectedly (often stuck "private" when it was meant to be public) with no error anywhere.
2. **Overlapping CIDR ranges** across VPCs that are later peered — this cannot be resolved without re-IP'ing one of the VPCs, which is disruptive at any scale.
3. **Treating "has a route to an IGW" and "is actually reachable from the internet" as the same thing** — an instance in a route-table-public subnet still needs a public IP/Elastic IP AND a security group allowing the inbound traffic; the route table is necessary but not sufficient.
4. **Assuming NAT Gateway routes make a subnet "public."** A private subnet routing outbound traffic through a NAT Gateway still cannot receive unsolicited inbound connections from the internet — NAT is outbound-initiated only.
5. **Sizing subnets too small.** A `/28` (11 usable addresses after AWS's reservations) fills up fast once you add load balancer ENIs, NAT gateway ENIs, and autoscaled instances — undersized subnets are a frequent "why can't I launch another instance" surprise.

## 9. Quick Reference

| Concept | What it does |
|---|---|
| VPC | isolated IP address space in a Region (CIDR block) |
| Subnet | slice of the VPC CIDR, tied to exactly one AZ |
| Route table | next-hop lookup by longest-prefix-match; defines public vs private |
| Local route | implicit, unremovable, routes in-VPC traffic directly |
| Internet Gateway | the target of `0.0.0.0/0` that makes a route table's subnet "public" |
| NACL | stateless, subnet-level, ordered rules, can explicitly Deny |
| Security Group | stateful, instance-level, allow-only |

```bash
aws ec2 describe-route-tables --filters "Name=vpc-id,Values=vpc-0123456789abcdef0"
aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-0123456789abcdef0"
```

![A VPC split into public and private subnets across two AZs, with route tables pointing public subnets to an Internet Gateway and private subnets to a NAT Gateway](diagrams/01-vpc-layout.svg)
