Unknown values in for_each

I have a similiar question getting the error:
" The “for_each” map includes keys 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 define the map
keys statically in your configuration and place apply-time results only in
the map values.
"
I have code that’s going to conditionally assign an IPv6 CIDR to an AWS VPC based on a Boolean key/value pair in tfvars. That’s all fine and works. I’m passing this variable to the subnets module, and get the error above when testing with terraform plan. How could I possibly get around this, based on this resource block in tf subnets.tf

From parent module:

module "subnets" {
  source  = "../subnets"
  vpc_id  = aws_vpc.this.id
  vpc     = var.vpc
  v6_cidr_block = aws_vpc.this.ipv6_cidr_block
  #vpc_resource = aws_vpc.this
  #account = var.account
}

in subnets. Module

resource "aws_subnet" "ipv6_subnets" {
  for_each = { for index, subnet in local.subnets_extrapolated : subnet["name"] =>
    subnet if var.v6_cidr_block != "" && length(regexall("internet_facing", subnet["name"])) > 0
  }
  vpc_id                          = var.vpc_id
  availability_zone               = each.value.az
  ipv6_cidr_block                 = cidrsubnet(var.v6_cidr_block, 8, each.value.azindex)
  assign_ipv6_address_on_creation = true
}

Assuming the only issue is with var.v6_cidr_block …if that is removed, plan builds without error? Any ideas of how I might be able to get around this, since the value of v6_cidr_block (which is the v6 VPC cidr block which will only get assigned conditionally on the tfvars.json Boolean being set to true, and then only on specific public subnets, identified via the regexall) Thanks in advance…

Although I still need test this in a lab environment, I think I may be on the right track by evaluating the Boolean variable from the tfvars.json at the start of the for_each block, e.g.

resource "aws_subnet" "ipv6_subnets" {
  for_each = var.ipv6_enabled_to_subnet_module ? { for index, subnet in local.subnets_extrapolated : subnet["name"] =>
    subnet if length(regexall("internet_facing", subnet["name"])) > 0
  }: {}
  vpc_id                          = var.vpc_id
  availability_zone               = each.value.az
  ipv6_cidr_block                 = cidrsubnet(var.v6_cidr_block, 8, each.value.azindex)
  assign_ipv6_address_on_creation = true
  #depends_on = [
  #  var.vpc_resource
  #]
}

Note: the subnets module is a nested module of a child module to a root module…And the variable should have the code passing from root module all the way down to the nested module subnets…e.g. assuming it does (which I think I am doing…) the plan is good, but I’ve got the variable set to false, so I need to run it in a lab, and test if the modification works/code works still… If there’s anything anyone can add to this that jumps out as incorrect logic or syntax, please let me know. Much appreciated.

This is making progress, and matches correctly and the logic is there, except, it’s still way off, in that I didn’t realize, it’s just going to create totally new subnet resources, so now I’m down to trying to figure out how to incorporate this conditionally into the main aws_subnet block which creates all the subnets (for all zones, different types)… that code block looks like this by default (without the conditional ipv6)

locals {
  subnets_extrapolated = flatten([
    for group_name, group_data in var.vpc["subnets"] : [
      for index, cidr in group_data["cidrs"] : {
        name    = "${group_name}-az${index + 1}"
        type    = group_data["type"]
        cidr    = cidr
        az      = var.vpc["azs"][index]
        tags    = group_data["tags"]
        azindex = index + 1
      }
    ]
  ])
}
resource "aws_subnet" "subnets" {
  for_each          = { for index, subnet in local.subnets_extrapolated : subnet["name"] => subnet }
  vpc_id            = var.vpc_id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  tags = merge(
    each.value.tags,
    {
      "Name" : lookup(each.value.tags, "Name", null) != null ? "${each.value.tags["Name"]}-az${each.value.azindex}" : each.key,
      "type" : each.value.type
    }
  )
}

Hi @bspunt_2000,

In your for expression there are two parts that could make the result be fully unknown and therefore unsuitable for for_each:

  • The key result expression must always be known: subnet["name"]
  • The for clause must always produce a known result, which means all of the values used to make the decision must be known: var.v6_cidr_block and subnet["name"] again.

I think you’re right that it’s the aws_vpc.this.ipv6_cidr_block value that’s causing the problem here: presumably the provider populates that only during the apply phase for some reason.

You mentioned that the value for that attribute depends on whether another input variable is set, and so one way to proceed here would be to rely on that knowledge to tell Terraform when you expect that attribute to be unset even though the provider itself makes its decision too late:

  v6_cidr_block = var.ipv6_enabled ? aws_vpc.this.ipv6_cidr_block : null

Inside your for expression if clause you could then test var.v6_cidr_block != null because null will represent the total absence of an IPv6 CIDR block address. (null is the expected way to represent the absence of something in Terraform, as opposed to using “magic values” like an empty string.)

It’s unfortunate that you would need to replicate the logic for deciding if this will be set in two places, but this means that you can give Terraform more complete instructions based on something you know about how this system works, to compensate for the provider producing this information too late.

(If it seems like the AWS provider ought to be able to know the value of this attribute during planning then you might also report that as a feature request in the AWS provider’s GitHub repository; if the provider has enough information during planning to populate this then Terraform’s plugin protocol allows it to do so.)

Thank you! I appreciate your time and recommendations.

I’m still not able to figure this out. What I’m trying to do if have a nested loop in addition to the for_each (where there’s also a for loop on a local) but I need to identify a subset of public subnets and assign the variable ipv6_cidr_block only to those public subnets.

The code to identify those subnets exists as an output in the output.tf shown below:

output "internet_facing" {
  value = flatten([
    for subnet in aws_subnet.subnets : subnet
    if lookup(subnet.tags_all, "type", null) == "internet_facing"
  ])
}

And the existing code in the subnets.tf is the following (which created all the subnets in all zones, not just the public facing subnets…but I only want to assign ipv6 cidrs to the internet_facing subnets):

resource "aws_subnet" "subnets" {
  for_each          = { for index, subnet in local.subnets_extrapolated : subnet["name"] => subnet 
  }
  vpc_id            = var.vpc_id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  tags = merge(
    each.value.tags,
    {
      "Name" : lookup(each.value.tags, "Name", null) != null ? "${each.value.tags["Name"]}-az${each.value.azindex}" : each.key,
      "type" : each.value.type
    }
  )
}

So, let’s say maybe I could create a local variable, like this?

locals {
  internet_facing_subnets = flatten([
    for pubsubnet in aws_subnet.subnets : pubsubnet
    if lookup(subnet.tags_all, "type", null) == "internet_facing" 
  ])
}

Or refer to the output variable? which is in the same module, and then, my question is, is it possible to nest ANOTHER loop inside the aws_subnet.subnets resource that identifies these public subnets only and assign the variable “ipv6_cidr_block = var.ipv6_enabled_to_subnet_module ? cidrsubnet(var.v6_cidr_block, 8, ): null”

Any help is greatly appreciated!

Sorry and here’s the other local variable that’s used in the existing working code:

locals {
  subnets_extrapolated = flatten([
    for group_name, group_data in var.vpc["subnets"] : [
      for index, cidr in group_data["cidrs"] : {
        name    = "${group_name}-az${index + 1}"
        type    = group_data["type"]
        cidr    = cidr
        az      = var.vpc["azs"][index]
        tags    = group_data["tags"]
        azindex = index + 1
      }
    ]
  ])
}

The line code of below didn’t paste in correctly before, the correct functionality I need is the following:

“ipv6_cidr_block = var.ipv6_enabled_to_subnet_module ? cidrsubnet(var.v6_cidr_block, 8, azindex ): null”

because the public subnets are a one to one to AZ and I do know that part of it works

Thanks in advance!

I did work out the code for this and here’s what worked to selectively assign Ipv6 CIDR to a subset of subnets (public facing) within a single resource block

First, I add a variable to the locals - shown below, then a update the conditional in the subnets module…

locals {
  subnets_extrapolated = flatten([
    for group_name, group_data in var.vpc["subnets"] : [
      for index, cidr in group_data["cidrs"] : {
        name            = "${group_name}-az${index + 1}"
        type            = group_data["type"]
        cidr            = cidr
        az              = var.vpc["azs"][index]
        tags            = group_data["tags"]
        azindex         = index + 1
        internet_facing = group_data["type"] == "internet_facing"
      }
    ]
  ])
}

Subnets module update:


resource "aws_subnet" "subnets" {
  for_each = {
    for index, subnet in local.subnets_extrapolated : subnet["name"] => subnet
  }
  vpc_id            = var.vpc_id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  ipv6_cidr_block = var.ipv6_enabled_to_subnet_module && each.value.internet_facing ? cidrsubnet(var.v6_cidr_block, 8, each.value.azindex)