Terraform CIDR Planning with Subnet Examples
Use Terraform cidrsubnet and cidrsubnets to divide a VPC range into stable subnet blocks, validate the result, and avoid renumbering deployed networks.

On this page
Terraform can calculate subnet addresses from a parent CIDR, which removes hand written arithmetic from network code. The difficult part is choosing a calculation that stays predictable when a team adds an availability zone, environment, or new workload later.
This tutorial builds a small IPv4 plan from 10.40.0.0/16. You will calculate fixed subnet slots with cidrsubnet, create consecutive variable sized blocks with cidrsubnets, inspect the output, and preserve deployed assignments as the plan grows.
Choose fixed or sequential allocation
Use cidrsubnet(prefix, newbits, netnum) when a subnet needs a stable slot. newbits is the number of bits added to the parent prefix, and netnum selects a slot inside the resulting subnet space. A /16 with four additional bits produces sixteen /20 slots.
Use cidrsubnets(prefix, newbits...) when you are carving consecutive ranges of different sizes. Each argument adds bits for the next returned block. The function assigns the blocks in order, so changing an earlier argument can move every later result.
For long lived cloud networks, fixed slots are usually easier to review. Sequential allocation is useful for a new address plan that has not yet been attached to deployed infrastructure.
Prefix math matters when choosing a slot.
Each added network bit halves the number of addresses in a block. A /16 contains 65,536 IPv4 addresses. Adding four bits makes a /20, which contains 4,096 addresses, and adding eight makes a /24, which contains 256. Cloud platforms reserve some addresses in each subnet, so the usable host count is lower than the total.
| Parent prefix | Added bits | Child prefix | Total IPv4 addresses |
|---|---|---|---|
/16 |
4 | /20 |
4,096 |
/16 |
8 | /24 |
256 |
/20 |
4 | /24 |
256 |
The netnum argument is an index, not an address offset. With a /16 parent and eight added bits, a netnum of 1 means the second /24, not the second IP address. A netnum of 256 is outside the available set because eight additional bits represent indexes from 0 through 255.
When someone asks for a block with a particular number of hosts, first find the smallest prefix that can contain that many addresses, then account for provider reservations and growth. For example, a subnet for 180 devices cannot use a /25, because a /25 has only 128 total IPv4 addresses. A /24 provides more headroom and maps cleanly to one fixed slot in this example.
Create a Terraform working directory
Install Terraform and create a directory named cidr-lab. Add main.tf with a version constraint and a small set of named network inputs:
terraform {
required_version = ">= 1.5.0"
}
variable "vpc_cidr" {
type = string
default = "10.40.0.0/16"
}
variable "zones" {
type = list(string)
default = ["a", "b", "c"]
}
The version constraint gives this example access to Terraform’s current configuration language without tying it to a single patch release. In a production repository, choose and pin a version that your team has tested.
Calculate stable subnet slots
Add locals that assign one /20 per zone. Because the subnet number comes from the list index, keep the existing zone order stable after applying this plan to a real VPC.
locals {
app_subnets = {
for index, zone in var.zones : zone => cidrsubnet(var.vpc_cidr, 4, index)
}
}
output "app_subnets" {
value = local.app_subnets
}
With the defaults, Terraform calculates 10.40.0.0/20, 10.40.16.0/20, and 10.40.32.0/20. Each block contains 4,096 addresses before provider reservations. Add a new zone at the end of the list to keep existing indexes unchanged.
Carve variable sized blocks
For a fresh range, cidrsubnets can allocate several block sizes in sequence. Add this second local to see the difference:
locals {
sequential_ranges = cidrsubnets(var.vpc_cidr, 4, 4, 8, 8)
}
output "sequential_ranges" {
value = local.sequential_ranges
}
The result is 10.40.0.0/20, 10.40.16.0/20, 10.40.32.0/24, and 10.40.33.0/24. Do not insert a new argument between existing entries after those ranges have been assigned. Append new arguments only when the remaining parent space has enough room.
Sequential allocation is not a substitute for documenting intent. If the first /20 belongs to production applications and the next /20 belongs to shared services, keep those names beside the calculated values rather than relying on a reader to infer purpose from list order. Avoid using one long cidrsubnets call for unrelated environments because adding a range for one team can shift another team’s result.
Use separate parent prefixes when an address plan needs independent ownership or separate growth schedules. Within one parent, leave deliberate gaps by assigning fixed netnum values with cidrsubnet. A gap gives the network team room to reserve an expansion block without renumbering existing allocations. Record the reason for the gap in a comment or input map so it is not mistaken for unused capacity.
Inspect the result locally
Run the following commands before connecting the values to cloud resources:
terraform init
terraform fmt -recursive
terraform validate
terraform console
At the console prompt, evaluate cidrsubnet("10.40.0.0/16", 4, 2) and cidrsubnets("10.40.0.0/16", 4, 8). Exit with Ctrl+D, then run terraform plan to review the named outputs. You do not need cloud credentials to evaluate these expressions.
Check the calculation in small steps if the result surprises you. First confirm the parent prefix. Then confirm that newbits is the difference between the parent and desired child prefix. Finally confirm that the slot number is within the available range. For a /16 divided into /20 blocks, there are 2^4, or 16, possible slots. A desired /24 from that same parent uses eight added bits and offers 256 slots.
Terraform also has cidrhost for calculating a host address inside a prefix and cidrnetmask for converting an IPv4 prefix into a dotted decimal mask. These helpers are useful for outputs and assertions, but they do not reserve an address, check a route table, or query a provider’s inventory. Keep arithmetic separate from allocation ownership.
Connect a result to a subnet resource
After checking that the selected blocks are unused in your environment, declare a cloud subnet using the value from the map. This AWS example assumes the VPC and provider are configured in the same root module:
resource "aws_subnet" "app" {
for_each = local.app_subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = "${var.region}${each.key}"
}
Use real availability zone identifiers from your account because zone letters can map to different physical zones between accounts. Keep route tables, gateways, and security rules in explicit resources so the CIDR calculation does not hide network behavior.
An AWS subnet also has an availability zone and a route table association. The CIDR formula only determines the subnet’s address range. It does not decide whether instances have public addresses, whether they can reach the internet, or which security groups can reach them. Add those resources intentionally, then inspect the full plan so an address calculation does not distract from a risky route or exposure change.
If you use for_each with a map, prefer stable keys such as "us-east-1a-app" over transient numeric list indexes. Terraform resource addresses include those keys, so renaming a key can appear as a resource removal and creation even when the CIDR stays the same. Use a moved block when you need to rename a tracked address without replacing the subnet, and review the plan before applying the refactor.
Keep future allocations stable
Terraform only calculates from the prefix and arguments in this configuration. It cannot detect a range reserved in a spreadsheet, another state, a peered VPC, or an on premises route. Check the complete address plan before applying, and reserve enough unused space for future zones and services.
Never change a deployed subnet’s index or reorder a cidrsubnets argument to make the file look tidier. If a range must move, plan a separate migration with parallel capacity, route changes, workload moves, and rollback steps. The cidrsubnets reference explains why existing arguments must remain unchanged after allocation: Terraform CIDR functions.
Apply the plan to a real network
Replace the sample parent with a range assigned to your environment, run terraform plan, and compare every proposed CIDR against all connected networks. Apply only after a reviewer confirms that the block size, zone mapping, and route design match the approved network plan.
If the plan proposes replacing an existing subnet, stop and find out why before continuing. A changed CIDR is often a replacement, and deleting a subnet can interrupt every attached workload. Compare the old and new resource addresses, check the state selected by the backend, and confirm that you did not change the order of a list or the keys in a map. Test a refactor against a disposable VPC before applying it to a live network.
After deployment, compare the actual subnet CIDR and availability zone with the values in the plan. Keep the parent, child range, environment, account, and owner together in the address plan. This turns the Terraform expression into a repeatable implementation of an allocation decision instead of an undocumented source of address assignments.
For a quick manual review, compare the parent and proposed ranges against the complete address plan. Treat any calculator or overlap check as an extra check, not as an allocation lock shared by separate Terraform states.
Write down the parent range, the purpose of each subnet, and the slot reserved for future growth before you provision resources. Review the plan after every address change, because preserving the original mapping is part of keeping Terraform network code safe.
Frequently Asked Questions
What is the difference between cidrsubnet and cidrsubnets?
How do I create a /24 subnet from a /16 network in Terraform?
Can Terraform CIDR functions detect overlap with another VPC?
Stop reaching for a spreadsheet
Obelinf keeps every subnet, device, circuit, and rack in one live source of truth, with audit logs and a topology view. Free for personal use.
Related Articles

Test Terraform Network Modules with Mocks
Write Terraform tests for a reusable AWS VPC module, mock the provider to avoid cloud resources, and catch invalid CIDR plans before deployment.
Read more
How to Use Terraform with IPAM for Network Subnets
Connect Terraform to IPAM with this hands on guide. Use Obelinf to find an approved subnet, read its CIDR safely, and plan an AWS network from it.
Read more
AWS VPC Design Best Practices: CIDR, Subnets, and Routing
Design an AWS VPC the right way: size the CIDR block, lay out public, private, and isolated subnets across availability zones, and wire route tables without common mistakes.
Read more