22.2 Subnets: Public vs Private, CIDR Sizing, and AZ Assignment
Right, let’s talk about subnets. This is where the rubber meets the road in your VPC, and frankly, it’s where a lot of people screw it up because they don’t stop to think about why things are the way they are. You don’t just toss subnets around like confetti; you’re carving up your private network with surgical precision. Or at least, you will be after this.
Think of your VPC’s CIDR block (like 10.0.0.0/16) as your entire digital kingdom. A subnet is a smaller, walled-off province within that kingdom. The key thing to remember is that subnets are Availability Zone (AZ) specific. This is non-negotiable. You create a subnet in us-east-1a, or eu-west-2b. You can’t stretch a subnet across two AZs—AWS won’t let you, and it’s a terrible idea anyway. The entire point is to isolate failure domains. If us-east-1a decides to take a nap, the subnets in us-east-1b should blissfully carry on without it.
Public vs. Private: It’s All About the Route Table
This is the big one. The distinction between a “public” and “private” subnet has nothing to do with a setting you check in a box. AWS doesn’t label them. The distinction is 100% determined by the route table attached to the subnet. It’s that simple.
A public subnet has a route table that sends non-local traffic (0.0.0.0/0) to an Internet Gateway (IGW). This is the magic doorway that allows bidirectional communication with the internet. An instance in a public subnet can have a public IP and, if you want it to be directly reachable from the internet, you’d also assign it an Elastic IP.
A private subnet has a route table that does not have a route to the Internet Gateway. Its 0.0.0.0/0 traffic, if it exists, goes somewhere else (like a NAT Gateway) or nowhere at all. Instances here cannot be directly contacted from the public internet, which is exactly what you want for your application servers, databases, and all the other juicy bits.
Here’s the visual difference. This is a route table for a public subnet:
# The key is the destination '0.0.0.0/0' targeting 'igw-...'
aws ec2 create-route --route-table-id rtb-abc123123 \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id igw-a1b2c3d4e5f6g7h8
And this is what the route table itself would show:
Destination Target
--------------- --------------------------------------------------
10.0.0.0/16 local
0.0.0.0/0 igw-a1b2c3d4e5f6g7h8
For a private subnet, that second route simply wouldn’t be there. Or, it would point to a NAT device.
CIDR Sizing: Don’t Paint Yourself Into a Corner
This is the part everyone gets a headache over, but it’s crucial. When you carve up your VPC’s CIDR block into subnets, you have to think about two things: what you need now and what you might need later.
First, AWS reserves 5 IP addresses in every subnet for its own nefarious purposes. For a /24 (256 addresses), they take the first four and the last one: .0 (network address), .1 (reserved for the VPC router), .2 (DNS server), .3 (reserved for future use), and .255 (network broadcast address). So your useful addresses are actually 251. Plan accordingly.
The golden rule: Plan for growth, but don’t be wasteful. A /28 (16 addresses, 11 usable) is probably too small for anything but the tiniest resource. A /26 (64 addresses, 59 usable) is a good starting point for a moderate-sized application subnet. A /24 (256 addresses) is common and gives you plenty of room.
Most importantly, ensure your subnet CIDRs don’t overlap! If your VPC is 10.0.0.0/16, you can have subnets like 10.0.1.0/24, 10.0.2.0/24, and 10.0.3.0/24. Trying to create 10.0.1.0/24 and 10.0.1.128/25 in the same VPC will work—they don’t overlap—but it’s messy. Keep it simple and sequential.
AZ Assignment: Embrace the Spread
Remember: one subnet per AZ. The best practice is to create mirrored subnets across multiple AZs for high availability. You don’t have a “public subnet”; you have a “public subnet in us-east-1a” and a “public subnet in us-east-1b”.
This is how you build resilient architectures. Your load balancer will span AZs and drop traffic into instances in both your private subnets. If one AZ fails, the load balancer just stops sending traffic there and your application in the other AZ keeps running. You can see this structure in the CloudFormation snippet below.
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: us-east-1a
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: us-east-1b
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.101.0/24
AvailabilityZone: us-east-1a
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.102.0/24
AvailabilityZone: us-east-1b
Note the logical CIDR grouping: 10.0.1.x and 10.0.2.x for public, 10.0.101.x and 10.0.102.x for private. This makes reading route tables and security groups infinitely easier later.
The most common pitfall? Not creating enough subnets across enough AZs at the beginning. Adding a new subnet later is easy, but if you didn’t reserve the IP space for it in your VPC’s overall CIDR, you’re going to have a bad time. Think ahead. Draw it on a napkin. Just don’t wing it.