Random key for resources in for_each

Hi,

If you have a list(object), containing let’s say the following data:

demo_list_object = [
  {
    "Key1" = "Value1"
    "Key2" = "Value2"
    "Key3" = "Value3"
    "Key4" = "Value4"
  },
  {
    "Key1" = "Value1"
    "Key2" = "Value2"
    "Key3" = "ValueX"
    "Key4" = "Value4"
  },
  {
    "Key1" = "Value1"
    "Key2" = "Value2"
    "Key3" = "ValueY"
    "Key4" = "Value4"
  }
]

In this object, all of the single values could potentially be the same, but not all of them at the same time. So in others words, to be able to uniquely identify each of the objects, it has to be done by combining all of the key values to get something unique.

I need this unique value to be able to key up the resources in a for_each, so that it wont complain similarly to this:

Two different items produced the key “XXX” in this ‘for’ expression. If
duplicates are expected, use the ellipsis (…) after the value expression to
enable grouping by key.

Inside the same for_each, i’d also like to use a randomly generated string to name resources.

To achieve this right now, I’m doing the following (which for me looks quite ugly):

resource "random_string" "random" {
  for_each = { for demolistobject in var.demo_list_object : "${demolistobject.Key1} ${demolistobject.Key2} ${demolistobject.Key3} ${demolistobject.Key4}" => demolistobject }

  special = false
  number  = true
  length  = 6
}

resource "azurerm_virtual_machine" "main" {
  for_each = { for demolistobject in var.demo_list_object : "${demolistobject.Key1} ${demolistobject.Key2} ${demolistobject.Key3} ${demolistobject.Key4}" => merge(demolistobject, {
    resources_name_affix = "${random_string.random["${demolistobject.Key1} ${demolistobject.Key2} ${demolistobject.Key3} ${demolistobject.Key4}"].result}"
    })
  }

  name = "VM-${each.value.resources_name_affix}"
  ...
}

This enbles me to couple a random generated string, from the random_string loop created resources, with the azurerm_virtual_machine loop created resources. Hence coupling each of the single randomly generated strings, to a specific virtual machine. This takes care of the key of the resources being unique, as well as the acutal virtual machine scale set name being unique as well. But I wonder, is there any nicer way to do this? Am I going at this in the wrong way? Is there something that could be improved in the code?

You can’t in any way have the same for_each for two resources, right?
Something like this I mean:

for_each {
  resource "random_string" "random" {
    special = false
    number  = true
    length  = 6
  }

  resource "azurerm_virtual_machine" "main" {
    name = "VM-${each.value.random_string.random.result}"
    ...
  }
}

I appreciate all input, I want to go at this in the best way possible!

Thank you!

Hi,

Yes, the key in thefor_each statement must be unique. In your situation, unless you can bring a map of map instead of a list of map, you can prefix your for_each map key by the list index but it will drift if the input map has the same content in different order. if you had a map of map, you could prefix your for_each map with the outer map key. random_string does not guarantee uniqueness but a very low likeliness of duplicate.

Hope it helps.

Cheers

Hello @ohmer and thank you for your reply! :slight_smile:

“you can prefix your for_each map key by the list index but it will drift if the input map has the same content in different order.”
As you’re saying, if I do it in this way, I can never remove any object from the list without causing complications to other objects in the list.

if you had a map of map, you could prefix your for_each map with the outer map key.
By that do you mean something similar of the below?

variable "map_of_map" {
    default = {
        "OuterMapKey1" = {
            "Key1" = "Value1"
            "Key2" = "Value2"
            "Key3" = "Value3"
            "Key4" = "Value4"
        },
        "OuterMapKey2" = {
            "Key1" = "Value1"
            "Key2" = "Value2"
            "Key3" = "ValueX"
            "Key4" = "Value4"
        }

    etc........
}

Then someone has to input a unique value as “OuterMapKey1” etc, correct? Or perhaps I’m misunderstanding you? I could also use the type as map(object), for example:

type = map(object({
        Key1 = string
        Key2 = string
        Key3 = number
        Key4 = bool
    })))
    
    default = {
        "OuterMapKey1" = {
            "Key1" = "Value1"
            "Key2" = "Value2"
            "Key3" = 123
            "Key4" = true
        },
        "OuterMapKey2" = {
            "Key1" = "Value1"
            "Key2" = "Value2"
            "Key3" = 456
            "Key4" = true
        }

        etc........
    }
}

Which allows me to work with object types as well. I’ll keep this in mind, but I’d rather not see it as a requirement to also add a specific “OuterMapKey”.

random_string does not guarantee uniqueness but a very low likeliness of duplicate.
Aware of it not guaranteeing uniqueness, buth thanks for emphasizing that. Could be changed to a GUID or some kind of hash of the different values, but for example purposes I’ll stick with random string here. Just wanted to show that I wan’t something to generate the unique key, instead of having to input it somewhere (i.e. as an “OuterMapKey”).

Thank you for your input so far, I do appreciate it! :slight_smile:

/Alexander

Yep, that’s where I was going. GUID is the same, unless you have a register of already attributed GUID, this is very low likeliness as well.

I reread your initial message carefully. As one of your assumption is not all 4 keys can’t be duplicate in 2 objects, you can use this as a unique key for your for_each. It looks like I made the wrong assumption that your object had variable attributes.

variable "demo_list_object" {
  type = list(object({
    Key1 = string
    Key2 = string
    Key3 = number
    Key4 = bool
  }))

}

resource "null_resource" "test" {
  for_each = {
    for i in var.demo_list_object :
    format("%s-%s-%s-%s", i.Key1, i.Key2, i.Key3, i.Key4) => i
  }
}

with that as input:

demo_list_object = [
  {
    "Key1" = "Value1"
    "Key2" = "Value2"
    "Key3" = 10
    "Key4" = true
  },
  {
    "Key1" = "Value1"
    "Key2" = "Value2"
    "Key3" = 20
    "Key4" = true
  },
  {
    "Key1" = "Value1"
    "Key2" = "Value2"
    "Key3" = 10
    "Key4" = false
  }
]

gives the following plan

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # null_resource.test["Value1-Value2-10-false"] will be created
  + resource "null_resource" "test" {
      + id = (known after apply)
    }

  # null_resource.test["Value1-Value2-10-true"] will be created
  + resource "null_resource" "test" {
      + id = (known after apply)
    }

  # null_resource.test["Value1-Value2-20-true"] will be created
  + resource "null_resource" "test" {
      + id = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

You could also couple your random string and virtual machine resource together into a module. Then you trigger the module with a for_each loop, and each machine is created with a unique string.

This is what I’m doing right now, as shown in my original post! :slight_smile:

Indeed that’s one way to do it (and for some scenarios I guess even the nicest way :slight_smile: ), but because of other reasons it unfortunately doesn’t work for my scenario. Thank you for your input though, it’s nice to discuss around the topic!

2 Likes