Using flatten on a nested map of differing value types

Hi, I’m using terraform v0.12.26 and have a structure very similar to the example on the flatten page, except I have an extra key in the map.

I’m trying to follow the example as closely as possible. I’ve gotten it working using a structure of the same shape, but I’ve had to modify my structure to the following:

<instance_uid> = {
    instance_id = <instance_id>
    vols_list   = [volume_label,volume_label,...]
}

an example being:

instances_vols = {
    "hello_world1" = {
        instance_id = "i-1234"
        vols_list = ["data01","data02","inputs01"]
    }
    "hello_world2" = {
        instance_id = "i-4321"
        vols_list = ["data01","inputs01","outputs01","outputs02"]
    }
}

My plan is to have a resource where I can use a for_each like the following:

for_each = {
    for instance in local.instances_volumes : "${instance.instance_uid}.${instance.volume_label}" => instance
}

and I can then use the values as “each.value.instance_uid” in my resource.

The flattened local looks as follows:

locals {
  instances_volumes = flatten([
    for instance_uid, instance_vol_list in var.instances_vols : [
      for instance_keys, instance_values in instance_vol_list : [
        for vol in instance_keys.vols_list : {
          instance_uid = instance_uid
          instance_id  = instance_keys.instance_id
          volume_label = vol
        }
      ]
    ]
  ])
}

but this doesn’t work, since “for vol in instance_keys.vols_list” doesn’t have any attributes. I’m trying to use the keys my name but this doesn’t seem to work.

The map key “hello_world1” and it’s inner key “instance_uid” are a 1:1 mapping - “hello_world1” would always have an instance_uid of “i-1234” (I mean, it would change in practice, but I could refer to “hello_world1” or “i-1234” and it would be the same thing).

How do I structure my flattened local? Or, can I restructure my input to be more suitable? instances_vols is generated elsewhere as an output to feed into this input, using the following:

output "fileserver_instance_vols" {
    value = {
        for fileserver_module in local.fileserver_modules :
        fileserver_module.uid => {
            instance_id = fileserver_module.instance_id
            vols_list   = fileserver_module.vols_list
        }
    }
}

Is there a better way to structure my output such that it’s much more simple to use as the input later?

Thanks.

(also, just curious, how do you make the markdown appear on this page where it highlights the terraform keywords?)

Hi @rba1-source!

I think the following would get the result you were looking for:

locals {
  instances_volumes = flatten([
    for inst_key, inst in var.instances_vols : [
      for vol_key in inst.vols_list : {
        instance_uid = inst_key
        instance_id  = inst.instance_id
        volume_label = vol_key
      }
    ]
  ])
}

In this case the instance id is coming from inst, which is the object like this:

{
  instance_id = "i-1234"
  vols_list = ["data01","data02","inputs01"]
}

Hi @apparentlymart. Thanks, this did the trick. I guess I was just getting confused at which level I was specifying values and keys. Thanks for the help!

A second option is to simplify and use a second map, since there’s a 1:1 mapping between uid and instance_id. As the key would be the same in each case (uid), I could have one map which was { uid = vols_list } and a second which is { uid = instance_id } , and then just use one as an index into the other, i.e.

for_each = var.instances_vols
instance_id = uid_instances[each.key].instance_id

It would mean an extra map, but it would keep things simpler and means I can use the new map to pass into other inputs that don’t need the volume list.

It’s all still a useful exercise in learning how to use for and for_each though, so whichever solution I choose, it was worth asking.

Thanks again!

@apparentlymart

can you help to flatten this also?

variable vpc_peer {
  default = {
    vpc_peer1 = {
      aws_account_id = "acct-id"
      region  = "region-1"
      vpc_id  = "vpc-id"
      route_table_cidr_block = "10.0.0.0/16"
      route_table_ids = [
         "rtb-1",
         "rtb-2"
      ]
    }
  }
}

Expectation:

With terraform local resource

    vpc_peer1 = {
      aws_account_id =>  "acct-id"
      region  =>  "region-1"
      vpc_id  =>  "vpc-id"
      route_table_cidr_block =>  "10.0.0.0/16"
      route_table_ids =>  [ "rtb-1", "rtb-2" ]
    }

@apparentlymart

i am able to test with this code and giving desire output

variable "vpc_peer" {
  type = map(object({
     route_table_cidr_block  = string
     aws_account_id          = string
     region                  = string
     vpc_id                  = string
     route_table_ids         = list(string)
  }))
  description = "An object that contains all VPC peering requests from the cluster to AWS VPC's"
  default = {
    vpc_peer1 = {
      route_table_cidr_block  = "10.10.0.0/24"
      aws_account_id          = "didhkdn222"
      region                  = "eu-east-1"
       vpc_id                 = "vpc-id-1"
       route_table_ids        = [ "rtb-1", "rtb-2", "rtb-20" ]
    },
    vpc_peer2 = {
      route_table_cidr_block  = "10.100.0.0/24"
      aws_account_id          = "e2234kdn222"
      region                  = "eu-west-2"
       vpc_id                 = "vpc-id-2"
       route_table_ids        = [ "rtb-3", "rtb-4", "rtb-10" ]
    }
   }
}


locals {
  vpc_peering = flatten([
    for vpc_peers, vpc_peer_ids in var.vpc_peer : {
       route_table_cidr_block  = vpc_peer_ids.route_table_cidr_block
       aws_account_id          = vpc_peer_ids.aws_account_id
       region                  = vpc_peer_ids.region
       vpc_id                  = vpc_peer_ids.vpc_id
       route_table_ids         = vpc_peer_ids.route_table_ids
    }
  ])
}

output vpc_peer {
  value = local.vpc_peering
}

Desire Output

Changes to Outputs:
  + vpc_peer = [
      + {
          + aws_account_id         = "didhkdn222"
          + region                 = "eu-east-1"
          + route_table_cidr_block = "10.10.0.0/24"
          + route_table_ids        = [
              + "rtb-1",
              + "rtb-2",
              + "rtb-20",
            ]
          + vpc_id                 = "vpc-id-1"
        },
      + {
          + aws_account_id         = "e2234kdn222"
          + region                 = "eu-west-2"
          + route_table_cidr_block = "10.100.0.0/24"
          + route_table_ids        = [
              + "rtb-3",
              + "rtb-4",
              + "rtb-10",
            ]
          + vpc_id                 = "vpc-id-2"
        },
    ]

But using inside the resource with for_each = local.vpc_peering.
its giving below error:

  127:   for_each = local.vpc_peering
│     ├────────────────
│     │ local.vpc_peering is tuple with 1 element
│ 
│ 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.
╵

my main resource code

resource "aws_route" "mongoatlas_route" {
  for_each = local.vpc_peering

  route_table_id            = each.value.route_table_ids
  destination_cidr_block    = var.atlas_vpc_cidr
  vpc_peering_connection_id = mongodbatlas_network_peering.mongoatlas_peer[each.key].connection_id
  depends_on                = [aws_vpc_peering_connection_accepter.peer]
}```

pls help.. thanks

i am able to the fix the issue.

variable "vpc_peer" {
  description = "An object that contains all the groups that should be created in the project"
  type        = map(any)
  default     = {
      vpc_peer1: {
        aws_account_id : "209769635771"
        region  : "eu-central-1"
        route_table_cidr_block : "10.57.0.0/16"
        route_table_ids: ["rtb-1", "rtb-2", "rtb-3", "rtb-4"]
        vpc_id: "vpc-id-1"
      },
      vpc_peer2: {
        aws_account_id : "2097696jddddd"
        region  : "eu-west-1"
        route_table_cidr_block : "10.100.0.0/16"
        route_table_ids: [ "rtb-20", "rtb-21" ]
        vpc_id: "vpc-id-3"
      }

  }
}
locals {
  vpc_peering = flatten([
    for vpc_peers, vpc_peer_ids in var.vpc_peer : [
       for route_table_id in vpc_peer_ids.route_table_ids : {
       aws_account_id          = vpc_peer_ids.aws_account_id
       region                  = vpc_peer_ids.region
       route_table_cidr_block  = vpc_peer_ids.route_table_cidr_block
       vpc_id                  = vpc_peer_ids.vpc_id
       route_table_ids         = route_table_id
       peering_id              = mongodbatlas_network_peering.mongoatlas_peer[vpc_peers].connection_id
       }
    ]
  ])
}
resource "aws_route" "mongoatlas_route" {
  for_each = { for route_id in local.vpc_peering : route_id.route_table_ids => route_id }

  route_table_id            = each.value.route_table_ids
  destination_cidr_block    = var.atlas_vpc_cidr
  vpc_peering_connection_id = each.value.peering_id
}

Thanks

1 Like