I need to create multiple subnets in AWS using Terraform, and wanted to make the process as easily repeatable and standardized as possible. I would like to create a variable with a list
of maps
that contains the information necessary to create the subnets, like this:
my_subnet_map = [
{
name = "subnetA",
cidr_blocks = ["10.0.0.0/24"]
},
{
name = "subnetB",
cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"]
},
{
name = "subnetC",
cidr_blocks = ["10.0.3.0/24", "10.0.4.0/24"]
},
...
]
I envisioned being able to do something like this:
resource "aws_subnet" "private" {
count = ???
vpc_id = var.vpc_id
availability_zone = XXX
cidr_block = var.my_subnet_map[SOME KIND OF LOOKUP]
}
I know how to use count
or for_each
to loop over values, but in this case the values have a varying number of cidr_blocks
. So, the count
doesn't correspond to the number of entries in the list
. It corresponds to the TOTAL number of cidr_block
values across all members of the list
.
How would I accomplish something like this? It almost seems like some sort of nested count
to me, but I'm not familiar enough with Terraform to know how to model it. Thoughts?
The data structure you've shown here is a sequence of objects, and might have the following type if declared as an input variable:
list(object({
name = string
cidr_blocks = set(string)
}))
The name "my_subnet_map" is therefore a little confusing, because there aren't any maps here. I mention this only because to make this work you will need to project this list of objects into a map of objects, since that's what for_each
requires, and so having these names straight will make things less confusing as we start deriving other collections from this initial one.
It also seems like this is a list of "subnet groups" -- a concept you've invented yourself, which I've now named -- where each object represents a set of subnets. So for clarity, let's name it subnet_groups
, and I'll also add a few type explicit conversions just to make it clearer what types we're going for here:
locals {
subnet_groups = tolist([
{
name = "subnetA",
cidr_blocks = toset(["10.0.0.0/24"])
},
{
name = "subnetB",
cidr_blocks = toset(["10.0.1.0/24", "10.0.2.0/24"])
},
{
name = "subnetC",
cidr_blocks = toset(["10.0.3.0/24", "10.0.4.0/24"])
},
...
])
}
To get this into the shape that aws_subnet
needs it'll be necessary to flatten this to be a collection with one element per actual subnet, not per subnet group. Here's one way to do that:
locals {
subnets = toset(flatten([
for group in local.subnet_groups : [
for cidr_block in group.cidr_blocks : {
group_name = group.name
cidr_block = cidr_block
}
]
]))
}
resource "aws_subnet" "private" {
for_each = tomap({
for subnet in local.subnets :
"${subnet.group_name}:${subnet.cidr_block}" => subnet
})
vpc_id = var.vpc_id
cidr_block = each.value.cidr_block
}
The expression assigned to subnets
is intended to construct a flat set of objects shaped like this:
toset([
{
group_name = "subnetA"
cidr_block = "10.0.0.0/24"
},
{
group_name = "subnetB"
cidr_block = "10.0.1.0/24"
},
{
group_name = "subnetB"
cidr_block = "10.0.2.0/24"
},
{
group_name = "subnetC"
cidr_block = "10.0.3.0/24"
},
{
group_name = "subnetC"
cidr_block = "10.0.4.0/24"
},
])
for_each
in a resource
block requires a map with a unique key to track each instance by though, so in the actual for_each
expression I projected this once more into a map with some keys containing both the group name and the CIDR block to make sure they are all unique. With the set of subnet groups we started with, that would declare the following resource instances:
aws_subnet.private["subnetA:10.0.0.0/24"]
aws_subnet.private["subnetB:10.0.1.0/24"]
aws_subnet.private["subnetB:10.0.2.0/24"]
aws_subnet.private["subnetC:10.0.3.0/24"]
aws_subnet.private["subnetC:10.0.4.0/24"]
References: