AWS VPC Subnet Calculator
Lay out public, private and database subnets across Availability Zones, then copy the Terraform or CloudFormation
Subnets use 13,824 of 65,536 addresses (21%). AWS reserves 5 in each subnet.
Subnet tiers, each repeated in every zone
9 subnets in 10.0.0.0/16
| Tier | Zone | Subnet | First usable | Last usable | Usable IPs |
|---|---|---|---|---|---|
| private | a | 10.0.0.0/20 | 10.0.0.4 | 10.0.15.254 | 4,091 |
| private | b | 10.0.16.0/20 | 10.0.16.4 | 10.0.31.254 | 4,091 |
| private | c | 10.0.32.0/20 | 10.0.32.4 | 10.0.47.254 | 4,091 |
| public | a | 10.0.64.0/24 | 10.0.64.4 | 10.0.64.254 | 251 |
| public | b | 10.0.65.0/24 | 10.0.65.4 | 10.0.65.254 | 251 |
| public | c | 10.0.66.0/24 | 10.0.66.4 | 10.0.66.254 | 251 |
| database | a | 10.0.68.0/24 | 10.0.68.4 | 10.0.68.254 | 251 |
| database | b | 10.0.69.0/24 | 10.0.69.4 | 10.0.69.254 | 251 |
| database | c | 10.0.70.0/24 | 10.0.70.4 | 10.0.70.254 | 251 |
Zone a is the first zone AWS lists in your region, b the second, and so on. The code picks real zone names when it runs.
Still free: 10.0.48.0/20, 10.0.67.0/24, 10.0.71.0/24, 10.0.72.0/21, 10.0.80.0/20, 10.0.96.0/19, 10.0.128.0/17
# Availability Zones come from the provider's region, so this works in any region.
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "production"
}
}
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.0.0/20"
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = false
tags = {
Name = "production-private-a"
Tier = "private"
}
}
resource "aws_subnet" "private_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.16.0/20"
availability_zone = data.aws_availability_zones.available.names[1]
map_public_ip_on_launch = false
tags = {
Name = "production-private-b"
Tier = "private"
}
}
resource "aws_subnet" "private_c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.32.0/20"
availability_zone = data.aws_availability_zones.available.names[2]
map_public_ip_on_launch = false
tags = {
Name = "production-private-c"
Tier = "private"
}
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.64.0/24"
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = {
Name = "production-public-a"
Tier = "public"
}
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.65.0/24"
availability_zone = data.aws_availability_zones.available.names[1]
map_public_ip_on_launch = true
tags = {
Name = "production-public-b"
Tier = "public"
}
}
resource "aws_subnet" "public_c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.66.0/24"
availability_zone = data.aws_availability_zones.available.names[2]
map_public_ip_on_launch = true
tags = {
Name = "production-public-c"
Tier = "public"
}
}
resource "aws_subnet" "database_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.68.0/24"
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = false
tags = {
Name = "production-database-a"
Tier = "database"
}
}
resource "aws_subnet" "database_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.69.0/24"
availability_zone = data.aws_availability_zones.available.names[1]
map_public_ip_on_launch = false
tags = {
Name = "production-database-b"
Tier = "database"
}
}
resource "aws_subnet" "database_c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.70.0/24"
availability_zone = data.aws_availability_zones.available.names[2]
map_public_ip_on_launch = false
tags = {
Name = "production-database-c"
Tier = "database"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "production"
}
}
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
}
tags = {
Name = "production-public"
}
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_b" {
subnet_id = aws_subnet.public_b.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_c" {
subnet_id = aws_subnet.public_c.id
route_table_id = aws_route_table.public.id
}
# Private subnets use the VPC's main route table: they reach each other but not the internet.
# Choose a NAT gateway option above if they need outbound access.
resource "aws_db_subnet_group" "database" {
name = "production-database"
subnet_ids = [aws_subnet.database_a.id, aws_subnet.database_b.id, aws_subnet.database_c.id]
tags = {
Name = "production-database"
}
}
Everything runs in your browser. For networks outside AWS, use the VLSM Subnet Planner.
Was the AWS VPC Subnet Calculator helpful?
About this tool
The AWS VPC Subnet Calculator lays out a VPC for you. Enter the VPC range, how many Availability Zones you want, and your tiers (usually public, private and database), and it gives every tier a subnet in every zone, with no overlaps and the usable range worked out using AWS's rules. AWS reserves five addresses in every subnet: the first four and the last, so a /24 has 251 usable addresses, not 254. It then writes the same network as Terraform or CloudFormation: the VPC, subnets, internet gateway, route tables and associations, optional NAT gateways (one shared, or one per zone), and a database subnet group for RDS. Zone names come from the region when the code runs, so the output works in any region. Leaving room for more zones keeps each tier in one contiguous block as you grow, which keeps route tables and security group rules simple. Everything runs in your browser.
When to use it
- →Planning the subnet layout for a new AWS VPC before creating anything
- →Starting a Terraform module or CloudFormation stack with a correct network
- →Checking whether a tier design fits in a /16, /20 or smaller VPC range
- →Comparing a single NAT gateway against one per zone in generated code
Tips
- ◆Pick a VPC range that won't clash with your other VPCs or your office network. Everyone picks 10.0.0.0/16, and overlapping ranges can't be peered.
- ◆AWS advises against 172.17.0.0/16 because some of its services use it.
- ◆Size for growth. Kubernetes on AWS and Lambda in a VPC can use many addresses, so private tiers are usually much bigger than public ones.
- ◆A VPC's primary range can't be changed later, though you can add secondary ranges.
- ◆Load balancers need a subnet of at least /27 with 8 free addresses.
Frequently asked questions
Why does a /24 subnet in AWS have 251 usable IPs?
AWS reserves five addresses in every subnet: the network address, the VPC router (.1), DNS (.2), one kept for future use (.3), and the last address. So the first address you can use is .4.
What's the difference between public, private and database subnets?
A subnet is public if its route table has a route to an internet gateway. A private subnet has no such route and reaches the internet only through a NAT gateway, if you add one. A database tier here is a private subnet with no internet route at all, grouped so RDS can use it.
How large can a VPC be?
A VPC's range can be from /16 (65,536 addresses) to /28 (16 addresses). Subnets have the same limits, and each one sits in a single Availability Zone.
Should I use one NAT gateway or one per zone?
One is cheaper, but if its zone has an outage, private subnets in the other zones lose internet access too, and traffic between zones costs extra. One per zone costs more and avoids both problems. NAT gateways are billed per hour and per GB.