7 min read

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.

ByAndré Ribeiro· Founder, Obelinf
Terraform CIDR Planning with Subnet Examples
Terraform CIDR Planning with Subnet Examples · October 3, 2026
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.

A /16 VPC divided into smaller subnet prefixes10.40.0.0/16 parent range65,536 addresses before cloud reservations10.40.0.0/20, application zone A10.40.16.0/20, application zone BThe prefix length grows as each subnet contains fewer addresses.

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.

Review calculated ranges before applying a network changeSet parent CIDRreserve the address planCalculate slotsinspect every prefixReview plancheck overlap and sizeApply changeafter approvalA Terraform expression cannot see address assignments outside its configuration and state.

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?
`cidrsubnet` calculates one subnet when you provide its subnet number. `cidrsubnets` calculates a sequence of subnet blocks from a list of additional prefix bits.
How do I create a /24 subnet from a /16 network in Terraform?
Add eight network bits with `cidrsubnet("10.40.0.0/16", 8, 0)`. The resulting prefix is /24 because 16 plus 8 equals 24.
Can Terraform CIDR functions detect overlap with another VPC?
No. These functions calculate ranges inside the prefix you pass in. Compare the allocation against your other VPCs, routes, and reserved ranges separately.

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