Terraform looping in list of map

Viewed 3169

I have a Terraform module that produces a list of map output:

object_ids = [
  {
    "object_id" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
    "upn" = "john@domain.com"
    "user" = "john"
  },
  {
    "object_id" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
    "upn" = "martin@domain.com"
    "user" = "martin"
  },
]

Using a for_each I can loop on one value to build this resource:

resource "azurerm_role_assignment" "subread" {
    for_each = toset(module.user.map_object_ids[*].object_id)
    scope                = data.azurerm_subscription.primary.id
    role_definition_name = "Reader"
    principal_id         = each.value
}

However I don't know how to loop on more than one value.

I tried the following for another resource requiring 2 different values from the output:

resource "azurerm_role_assignment" "contribrg" {
    scope                = [for map in module.user.map_object_ids[*]: "${data.azurerm_subscription.primary.id}/resourceGroups/${lookup(map,"user")}"]
    role_definition_name = "Contributor"
    principal_id         = [for map in module.user.map_object_ids[*]: lookup(map,"object_id")]
}

Got the following errors :

Error: Incorrect attribute value type
module.user.map_object_ids is tuple with 2 elements
Inappropriate value for attribute "scope": string required.
Inappropriate value for attribute "principal_id": string required.
1 Answers

A fundamental requirement of resource for_each is that the collection you are using must have one element per instance you want to create, so it isn't possible to repeat based on more than one value, but fortunately I think that isn't actually what you are asking here.

Instead, it seems like you want to have one instance for each element of module.user.map_object_ids, and so the only additional problem we need to deal with is that for_each expects to be given a map of objects rather than a list of objects, so it can use the map keys as the identifiers for each of the instances.

We can convert a list of objects into a map of objects by using a for expression, though we'll need to identify one of the attributes of the nested objects that will serve as the unique key for each element. I'm going to use user here because it seems like it's a good human-understandable, configuration-selected unique identifier:

resource "azurerm_role_assignment" "contribrg" {
  for_each = { for obj in module.user.map_object_ids : obj.user => obj }

  scope                = "${data.azurerm_subscription.primary.id}/resourceGroups/${each.value.user}"
  role_definition_name = "Contributor"
  principal_id         = each.value.object_id
}

The for expression above will project your list of objects into a map of objects, like this:

{
  "john" = {
    "object_id" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
    "upn" = "john@domain.com"
    "user" = "john"
  }
  "martin" = {
    "object_id" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
    "upn" = "martin@domain.com"
    "user" = "martin"
  }
}

Then inside the resource argument expressions each.key will refer to the usernames (because they are now the keys) while each.value will refer to the objects, and so we can use each.value.object_id to get the corresponding object identifier.

From this, Terraform will plan to create resource instances with the following addresses:

  • azurerm_role_assignment.contribrg["john"]
  • azurerm_role_assignment.contribrg["martin"]

A side note: I found it a little confusing that your output is named object_ids when it isn't just returning ids. It might be clearer to name it objects.

Related