network-programmingterraformip-addresssubnetcidr

How to subdivide CIDR subnet in Terraform with cidrsubnet Function?


I would like to have addresses like this for subnets within an AWS region:

10.0.0.0/21
10.0.8.0/21
10.0.16.0/21
10.0.24.0/21
10.0.32.0/21
...

Basically, increment the second number by 8.

I also will have a VPN per region, and they should share the same network. So I want them like this:

10.1.0.0/16
10.2.0.0/16
10.3.0.0/16
10.4.0.0/16
10.5.0.0/16
...

Basically incrementing the 3rd number by 1 each time. If not possible to go 1 each step, then 8 as well.

I have read in depth about CIDR blocks, but still don't quite no how to apply, specifically in the following context.

I would like to have a suite of nested Terraform modules that start with 1 CIDR block, and subnet it into smaller and smaller chunks. First, it will subdivide it by region (3rd number), and then by availability zone (second number). Everything will be dynamic, so you only supply the initial CIDR block literally.

It seems that I should be using cidrsubnet:

cidrsubnet(prefix, newbits, netnum)

I got this far sort of:

cidrsubnet(var.cidr_block, 13, netnum?)

That 13 I know will go from /8 to /21. But that goes straight to the availability zone (and I don't know what to do with netnum). I would like to do something like this:

# main.tf
variable "cidr_block" {
  default = "10.0.0.0/8"
}

module "region1" {
  source = "./region" # ./region/main.tf
  cidr_block = cidrsubnet(var.cidr_block, 8?, count.index?) # 10.0.0.0/16?
}

module "region2" {
  source = "./region" # ./region/main.tf
  cidr_block = cidrsubnet(var.cidr_block, 8?, count.index?) # 10.1.0.0/16?
}

// ...

Then in the region submodule:

# region/main.tf
module "availability_zone1" {
  source = "./availability_zone" # ./availability_zone/main.tf
  cidr_block = cidrsubnet(var.cidr_block, 5?, count.index?) # 10.1.0.0/21
}

module "availability_zone2" {
  source = "./availability_zone" # ./availability_zone/main.tf
  cidr_block = cidrsubnet(var.cidr_block, 5?, count.index?) # 10.1.8.0/21
}

module "availability_zone3" {
  source = "./availability_zone" # ./availability_zone/main.tf
  cidr_block = cidrsubnet(var.cidr_block, 5?, count.index?) # 10.1.16.0/21
}

// ...

So basically, subdividing it once from /8 -> /16, then again from /16 -> /21. The count.index I have not used yet, but somehow I want it to know what position it is in the call sequence. If not possible, that's fine I can just pass an index along with the module.

How do I write this using the cidrsubnet function? The most important part of the question is what I am supposed to put in each slot in the function: cidrsubnet(prefix, newbits, netnum), I don't know specifically what netnum should be like.


Solution

  • At the time I'm writing this answer, Terraform 0.13.0 is at release candidate 1 and due to be released in a few weeks. Terraform 0.13.0 includes the ability to use for_each and count for modules, which makes it more straightforward to use the hashicorp/subnets/cidr module for a multi-level addressing plan like this. An advantage of that module, compared to doing individual cidrsubnet calls directly, is that it allows you to assign symbolic names to each of your networks and it has some conventions to help with deprecating and later reusing parts of your address space without reassigning all of the neighboring networks.

    You mentioned AWS regions and subnets in your example so for the sake of showing this I'm going to use AWS region names and availability zones as the symbolic names for the networks, but you can use whatever names make sense for your architecture as long as each network ends up with a unique identifier.

    locals {
      aws_region_zones = tolist([
        {
          region = "us-west-2"
          zones  = ["us-west-2a", "us-west-2b"]
        },
        {
          region = "us-east-1"
          zones  = ["us-east-1a", "us-east-2f"]
        },
        # When allocating new regions or zones,
        # always add them at the end of their
        # respective list to avoid renumbering
        # the existing address allocations.
      ])
    }
    
    module "regional" {
      source = "hashicorp/subnets/cidr"
    
      base_cidr_block = "10.0.0.0/8"
      networks = [
        for regional in local.aws_region_zones : {
          name     = regional.region
          new_bits = 8
        }
      ]
    }
    
    module "zonal" {
      source   = "hashicorp/subnets/cidr"
      for_each = {
        for net in module.regional.networks : net.name => net
      }
    
      base_cidr_block = each.value.cidr_block
      networks = [
        for zone_name in local.aws_region_zones[each.key].zones : {
          name     = regional.region
          new_bits = 5
        }
      ]
    }
    
    output "region_cidr_blocks" {
      value = tomap({
        for net in module.regional.networks : net.name => {
          cidr_block = net.cidr_block
          zones = tomap({
            for subnet in module.zonal[net.name].networks : subnet.name => {
              cidr_block = subnet.cidr_block
            }
          })
        }
      })
    }
    

    The region_cidr_blocks output value will be a map of objects where each object represents a region, and then a nested map for each availability zone.