Loop over a Map of Objects

Viewed 3716

How do I do a for_each loop for the following?

I want to create a tfe_variable node_count & vm_size. I need both these tfe_variables in both wksp1 and wksp2

variable "custom_variables" {
  type = map(object({
    node_count = number
    vm_size    = string
  }))

  default = {
    wksp1 = {
      node_count = 2
      vm_size    = "Standard_D2_v3"
    },
    wksp2 = {
      node_count = 5
      vm_size    = "Standard_D2_v5"
    }
  }
}

resource "tfe_variable" "custom" {
  for_each = {
    # for each workspace & variable in var.custom_variables create a tfe_variable
  }

  key          = each.value.name
  value        = each.value.value
  category     = "terraform"
  workspace_id = each.value.workspace_id
}
2 Answers

You're really close! Here are a couple of things to consider:

Option 1: Multiple tfe_variable resources

  1. Create a tfe_variable resource for each variable you want to create
  2. Make sure the key in you custom_variables map is the workspace ID.
variable "custom_variables" {
  type = map(object({
    node_count = number
    vm_size    = string
  }))

  default = {
    wksp1_id = {
      node_count = 2
      vm_size    = "Standard_D2_v3"
    },
    wksp2_id = {
      node_count = 5
      vm_size    = "Standard_D2_v5"
    }
  }
}

resource "tfe_variable" "node_count" {
  for_each = var.custom_variables

  key          = "node_count"
  value        = each.value.node_count
  category     = "terraform"
  workspace_id = each.key
}

resource "tfe_variable" "vm_size" {
  for_each = var.custom_variables

  key          = "vm_size"
  value        = each.value.vm_size
  category     = "terraform"
  workspace_id = each.key
}

The drawback to this option is that you'll need an additional resource for each variable.

Option 2: A list of variable objects

  1. Define a list of the keys, values, and workspace IDs of each variable
  2. Use count to iterate the list
variable "custom_variables" {
  type = list(object({
    key          = string
    value        = string
    workspace_id = string
  }))
  default = [
    {
      key          = "node_count"
      value        = "2"
      workspace_id = "wksp1_id"
    },
    {
      key          = "node_count"
      value        = "5"
      workspace_id = "wksp2_id"
    },
    {
      key          = "vm_size"
      value        = "Standard_D2_v3"
      workspace_id = "wksp1_id"
    },
    {
      key          = "vm_size"
      value        = "Standard_D2_v5"
      workspace_id = "wksp2_id"
    }
  ]
}

resource "tfe_variable" "custom" {
  count = length(var.custom_variables)

  key          = var.custom_variables[count.index].key
  value        = var.custom_variables[count.index].value
  workspace_id = var.custom_variables[count.index].workspace_id
  category     = "terraform"
}

There are a couple of drawbacks to this approach as well:

  • There is a fair amount of duplicated code in the variable definition
  • The value must always be of the same type

If you're struggling with loop concepts in Terraform, this blog post might help you.

The main requirement to keep in mind for for_each is that we always need to create a map that has one element for each instance of the resource we want to create. In this case, that means you need a map with one element per workspace per variable, because tfe_variable describes a single variable on a single workspace.

Our job then is to write an expression to project the map-of-objects value coming in via the variable to a collection that has a separate element per variable. Here's one way to get that done, using the flatten function in a way similar to an example in its documentation:

locals {
  workspace_variables = flatten([
    for ws_name, ws in var.custom_variables : [
      for var_name, value in ws : {
        workspace = ws_name
        variable  = var_name
        value     = value
      }
    ]
  ])
}

The above should produce a local.workspace_variables that looks like this:

[
  { workspace = "wksp1", variable = "node_count", value = 2 },
  { workspace = "wksp1", variable = "vm_size", value = "Standard_D2_v3" },
  { workspace = "wksp2", variable = "node_count", value = 5 },
  { workspace = "wksp2", variable = "vm_size", value = "Standard_D2_v5" },
]

This now meets the requirement of having one element per desired tfe_variable instance, so our only remaining job is to project this into a map to provide unique identifiers for each element and describe how to populate the tfe_variable arguments based on these objects:

resource "tfe_variable" "custom" {
  for_each = {
    for wsv in local.workspace_variables : "${wsv.workspace}.${wsv.variable}" => wsv
  }

  key          = each.value.variable
  value        = each.value.value
  category     = "terraform"
  workspace_id = each.value.workspace
}

One thing I didn't contend with above, because it wasn't directly you question, is the value of workspace_id in tfe_variable. If I recall correctly, that argument is expecting a workspace id rather than a workspace name, in which case you might need a slightly more complicated expression for the workspace_id argument. If you already have a tfe_workspace resource using the workspace names as keys then something like this might work, for example:

  workspace_id = tfe_workspace.example[each.value.workspace].id

If your workspaces are created in a different way then you may have to do something more complicated here, but that's getting far off the topic of your original question so I won't try to dig into that here. I'm happy to try to help with it in a separate question on this site though, if you like.

Related