Blog ·
Conflict-free CIDR allocation in Terraform
Stop picking CIDRs by hand.
TerraformIPAMAWSAzureNetworking
Here is how subnet addressing works in almost every Terraform codebase I have seen:
variable "subnet_cidr" {
default = "10.20.4.0/24"
}Somebody chose that /24. They probably checked a spreadsheet, or asked in Slack, or looked at what the last environment used and added one. Then they wrote it down, and from that moment it is a fact about your network that lives in a variable default.
Everything downstream inherits the assumption that the choice was correct. Nothing anywhere will tell you if it was not.
The problem is the picking, not the storing
The usual response to this is a better record. A wiki page, a proper IPAM, a naming convention, a spreadsheet with conditional formatting. All of these improve how well you remember what you decided.
None of them change the fact that a human decided it, alone, using whatever information they happened to have. And a record cannot refuse. There is nothing in a spreadsheet that sits between an engineer and terraform apply.
So the failure mode survives every improvement to the record. Two people consult the same wiki page a week apart, both see 10.20.4.0/24 is free, both take it. The wiki was accurate. It was just never in a position to say no.
Ask for a size, not a block
The alternative is to stop choosing. Rather than picking a CIDR and recording it, you describe what you need and something that can see every range you own hands one back.
Declare the space you own as a pool:
resource "nxip_pool" "production_us_east" {
name = "prod-us-east-1"
cidr = "10.0.0.0/16"
family = "IPV4"
environment = "production"
region = "us-east-1"
}Then request from it, by size:
resource "nxip_subnet" "web_tier" {
environment = "production"
region = "us-east-1"
family = "IPV4"
prefix_length = 24
name = "web-tier"
}There is no CIDR in that resource. You asked for a /24 in production us-east-1, and the allocation happens against an authority that knows every other subnet already carved from that pool.
The block it returns is readable as an attribute, so the actual infrastructure references the allocation rather than a literal:
resource "aws_subnet" "web" {
vpc_id = aws_vpc.main.id
cidr_block = nxip_subnet.web_tier.cidr
availability_zone = "us-east-1a"
}That is the whole point of the exercise. cidr_block is no longer a number somebody chose, it is the output of an allocation, and the dependency between the two is expressed in the graph rather than in somebody's memory. The same attribute feeds an azurerm_subnet, a route table, a security group rule, or anything else that needs to know the address.
The important property is not convenience. It is that two engineers running this simultaneously cannot receive the same block, because neither of them is choosing. Conflicts are not detected after the fact. They are not creatable.
Why terraform alone cannot do this
It is worth being precise, because "just use a module" is the obvious objection.
A module can compute subnets from a base CIDR, and cidrsubnet() is genuinely good at that. What a module cannot do is know about address space outside its own state. terraform plan is authoritative about exactly one state file. Address space is an org-wide resource, spanning workspaces, accounts, teams, and often more than one cloud. Not just cloud, but also your on-premise resources too.
So a module will confidently and correctly hand you 10.20.4.0/24, while having no way to know another team's workspace took it last Tuesday. That is not a shortcoming in Terraform. State is per-workspace by design, and no per-workspace tool can answer authoritatively about IP space across the org. The authority has to live outside the state file, or it cannot see far enough to be right.
## The tradeoff worth knowing before you adopt this
Auto-allocation has a real cost, and any honest description has to include it.
An auto-allocated subnet gets the next block which is free. So if you destroy and recreate a set of subnets in a different order, they come back with different addresses. For some workloads that is fine. For other systems which reference these subnets and addresses it creates an issue.
Our network and security infrastructure components are complicated, addresses, address-groups, FQDNs, NSGs, ACLs, route-maps and so on. Having to update these because of an update in the address allocations becomes a very arduous process with change control and approvals.
The answer is to pin those explicitly, this is an alternative to auto assignment.
resource "nxip_subnet" "database_tier" {
environment = "production"
region = "us-east-1"
family = "IPV4"
cidr = "10.0.10.0/24"
name = "database-tier"
}Which looks exactly like the hardcoded variable from the start of this article, and that is the point. The difference is that this block is now registered with an authority that will refuse to hand it to anybody else, rather than being a number in a file that nothing enforces.
The rule of thumb: let it allocate by default. Explicitly assign only if regular Terraform changes involve these subnets.
What this makes possible at review time
Once allocation runs through something that sees the whole estate, a code review can show the address impact of a change before it merges: which CIDR each new resource will be allocated. Which pool it comes from, and what that does to remaining capacity. None of which is derivable from the plan output alone, for the reason above.
It also closes the multi-cloud case almost incidentally. AWS IPAM and Azure's address management are each authoritative over their own control plane and structurally blind to the other, so a VPC in eu-west-1 and a VNet in uksouth can both hold 10.20.0.0/16 with both clouds reporting success. Nothing surfaces until somebody tries to route between them, at which point one side requires renumbering. An authority that sits above both never creates the collision in the first place.
Where to start
If you want to know whether this is theoretical for you or not, the fastest check is to enumerate every VPC and VNet CIDR across every account, subscription and region, and look for overlaps. It is not conceptually hard, just tedious, and it goes stale the moment anyone applies.
I built a tool that does it for you:
npx nxip-cli scan aws azureRead-only, no signup, nothing uploaded. Either your estate has collisions or it does not, and that is worth knowing either way. What it checks, and the least-privilege credentials it needs, are written up at https://nx-ip.com/docs/discovery
Scanning is only one way in. If your address space lives in a spreadsheet, a wiki, or somebody's head rather than in a cloud API, the other routes are at https://nx-ip.com/docs/getting-data-in
The broader point stands whatever you use to act on it. As long as a person is picking the number, the tooling around them can only ever record the mistake faithfully.
