0

Currently learning terraform and I am trying to create 2 VPCs (dev and stg) below. I would like to populate cidr_block and availability_zone in subnets.tf by accessing ap-southeast-1* and public_cidr_block in the map I created. What is the proper way to do this?

variables.tf

variable "vpcs" {
  type        = map(object({}))
  description = "VPCs"
}

vpc.tfvars

vpcs = {
  "dev" = {
     "ap-southeast-1a" = {
        "public_cidr_block" : "10.1.1.0/24",
        "app_cidr_block" : "10.1.2.0/24",
        "db_cidr_block" : "10.1.3.0/24"
      },
      "ap-southeast-1b" = {
        "public_cidr_block" : "10.1.4.0/24",
        "app_cidr_block" : "10.1.5.0/24",
        "db_cidr_block" : "10.1.6.0/24"
      }
    },
    "stg" = {
       "ap-southeast-1a" = {
         "public_cidr_block" : "10.1.1.0/24",
         "app_cidr_block" : "10.1.2.0/24",
         "db_cidr_block" : "10.1.3.0/24"
       },
       "ap-southeast-1b" = {
         "public_cidr_block" : "10.1.4.0/24",
         "app_cidr_block" : "10.1.5.0/24",
         "db_cidr_block" : "10.1.6.0/24"
       }
     }
   }

vpc.tf

resource "aws_vpc" "vpc" {
  for_each             = var.vpcs
  cidr_block           = var.vpc_cidr_block
  instance_tenancy     = var.instance_tenancy
  enable_dns_support   = var.enable_dns_support
  enable_dns_hostnames = var.enable_dns_hostnames
}

subnets.tf

resource "aws_subnet" "public_subnet" {
  for_each                = var.vpcs
  vpc_id                  = each.key
  cidr_block              = ???
  availability_zone       = ???
  map_public_ip_on_launch = var.map_public_ip_on_launch
}
1

1 Answer 1

2

Fro your question I'm understanding that you have a single VPC and that your var.vpcs map is, despite its name, intended to represent a set of three subnets for each availability zone in each of two environments.

To start then, I'd redefine the input variables as follows, since your current definition doesn't work for the data you want to represent: you've declared a map of empty objects, which therefore provides nowhere to represent the availability zones and subnets.

variable "vpc_subnets" {
  type = map(map(object({
    public_cidr_block = string
    app_cidr_block    = string
    db_cidr_block     = string
  })))
}

variable "vpc_cidr_blocks" {
  type = map(string)
}

A value for these input variables might then be defined like this:

vpc_cidr_blocks = {
  "dev" = "10.1.0.0/16"
  "stg" = "10.1.0.0/16"
}
vpc_subnets = {
  "dev" = {
    "ap-southeast-1a" = {
      public_cidr_block = "10.1.1.0/24",
      app_cidr_block    = "10.1.2.0/24",
      db_cidr_block     = "10.1.3.0/24"
    }
    "ap-southeast-1b" = {
      public_cidr_block = "10.1.4.0/24",
      app_cidr_block    = "10.1.5.0/24",
      db_cidr_block     = "10.1.6.0/24"
    }
  }
  "stg" = {
    "ap-southeast-1a" = {
      public_cidr_block = "10.1.1.0/24",
      app_cidr_block    = "10.1.2.0/24",
      db_cidr_block     = "10.1.3.0/24"
    }
    "ap-southeast-1b" = {
      public_cidr_block = "10.1.4.0/24",
      app_cidr_block    = "10.1.5.0/24",
      db_cidr_block     = "10.1.6.0/24"
    }
  }
}

You can declare the VPCs in a similar way to how you already declared them:

resource "aws_vpc" "vpc" {
  for_each = var.vpc_cidr_blocks

  cidr_block           = each.value
  instance_tenancy     = var.instance_tenancy
  enable_dns_support   = var.enable_dns_support
  enable_dns_hostnames = var.enable_dns_hostnames
}

However, the structure of var.vpc_subnets does not yet match the requirements of for_each, because it contains one element per VPC, rather than one element per subnet. Therefore you'll need to first transform that data structure into a single flat collection with one element per subnet. A common way to do that is using the flatten function, as described in Flattening nested structures for for_each.

The following example adapts the example in the Terraform documentation for your slightly-different structure where the VPC cidr_blocks and subnet cidr_blocks are represented separately and where there is more than one subnet per availability zone:

locals {
  vpc_subnets = flatten([
    for env, azs in var.vpc_subnets : [
      for az, subnets in azs : [
        for attr_name, cidr_block in subnets : {
          env         = env
          az          = az
          type        = trimsuffix(attr_name, "_cidr_block")
          cidr_block  = cidr_block
        }
      ]
    ]
  ])
}

With this definition, local.vpc_subnets is a list with one element per subnet, and with the environment, availability zone, and subnet name information encoded as part of the element value rather than as map keys.

This list can therefore be transformed one more time to produce a map with one element per subnet, using the three discriminating attributes to form a compound key for each element, like this:

resource "aws_subnet" "all" {
  for_each = {
    for subnet in local.vpc_subnets :
    "${subnet.env}:${subnet.az}:${subnet.type}" => subnet
  }

  vpc_id                  = aws_vpc.vpc[each.value.env].id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.az
  map_public_ip_on_launch = var.map_public_ip_on_launch
}

Here I made the totally-arbitrary decision to join the discriminating keys together using colons :, which means that (given the example values I included above) this block declares the following resource instance addresses:

  • aws_subnet.all["dev:ap-southeast-1a:public"]
  • aws_subnet.all["dev:ap-southeast-1a:app"]
  • aws_subnet.all["dev:ap-southeast-1a:db"]
  • aws_subnet.all["dev:ap-southeast-1b:public"]
  • aws_subnet.all["dev:ap-southeast-1b:app"]
  • aws_subnet.all["dev:ap-southeast-1b:db"]
  • aws_subnet.all["stg:ap-southeast-1a:public"]
  • aws_subnet.all["stg:ap-southeast-1a:app"]
  • aws_subnet.all["stg:ap-southeast-1a:db"]
  • aws_subnet.all["stg:ap-southeast-1b:public"]
  • aws_subnet.all["stg:ap-southeast-1b:app"]
  • aws_subnet.all["stg:ap-southeast-1b:db"]

Above I assumed that you'd prefer to declare all of the subnets using a single resource block, but it's also possible to shape this differently and use a separate resource block for each of the three subnet types. In that case you can use an additional step to split the flat list of all subnets into three separate lists that each contain only one subnet type.

locals {
  vpc_subnets_by_type = {
    for subnet in local.vpc_subnets :
    subnet.type => subnet...
  }
}

This particular for expression is using the extra ... symbol, used for grouping results. This means that the result is a map of lists where the map keys are the three subnet types and each lists contains only the subnets of one type.

You can then use this data structure to write out three resource blocks similar to the one above but where each one uses only the subnets of a particular type. For example:

resource "aws_subnet" "public" {
  for_each = {
    for subnet in local.vpc_subnets_by_type["public"] :
    "${subnet.env}:${subnet.az}" => subnet
  }

  vpc_id                  = aws_vpc.vpc[each.value.env].id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.az
  map_public_ip_on_launch = var.map_public_ip_on_launch
}

resource "aws_subnet" "app" {
  for_each = {
    for subnet in local.vpc_subnets_by_type["app"] :
    "${subnet.env}:${subnet.az}" => subnet
  }

  vpc_id                  = aws_vpc.vpc[each.value.env].id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.az
  map_public_ip_on_launch = var.map_public_ip_on_launch
}

resource "aws_subnet" "db" {
  for_each = {
    for subnet in local.vpc_subnets_by_type["db"] :
    "${subnet.env}:${subnet.az}" => subnet
  }

  vpc_id                  = aws_vpc.vpc[each.value.env].id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.az
  map_public_ip_on_launch = var.map_public_ip_on_launch
}

This variation therefore declares the following resource instances, using a separate resource for each subnet type:

  • aws_subnet.public["dev:ap-southeast-1a"]
  • aws_subnet.public["dev:ap-southeast-1b"]
  • aws_subnet.public["stg:ap-southeast-1a"]
  • aws_subnet.public["stg:ap-southeast-1b"]
  • aws_subnet.app["dev:ap-southeast-1a"]
  • aws_subnet.app["dev:ap-southeast-1b"]
  • aws_subnet.app["stg:ap-southeast-1a"]
  • aws_subnet.app["stg:ap-southeast-1b"]
  • aws_subnet.db["dev:ap-southeast-1a"]
  • aws_subnet.db["dev:ap-southeast-1b"]
  • aws_subnet.db["stg:ap-southeast-1a"]
  • aws_subnet.db["stg:ap-southeast-1b"]

This variation would presumably be better if the configuration settings for each type of subnet need to be significantly different.

Sign up to request clarification or add additional context in comments.

2 Comments

Hello Martin. First of all, thank you! I have been stuck for 2 weeks trying to properly organize my data and figuring out how to manipulate it properly. Your answer is of great help. It gave me ideas which I can use on other modules I am working on. Also, in resource "aws_vpc" "vpc" section, I think you meant cidr_block = each.value instead of cidr_block = each.key?
You're right about each.value vs. each.key. I had originally written the example differently because I initially misunderstood your goal, and forgot to rewrite that part. Sorry for that oversight!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.