How to pass for_each map keys to settings block

Hello,

This post is a continuation of an effort that has a previous post (Completed) that can be found here.

I have attempted to follow the guidance on the posting etiquette via the Guide to asking for help in this forum. It was written right around the same time as my first post :slight_smile:

My goal:
To build a Landing Zone for compute workloads. The Landing Zone contains a vnet, subnets, NSG, Route tables, Peering back to the hub, Key Vault, Recovery Service Vault, Compute with ADE, etc.

Thanks to the help from @apparentlymart I was able to accomplish this using a variable map object that includes specifics on the workloads that will use in the Landing Zone.

This post is about the next steps of the effort. I would like to include the provisioning of Azure Key Vaults based on bool variable type. I have added this into the code shown below

Using the following structure I am able to use for_each to deploy resources groups and resources based on the workload I wish to deploy. Except for azurerm_virtual_machine_extension because I am running into an issue using map keys from the for_each build of azurerm_key_vault in the settings - (Optional) The settings passed to the extension, these are specified as a JSON object in a string.

for each workload where workload_kv in var.workloads is set to true

In my variables.tf:

#####------[Workloads]------#####
variable "workloads" {
  type = map(object({
    workload_name                  = string
    workload_tags                  = map(string)
    workload_subnet_address_prefix = list(string)
    vms = map(object({
      hostname  = string
      ip_address = string
      vm_size   = string
    }))
    workload_kv = bool    
    lb_frontend_private_ip_address = string
    lb_backend_vms                  = map(object({
      hostname  = string
    }))
  }))
  default = {
  }
}

Reaching the VM values during a for_each required an inverted variable map to be created in the locals section.

In my locals.tf:

locals {

  ####-----[Workload VMs]-----####
  workload_vms = merge([
    for wlk, wl in var.workloads : tomap({
      for vmk, vm in wl.vms : "${wlk}:${vmk}" => {
        vms                            = vm
        hostname                       = vm.hostname
        ip_address                     = vm.ip_address
        vm_size                        = vm.vm_size #workload = wl; vm_key = vmk
        workload_key                   = wlk
        workload_name                  = wl.workload_name
        workload_tags                  = wl.workload_tags
        workload_subnet_address_prefix = wl.workload_subnet_address_prefix
        workload_kv                    = wl.workload_kv
        lb_frontend_private_ip_address = wl.lb_frontend_private_ip_address
      }
    })
  ]...)
}

Azure Key Vault built from for_each

#####------[Key Vault Vault]------#####
resource "azurerm_key_vault" "workload" {
  provider                        = azurerm.target_subcription
  for_each                        = { for i, item in var.workloads : i => item if item.workload_kv == true }
  name                            = var.name_string_randomizer ? replace("${local.name_prefix_landing_zone_short}", "${var.landing_zone}", "${each.value.workload_name}${random_string.randomizer.id}kv") : replace("${local.name_prefix_landing_zone_short}", "${var.landing_zone}", "${each.value.workload_name}kv")
  resource_group_name             = one([for item in azurerm_resource_group.workload : item.name if can(regex("${each.value.workload_name}", item.name))])
  location                        = one([for item in azurerm_resource_group.workload : item.location if can(regex("${each.value.workload_name}", item.name))])
  tenant_id                       = data.azurerm_client_config.context.tenant_id
  enabled_for_deployment          = true
  enabled_for_disk_encryption     = true
  enabled_for_template_deployment = true
  purge_protection_enabled        = false
  soft_delete_retention_days      = 7
  sku_name                        = "standard"

  network_acls {
    bypass         = "AzureServices"
    default_action = "Allow"
  }

  tags = merge(each.value.workload_tags, local.calculated_tags, local.function_kv)

  lifecycle {
    prevent_destroy = true
  }
}

I have tried this:

###----[Enable VM Extension - AzureDiskEncryption]----###
resource "azurerm_virtual_machine_extension" "workload_ade" {
  provider                   = azurerm.target_subcription
  for_each                   = local.workload_vms
  name                       = "AzureDiskEncryption"
  publisher                  = "Microsoft.Azure.Security"
  type                       = "AzureDiskEncryption"
  type_handler_version       = "2.2"
  auto_upgrade_minor_version = false
  virtual_machine_id         = azurerm_windows_virtual_machine.workload[each.key].id

  settings = <<SETTINGS
   {
    "EncryptionOperation": "EnableEncryption",
    "KeyEncryptionAlgorithm": "RSA-OAEP",
    "KeyVaultURL": "${azurerm_key_vault.workload[each.key.workload_key].vault_uri}",
    "KeyVaultResourceId": "${azurerm_key_vault.workload[each.key.workload_key].id}",
    "VolumeType": "All"
   }
   SETTINGS

  tags = merge(each.value.workload_tags, local.calculated_tags, local.function_vm_extension)

  depends_on = [
    azurerm_network_interface_security_group_association.workload,
    azurerm_network_interface_application_security_group_association.workload
  ]

  lifecycle {
    prevent_destroy = true
  }
}

The terraform code is currently being run in an Azure DevOps Pipeline and I am seeing the following error:

Starting: Validate
==============================================================================
Task         : Terraform
Description  : Execute terraform commands to manage resources on AzureRM, Amazon Web Services(AWS) and Google Cloud Platform(GCP)
Version      : 3.209.19
Author       : Microsoft Corporation
Help         : [Learn more about this task](https://aka.ms/AAf0uqr)
==============================================================================
C:\agent\_work\_tool\terraform\1.3.7\x64\terraform.exe validate
β•·
β”‚ Error: Unsupported attribute
β”‚ 
β”‚   on ..\modules\compute-landing-zone\c8-virtual-machines-workload.tf line 156, in resource "azurerm_virtual_machine_extension" "workload_ade":
β”‚  156:     "KeyVaultURL": "${azurerm_key_vault.workload[each.key.workload_key].vault_uri}",
β”‚     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     β”‚ each.key is a string
β”‚ 
β”‚ Can't access attributes on a primitive-typed value (string).
β•΅
β•·
β”‚ Error: Unsupported attribute
β”‚ 
β”‚   on ..\modules\compute-landing-zone\c8-virtual-machines-workload.tf line 157, in resource "azurerm_virtual_machine_extension" "workload_ade":
β”‚  157:     "KeyVaultResourceId": "${azurerm_key_vault.workload[each.key.workload_key].id}",
β”‚     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     β”‚ each.key is a string
β”‚ 
β”‚ Can't access attributes on a primitive-typed value (string).
β•΅
##[error]Error: The process 'C:\agent\_work\_tool\terraform\1.3.7\x64\terraform.exe' failed with exit code 1
Finishing: Validate

Based on how the β€˜SETTINGS’ is being used the error makes sense to me because SETTINGS is the delimiter/identifier for the β€œheredoc” style string literal from what I have read so far.

I also tried using the jsonencode fucntion:

###----[Enable VM Extension - AzureDiskEncryption]----###
resource "azurerm_virtual_machine_extension" "workload_ade" {
  provider                   = azurerm.target_subcription
  for_each                   = local.workload_vms
  name                       = "AzureDiskEncryption"
  publisher                  = "Microsoft.Azure.Security"
  type                       = "AzureDiskEncryption"
  type_handler_version       = "2.2"
  auto_upgrade_minor_version = false
  virtual_machine_id         = azurerm_windows_virtual_machine.workload[each.key].id

  settings = jsonencode({
    EncryptionOperation    = "EnableEncryption"
    KeyEncryptionAlgorithm = "RSA-OAEP"
    KeyVaultURL            = azurerm_key_vault.workload[each.key.workload_key].vault_uri
    KeyVaultResourceId     = azurerm_key_vault.workload[each.key.workload_key].id
    VolumeType             = "All"
  })

  tags = merge(each.value.workload_tags, local.calculated_tags, local.function_vm_extension)

  depends_on = [
    azurerm_network_interface_security_group_association.workload,
    azurerm_network_interface_application_security_group_association.workload
  ]

  lifecycle {
    prevent_destroy = true
  }
}

That results in the same error from what I can tell

Starting: Validate
==============================================================================
Task         : Terraform
Description  : Execute terraform commands to manage resources on AzureRM, Amazon Web Services(AWS) and Google Cloud Platform(GCP)
Version      : 3.209.19
Author       : Microsoft Corporation
Help         : [Learn more about this task](https://aka.ms/AAf0uqr)
==============================================================================
C:\agent\_work\_tool\terraform\1.3.7\x64\terraform.exe validate
β•·
β”‚ Error: Unsupported attribute
β”‚ 
β”‚   on ..\modules\compute-landing-zone\c8-virtual-machines-workload.tf line 155, in resource "azurerm_virtual_machine_extension" "workload_ade":
β”‚  155:   KeyVaultURL            = azurerm_key_vault.workload[each.key.workload_key].vault_uri
β”‚     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     β”‚ each.key is a string
β”‚ 
β”‚ Can't access attributes on a primitive-typed value (string).
β•΅
β•·
β”‚ Error: Unsupported attribute
β”‚ 
β”‚   on ..\modules\compute-landing-zone\c8-virtual-machines-workload.tf line 156, in resource "azurerm_virtual_machine_extension" "workload_ade":
β”‚  156:   KeyVaultResourceId     = azurerm_key_vault.workload[each.key.workload_key].id
β”‚     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     β”‚ each.key is a string
β”‚ 
β”‚ Can't access attributes on a primitive-typed value (string).
β•΅
##[error]Error: The process 'C:\agent\_work\_tool\terraform\1.3.7\x64\terraform.exe' failed with exit code 1
Finishing: Validate

Using other code like PowerShell I can create variables on the fly that could be used in a situation such as this where a property in a for_each can be converted into a useable string. I have been looking for a way to do this in Terraform but have not been able to find one.

In Summary I am trying to create Azure Key Vault per workload using for_each and then point Azure Virtual Machine extensions to the workload appropriate Key Vault for that workload.

How can I take a key or value map from a for_each build and store it in a string that can be used in a β€˜SETTINGS’ block?

I greatly appreciate any help or guidance on this anyone can provide.

Hi @Joseph2290w,

The value of each.key will always be a string because it represents the key from the current element of the map that this resource instance is based on. In this case that means it would be the result of the "${wlk}:${vmk}" expression in your local.workload_vms definition, so it’s a string containing the two keys concatenated with a colon.

From the surrounding context I’m guessing that you intended to use the workload_key attribute of each object from the values of the elements in your map, rather than the keys of those elements. local.workload_vms is a map of objects, and those objects do seem to have workload_key attributes.

If that’s true then the answer is thankfully relatively straightforward: you need to use each.value.workload_key instead of each.key.workload_key. each.value represents the value of the current element of the map, which in this case will be an object which has that attribute.


Separate note: As you saw the decision between using a β€œheredoc” template and jsonencode was not significant in this error because each.key was still a string either way, but FWIW I’d also recommend the jsonencode version as the better choice here because it will avoid any risk that the generated JSON might have invalid syntax as a result of special characters appearing in any of your substituted strings.

It’s typically a bad idea to try to generate JSON by string concatenation; jsonencode is there so you don’t need to.

Fantastic! That was it! Replacing each.key.workload_key with each.value.workload_key did the trick and I appreciate the advice on using the jsonencode function.

Thank you so very much for taking the time to help me.