Unable to iterate over map of map in terraform

I am trying to iterate over a variable i.e. map(any) as below:

variable "api_operation" {
    description = "(optional) describe your variable"
    type    = map(any)
    default = {
        "example-api" = {
            "user-delete" = {
                "display_name" = "Delete User Operation"
                "method" = "DELETE"
                "url_template" = "/users/{id}/delete"
                "description" = "This can only be done by the logged in user."
            },
            "user-get" = {
                "display_name" = "Get User Operation"
                "method" = "GET"
                "url_template" = "/users/{id}/get"
                "description" = "This can only be done by the logged in user."
            }
        },
        "example-api2" = {
            "user-delete" = {
                "display_name" = "Delete User Operation"
                "method" = "DELETE"
                "url_template" = "/users/{id}/delete"
                "description" = "This can only be done by the logged in user."
            },
            "user-get" = {
                "display_name" = "Get User Operation"
                "method" = "GET"
                "url_template" = "/users/{id}/get"
                "description" = "This can only be done by the logged in user."
            },
            "user-put" = {
                "display_name" = "PUT User Operation"
                "method" = "PUT"
                "url_template" = "/users/{id}/put"
                "description" = "This can only be done by the logged in user."
            }
        }
    }
}

How to use this in a resource. I have tried in below way. But unable to achieve

resource "azurerm_api_management_api_operation" "example" {
  for_each            = var.api_operation
  api_name            = each.key
  operation_id        = each.value.key
  api_management_name = azurerm_api_management.example.name
  resource_group_name = azurerm_resource_group.example.name
  display_name        = each.value.value.display_name
  method              = each.value.value.method
  url_template        = each.value.value.url_template
  description         = each.value.value.description

}

TIA

You can’t directly use a map of maps like this to create resources. Instead, you need to make sure your data structure matches what you need here: a single-level map with all the attributes needed to make a resource.

I’m not familiar with this resource, so bear with me if I have any mistakes. It looks like the attributes you need are:

  • api_name , e.g. "example-api"
  • operation_id, e.g. "user-get"
  • …and all the attributes in your leaf nodes: display_name, method, url_template, and description

You can flatten this structure down. Instead of:

{
  "example-api" = {
    "user-delete" = {
      "display_name" = "Delete User Operation"
      "method" = "DELETE"
      "url_template" = "/users/{id}/delete"
      "description" = "This can only be done by the logged in user."
    },
    "user-get" = {
      "display_name" = "Get User Operation"
      "method" = "GET"
      "url_template" = "/users/{id}/get"
      "description" = "This can only be done by the logged in user."
    }
  },
}

You need something like:

{
  "example-api-user-delete" = {
    "api_name" = "example-api"
    "display_name" = "Delete User Operation"
    "method" = "DELETE"
    "url_template" = "/users/{id}/delete"
    "description" = "This can only be done by the logged in user."
  },
  "example-api-user-get" = {
    "api_name" = "example-api"
    "display_name" = "Get User Operation"
    "method" = "GET"
    "url_template" = "/users/{id}/get"
    "description" = "This can only be done by the logged in user."
  }
},

Differences:

  • Top-level keys have the API name and the operation ID, so they’re globally unique
  • Additional elements "api_name" and "operation_id" in the map for the values that were previously keys

You would then use this like so:

resource "azurerm_api_management_api_operation" "example" {
  for_each            = var.api_operation
  api_name            = each.value.api_name
  operation_id        = each.value.operation_id
  api_management_name = azurerm_api_management.example.name
  resource_group_name = azurerm_resource_group.example.name
  display_name        = each.value.display_name
  method              = each.value.method
  url_template        = each.value.url_template
  description         = each.value.description
}

Hope this helps!

Bonus: if you want to keep your original data structure, you can convert it in a local variable using for expressions along with the merge function and the splat expression:

locals {
  flattened_operations = merge([
    for api, ops in var.api_operation : {
      for op, attrs in ops : "${api}-${op}" => merge(
        { api_name : api, operation_id : op },
        attrs
      )
    }
  ]...)
}

Then you can use for_each = local.flattened_operations in the resource.

1 Like

Thanks @alisdair, but I need to maintain the original data structure. So I have to use locals here . But getting error for the locals

The expanding argument (indicated by ...) must be of a tuple, list or set type.

That’s strange. Did you copy and paste the locals definition I gave? I tested it locally (with Terraform 0.13.2) and it works for me.

If it’s still not working, please reduce your configuration as much as possible while still preserving the error, paste it here, and let me know which Terraform version you’re using.

Thanks a lot. Now it’s working. I was using 12.25 previously. Updated with 13. :slight_smile:

1 Like