For_each iterate over list when creating resource (tag)

I have made a vsphere module to deploy some vm’s on vcenter with terraform. I use the code below to create the vms.

The issue I keep having is creating multiple tags per vm, one tag is working fine.

I have the variable vtags, which is a list of strings that I would like to attach to the vm.
When I use vtags[0]. The first tag (webserver1) gets created without an issue. How do I create both tags (webserver1 and nginx1 ) for a vm?

I tried it with count instead of for_each but It seems to defiate me further away to a solution.

Can somebody give some tips to point me in the right direction?

create-vms.tf:

module "vsphere" {
        source = "../vsphere"
        omgeving = "developmine"
        stack_name = "stackab"
        vms = {
            bla-01 = {
              vmname        = "test-01"
              vmip          = "10.222.111.156"
              vtemplate     = "rhel83-v2"
              vtags = [ "webserver1", "nginx1" ]
              vcpu = {
                develop     = "1"
                developmine = "1"
              }
              vmemory = {
                develop     = "1024"
                developmine = "1024"
              }
              vdisk = {
                develop     = "20"
                developmine = 30
              }
            }
        }
}

variables.tf:

variable "omgeving" {
   default=[]
}

variable "stack_name" {
   default=[]
}

variable "vmname" {
   default=[]
}

variable "vmip" {
   default=[]
}

variable "vsphere" {
   default=[]
}

variable "vtemplate" {
   default=[]
}

variable "naam" {
   default=[]
}

variable "rol" {
   default=[]
}



variable "vms" {
   type = map(object({
      vmname    = string
      vmip      = string
      vtemplate = string
      vtags     = string
      vmemory   = map(string)
      vtags     = list(string)
      vcpu      = map(number)
      vdisk     = map(number)
   }))
}

main.tf:

...
resource "vsphere_tag" "tag" {
  for_each    = var.vms
  category_id = data.vsphere_tag_category.iac.id
  name        = each.value.vtags[1]
}

resource "vsphere_virtual_machine" "vm_linux" {
  for_each = var.vms
  tags              = [vsphere_tag.tag[each.key].id]

...

Hi @deanso,

What you have here is a pretty interesting different variant of the common hierarchical data structure that we often solve with the flatten function.

Your case is a bit different because rather than declaring one tag for each pair of VM and tag name, we instead need to declare each distinct tag name only once and then attach of the tags to its appropriate VMs. Therefore we won’t use flatten here, and instead we’ll do some set arithmetic.

The first problem is to derive a set that’s the union of all of the sets of tags across all of your VMs. As far as I can tell, vsphere treats the tags as a set rather than a list (each unique tag can only be declared once and their order isn’t significant) so I’d start by changing the definition of vtags to be set(string) instead of list(string), and then from there deriving the union set we need:

variable "vms" {
  type = map(object({
    vmname    = string
    vmip      = string
    vtemplate = string
    vtags     = string
    vmemory   = map(string)
    vtags     = set(string)
    vcpu      = map(number)
    vdisk     = map(number)
  }))
}

locals {
  all_tags = setunion([for vm in var.vms : vm.vtags]...)
}

This uses a for expression to retrieve the vtags sets from each of the VM objects, and passes that to setunion using the ... spread operator so that each of the sets will be a separate argument to setunion, because that’s what that function expects.

With the example input you showed, there was only one VM declared anyway and so this result would end up being just the same tags associated with that VM: "webserver1" and "nginx1". But if you had more than one VM then this would end up containing the unique tags across all of them, which seems to be what we need to get vsphere_tag to create all of the tag objects needed:

resource "vsphere_tag" "tag" {
  for_each = local.all_tags

  category_id = data.vsphere_tag_category.iac.id
  name        = each.value
}

Notice that we’re declaring one vsphere_vtag.tag instance per element of local.all_tags, not per VM. That means each distinct tag will get registered in vSphere only once. With the input you showed you’d end up with the following two instances:

  • vsphere_tag.tag["webserver1"]
  • vsphere_tag.tag["nginx1"]

The final step then would be to get the IDs from each VM’s declared tags written into the tags argument of your vsphere_virtual_machine. Since your vtags set contains the tag names and vsphere.tag.tag is a mapping from tag names to tag objects, we can achieve that with a for expression to look up each of the needed tag IDs:

resource "vsphere_virtual_machine" "vm_linux" {
  for_each = var.vms

  # ...
  tags = [
    for tag_name in each.value.vtags : vsphere_tag.tag[tag_name].id
  ]
}

If you had multiple VMs that all declared the same tag, they’d each end up referring to the same tag ID here, because we arranged for each distinct tag to be registered only once.

Hopefully that’s a useful starting point!

2 Likes

Thanks @apparentlymart, that’s a nice solution!
This works as a charm, much appreciate your well written answer, learnt from it. :+1: