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
}
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.