Loop over variable to get private subnets for "aws_route_table_association"

I have a variable of spoke_vpcs with a type of:

type = map(object({
name = string
cidr = string
az_list = list(string)
private_subnet_cidr = list(string)
public_subnet_cidr = list(string)
enable_nat_gateway = bool
enable_vpn_gateway = bool
}))

And the variable looks like this:

spoke_vpcs = {
"security" = {
name = "security",
cidr = "10.1.0.0/16",
az_list = ["us-east-1a", "us-east-1b", "us-east-1c"],
private_subnet_cidr = ["10.1.4.0/24", "10.1.5.0/24", "10.1.6.0/24"],
public_subnet_cidr = ["10.1.104.0/24", "10.1.105.0/24", "10.1.106.0/24"],
enable_nat_gateway = false
enable_vpn_gateway = false
},
"logging" = {
name = "logging",
cidr = "10.0.0.0/16",
az_list = ["us-east-1a", "us-east-1b", "us-east-1c"],
private_subnet_cidr = ["10.0.7.0/24", "10.0.8.0/24", "10.0.9.0/24"],
public_subnet_cidr = ["10.0.107.0/24", "10.0.108.0/24", "10.0.109.0/24"],
enable_nat_gateway = true
enable_vpn_gateway = true
}
}

What I’m looking to do is create a vpc from each object in the “spoke_vpcs” map and then create a aws_route_table_association for each of the private_subnets.

So, I use a module to create the vpc:


module "vpc_spokes" {
source = "terraform-aws-modules/vpc/aws"
for_each = var.spoke_vpcs
providers = {
aws = aws.shared
}
name = each.value.name
cidr = each.value.cidr
azs = each.value.az_list
private_subnets = each.value.private_subnet_cidr
public_subnets = each.value.public_subnet_cidr
enable_nat_gateway = each.value.enable_nat_gateway
enable_vpn_gateway = each.value.enable_vpn_gateway
}

Add create the route table:


resource "aws_route_table" "spoke_route_table" {
for_each = module.vpc_spokes
vpc_id = each.value.vpc_id
}

What is the best way to create the aws_route_table_association?

I’ve tried:


resource "aws_route_table_association" "private_subnet" {
for_each = module.vpc_spokes["security"].private_subnets
subnet_id = each.value
route_table_id = aws_route_table.spoke_route_table["security"].id
}

But that throws an error of "The given “for_each” argument value is unsuitable: the “for_each” argument must be a map, or set of strings, and you have provided a value of type tuple.

I’ve tried using toset() But it throws the following error:

│   on main.tf line 87, in resource "aws_route_table_association" "private_subnet":
│   87:   for_each = toset(module.vpc_spokes["security"].private_subnets)
│     ├────────────────
│     │ module.vpc_spokes["security"].private_subnets is tuple with 3 elements
│ 
│ The "for_each" set includes values derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that
│ will identify the instances of this resource.
│ 
│ When working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values
│ contain apply-time results.
│ 
│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully
│ converge.

Any ideas on how I can get this to work?

Please see: Guide to asking for help in this forum

This important information is not shown.

You’re right, my mistake. I’ve updated the original post

Hi @thedraketaylor79,

From the context I’m guessing that this private_subnets attribute contains subnet IDs as decided by the remote system, like subnet-abc123, and therefore this error message is correct that those won’t be unknown until the apply phase because they’ll be decided by the remote API only once the objects have been created.

Unfortunately I don’t think you’ll be able to achieve this goal without modifying the module itself, which means you’ll need to fork it since it’s a third-party module from the module registry.

The modification I would suggest is to change output "private_subnets" to return a map of strings instead of a tuple containing some strings, and make the map keys be something known statically from the input, such as the availability zone that each subnet belongs to.

For example:

output "private_subnets" {
  value = tomap({
    for s in aws_subnet.private : s.availability_zone => s.id
  })
}

Since you’ve specified the availability zones as static values in the input to the module, the keys of this constructed map should be known during planning, in which case it would be acceptable to use this whole map directly as the for_each value like this:

resource "aws_route_table_association" "private_subnet" {
  for_each = module.vpc_spokes["security"].private_subnets

  subnet_id      = each.value
  route_table_id = aws_route_table.spoke_route_table["security"].id
}

Terraform will identify these instances with addresses like aws_route_table_association.private_subnet["us-east-1a"], since the availability zone names are the keys of the given map.

I was thinking you could avoid this by combining static input to the module with the known-after-apply output:

  for_each = zipmap(
    var.spoke_vpcs["security"].private_subnet_cidr,
    module.vpc_spokes["security"].private_subnets
  )

HOWEVER, given the module itself is already creating aws_route_table and aws_route_table_association resources, I think the final answer is to just not do any of this because the module already did it anyway.

Thank you! I will look into this.

If the module guarantees to return the items in some specific order that you can replicate in the calling module then indeed using zipmap, or similar ideas with a for expression, could work.

That guarantee is important though, because otherwise the correlation between the elements returned by the module and the elements provided as the keys might become misaligned in future, which would be very confusing at best and possibly very disruptive if it causes route tables to get restructured during apply.

As you say though, if this module already provides a suitable abstraction then probably better to use it to help keep things encapsulated and consistent.