VPCs, CIDR Planning, Subnets, and Route Tables
A Virtual Private Cloud is the private network boundary where AWS resources attach elastic network interfaces, receive IP addresses, and use route tables to choose the next hop for packets. The outcome of this lesson is practical: given an application, you should be able to choose a non-overlapping VPC CIDR, divide it into subnets across Availability Zones, attach route tables, and predict whether traffic can reach another subnet, the internet, a NAT gateway, or a connected network.
This matters because many AWS services inherit these choices. EC2, RDS, EKS, internal load balancers, Lambda functions with VPC access, VPC endpoints, NAT gateways, peering, VPN, Direct Connect, and Transit Gateway all consume subnet addresses and follow VPC routing. A CIDR plan that is too small or overlaps with another network becomes expensive to repair after databases, security rules, and integrations depend on it.
Purpose and Outcome
A VPC is a regional routing domain. You assign one or more IPv4 CIDR blocks, and optionally IPv6 blocks. AWS automatically creates a local route for each VPC CIDR so addresses inside the VPC have a path to each other. A subnet is a CIDR slice in exactly one Availability Zone. A route table is a set of destination prefixes and targets associated with subnets.
Separate addressing from reachability. Addressing says which IPs may exist. Subnets place those IPs in an Availability Zone fault boundary. Route tables choose where packets leaving a network interface go. Security groups and network ACLs then decide whether traffic on that path is allowed. A complete answer usually names all four layers: source address, source subnet, selected route, and security policy.
How VPC Routing Works
Every subnet has one effective route table. If you do not explicitly associate one, the subnet uses the VPC main route table. For each outbound packet, the VPC router compares the packet destination with the table and selects the most specific matching prefix. A route for 10.40.8.0/21 beats 10.40.0.0/16; 0.0.0.0/0 is only the default when nothing narrower matches.
Common route targets include local, an internet gateway, NAT gateway, VPC peering connection, transit gateway, gateway endpoint, or network interface. A route only selects a target. It does not authorize the connection, and it does not translate addresses unless the selected target is a NAT device. Routes can be active, blackhole, propagated, or static depending on target state and attachment type. A blackhole route usually means the target was deleted or detached while the route entry remained.
Public subnet behavior requires three conditions: a route to an internet gateway, a public IPv4 address or IPv6 address on the workload or load balancer, and security controls that permit the flow. Private IPv4 subnets commonly send 0.0.0.0/0 to a NAT gateway for outbound updates while blocking unsolicited inbound internet connections. Isolated subnets omit the default route and only reach local or explicitly routed destinations.
AWS reserves the first four and last IPv4 address in every subnet. A /28 has 16 total addresses but only 11 usable. That is risky for services that create extra network interfaces during scaling, deployments, endpoint creation, or failover, so plan spare addresses per Availability Zone, not only today's instance count.
CIDR and Configuration Anatomy
CIDR notation combines a network address and prefix length. In 10.40.32.0/20, the first 20 bits are the network and the remaining bits are host addresses. Shorter prefixes are larger. A /16 contains 65,536 IPv4 addresses before AWS reservations; a /24 contains 256; a /28 contains 16.
A useful network plan records VPC CIDRs, subnet CIDRs, purpose, Availability Zone, route table, default route target, expected consumers, and tags. Name route tables by behavior, such as prod-private-a-egress or dev-public-ingress, so diagnostics are faster when a subnet accidentally inherits the main route table. Also record which CIDRs are reachable through peering, Transit Gateway, VPN, or Direct Connect; those external ranges determine whether your VPC block is routable later.
Subnet sizing should also reflect service allocation behavior. Load balancers need addresses in each enabled subnet, interface endpoints create one elastic network interface per selected subnet, and container platforms may allocate many secondary IPs. Reserve unused ranges for future tiers instead of filling every gap immediately.
Example 1: Choose a VPC Block
A production workload needs three Availability Zones, public load balancer subnets, private application subnets, isolated database subnets, and growth from 30 to 400 compute addresses per zone. A /24 VPC is too small because nine subnets plus load balancers, endpoints, blue-green deployments, and managed-service ENIs leave little margin. A 10.40.0.0/16 VPC is easier to split if it does not overlap with corporate, peer, or transit networks.
import ipaddress
vpc = ipaddress.ip_network("10.40.0.0/16")
for subnet in vpc.subnets(new_prefix=20):
print(subnet)
if subnet.prefixlen == 20 and subnet.network_address.exploded == "10.40.48.0":
break
The output starts with 10.40.0.0/20, 10.40.16.0/20, 10.40.32.0/20, and 10.40.48.0/20. Each /20 has 4,096 total addresses, enough for a busy tier in one Availability Zone after reservations. The design spends address space for clarity and growth instead of forcing unrelated tiers into tiny ranges.
Example 2: Split by Tier and Zone
Allocate one public, one private application, and one isolated database subnet in each of three zones. Using sequential /20 blocks and a consistent tier order gives operators a pattern they can read quickly.
| Purpose | AZ A | AZ B | AZ C |
|---|---|---|---|
| Public | 10.40.0.0/20 |
10.40.16.0/20 |
10.40.32.0/20 |
| Private app | 10.40.64.0/20 |
10.40.80.0/20 |
10.40.96.0/20 |
| Database | 10.40.128.0/20 |
10.40.144.0/20 |
10.40.160.0/20 |
An EC2 instance in 10.40.80.0/20 receives a private address from that subnet, resides in AZ B, and uses the route table associated with the AZ B private app subnet. It can route to 10.40.144.25 through the implicit local route, but the database still needs a security group rule allowing the application security group on the database port. The expected routing path is local VPC routing, not a NAT gateway, even though both subnets are called private.
Example 3: Predict Route Selection
A private app route table contains 10.40.0.0/16 local, 10.50.0.0/16 pcx-analytics, 10.50.12.0/24 tgw-shared, and 0.0.0.0/0 nat-gateway-a. Traffic to 10.50.12.77 uses the transit gateway because the /24 is more specific than the peering /16. Traffic to 10.50.30.10 uses peering.
More specific routes are useful for migrations, inspection, and exceptions, but they surprise teams that assume one broad route owns a partner network. During troubleshooting, always identify the exact route table associated with the source subnet and compare destination prefixes by specificity.
Design Choices and Trade-Offs
Large VPC ranges simplify future subnets and managed-service growth, but they consume private address space needed by other accounts, Regions, on-premises networks, or acquisitions. Small ranges conserve space but increase renumbering risk, subnet exhaustion, and awkward service placement.
Public subnets are appropriate for internet-facing load balancers and tightly controlled administrative entry points. Private subnets with NAT gateways fit outbound-only workloads, but NAT adds cost, zonal dependency, connection capacity considerations, and another route target to monitor. Isolated subnets suit databases and control planes that should only speak to local, endpoint, or explicitly routed destinations.
One route table per subnet purpose is simple. One route table per purpose per Availability Zone supports zonal NAT routing, so an AZ A instance does not depend on a NAT gateway in AZ B. That adds automation work, but avoids cross-zone data charges and reduces the effect of a NAT or zone failure. The main route table should usually be conservative; explicit associations make intent easier to audit.
Route table ownership is another trade-off. Central networking teams often manage shared transit and inspection routes, while application teams manage subnet placement. Clear ownership reduces accidental broad defaults, such as sending database traffic through an inspection appliance when local routing and security groups were intended.
Failure Modes and Troubleshooting
Symptom: a private instance cannot download packages. Cause: its subnet route table lacks a default route to a healthy NAT gateway, the NAT subnet lacks an internet gateway route, or egress controls block traffic. Diagnose: find the instance subnet, inspect its associated route table, confirm 0.0.0.0/0 points to the intended NAT gateway, confirm NAT gateway state, and inspect the NAT gateway subnet route table.
Symptom: peered VPCs cannot communicate. Cause: CIDRs overlap, routes exist on only one side, security groups reject the peer source, or DNS expectations do not match peering settings. Diagnose: compare both CIDRs, inspect reciprocal route tables, check security groups and network ACLs, and test by private IP before testing by name. Correct: use non-overlapping CIDRs, add routes in both VPCs, and allow the required protocol from the correct peer CIDR or supported security group reference.
Symptom: a load balancer or EKS node group reports insufficient addresses. Cause: subnet CIDRs are too small or fragmented by existing ENIs. Diagnose: check available IPv4 counts per subnet, list service-created network interfaces, and compare headroom in every enabled Availability Zone. Correct: add larger replacement subnets, move workloads through controlled deployment, or add a secondary VPC CIDR and create new subnets from it.
Security, Reliability, and Performance
VPC routes are not identity controls. Use security groups for stateful workload rules, network ACLs for stateless subnet guardrails when needed, VPC endpoints to keep AWS service traffic off internet paths, and flow logs to record accepted and rejected traffic metadata. Do not treat a private IP as proof of trust when peering and transit routing connect many teams.
Reliability improves when each Availability Zone has local dependencies: local subnets, local NAT gateways, and route tables that keep routine traffic inside the zone. Performance and cost follow the path; centralized inspection, cross-zone NAT, and unnecessary transit hops can dominate small workloads. For shared service networks, document route ownership clearly because one propagated prefix can change the path for many application subnets.
Gateway endpoints and prefix-list routes can improve reliability by steering traffic to services such as Amazon S3 without public internet or NAT traversal. When these routes are added, verify that they are attached to every subnet route table that needs the service; missing one zone creates inconsistent behavior.
Hands-On Lab: Build and Verify a Small VPC
Prerequisites: an AWS account for practice, AWS CLI credentials allowed to manage VPC networking, a configured Region, and no production dependency on 10.44.0.0/16. The lab creates a VPC, one public subnet, one private subnet, an internet gateway, and separate route tables. It does not launch EC2 instances.
set -euo pipefail
REGION="${AWS_REGION:-us-east-1}"
VPC_CIDR="10.44.0.0/16"
PUBLIC_CIDR="10.44.0.0/24"
PRIVATE_CIDR="10.44.10.0/24"
VPC_ID=$(aws ec2 create-vpc --region "$REGION" --cidr-block "$VPC_CIDR" --query 'Vpc.VpcId' --output text)
aws ec2 create-tags --region "$REGION" --resources "$VPC_ID" --tags Key=Name,Value=course-vpc-lab
IGW_ID=$(aws ec2 create-internet-gateway --region "$REGION" --query 'InternetGateway.InternetGatewayId' --output text)
aws ec2 attach-internet-gateway --region "$REGION" --vpc-id "$VPC_ID" --internet-gateway-id "$IGW_ID"
AZ=$(aws ec2 describe-availability-zones --region "$REGION" --query 'AvailabilityZones[0].ZoneName' --output text)
PUBLIC_SUBNET_ID=$(aws ec2 create-subnet --region "$REGION" --vpc-id "$VPC_ID" --cidr-block "$PUBLIC_CIDR" --availability-zone "$AZ" --query 'Subnet.SubnetId' --output text)
PRIVATE_SUBNET_ID=$(aws ec2 create-subnet --region "$REGION" --vpc-id "$VPC_ID" --cidr-block "$PRIVATE_CIDR" --availability-zone "$AZ" --query 'Subnet.SubnetId' --output text)
PUBLIC_RT_ID=$(aws ec2 create-route-table --region "$REGION" --vpc-id "$VPC_ID" --query 'RouteTable.RouteTableId' --output text)
PRIVATE_RT_ID=$(aws ec2 create-route-table --region "$REGION" --vpc-id "$VPC_ID" --query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --region "$REGION" --route-table-id "$PUBLIC_RT_ID" --destination-cidr-block 0.0.0.0/0 --gateway-id "$IGW_ID"
aws ec2 associate-route-table --region "$REGION" --subnet-id "$PUBLIC_SUBNET_ID" --route-table-id "$PUBLIC_RT_ID"
aws ec2 associate-route-table --region "$REGION" --subnet-id "$PRIVATE_SUBNET_ID" --route-table-id "$PRIVATE_RT_ID"
aws ec2 describe-route-tables --region "$REGION" --filters Name=vpc-id,Values="$VPC_ID" --query 'RouteTables[].{RouteTable:RouteTableId,Routes:Routes[].{Destination:DestinationCidrBlock,Target:GatewayId}}' --output table
Verification: the public route table should show a local route for 10.44.0.0/16 and a default route to the internet gateway. The private route table should show the local route only. Therefore a public-subnet resource can be internet reachable only if it also has a public address and permissive security controls, while a private-subnet resource has no IPv4 internet egress until you add NAT, an endpoint, or another route target.
set -euo pipefail
REGION="${AWS_REGION:-us-east-1}"
VPC_ID=$(aws ec2 describe-vpcs --region "$REGION" --filters Name=tag:Name,Values=course-vpc-lab --query 'Vpcs[0].VpcId' --output text)
aws ec2 describe-subnets --region "$REGION" --filters Name=vpc-id,Values="$VPC_ID" --query 'Subnets[].{Subnet:SubnetId,Cidr:CidrBlock,Available:AvailableIpAddressCount}' --output table
The subnet verification command should return two subnets, each with a CIDR matching the variables above and an available address count lower than 256 because AWS reserved addresses are excluded. If the VPC lookup returns None, check that the tag was created in the same Region used by the command.
Before cleanup, save the created IDs from the command output or find them by the course-vpc-lab tag. Deleting in dependency order matters because AWS will not delete a VPC while subnets, gateways, or interfaces still reference it.
Cleanup: disassociate explicit subnet route table associations, delete the created route tables, detach and delete the internet gateway, delete both subnets, and delete the VPC. If deletion fails, inspect dependencies such as network interfaces, endpoints, NAT gateways, or load balancers still attached to the VPC.
Assessment Exercises
- You have
10.80.0.0/16and need public, app, database, and endpoint subnets in four Availability Zones. Propose a subnet plan and explain growth headroom. - A private instance reaches
10.20.5.5but not10.20.9.9. The route table has10.20.0.0/16to peering and10.20.9.0/24to a transit gateway. Which target is selected for each address, and what would you inspect next? - Two teams want to peer VPCs that both use
10.0.0.0/16. Explain why this fails and give two migration options. - A database subnet has no default route, and an application in another private subnet cannot connect. List routing and security checks that distinguish a missing path from a blocked path.
- Design a rollback for replacing undersized
/28application subnets with/22subnets while preserving availability.
Summary
VPC design defines AWS reachability. Pick non-overlapping CIDR space with growth room, divide subnets by Availability Zone and purpose, and remember that route tables use longest-prefix matching from the source subnet's effective table. Public, private, and isolated subnet patterns are route-table behaviors, not labels. Troubleshoot from the source subnet outward: route table, most specific route, target health, security groups, network ACLs, endpoint settings, and available IP capacity.
