skip to Main Content

Using Terraform variables, how can I output a CIDR block for each availability zone in a region?

I can get close, but I’m obviously missing something with the cidrsubnet function.

[for az in data.aws_availability_zones.available.names : cidrsubnet("10.0.0.0/16", 8, 101)]

My current output is –

[
  "10.0.101.0/24",
  "10.0.101.0/24",
  "10.0.101.0/24",
  "10.0.101.0/24"
]

What I desire is –

[
  "10.0.101.0/24",
  "10.0.102.0/24",
  "10.0.103.0/24",
  "10.0.104.0/24"
]

Example Code

data "aws_ami" "app_ami" {
  most_recent = true

  filter {
    name   = "name"
    values = ["bitnami-tomcat-*-x86_64-hvm-ebs-nami"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["979382823631"] # Bitnami
}

data "aws_availability_zones" "available" {
  state = "available"
}

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs            = data.aws_availability_zones.available.names
  public_subnets = [for az in data.aws_availability_zones.available.names : cidrsubnet("10.0.0.0/16", 8, 101)]

  enable_nat_gateway = true
}

resource "aws_instance" "blog" {
  ami           = data.aws_ami.app_ami.id
  instance_type = "t3.nano"

  vpc_security_group_ids = [module.blog_security_group.security_group_id]

  tags = {
    Name = "HelloWorld"
  }
}

module "blog_security_group" {
  source      = "terraform-aws-modules/security-group/aws"
  version     = "5.1.0"
  name        = "blog"
  description = "Allow HTTP and HTTPS from my IP in."

  vpc_id = data.aws_vpc.default.id

  ingress_rules       = ["http-80-tcp", "https-443-tcp"]
  ingress_cidr_blocks = ["0.0.0.0/0"]

  egress_rules       = ["all-all"]
  egress_cidr_blocks = ["0.0.0.0/0"]
}

3

Answers


  1. I’ve tested this and it works for me using Terraform version 1.5.3

    data "aws_vpc" "selected" {
      filter {
        name = "tag:Name"
        values = ["YOUR-VPC-NAME"]
      }
    }
    
    # Selects all subnets
    #data "aws_subnets" "example" {
    #  filter {
    #    name = "vpc-id"
    #    # uses data source above
    #    values = [data.aws_vpc.selected.id]
    #  }
    #}
    
    data "aws_subnets" "example" {
      filter {
        name = "vpc-id"
        # uses data source above
        values = [data.aws_vpc.selected.id]
      }
      filter {
        # assumes you have Name tags populated on your subnets
        # replace Name tag values with your own 
        # or use any tag:value you have that clearly indicates what your public subnets are
        name = "tag:Name"
        values = ["public-subnet-a","public-subnet-b","public-subnet-c","public-subnet-d"]
      }
    }
    
    data "aws_subnet" "example" {
      for_each = { for index, subnetid in data.aws_subnets.example.ids : index => subnetid }
      id = each.value
    }
    
    output "subnet_cidr_blocks" {
      value = [for s in data.aws_subnet.example : s.cidr_block]
    }
    
    Login or Signup to reply.
  2. It sounds like you want to create a list depending on how many elements are present in data.aws_availability_zones.available.names, in which case you can use:

    $ terraform console
    > formatlist("10.0.10%v.0/24", range(1, length(data.aws_availability_zones.available.names)+1))
    tolist([
      "10.0.101.0/24",
      "10.0.102.0/24",
      "10.0.103.0/24",
      "10.0.104.0/24",
    ])
    
    Login or Signup to reply.
  3. One tricky aspect of a design like this is that AWS availability zones are not inherently ordered, and the zones available to your account could potentially change over time in ways that might renumber previously-allocated subnets, which would be very disruptive.

    To avoid such problems, a typical approach is to preallocate a subnet number to each possible availability zone letter, and then sparsely populate your address space with only the subnets corresponding to availability zones that are really available in your account. For example:

    data "aws_availability_zones" "available" {
      state = "available"
    }
    
    locals {
      az_network_numbers = tomap({
        a = 101
        b = 102
        c = 103
        d = 104
        e = 105
        # (and so on for however many AZ letters you'd like to support)
      })
      available_zone_letters = tomap({
        for az in data.aws_availability_zones.available.names :
        az => regex("[a-z]$", az) # take only the final letter
      })
      available_zone_cidr_blocks = tomap({
        for az, k in local.available_zone_letters :
        az => cidrsubnet("10.0.0.0/16", 8, local.az_network_numbers[k])
      })
    }
    

    local.available_zone_cidr_blocks will then be a map from availability zone to generated CIDR prefix address, such as:

    tomap({
      "us-east-1a" = "10.0.0.101/24"
      "us-east-1c" = "10.0.0.103/24"
      "us-east-1d" = "10.0.0.104/24"
    })
    

    Notice that in this example us-east-1b isn’t available and so it’s left 10.0.0.102/24 unassigned. If us-east-1b were to become available later then it could be allocated 10.0.0.102/24 without renumbering anything else. If us-east-1c were to become unavailable later then 10.0.0.103/24 would become vacant but us-east-1d would stay allocated to "10.0.0.104/24" rather than being reallocated to "10.0.0.103/24", preventing disruption to objects in that availability zone.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search