Subnets
Carve public, private, and isolated subnets across AZs, understand AWS's 5 reserved IPs per subnet, and prove that public vs private is a route table property.
- This builds directly on The VPC Itself. Your
$VPC_IDmust be set before starting. - In a new terminal session, run the resume script at the end of The VPC Itself.
- Read the callout before every step. Predict what will happen, then run it.
Create Public and Private Subnets
Goal
Carve subnets from your VPC CIDR across multiple AZs. Understand the difference between public and private subnets, and why that difference is not a subnet property.
Estimated time: 1–2 hours
Enumerate available AZs
What's happening here
Subnets are AZ-scoped: each subnet lives in exactly one Availability Zone and cannot span multiple. AZs are isolated failure domains within a region: independent power, cooling, and physical infrastructure. Spreading subnets (and the resources inside them) across AZs is how you achieve high availability. If one AZ goes down, resources in the other AZs keep running. You need at least two AZs for any production workload. We list them first so we know which AZ names to use when creating subnets.
# List AZs in your region
aws ec2 describe-availability-zones \
--region $AWS_REGION \
--query 'AvailabilityZones[?State==`available`].{Name:ZoneName,State:State}'
# Export the first two for use in subnet creation
export AZ1=$(aws ec2 describe-availability-zones \
--region $AWS_REGION \
--query 'AvailabilityZones[?State==`available`][0].ZoneName' --output text)
export AZ2=$(aws ec2 describe-availability-zones \
--region $AWS_REGION \
--query 'AvailabilityZones[?State==`available`][1].ZoneName' --output text)
echo "AZ1: $AZ1"
echo "AZ2: $AZ2"Checkpoint: two AZ names exported.
Plan your subnet layout
What's happening here
Before creating subnets, plan on paper. Our VPC is 10.0.0.0/16 (65,536 IPs). We'll carve it into /24 subnets (256 IPs each, 251 usable after AWS reserves 5). The layout follows a common three-tier pattern:
- Public subnets: resources that need to be directly reachable from the internet (ALBs, NAT Gateways, bastion hosts)
- Private subnets: resources that need outbound internet but must not be directly reachable inbound (ECS tasks, Lambda in VPC, application servers)
- Isolated subnets: resources with no internet access at all, inbound or outbound (RDS, ElastiCache, internal data stores)
Each tier gets a subnet in each AZ, so resources can survive an AZ failure. That's 6 subnets minimum for a two-AZ, three-tier layout.
| Name | CIDR | AZ | Tier |
|---|---|---|---|
| public-az1 | 10.0.1.0/24 | AZ1 | public |
| public-az2 | 10.0.2.0/24 | AZ2 | public |
| private-az1 | 10.0.11.0/24 | AZ1 | private |
| private-az2 | 10.0.12.0/24 | AZ2 | private |
| isolated-az1 | 10.0.21.0/24 | AZ1 | isolated |
| isolated-az2 | 10.0.22.0/24 | AZ2 | isolated |
All are subsets of 10.0.0.0/16: the VPC has 64,000 IPs remaining after these six /24 subnets (65,536 − 6 × 256).
Create the public subnets
What's happening here
create-subnet carves the CIDR range out of the VPC's address space and associates it with a specific AZ. At this point, the subnet is neither public nor private, that designation is determined by what route table is attached to it (covered in Routing). The map-public-ip-on-launch flag we set here means: when an EC2 instance launches in this subnet, automatically assign it a public IP. This is a convenience flag, it doesn't make the subnet public by itself. A subnet is only truly public if it also has a route to an Internet Gateway.
# Public subnet in AZ1
export PUBLIC_SUBNET_AZ1=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.1.0/24 \
--availability-zone $AZ1 \
--region $AWS_REGION \
--query 'Subnet.SubnetId' --output text)
aws ec2 create-tags \
--resources $PUBLIC_SUBNET_AZ1 \
--tags Key=Name,Value=public-az1 Key=Tier,Value=public \
--region $AWS_REGION
# Public subnet in AZ2
export PUBLIC_SUBNET_AZ2=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.2.0/24 \
--availability-zone $AZ2 \
--region $AWS_REGION \
--query 'Subnet.SubnetId' --output text)
aws ec2 create-tags \
--resources $PUBLIC_SUBNET_AZ2 \
--tags Key=Name,Value=public-az2 Key=Tier,Value=public \
--region $AWS_REGION
# Enable auto-assign public IP on launch for public subnets
aws ec2 modify-subnet-attribute \
--subnet-id $PUBLIC_SUBNET_AZ1 \
--map-public-ip-on-launch \
--region $AWS_REGION
aws ec2 modify-subnet-attribute \
--subnet-id $PUBLIC_SUBNET_AZ2 \
--map-public-ip-on-launch \
--region $AWS_REGION
echo "Public subnets: $PUBLIC_SUBNET_AZ1 $PUBLIC_SUBNET_AZ2"Checkpoint:
aws ec2 describe-subnets \
--subnet-ids $PUBLIC_SUBNET_AZ1 $PUBLIC_SUBNET_AZ2 \
--region $AWS_REGION \
--query 'Subnets[*].{Id:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,AutoPublicIP:MapPublicIpOnLaunch,AvailableIPs:AvailableIpAddressCount}'Observe: AvailableIpAddressCount is 251: not 256. AWS reserves 5 addresses. MapPublicIpOnLaunch is true.
Create the private subnets
What's happening here
Private subnets do NOT get map-public-ip-on-launch. Resources here will have private IPs only. They can still reach the internet via a NAT Gateway (Routing), but nothing on the internet can initiate a connection to them. This is the correct posture for application servers, ECS tasks, and Lambda functions: they can call out (to APIs, S3, databases), but can't be directly accessed from outside. Note: at this point the private subnets are attached to the VPC's main route table, which only has the local route. They can reach resources in the same VPC but nowhere else.