Featured image of post Terraform CIDR Functions

Terraform CIDR Functions

Recently, when I was using Terraform to provision virtual network structure, I had to find a solution to create 4 predefined subnets within a given network range. Initially - as PoC - I used string manipulation to obtain the ranges, but I haven’t liked it. Then I found two built-in terraform functions, that are a great help when working with network aspect: cidrsubnet and cidrsubnets.

CIDR notation 101

Wikipedia states, that

Classless Inter-Domain Routing (CIDR /ˈsaɪdər, ˈsɪ-/) is a method for allocating IP addresses and for IP routing

and

CIDR notation is a compact representation of an IP address and its associated network mask.

An example would be 10.0.0.0/16. This is all nice, but I struggle to remember the CIDR notation stuff, so I always have a short cheatsheet for it:

/16 == 65 536 addresses, ex. 10.0.0.0 - 10.0.255.255
/17 == 32 768 addresses
/18 == 16 384 addresses
...
/23 ==    512 addresses
/24 ==    256 addresses, ex. 10.0.0.0 - 10.0.0.255
/25 ==    128 addresses
/26 ==     64 addresses
/27 ==     32 addresses
...
/32 ==      1 address

I work with Azure and I usually see or use /16, /23, /24, /26, and /27 networks and subnets. And when I divide a VNet into subnets, I have them of the same size.

An example: /24 address space (256 addresses) divided into four equal /26 ranges, 64 addresses each:

vnet:    10.0.0.0/24   -> 256 addresses, 10.0.0.0   - 10.0.0.255

subnet0: 10.0.0.0/26   -> 64 addresses,  10.0.0.0   - 10.0.0.63
subnet1: 10.0.0.64/26  -> 64 addresses,  10.0.0.64  - 10.0.0.127
subnet2: 10.0.0.128/26 -> 64 addresses,  10.0.0.128 - 10.0.0.191
subnet3: 10.0.0.192/26 -> 64 addresses,  10.0.0.192 - 10.0.0.255

CIDR notation handling in Terraform

To avoid mundane string manipulation, and to ease working with CIDR notation Terraform has four functions: cidrhost, cidrnetmask, cidrsubnet, and cidrsubnets. For my purpose, I will use the last two:

cidrsubnet(prefix, newbits, netnum) - calculates subnet at netnum position for a given prefix cidrsubnets(prefix, newbits...) - calculates subsequent subnets for a given prefix

Let’s stick to the /24 -> 4 x /26 example above. To split the /24 VNet into four subnets I can write four cidrsubnet functions:

D:\temp\_terraform> terraform console

> cidrsubnet("10.244.200.0/24", 2, 0)
"10.244.200.0/26"
> cidrsubnet("10.244.200.0/24", 2, 1)
"10.244.200.64/26"
> cidrsubnet("10.244.200.0/24", 2, 2)
"10.244.200.128/26"
> cidrsubnet("10.244.200.0/24", 2, 3)
"10.244.200.192/26"
> cidrsubnet("10.244.200.0/24", 2, 4)
│ Error: Error in function call
│   on <console-input> line 1:
│   (source code not available)
│ Call to function "cidrsubnet" failed: prefix extension of 2 does not accommodate a subnet numbered 4.

I’ll explain the above example step by step.

The definition of cidrsubnet is cidrsubnet(prefix, newbits, netnum). prefix is the VNet address space, specified in the CIDR format. In my case: 10.244.200.0/24. I want /26 subnets, so as newbits I pass the value 2, which means dear Terraform, please add 2 to the /24 space, to obtain a few /26 address ranges. In the background, Terraform splits the /24 range into four equal /26 subranges, and enumerates them starting from 0. I imagine this as Terraform keeping internally an array of four subnets, like in the example above. If I want the first range, I set netnum as 0, if I want the third, I set netnum as 2, etc. And since the /24 range is split equally into four /26 ranges, I can’t assign newnum == 4, as the range does not exist.

I can either write four separate cidrsubnet() expressions to get four subnets, or I can ask Terraform to do it for me in one command using cidrsubnets(). Again as a reminder:

cidrsubnets(prefix, newbits...)

D:\temp\_terraform> terraform console

> cidrsubnets("10.244.200.0/24", 2, 2, 2, 2)
tolist([
  "10.244.200.0/26",
  "10.244.200.64/26",
  "10.244.200.128/26",
  "10.244.200.192/26",
])

The above means: dear Terraform, take this /24 space, then split it for me into ranges; first take 64 addresses, then again take 64 addresses, then again take 64 addresses, then again take 64 addresses. Terraform knows I want 64 addresses, as he creates subsequent ranges by adding the newbits value to the given /24 space. This way, he obtains /26 range, that contains 64 addresses. Each newbits value adds a range to the previously created ones.

It’s still unclear, so let’s go again with an example:

                 cidrsubnets("10.244.200.0/24", 2, 2, 2, 2)
                                           |    |  |  |  |
                        /24 address space -+    |  |  |  |
     /26 address space (/24 + 2), 64 addresses -+  |  |  | 10.244.200.0   .. 10.244.200.63
     /26 address space (/24 + 2), 64 addresses ----+  |  | 10.244.200.64  .. 10.244.200.127
     /26 address space (/24 + 2), 64 addresses -------+  | 10.244.200.128 .. 10.244.200.191
     /26 address space (/24 + 2), 64 addresses ----------+ 10.244.200.192 .. 10.244.200.255

Using these techniques, I can split my address range in multiple ways, and I do not have to stick to the one newbits value. Both examples below are equivalent:

D:\temp\_terraform> terraform console

> cidrsubnets("10.244.200.0/24", 2, 2, 3, 3, 2)
tolist([  
"10.244.200.0/26",  
"10.244.200.64/26",  
"10.244.200.128/27",  
"10.244.200.160/27",  
"10.244.200.192/26",  
])

> cidrsubnet("10.244.200.0/24", 2, 0)
"10.244.200.0/26"
> cidrsubnet("10.244.200.0/24", 2, 1)
"10.244.200.64/26"
> cidrsubnet("10.244.200.0/24", 3, 4)
"10.244.200.128/27"
> cidrsubnet("10.244.200.0/24", 3, 5)
"10.244.200.160/27"
> cidrsubnet("10.244.200.0/24", 2, 3)
"10.244.200.192/26"

One thing to remember when using cidrsubnet() - it splits the initial range into an equal number of subranges. So in the example above I want /26 and /27 subnets, so Terraform creates these arrays in the background:

tolist([
  "10.244.200.0/26",
  "10.244.200.64/26",
  "10.244.200.128/26",
  "10.244.200.192/26",
])

tolist([
  "10.244.200.0/27",
  "10.244.200.32/27",
  "10.244.200.64/27",
  "10.244.200.96/27",
  "10.244.200.128/27",
  "10.244.200.160/27",
  "10.244.200.192/27",
  "10.244.200.224/27",
])

As the first /26 range encompasses the first two /27 ranges, and the second /26 range encompasses the third and fourth /27 ranges, I need to use netnum == 4 (fifth range) for the third subnet.

For completness the cidrsubnets("10.244.200.0/24", 2, 2, 3, 3, 2) diagram

                 cidrsubnets("10.244.200.0/24", 2, 2, 3, 3, 2)
                                           |    |  |  |  |  |
                        /24 address space -+    |  |  |  |  |
     /26 address space (/24 + 2), 64 addresses -+  |  |  |  | 10.244.200.0   .. 10.244.200.63
     /26 address space (/24 + 2), 64 addresses ----+  |  |  | 10.244.200.64  .. 10.244.200.127
     /27 address space (/24 + 3), 32 addresses -------+  |  | 10.244.200.128 .. 10.244.200.159
     /27 address space (/24 + 3), 32 addresses ----------+  | 10.244.200.160 .. 10.244.200.191
     /26 address space (/24 + 2), 64 addresses -------------+ 10.244.200.192 .. 10.244.200.255

Assign ranges to subnets

Once I know how to use cidrsubnet() and cidrsubnets() functions, I can assing ranges to the subnets. Let’s say, I have this kind of network segmentation:

  • subnetA range 10.x.y.0 .. 10.x.y.63
  • subnetB range 10.x.y.64 .. 10.x.y.127
  • subnetC range 10.x.y.128 .. 10.x.y.191
  • subnetD range 10.x.y.192 .. 10.x.y.255

With this terraform code I can create subnets for a given virtual network:

provider "azurerm" {
  features {}
}

locals {
  vnet1_address_space = "10.244.200.0/24"
  vnet2_address_space = "10.244.204.0/24"
  
  subnets       = {
    "subnetA" = 0
    "subnetB" = 1
    "subnetC" = 2
    "subnetD" = 3
  }

  # for cidrsubnets() approach
  subnet_ranges = cidrsubnets(local.vnet2_address_space, 2, 2, 2, 2)
}

resource "azurerm_resource_group" "rg" {
  name     = "rg-shared-networking"
  location = "West Europe"
}

# using cidrsubnet()
resource "azurerm_virtual_network" "vnet1" {
  name                = "vnet-shared1"
  address_space       = [local.vnet1_address_space]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_subnet" "snet1" {
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet1.name
  for_each             = local.subnets
  name                 = "snet-${each.key}"
  address_prefixes     = [cidrsubnet(local.vnet1_address_space, 2, each.value)]
}

# using cidrsubnets()
resource "azurerm_virtual_network" "vnet2" {
  name                = "vnet-shared2"
  address_space       = [local.vnet2_address_space]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_subnet" "snet2" {
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet2.name
  for_each             = local.subnets
  name                 = "snet-${each.key}"
  address_prefixes     = [local.subnet_ranges[each.value]]
}

When run with terraform plan, the code above produces this output:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "westeurope"
      + name     = "rg-shared-networking"
    }

  # azurerm_subnet.snet1["subnetA"] will be created
  + resource "azurerm_subnet" "snet1" {
      + address_prefixes                               = [
          + "10.244.200.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetA"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared1"
    }

  # azurerm_subnet.snet1["subnetB"] will be created
  + resource "azurerm_subnet" "snet1" {
      + address_prefixes                               = [
          + "10.244.200.64/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetB"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared1"
    }

  # azurerm_subnet.snet1["subnetC"] will be created
  + resource "azurerm_subnet" "snet1" {
      + address_prefixes                               = [
          + "10.244.200.128/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetC"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared1"
    }

  # azurerm_subnet.snet1["subnetD"] will be created
  + resource "azurerm_subnet" "snet1" {
      + address_prefixes                               = [
          + "10.244.200.192/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetD"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared1"
    }

  # azurerm_subnet.snet2["subnetA"] will be created
  + resource "azurerm_subnet" "snet2" {
      + address_prefixes                               = [
          + "10.244.204.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetA"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared2"
    }

  # azurerm_subnet.snet2["subnetB"] will be created
  + resource "azurerm_subnet" "snet2" {
      + address_prefixes                               = [
          + "10.244.204.64/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetB"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared2"
    }

  # azurerm_subnet.snet2["subnetC"] will be created
  + resource "azurerm_subnet" "snet2" {
      + address_prefixes                               = [
          + "10.244.204.128/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetC"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared2"
    }

  # azurerm_subnet.snet2["subnetD"] will be created
  + resource "azurerm_subnet" "snet2" {
      + address_prefixes                               = [
          + "10.244.204.192/26",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "snet-subnetD"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "rg-shared-networking"
      + virtual_network_name                           = "vnet-shared2"
    }

  # azurerm_virtual_network.vnet1 will be created
  + resource "azurerm_virtual_network" "vnet1" {
      + address_space       = [
          + "10.244.200.0/24",
        ]
      + dns_servers         = (known after apply)
      + guid                = (known after apply)
      + id                  = (known after apply)
      + location            = "westeurope"
      + name                = "vnet-shared1"
      + resource_group_name = "rg-shared-networking"
      + subnet              = (known after apply)
    }

  # azurerm_virtual_network.vnet2 will be created
  + resource "azurerm_virtual_network" "vnet2" {
      + address_space       = [
          + "10.244.204.0/24",
        ]
      + dns_servers         = (known after apply)
      + guid                = (known after apply)
      + id                  = (known after apply)
      + location            = "westeurope"
      + name                = "vnet-shared2"
      + resource_group_name = "rg-shared-networking"
      + subnet              = (known after apply)
    }

Plan: 11 to add, 0 to change, 0 to destroy.
(c) Bartosz Ratajczyk
Built with Hugo
Theme Stack designed by Jimmy