How the layout works
Each tier gets one block of the VPC, big enough for one subnet in every AZ slot. With the defaults (a 10.0.0.0/16 VPC, 3 AZs and 4 slots per tier), the public tier's /24 subnets need a /22 block, so public gets 10.0.0.0/22. Its subnets are 10.0.0.0/24, 10.0.1.0/24 and 10.0.2.0/24, and 10.0.3.0/24 is kept for a fourth AZ.
The private tier's /22 subnets need a /20, which has to start on a multiple of 16 in the third octet, so it lands at 10.0.16.0/20 and leaves 10.0.4.0 to 10.0.15.255 free. Blocks are placed in list order, each aligned to its own size, exactly the way Terraform's cidrsubnets() places them, which is why the generated code reproduces the table.
Every subnet has 5 fewer usable addresses than its size, because AWS reserves the first four and the last one. A /24 has 251 usable IPs. To check a single cidrsubnet() call, use the Terraform cidrsubnet calculator.
Pitfall: renumbering live subnets
A subnet's CIDR cannot change in place. In the Terraform AWS provider, a new cidr_block on aws_subnet forces a replacement, and AWS refuses to delete a subnet that still has network interfaces in it. Layouts usually break this way when someone inserts a tier in the middle of the list, makes an early tier bigger, or adds more AZs than the layout planned for.
Append new tiers at the end and the earlier blocks keep their addresses; the Terraform docs give the same advice for cidrsubnets(). Adding AZs is safe up to the slot count. Going past it doubles the slots and moves every subnet, so pick slots for the most AZs you might ever use. The page warns you when every slot is taken.
Using the code
The code creates the VPC and one subnet per tier and AZ, keyed like private-1, so adding an AZ or a tier only adds resources. AZ names come from the aws_availability_zones data source, without Local Zones. Keep az_count at or below the number of AZs in your Region, or terraform plan fails with an index error.
It creates no internet gateway, NAT gateways or route tables, so a tier named public is not reachable from the internet until you add them (see the FAQ). Auto-assign public IPv4 is off by default because AWS charges for every public IPv4 address. To use an existing VPC, set vpc_cidr to its range, drop the aws_vpc resource and pass the VPC ID:
variable "vpc_id" {
type = string
}
resource "aws_subnet" "this" {
for_each = local.subnets
vpc_id = var.vpc_id
# cidr_block, availability_zone and the rest stay the same
}FAQ
Why does AWS reserve 5 IP addresses in every subnet?
AWS keeps the first four addresses and the last one in each subnet. In 10.0.0.0/24 those are 10.0.0.0 (network address), 10.0.0.1 (VPC router), 10.0.0.2 (reserved by AWS; in the first subnet of a VPC this is the Amazon DNS server, at the VPC base plus two), 10.0.0.3 (reserved for future use) and 10.0.0.255 (broadcast, which VPCs do not support). So a /24 has 251 usable addresses, and a /28, the smallest subnet AWS allows, has 11.
How big should each subnet be?
Size for peak use plus growth, because a subnet cannot be resized later. Count every network interface: instances, load balancer nodes, NAT gateways, Lambda Hyperplane ENIs (one per subnet and security group combination), and on EKS with the VPC CNI one address per pod plus the addresses the CNI keeps warm on each node. AWS asks for at least a /27 with 8 free addresses in each Application Load Balancer subnet, so a /24 leaves plenty of room there. For EKS, AWS notes that eksctl creates /19 subnets by default and calls a /19 suitable for most workload types. If unsure, enter the number of IPs you need and the planner picks the smallest subnet that fits.
Why reserve AZ slots I am not using yet?
Each tier block holds a subnet for every slot, used or not. Spare slots let you add an Availability Zone later by raising az_count: the new subnets go into the reserved space and nothing else moves. Without a spare slot, a new AZ needs a bigger block, which moves every subnet. The planner defaults to 4 slots for 1 to 4 AZs, 8 slots for 5 to 8 AZs and 16 slots for 9 to 16 AZs.
What makes a subnet public or private?
Its route table, not its name. A subnet is public when its route table has a direct route to an internet gateway, usually for 0.0.0.0/0. Private subnets usually send outbound traffic through a NAT gateway that sits in a public subnet. The generated code creates only the VPC and subnets, so add an internet gateway, NAT gateways and route tables before anything in the public tier is reachable from the internet.
Can I change a subnet CIDR later?
No. AWS does not let you change the IPv4 CIDR of an existing subnet, and in Terraform a new cidr_block replaces the subnet. You can add a secondary CIDR block to the VPC and create new subnets in it, but existing subnets keep their ranges. That is why this planner adds new tiers after the existing ones instead of reshuffling them.
Which IP range should my VPC use?
AWS recommends the private ranges from RFC 1918: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. Pick a block that does not overlap your other VPCs or on-premises networks, so peering and VPN connections stay possible later. Avoid 172.17.0.0/16, which some AWS services such as Cloud9 and SageMaker AI use, and start with a /16 when you can, since a VPC CIDR block cannot be resized.