PureDevOps Community

How to configure vpc, subnets using terraform cidrsubnet, cidrhost

I am trying to create two subnet using the cidrsubnet function that Terraform supports. The VPC cidr I have is “10.32.0.0/16”. I am trying to have subnets 10.32.1.0/27 and 10.32.3.0/27. I am having some trouble getting the cidrsubnet function in order to achieve this. What I have so far is:

cidrsubnet(10.32.0.0/16, 11, netnum???)

I do not understand what value I need for the netnum in order to get the value I want. Any explanation on this part of the function would be helpful. I’ve tried reading the documentation on this function, but it seems limited.

The documentation section Netmasks and Subnets tries to give enough information to understand how this function relates to IP address conventions, and mentions the Unix command ipcalc as a way to visualize how your CIDR netmasks are represented.

Let’s see how ipcalc describes both your starting prefix and your two intended subnet prefixes:

$ ipcalc 10.32.0.0/16
Address:   10.32.0.0            00001010.00100000. 00000000.00000000
Netmask:   255.255.0.0 = 16     11111111.11111111. 00000000.00000000
Wildcard:  0.0.255.255          00000000.00000000. 11111111.11111111
=>
Network:   10.32.0.0/16         00001010.00100000. 00000000.00000000
HostMin:   10.32.0.1            00001010.00100000. 00000000.00000001
HostMax:   10.32.255.254        00001010.00100000. 11111111.11111110
Broadcast: 10.32.255.255        00001010.00100000. 11111111.11111111
Hosts/Net: 65534                 Class A, Private Internet

$ ipcalc 10.32.1.0/27
Address:   10.32.1.0            00001010.00100000.00000001.000 00000
Netmask:   255.255.255.224 = 27 11111111.11111111.11111111.111 00000
Wildcard:  0.0.0.31             00000000.00000000.00000000.000 11111
=>
Network:   10.32.1.0/27         00001010.00100000.00000001.000 00000
HostMin:   10.32.1.1            00001010.00100000.00000001.000 00001
HostMax:   10.32.1.30           00001010.00100000.00000001.000 11110
Broadcast: 10.32.1.31           00001010.00100000.00000001.000 11111
Hosts/Net: 30                    Class A, Private Internet

$ ipcalc 10.32.3.0/27
Address:   10.32.3.0            00001010.00100000.00000011.000 00000
Netmask:   255.255.255.224 = 27 11111111.11111111.11111111.111 00000
Wildcard:  0.0.0.31             00000000.00000000.00000000.000 11111
=>
Network:   10.32.3.0/27         00001010.00100000.00000011.000 00000
HostMin:   10.32.3.1            00001010.00100000.00000011.000 00001
HostMax:   10.32.3.30           00001010.00100000.00000011.000 11110
Broadcast: 10.32.3.31           00001010.00100000.00000011.000 11111
Hosts/Net: 30                    Class A, Private Internet

In ipcalc's notation a space in the binary representation shows the boundary between the network part and the host part of the address. We can see that, as you showed in your example, the two desired results have 11 more bits in the network part than the base address has.

Let’s just use one of these as an example to figure out what the “netnum” will be. We’ll use 10.32.1.0/27, and let’s put the “address” line from the base address and the subnet address next to each other so we can see the difference more clearly:

Address:   10.32.0.0            00001010.00100000. 00000000.00000000
Address:   10.32.1.0            00001010.00100000.00000001.000 00000

The value for netnum is the number represented by those eleven additional binary digits in the second case: 00000001000. The decimal equivalent of that binary number is 8, so the netnum for this one would be 8, which we can confirm by calling cidrsubnet from the terraform console prompt:

> cidrsubnet("10.32.0.0/16", 11, 8)
"10.32.1.0/27"

To generalize this, notice that in the binary representation above the third decimal number in the IP address represents only a part of the network number: in 00000001.000 we can see that there are three binary digits that belong to the forth octet. Because the fourth binary place represents the number of eights, the general rule is that to find the netnum that will give a particular value for the third octet you’ll need to multiply that value by eight. Three times eight is 24, so if we set netnum to 24 then we’ll get the other prefix you wanted:

> cidrsubnet("10.32.0.0/16", 11, 24)
"10.32.3.0/27"

With that said, it’s worth noting that this particular addressing scheme is “wasting” part of the address space. If you leave those last three bits always set to zero then in practice you only have eight bits worth of network numbers, and so you can only have 256 networks. If you want to make use of all eleven bits of the network number space (2,048 networks) you’ll need to start your network numbering at zero and count upwards normally from there, which will then occupy all of your network space densely:

> cidrsubnet("10.32.0.0/16", 11, 0)
"10.32.0.0/27"
> cidrsubnet("10.32.0.0/16", 11, 1)
"10.32.0.32/27"
> cidrsubnet("10.32.0.0/16", 11, 2)
"10.32.0.64/27"
> cidrsubnet("10.32.0.0/16", 11, 3)
"10.32.0.96/27"
> cidrsubnet("10.32.0.0/16", 11, 4)
"10.32.0.128/27"
> cidrsubnet("10.32.0.0/16", 11, 5)
"10.32.0.160/27"
> cidrsubnet("10.32.0.0/16", 11, 6)
"10.32.0.192/27"
> cidrsubnet("10.32.0.0/16", 11, 7)
"10.32.0.224/27"
> cidrsubnet("10.32.0.0/16", 11, 8)
"10.32.1.0/27"

Because the conventional IP address notation is as four decimal numbers from 0 to 255, the counting above may seem counter-intuitive, but in the underlying binary notation this is just normal counting up from zero, which avoids skipping over network numbers 0 through 7, 9 through 15, etc.

If you are creating a systematic network addressing plan for a set of subnets that each have a particular identified purpose, you might find it helpful to use the higher level of abstraction offered by the hashicorp/subnets/cidr module, which just takes the number of bits to use for numbering each network and calculates suitable netnum values itself:

module "subnets" {
  source  = "hashicorp/subnets/cidr"
  version = "1.0.0"

  base_cidr_block = "10.32.0.0/16"
  networks = [
    {
      name     = "first",
      new_bits = 11
    },
    {
      name     = "second",
      new_bits = 11
    },
  ]
}

The above would produce a network_cidr_blocks map like this:

{
  first  = "10.32.0.0/27"
  second = "10.32.0.32/27"
}

Elsewhere in your configuration you can write module.subnets.network_cidr_blocks.first to get the first address. Assuming you pick meaningful names rather than my contrived “first” and “second”, that will tend to make the rest of your configuration easier to read because future readers won’t need to memorize what each IP address range represents.

Credit: Martin