WintelGuy.com

Terraform IP Network and CIDR Functions Explained

Contents

Introduction

Infrastructure defined with Terraform frequently needs to model and manage IP address space in a precise, repeatable, and environment-aware way. Whether provisioning cloud networks, creating application subnets, assigning host addresses, or integrating with existing on-premises systems, Terraform configurations must be able to handle IP network addressing dynamically. This is why CIDR address manipulation functionality is a foundational part of infrastructure as code.

Common real-world use cases include dynamically deriving subnets from a larger address block, calculating host IP addresses for gateways or services, validating that a network can support a required number of hosts, and generating consistent subnet layouts across multiple environments or regions. Hard-coding IP configuration quickly becomes unmanageable as infrastructure grows, environments diverge, or address plans evolve. Terraform's built-in IP network functions allow these calculations to be expressed declaratively, ensuring network layouts remain deterministic, scalable, and self-documenting.

Terraform provides a small but powerful set of IP and CIDR manipulation functions designed specifically for these tasks. Functions such as cidrhost, cidrnetmask, cidrsubnet, and cidrsubnets enable configurations to derive host addresses, compute network masks, and partition address space into subnets directly within Terraform expressions. These functions operate on standard CIDR notation and enforce strict validation rules, helping catch addressing errors early during plan and apply phases.

This article explains how Terraform's IP network and CIDR functions work, where they are most useful, and where they can be unintuitive or challenging. Through practical examples and analysis you will learn how to apply these functions effectively, understand their parameter semantics, and make informed decisions when designing subnetting logic for complex environments.

Back to Top

Host-Level CIDR Calculations in Terraform

Terraform's host-level CIDR functions are designed to assist with a common class of tasks at the individual host and network boundary level, making them ideal for calculating gateway addresses, service IPs, broadcast addresses, and validating subnet capacity.

The two primary functions in this category are cidrhost and cidrnetmask. Together, they allow Terraform configurations to derive host addresses and subnet masks directly from a CIDR prefix.

Back to Top

cidrhost: Calculating Host IP Addresses

The cidrhost function returns a specific IP address from a given CIDR block.

cidrhost(prefix, hostnum)

Where:

  • prefix is a CIDR-formatted network address (for example, 192.168.10.0/24)
  • hostnum is an integer offset into the host address space ("host number")

Terraform automatically performs the following:

  • Normalizes the network address
  • Validates the prefix length (/0../32)
  • Ensures the requested host number fits within the subnet

Special hostnum values have defined meanings:

  • cidrhost(prefix, 0) - network address
  • cidrhost(prefix, -1) - broadcast address
  • cidrhost(prefix, 1) - first host
  • cidrhost(prefix, -2) - last host

Practical use cases:

  • Determining gateway IP (typically hostnum = 1)
  • Selecting service endpoints at fixed offsets
  • Determining broadcast or last usable addresses

Back to Top

cidrnetmask: Deriving Subnet Masks

The cidrnetmask function returns the subnet mask in a dotted-decimal notation for a given CIDR prefix.

cidrnetmask(prefix)

Where:

  • prefix is any valid CIDR prefix (for example, 192.168.10.0/24)

This function is especially useful when interacting with providers that require subnet masks instead of CIDR notation.

Back to Top

Host-Level CIDR Analysis Example

This example demonstrates a comprehensive approach to host-level CIDR calculations in Terraform. It combines input validation, calculation of network properties, host selection, and binary/decimal visualization to demonstrate capabilities of Terraform host-level CIDR functions.

It does not provision any real infrastructure and is particularly useful as a learning and troubleshooting reference, rather than a practical code snippet. The complete code sample can be obtained from this repo.

variable "net_cidr" { type = string default = "192.168.10.15/24" description = "IPv4 network address in CIDR notation." validation { condition = can(regex(local.regex_cidr, var.net_cidr)) error_message = "Provide a valid IPv4 network address in CIDR notation." } } variable "host_num" { type = number default = 10 description = "Host number to determine IP address." validation { condition = local.max_host_num >= var.host_num error_message = "${var.net_cidr} cannot accommodate host #${var.host_num}" } } locals { regex_cidr = join("", [ "(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}", "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\/(?:[12]?\\d|3[0-2])" ]) max_host_num = pow(2, (32 - split("/", var.net_cidr)[1])) - 2 } locals { net_addr = split(".", cidrhost(var.net_cidr, 0)) net_addr_d = join(".", [for b in local.net_addr : format("%8d", b)]) net_addr_b = join(".", [for b in local.net_addr : format("%08b", b)]) net_mask = split(".", cidrnetmask(var.net_cidr)) net_mask_d = join(".", [for b in local.net_mask : format("%8d", b)]) net_mask_b = join(".", [for b in local.net_mask : format("%08b", b)]) net_bcst = split(".", cidrhost(var.net_cidr, -1)) net_bcst_d = join(".", [for b in local.net_bcst : format("%8d", b)]) net_bcst_b = join(".", [for b in local.net_bcst : format("%08b", b)]) frst_host = split(".", cidrhost(var.net_cidr, 1)) frst_host_d = join(".", [for b in local.frst_host : format("%8d", b)]) frst_host_b = join(".", [for b in local.frst_host : format("%08b", b)]) last_host = split(".", cidrhost(var.net_cidr, -2)) last_host_d = join(".", [for b in local.last_host : format("%8d", b)]) last_host_b = join(".", [for b in local.last_host : format("%08b", b)]) } output "net_info" { value = <<-EOT net_cidr : ${cidrsubnet(var.net_cidr, 0, 0)} net_addr : ${local.net_addr_d} : ${local.net_addr_b} net_mask : ${local.net_mask_d} : ${local.net_mask_b} net_bcst : ${local.net_bcst_d} : ${local.net_bcst_b} frst_host : ${local.frst_host_d} : ${local.frst_host_b} last_host : ${local.last_host_d} : ${local.last_host_b} num_hosts : ${local.max_host_num} EOT } output "host_ip" { value = <<-EOT subnet : ${cidrsubnet(var.net_cidr, 0, 0)} mask : ${cidrnetmask(var.net_cidr)} host_num : ${var.host_num} host_ip : ${cidrhost(var.net_cidr, var.host_num)} EOT }

Output:

$ terraform apply -auto-approve ... Outputs: host_ip = <<EOT subnet : 192.168.10.0/24 mask : 255.255.255.0 host_num : 10 host_ip : 192.168.10.10 EOT net_info = <<EOT net_cidr : 192.168.10.0/24 net_addr : 192. 168. 10. 0 : 11000000.10101000.00001010.00000000 net_mask : 255. 255. 255. 0 : 11111111.11111111.11111111.00000000 net_bcst : 192. 168. 10. 255 : 11000000.10101000.00001010.11111111 frst_host : 192. 168. 10. 1 : 11000000.10101000.00001010.00000001 last_host : 192. 168. 10. 254 : 11000000.10101000.00001010.11111110 num_hosts : 254 EOT

How it all works:

The validation block for the net_cidr variable uses a locally defined regular expression (local.regex_cidr) to ensure that the provided value (var.net_cidr) conforms to the IPv4 CIDR notation.

The validation block for the host_num variable relies on the calculated maximum number of usable hosts (local.max_host_num) to verify that the requested host number (var.host_num) fits within the usable address range of the specified subnet (var.net_cidr).

The configuration then computes a series of local values representing key network properties, such as the network address, broadcast address, and the first and last usable host addresses, using the cidrhost and cidrnetmask functions. Each value is rendered in both decimal (format("%8d", b)) and binary (format("%08b", b)) form, making it easier to visualize the addresses structure at the bit level.

The example produces two structured outputs (output "net_info" { ... } and output "host_ip" { ... }) that present network and host details in a clear, easy to read format.

It is also worth noting that cidrhost(var.net_cidr, 0) automatically normalizes the CIDR prefix by clearing the host portion of the input address. For example, the input 192.168.10.15/24 is treated as the network address 192.168.10.0/24, with the host portion (.15) removed.

As this example demonstrates, Terraform's host-level CIDR functions are powerful and flexible tools for network analysis and validation. That said, for deeper inspection or visual confirmation, it is often helpful to complement Terraform with external tools, such as the Visual IP and Mask Calculator, to validate calculations and gain additional insight into subnet address structure.

In the next section, we will move beyond individual host calculations and explore subnet generation using cidrsubnet and cidrsubnets, where Terraform's functionality becomes more powerful and more nuanced.

Back to Top

Subnet Address Generation with cidrsubnet and cidrsubnets

While cidrhost and cidrnetmask operate at the host and network boundary level, Terraform's cidrsubnet and cidrsubnets functions address a different class of problems: partitioning an address space into smaller networks. These functions are fundamental when designing repeatable VPC layouts, regional subnet structures, Kubernetes clusters, or environment-specific network topologies.

Although the two functions are related, they differ significantly in how parameters are supplied and how results are produced. Understanding these differences is essential to using them correctly and avoiding network configuration errors.

Back to Top

cidrsubnet: Deriving One Subnet

The cidrsubnet function derives a single subnet from a base CIDR block.

cidrsubnet(prefix, newbits, netnum)

Where:

  • prefix is the base CIDR block to subdivide.
  • newbits is the number of additional prefix bits to add to the base network prefix. This determines the prefix length ("size") of the generated subnet.
  • netnum is a zero-based index selecting which subnet to return.

A key point is that newbits is not the target prefix length. Instead, it represents the difference between the desired subnet prefix and the base prefix. Such approach differs from how subnetting is often described in networking documentation and can be a source of confusion for new users.

Example:

$ terraform console > cidrsubnet("172.16.0.0/16", 8, 0) "172.16.0.0/24" >

Here:

  • The base prefix is /16
  • Adding newbits = 8 yields /24
  • netnum = 0 selects the first /24 subnet

Back to Top

cidrsubnets: Producing Multiple Subnets

The cidrsubnets function generates multiple sequential subnets at once.

cidrsubnets(prefix, newbits...)

Where:

  • prefix is the base CIDR block to subdivide.
  • newbits... is a list of newbits values. Each entry defines the prefix length ("size") of the next subnet in sequence.

Example:

$ terraform console > cidrsubnets("172.16.0.0/16", 8, 4, 8) tolist([ "172.16.0.0/24", "172.16.16.0/20", "172.16.32.0/24", ]) > cidrsubnets("172.16.0.0/16", 8, 8, 4) tolist([ "172.16.0.0/24", "172.16.1.0/24", "172.16.16.0/20", ]) >

Here:

  • The base prefix is /16
  • In the first example cidrsubnets("172.16.0.0/16", 8, 4, 8):
    • The first newbits = 8 yields /24 and selects the first /24 subnet
    • The next newbits = 4 yields /20 and selects the next available /20 subnet
    • The last newbits = 8 yields /24 and selects the next available /24 subnet
    • Note the "gap" (172.16.1.0 - 172.16.15.255) between the first and the second subnets.
  • In the second example cidrsubnets("172.16.0.0/16", 8, 8, 4):
    • The first newbits = 8 yields /24 and selects the first /24 subnet
    • The next newbits = 8 yields /24 and selects the next available /24 subnet
    • The last newbits = 4 yields /20 and selects the next available /20 subnet

Important characteristics of cidrsubnets:

  • Subnets are allocated sequentially
  • You cannot skip indices or explicitly choose subnet numbers
  • All subnet sizes are determined solely by newbits

This makes cidrsubnets very effective when:

  • Subnets must be allocated contiguously
  • The number and size of subnets are fixed and known in advance
  • The resulting subnet list is accessed in a consistent way by index

However, it can become challenging when subnet counts vary by environment, or when it is required to reference subnets by names.

When working with cidrsubnets, it is often beneficial to complement Terraform with online networking tools similar to this IP Subnet Calculator. Such tools provide a visual breakdown of how a parent CIDR block is partitioned into smaller subnets and show network boundaries, address ranges, and broadcast addresses. With this information, you can more easily plan subnet allocation and identify potential address space fragmentation early in the design process. This is especially useful during preparation phases, where understanding trade-offs between subnet size, growth capacity, and efficient IP space utilization can help prevent costly network redesigns later.

Back to Top

Subnet Generation Example

The following example demonstrates how cidrsubnet can be used safely and predictably to generate multiple subnets from a base CIDR block.

It does not provision any real infrastructure and can be particularly useful as a learning and troubleshooting reference. The complete code sample can be obtained from this repo.

variable "base_cidr" { type = string default = "172.16.10.0/16" description = "IPv4 network address in CIDR notation." } variable "subnet_pref_length" { type = number default = 24 description = "Subnet prefix length" validation { condition = local.base_pref_length <= var.subnet_pref_length error_message = "Subnet prefix must be longer than the base CIDR prefix." } } variable "num_subnets" { type = number default = 4 description = "Number of subnets to generate" validation { condition = local.max_subnets >= var.num_subnets error_message = "${var.base_cidr} cannot accommodate ${var.num_subnets} x /${var.subnet_pref_length} subnets" } } variable "subnet_list" { type = list(string) default = ["us-central", "us-east", "us-west"] description = "List of subnet names" } locals { # Extract prefix length from the base CIDR (e.g. "16" from "x.x.x.x/16") base_pref_length = split("/", var.base_cidr)[1] # Determine the maximum number of subnets that can be created: max_subnets = pow(2, var.subnet_pref_length - local.base_pref_length) } output "subnets_by_num" { value = { base_cidr = cidrsubnet(var.base_cidr, 0, 0) subnets = [ for n in range(var.num_subnets) : cidrsubnet(var.base_cidr, var.subnet_pref_length - local.base_pref_length, n) ] } } output "subnets_by_name" { value = { base_cidr = cidrsubnet(var.base_cidr, 0, 0) subnets = { for n, s in var.subnet_list : s => cidrsubnet(var.base_cidr, var.subnet_pref_length - local.base_pref_length, n) } } }

Output:

$ terraform apply -auto-approve ... Outputs: subnets_by_name = { "base_cidr" = "172.16.0.0/16" "subnets" = { "us-central" = "172.16.0.0/24" "us-east" = "172.16.1.0/24" "us-west" = "172.16.2.0/24" } } subnets_by_num = { "base_cidr" = "172.16.0.0/16" "subnets" = [ "172.16.0.0/24", "172.16.1.0/24", "172.16.2.0/24", "172.16.3.0/24", ] }

How it all works:

The base CIDR (base_cidr) represents the parent network to be partitioned.

Rather than using newbits directly, the configuration works with prefix length (subnet_pref_length), which is more intuitive for most users.

The locals:

  • base_pref_length - Extracts the base prefix length
  • max_subnets - Calculates how many subnets of the requested size can fit into the base CIDR
  • Support validation of num_subnets and subnet_pref_length

The subnets_by_num output demonstrate how to use a for expression together with cidrsubnet() to generate a specific number (num_subnets) of contiguous subnets with a given prefix length (subnet_pref_length).

The subnets_by_name output demonstrate how to use a for expression together with cidrsubnet() to generate a list of subnets with a given prefix length (subnet_pref_length) based on a provided name or label list (subnet_list).

The demonstrated subnet generation approach is often preferred over cidrsubnets when:

  • Subnet must maintain stable identities and be easy addressable by names or labels
  • Environments differ in subnet count
  • IP address space allocation must be closely managed

Back to Top

Using Terraform Modules for Subnet Allocation

As network designs grow in complexity, CIDR calculations (cidrsubnet, cidrsubnets) directly embedded into root modules can quickly become difficult to maintain. This is where Terraform modules for subnet allocation become especially valuable. A well-designed module encapsulates IP address planning logic, enforces constraints, and presents a consistent interface to consumers.

Using a dedicated module for subnet allocation provides several important benefits:

  • Input validation - Modules can enforce rules such as:
    • Valid CIDR input format
    • Designated subnet boundaries
    • Minimum and maximum subnet sizes
  • Consistency across environments - By standardizing subnet calculation logic in a module, you can ensure that development, staging, and production environments follow the same IP addressing patterns, even if they differ in size or scale.
  • Improved readability and intent - Instead of embedding low-level CIDR math in every configuration, modules allow you to standardize common network allocation requests, such as "Create N subnets of size /24 from this CIDR block".
  • Reusability and extensibility - A subnet module can be reused across regions, accounts, or cloud providers, etc.

The official HashiCorp subnets module is a good example of these principles in practice. It calculates subnet addresses based on the following inputs:

  • base_cidr_block - A network address prefix in CIDR notation that all of the requested subnetwork prefixes will be allocated within.
  • networks - A list of objects describing requested subnetwork prefixes, each including a subnetwork name and new_bits specifying the number of additional network prefix bits to add to the existing prefix on base_cidr_block.

Alternatively, if you prefer a more traditional approach utilizing prefix length notation (instead of new_bits), model your code after the get_subnets module presented in our demo repository.

Back to Top

Summary

This article explored how Terraform's built-in IP network and CIDR functions can be used to analyze, validate, and generate IP addressing schemes directly within infrastructure-as-code configuration.

We examined the following Terraform's core IP and CIDR-related functionality in detail:

  • Host-level calculations with cidrhost and cidrnetmask, showing how to derive host IPs, normalize network prefixes, and extract netmask information.
  • Subnet generation with cidrsubnet and cidrsubnets, highlighting how Terraform partitions address space and how its parameter model differs from traditional subnet calculators.
  • Practical examples, including validation, structured outputs, and visualization techniques that make CIDR math easier to understand and safer to use.
  • Challenges and limitations, especially when subnet counts or sizes vary by environment, and when IP address space must be carefully planned over time.
  • Terraform modules, encapsulating subnet logic and improving code reuse and long-term maintainability.

The key takeaway is that Terraform's IP network functions are powerful but low-level. They are ideal building blocks, but for anything beyond simple scenarios, combining them with input validation, external IP address visualization tools, and reusable modules results in fewer errors, and more scalable network designs.

Back to Top

Wait, What About IPv6?

Terraform's IP network functions - cidrhost, cidrsubnet, and cidrsubnets work with both IPv4 and IPv6 CIDR blocks. From a functional perspective, the same concepts apply: Terraform treats IPv6 CIDRs as larger address spaces and performs the same prefix-based calculations. Note that cidrnetmask only accepts IPv4 addresses and returns an error if you use an IPv6 address.

There are some important differences and considerations when adapting the examples in this article for IPv6:

  • Much larger address space - IPv6 subnets are typically /64 or larger, and while cidrhost still works, direct address allocation to hosts is rarely practical or meaningful in IPv6 designs.
  • Subnet planning approach - IPv6 subnetting usually focuses on hierarchical allocation (for regions, environments, or tiers) rather than conserving address space. This often significantly simplifies planning tasks requiring cidrsubnet and cidrsubnets.
  • Validation - While prefix length validation approach remains the same, the regex-based CIDR validation must be adapted to reflect IPv6 address structure.

At a high level, Terraform's IP network functions, such as cidrhost, cidrsubnet, and cidrsubnets, behave consistently across both IPv4 and IPv6. The primary differences lie not in the functions themselves, but in the underlying design goals and the network planning patterns that are specific to IPv4 versus IPv6 addressing models.