Resource ForEach Value inside Block

I am looking to see if it is possible to call get a value in a resource level foreach into a block within the resource. Here is the example I have. Say I have a map object of Linux VM like this:

linux = {
  vm1 = {
    size = "Standard_FS"
    version = "8"
  },
  vm2 = {
    size = "Standard_FS"
    version = "7"
  },
}

I then want to take this and use this in a resource level for each:

resource "azurerm_linux_virtual_machine" "vm" {
  for_each                        = var.linux
  name                            = "${each.key}.${var.domain}"
  resource_group_name             = var.resource_group_name
  location                        = var.location
  size                            = lookup(each.value, "size", "Standard_F2")
  network_interface_ids = [
    azurerm_network_interface.rhel_vnic[each.key].id,
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "RedHat"
    offer     = "RHEL"
    sku       = "${lookup(each.value, "version"}-LVM"
    version   = "latest"
  }

  boot_diagnostics {
    storage_account_uri = var.storage_account_uri
  }
}

It seems as though the source_image_reference block is looking for its own for_each to iterate through, and is not aware of the higher level for each at the resource level. Is there a way to get the each.value from the resource into the sub block?

Hi @donwlewis,

I’m not sure I follow what you’ve concluded at the end of your question here. Did you see an error message when you tried this? If so, could you share that error message and then hopefully I can explain what it means and suggest what to do about it?

Here is the error message I get:

 on ../module/main.tf line 39, in resource "azurerm_linux_virtual_machine" "rhel":
  39:     sku       = "${lookup(each.value, "version")}-LVM"
    |----------------
    | each.value is object with no attributes

Invalid value for "inputMap" parameter: the given object has no attribute
"version".

Hi @donwlewis,

That’s an odd error indeed! I’m not sure yet what’s causing it – what you’ve written looks correct to me on first read – but maybe we can get a different error message from Terraform by using the current index syntax rather than the legacy lookup function syntax for accessing that attribute:

    sku       = "${each.value.version}-LVM"

(The lookup function is, since Terraform v0.12, only intended for looking up attributes that might not exist and providing a fallback default value. You don’t have a default value in this case, so there’s no need to use lookup.)

I don’t expect the above change to make it work, but I’m hoping it will give a better hint as to where the problem is coming from.

Another thing you could try, just to see if it works, is var.linux[each.key].version. It shouldn’t be necessary to write it out longhand like that, but I’m curious to see if it changes the result as it might give a clue as to why the normal reference each.value.version didn’t work.

Here is the output with suggested change:

Error: Unsupported attribute

  on ../module/main.tf line 39, in resource "azurerm_linux_virtual_machine" "rhel":
  39:     sku       = "${each.value.version}-LVM"
    |----------------
    | each.value is object with no attributes

This object does not have an attribute named "version".

Here is the error with the long hand:

Error: Unsupported attribute

  on ../module/main.tf line 39, in resource "azurerm_linux_virtual_machine" "rhel":
  39:     sku       = "${var.linux[each.key].version}-LVM"

This object does not have an attribute named "version".

Interestingly if I call that from outside of the source_image_reference block, it seems to find the value of the key version. I was wondering if it had something to do with the for_each scope, since I know blocks can be built dynamically with their own for_each.

Also interesting is that it seems that the block sees the elements in the map, but as soon as I try to get one of them, it fails:

Error: Invalid template interpolation value

  on ../module/main.tf line 39, in resource "azurerm_linux_virtual_machine" "rhel":
  39:     sku       = "${var.linux[each.key]}-LVM"
    |----------------
    | each.key is "d1-in-tst-l521"
    | var.linux is map of object with 2 elements

Cannot include the given value in a string template: string required.

The initial example you shared didn’t include a key named d1-in-tst-l521. If you’ve changed your inputs or other parts of your configuration since you originally shared it then please share the current configuration in full because otherwise I’m probably chasing ghosts! :wink:

I tried to make it more generic for the discussion. Here are the actual values:

linux = {
    "d1-in-tst-l521" = {
      size    = "Standard_DS1_v2"
      version = "8"
    },
    "d1-in-tst-l522" = {
      size    = "Standard_B2s"
      version = "7"
    },
    "d1-in-tst-l523" = {
      version = "8"
    }
  }

Hi @donwlewis

I don’t have a solution to the nested block weirdness (it feels like a scoping issue), but I may have a workaround.

Would it be possible to alter your configuration to something like the following (note: I haven’t tested this, so your mileage may vary)…

data "azurerm_platform_image" "rhel" {
  for_each = var.linux

  location  = var.location
  publisher = "RedHat"
  offer     = "RHEL"
  sku       = "${each.value.version}-LVM"
  version   = "latest"
}

resource "azurerm_linux_virtual_machine" "vm" {
  for_each = var.linux

  ...

  source_image_id = data.azurerm_platform_image.rhel[each.key]
  // I don't know if TF allows you to reference a data source using a for_each like this
}

Well I found the real problem, and I appreciate everyone’s help. The problem was in my declaration of the linux variable. I declared it as follows:

variable "linux" {
  description = "Map of objects of Linux VMs keyed by the name of the VM"
  type        = map(object({}))
}

I forgot that declaring in this way requires you to declare the elements in the map. Since I did not, it was not taking the elements I was putting in…sorry for the rabbit hole!

1 Like

Ahh yes @donwlewis, unfortunately it seems like when you edited the configuration to share it in the forum you removed the crucial part. :wink:

As some context for anyone else who finds this, the trick here is that when you define a type constraint for an input variable Terraform will attempt to convert the caller’s given value to conform to the given type constraint. object({}) is an entirely-empty object with no attributes at all, and so per the type conversion rules any object type can convert to that type but yet the result is rather useless because it has no attributes!

So as @donlewis discovered, getting the intended result requires writing an accurate type constraint that covers the attributes that the module depends on:

variable "linux" {
  description = "Map of objects of Linux VMs keyed by the name of the VM"
  type        = map(object({
    size    = string
    version = string
  }))
}

With the above type constraint, Terraform will accept any object that has at least the given size and version attributes, and will convert the input to match the constraint by removing any other attributes.

I appreciate the help! Sorry for leaving out the details. I this case I am just not using a type constraint, which isn’t great, but I want some of the attributes to have defaults so it seems the best option for now. I believe Terraform 14 is supposed to allow for optional/default attributes within a declared object, so I will try and look at that when it is out. Again appreciate the help!

I think the Terraform v0.14 thing you are referring to here is the optional attributes experiment, in which case it is indeed in v0.14 and now available.

That goes along with the experimental defaults function, which can help with concisely specifying non-null default values for optional attributes.

(The documentation for the function is, at the time of writing, confusingly annotated as being only in v0.15 and later. That was a documentation editoral error that results from the plan to stablize these features in Terraform v0.15 if they get good feedback in the v0.14 releases, so the function is actually available in v0.14, if you have the module_variable_optional_attrs experiment enabled. We’ll get the docs fixed up soon.)