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