Shifting map values one level up

I’m currently trying to create a map which is suitable for my for_each logic.

I have the following map:

subscriptions = {
  "subscriptions-S_FOOBAR_" = {
    "S_FOOBAR_PROD" = {
      "display_name" = "S_FOOBAR_PROD"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "PROD"
      "workload_name" = "FOOBAR"
    }
  }
  "subscriptions-S_FOOBAZ_" = {
    "S_FOOBAZ_DEV" = {
      "display_name" = "S_FOOBAZ_DEV"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "DEV"
      "workload_name" = "FOOBAZ"
    }
    "S_FOOBAZ_PROD" = {
      "display_name" = "S_FOOBAZ_PROD"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "PROD"
      "workload_name" = "FOOBAZ"
    }
    "S_FOOBAZ_TEST" = {
      "display_name" = "S_FOOBAZ_TEST"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "TEST"
      "workload_name" = "FOOBAZ"
    }
  }
}

What I actually need is a map where the only elements are the “S_FOOBAZ_*” objects, like so:

subscriptions = {
  "S_FOOBAR_PROD" = {
      "display_name" = "S_FOOBAR_PROD"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "PROD"
      "workload_name" = "FOOBAR"
    }
    "S_FOOBAZ_DEV" = {
      "display_name" = "S_FOOBAZ_DEV"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "DEV"
      "workload_name" = "FOOBAZ"
    }
    "S_FOOBAZ_PROD" = {
      "display_name" = "S_FOOBAZ_PROD"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "PROD"
      "workload_name" = "FOOBAZ"
    }
    "S_FOOBAZ_TEST" = {
      "display_name" = "S_FOOBAZ_TEST"
      "subscription_id" = "00000000-0000-0000-0000-000000000000"
      "tenant_id" = "00000000-0000-0000-0000-000000000000"
      "workload_environment" = "TEST"
      "workload_name" = "FOOBAZ"
    }
}

I’ve tried it with different nested for expressions, but was unable to get a working result.

I was hoping maybe someone here has a good idea how I can tackle this issue.

Some additional details:

I’m creating the map with the following code:

subscriptions = var.connected_workloads == null ? {} : {
    for workload in data.azurerm_subscriptions.workload_subscriptions : "subscriptions-${workload.display_name_prefix}" => {
      for subscription in workload.subscriptions : subscription.display_name => {
        display_name         = subscription.display_name
        subscription_id      = subscription.subscription_id
        tenant_id            = subscription.tenant_id
        workload_name        = split("_", subscription.display_name)[1]
        workload_environment = split("_", subscription.display_name)[2]
      }
    }
  }

If it would be possible to have a for loop without creating a map key, I could just create the map I want from the beginning - hopefully I’m missing something

Hi @tiwood!

It seems like your second-level keys are unique across all of the maps rather than just within each individual map, and so one way you could get this done is to use the merge function with all of the maps, producing a single map as a result. For example:

locals {
  subscriptions = {
    # (as you showed)
  }
  subscription_environments = merge(values(local.subscriptions)...)
}

The ... symbol after the call to values(local.subscriptions) makes that argument an expanding function argument, which means that instead of just passing the list result as a single argument Terraform will use each of the elements of the list as successive extra values to the function, which then matches the signature of merge.

This strategy wouldn’t be appropriate if the second-level keys were not unique across all maps; if there are any overlaps then the one belonging to the subscription whose name sorts later in lexical order will “win”, overwriting any others.

Alternatively, you could construct the collection in the flattened form immediately by using the flatten function pattern. That produces a list instead of a map, but you can then convert it into a map with one more for expression just in time, in the for_each argument itself:

locals {
  subscriptions = var.connected_workloads == null ? [] : flatten([
    for workload in data.azurerm_subscriptions.workload_subscriptions : [
      for subscription in workload.subscriptions : {
        display_name         = subscription.display_name
        subscription_id      = subscription.subscription_id
        tenant_id            = subscription.tenant_id
        workload_name        = split("_", subscription.display_name)[1]
        workload_environment = split("_", subscription.display_name)[2]
      }
    ]
  ])
}

resource "example" "example" {
  for_each = { for s in local.subscriptions : s.display_name => s }
}

One side-effect of this flatten pattern that I like is that the for expression inside the for_each makes it clearer to the reader that s.display_name is the unique key for this resource, rather than having to go and separately study the definition of local.subscriptions. Knowing how a particular resource uniquely identifies its individual instances can be helpful when reviewing a plan or when making future changes to the configuration.