Search code examples
terraform

Terraform nested for loop producing unexpected result


Given this module input:

variable "port_mapping_metadata" {
  type = map(object({
    target_type                       = optional(string, "instance")
    ssl_policy                        = optional(string)
    listener_cert_arn                 = optional(string)
    additional_SNI_listener_cert_arns = optional(list(string),[])
    ...

I'm trying to create a resource per port_mapping_metadata (if not null, keys may be duplicates across resources), per item in additional_SNI_listener_cert_arns (if not empty). So for the following input:

  "bar" = {
    vpc_name            = "foo"
    name                = "bar"
    type                = "application"
    port_mapping_metadata = {
      "forward" = {
        instance_names                    = ["inst"]
        listener_cert_arn                 = "arn:aws:acm:us-east-1:1234:certificate/abcd"
        additional_SNI_listener_cert_arns = ["arn:aws:acm:us-east-1:1234:certificate/defg", "arn:aws:acm:us-east-1:1234:certificate/hijk"]
  "bax" = {
    vpc_name            = "foo"
    name                = "bax"
    type                = "application"
    port_mapping_metadata = {
      "forward" = {
        instance_names                    = ["inst2"]
        listener_cert_arn                 = "arn:aws:acm:us-east-1:1234:certificate/abcd"
        additional_SNI_listener_cert_arns = ["arn:aws:acm:us-east-1:1234:certificate/zle"]
(under another key)
  "bar" = {
    vpc_name            = "foo"
    name                = "bar"
    type                = "application"
    port_mapping_metadata = {
      "forward" = {
        instance_names                    = ["inst"]
        listener_cert_arn                 = "arn:aws:acm:us-east-1:1234:certificate/abcd"

I would expect the following for_each result:

{
  "bar-0" = "arn:aws:acm:us-east-1:1234:certificate/defg"
  "bar-1" = "arn:aws:acm:us-east-1:1234:certificate/hijk"
  "bax-0" = "arn:aws:acm:us-east-1:1234:certificate/zle"
}

I have tried writing the resource as with a for_each as follows:

resource "aws_lb_listener_certificate" "additional_SNI_listener_certs" {
  for_each = {
    for k, v in var.port_mapping_metadata : k => {
      for index, cert_arn in try(v.additional_SNI_listener_cert_arns, []) : "${k}-${index}" => cert_arn
    }
    if try(v.additional_SNI_listener_cert_arns, []) != [] && try(v.additional_SNI_listener_cert_arns, null) != null
  }
  ##this _should_ be "{ "listener_name-0" = "arn1" , ... }"

  listener_arn   = aws_lb_listener.all[element(split("-", each.key), 0)].arn ##this should be "listener_name"
  certificate_arn = each.value ##this should be "arn1"
}

But for some reason I get the error:

│ Error: Incorrect attribute value type
│ 
│   on .terraform/modules/load_balancer/load_balancer.tf line 235, in resource "aws_lb_listener_certificate" "additional_SNI_listener_certs":
│  235:   certificate_arn = each.value
│     ├────────────────
│     │ each.value is object with no attributes
│ 
│ Inappropriate value for attribute "certificate_arn": string required.

I can't understand why each.value in the resource is not a single string arn1. I'm sure I'm missing something simple, but I can't figure out what it is. Also open to any other way of doing this.


Solution

  • By looking at your code, I think the for_each is getting the following map as input. However, you must verify it by adding a terraform output to print the result of the expression.

    {
      bar = {
        "bar-0" = "arn:aws:acm:us-east-1:1234:certificate/defg"
        "bar-1" = "arn:aws:acm:us-east-1:1234:certificate/hijk"
      }
      bax = {
        "bax-0" = "arn:aws:acm:us-east-1:1234:certificate/zle"
      }
    }
    

    Now as per your requirement, to get the following map input to the for_each

    {
      "bar-0" = "arn:aws:acm:us-east-1:1234:certificate/defg"
      "bar-1" = "arn:aws:acm:us-east-1:1234:certificate/hijk"
      "bax-0" = "arn:aws:acm:us-east-1:1234:certificate/zle"
    }
    

    You need to modify the code to the following

    locals {
    
      original_for_each_expr = {
        for k, v in var.port_mapping_metadata : k => {
          for index, cert_arn in try(v.additional_SNI_listener_cert_arns, []) : "${k}-${index}" => cert_arn
        }
        if try(v.additional_SNI_listener_cert_arns, []) != [] && try(v.additional_SNI_listener_cert_arns, null) != null
      }
    
      desired_for_each_expr = zipmap(
        flatten(
          [ for i in local.original_for_each_expr : keys(i) ]
        ),
        flatten(
          [ for i in local.original_for_each_expr : values(i) ]
        )
      ) 
    
    }
    
    resource "aws_lb_listener_certificate" "additional_SNI_listener_certs" {
      for_each = local.desired_for_each_expr
      ...
    }
    

    Also, as I suggested earlier, add terraform outputs to see if you are getting the desired value from the expression to pass it to the for_each.

    output "original_for_each_expr" {
      value = local.original_for_each_expr
    }
    
    output "desired_for_each_expr" {
      value = local.desired_for_each_expr
    }