Fun with Subnets in Terraform (with AWS)

Here’s a quick example of how you can use Terraform’s cidrsubnet function to streamline your use of IP Subnets and calculate a number of different subnets from a single CIDR range (or supernet).

https://developer.hashicorp.com/terraform/language/functions/cidrsubnet

Explaination

The cidrsubnet function can be a bit weird when first looking at it. But we’re going to have an example where we have a 10.192.36.0/22 CIDR Block (range) for the VPC, then CIDR Block (range) for each of the subnets within of /28 which is split across two /24 networks, one for each Availability Zone (AZ).

cidrsubnet(prefix, newbits, netnum)

We know the prefix, that is something like: “10.192.36.0/22”, in our case the CIDR range for the VPC. That’s simple enough.

The next part, newbits. What this means is the number of extra bits that are being added to this prefix to get us to the CIDR Block we want. For example, we want to have a number of /28s subnets, so, we be setting this to 6 (bits), because: 22 + 6 = 28.

Finally netnum, this is the network number (netnum), is a whole number that can be represented as a binary integer with no more than newbits binary digits, which will be used to populate the additional bits added to the prefix. So within a /22 we have a different of 6 bits with a /28 and so we have 26 = 64, which means we have 64 of the /28 networks within that particular /22 and therefore the netnum is the identifier of this particular /28 network.

(10.192.36.0/22, 6, 0) = 10.192.36.0/28, i.e. the first /28 in the /22.

(10.192.36.0/22, 6, 1) = 10.192.36.16/28, i.e. the second /28 in the /22. Where the “16” in the last octet of 10.192.36.16/28 is the network address of the next /22 subnet.

(10.192.36.0/22, 6, 16) = 10.192.37.0/28, i.e. the sixteenth /28 in the /22. Where the “0” in in the last octet of 10.192.37.0/28 is the network address of the sixteenth /22 subnet.

And so on up to the maximum of 64 of the /28 networks available within that /22.

Example

In this particular example I have a /22 network, 10.192.36.0/22 which I want to split into two /24 subnets, one for each Availability Zone, and within each of those /24 Subnets a number of /28 Subnets for each of the various Subnets within. 

Now you could specify all these manually, but instead I’m shortening it slightly by allowing the input of the CIDR range for the VPC, then all the remaining subnets created will crib off that to create themselves. Note that I’m also not using the for.each loops to mean I only need a single Subnet definition within the Terraform, because one this is an example, so want to make it clearer, and two in this particular use case we needed some named subnets defined to make it easier to manage for this very specific and static use case, but anyway, we first create a variable and assign it a value.

vpc_cidr = "10.192.36.0/22"

Now we create the variable for this value:

variable "vpc_cidr" {
  description = "The IP Subnet for the VPC."
  type        = string
}

Finally, to illustrate we can get this just to output the values in the output.tf to show what it would look like, then we can apply it to a real resource.

output "AZ1_cidr_block_1" {
    value = cidrsubnet(var.vpc_cidr,6,0)
}

output "AZ1_cidr_block_2" {
    value = cidrsubnet(var.vpc_cidr,6,1)
}

output "AZ1_cidr_block_3" {
    value = cidrsubnet(var.vpc_cidr,6,2)
}

output "AZ2_cidr_block_1" {
    value = cidrsubnet(var.vpc_cidr,6,16)
}

output "AZ2_cidr_block_2" {
    value = cidrsubnet(var.vpc_cidr,6,17)
}

output "AZ2_cidr_block_3" {
    value = cidrsubnet(var.vpc_cidr,6,18)
}

We then get this output when running a plan:

Changes to Outputs:
  + AZ1_cidr_block_1 = "10.192.36.0/28"
  + AZ1_cidr_block_2 = "10.192.36.16/28"
  + AZ1_cidr_block_3 = "10.192.36.32/28"
  + AZ2_cidr_block_1 = "10.192.37.0/28"
  + AZ2_cidr_block_2 = "10.192.37.16/28"
  + AZ2_cidr_block_3 = "10.192.37.32/28"

Nice, that is doing what we want, so, using it in a real subnet definition is as simple as:

resource "aws_subnet" "mgmt1" {
  vpc_id     = aws_vpc.vpc.id
  #cidr_block = "10.192.36.0/28"
  cidr_block = cidrsubnet(var.vpc_cidr,6,0) # First /28 Subnet VPC CIDR Range for AZ1
  availability_zone       = "eu-west-2a"
  map_public_ip_on_launch = "false"

  tags = {
    Name = "mgmt1"
  }
}

Leave a comment