Terraform - inverting a map

Viewed 450

I'm stuck trying to write a terraform expression which can turn this:

subnets = {
   my_subnet_1 = {
     nsg       = "my_nsg_1",
     addresses = "my_addresses_1"
   }
   my_subnet_2 = {
     nsg       = "my_nsg_2",
     addresses = "my_addresses_2"
   }
}

into

nsgs_assocs = {
  my_nsg_1 = "my_subnet_1"
  my_nsg_2 = "my_subnet_2"
}

I've tried the following:

locals {
  nsgs_assocs = zipmap(
    var.subnets.*.nsg,
    keys(var.subnets)
  )
}

but this gives an error:

Error: Invalid function argument

  on ..\..\modules\vnet\main.tf line 22, in locals:
  21:   nsgs_assocs = zipmap(
  22:     var.subnets.*.nsg,
  23:     keys(var.subnets)
  24:   )

Invalid value for "keys" parameter: element 0: string required.

For context, I've inherited a bunch of scripts which I'm trying to refactor without changing the results of a terraform plan.

One of the modules has a lot of related lookup maps - e.g:

nsgs_assocs = {
  my_nsg_1 = "my_subnet_1"
  my_nsg_2 = "my_subnet_2"
}

subnet_addresses = {
  my_subnet_1 = "my_addresses_1"
  my_subnet_2 = "my_addresses_2"
}

which I've condensed into my first above sample which I think will be more maintainable in the long run.

However, for backward compatibility with the existing terraform state I need to generate the original nsgs_assocs inside my module so that a for_each continues to use the nsg name as the resource key instead of the subnet name (which causes a destroy / create pair due to the key change).

3 Answers

You're on the right track. It does not work, because splat expression works with arrays, and var.subnets is a map. In order to fix it, you need to convert it into array and it can be done by using values terraform function:

locals {
  nsgs_assocs = zipmap(
    values(var.subnets)[*].nsg,
    keys(var.subnets)
  )
}

If you have:

variable "subnets" {
    default = {
       my_subnet_1 = {
         nsg       = "my_nsg_1",
         addresses = "my_addresses_1"
       }
       my_subnet_2 = {
         nsg       = "my_nsg_2",
         addresses = "my_addresses_2"
       }
    }
}

then the following is incorrect

var.subnets.*.nsg

Thus, it should be values(var.subnets).*.nsg:

locals {
  nsgs_assocs = zipmap(
    values(var.subnets).*.nsg,
    keys(var.subnets)
  )
}

resulting in:

{
  "my_nsg_1" = "my_subnet_1"
  "my_nsg_2" = "my_subnet_2"
}

There are a few different ways to achieve this, and the zipmap-based solutions others have shared are fine answers too, but I also wanted to show an example using for expressions because I (subjectively) tend to think this form is easiest to read and understand:

locals {
  nsgs_allocs = {
    for k, s in var.subnets : s.nsg => k
  }
}

As long as all of your subnets have unique nsg values, the above should produce the result you were looking for.

In situations where the new key isn't unique -- for example, if in your cases there could be multiple subnets with the same nsg value -- you can use the for expression's "grouping" mode, which would produce a map of lists of subnet values so that there can potentially be more than one value under each key:

locals {
  nsgs_allocs = {
    for k, s in var.subnets : s.nsg => k...
  }
}
nsgs_assocs = {
  my_nsg_1 = ["my_subnet_1"]
  my_nsg_2 = ["my_subnet_2"]
}
Related