Referencing outputs from a for_each module

I have a module which has a variable defined using for_each , and its output is as below:

output "nic_ids" {
    value = [for x in azurerm_network_interface.nic : x.id]
}
nic_ids = [
  "/subscriptions/*****/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic-1",
  "/subscriptions/*****/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic-2",
]

My aim is to pass above NIC ids to the VM module and have 1:1 mapping between NIC id and VM ( test-nic-1 should only be attached to vm-1 , test-nic-2 to vm-2 etc.)

module "vm" {
  source  = "*****/vm/azurerm"
  version = "0.1.0"
  
   vms = var.vms
   nic_ids = module.nic[each.value.id].nic_ids 
} 

I am getting below error:

Error: each.value cannot be used in this context

  on main.tf line 58, in module "vm":
  58:    nic_ids = module.nic[each.value.id].nic_ids 

A reference to "each.value" has been used in a context in which it
unavailable, such as when the configuration no longer contains the value in
its "for_each" expression. Remove this reference to each.value in your
configuration to work around this error.

Can you please suggest?

Hi @JiJo333

does your module "vm" create multiple VMs on it’s own or should that module actually get instantiated multiple times? It sounds like multiple NICs are created within the NIC module. You’d also have the option to create multiple instances of the module, though.

Your code doesn’t include any for/for_each of the module, hence each.xxx is invalid.

Based on that, wouldn’t you just pass the output of module.nics to vm?

module "vm" {
  source  = "*****/vm/azurerm"
  version = "0.1.0"
  
   vms = var.vms
   nic_ids = module.nic.nic_ids 
} 

Thanks @tbugfinder for looking into this.
Yes, my VM module also has a similar structure allowing me to create multiple VMs.
Here is my variables.tf file from the VM module:

variable "vms" {
  type = map(object(
    {
      vm_name        = string
      location       = string
      rg_name        = string
      admin_username = string
      os_disk_name   = string
    }
  ))
}
variable "admin_password" {
  type = string
}
variable "nic_ids" {
  type = list(string)

I am defining var.vms as

vms = {
    vm1 = {
        vm_name = "test-vm-1"
        location = "eastus2"
        rg_name = "test-rg"
        admin_username = "*******"
        os_disk_name = "test-vm-1-osdisk"
    },
    vm2 = {
        vm_name = "test-vm-2"
        location = "eastus2"
        rg_name = "test-rg"
        admin_username = "*****"
        os_disk_name = "test-vm-2-osdisk"
    }
}

What you mentioned below works, but that’s not exactly what I want

nic_ids = module.nic.nic_ids

It associates both the nic_ids to the same VM . . . I want first nic_id with VM-1 and second nic_id with VM-2.
Is there a way to achieve that?
Thanks in advance!

Well, then the question is how do you create the VMs inside the module and make use of nic_ids within the VM module?

Yes, thats what I am looking for… the VMs, which are created through the VM module, have to use the NIC_ID created by the NIC module.

Do you have VM module code you could share, to work on?

Sure, here it is :

resource "azurerm_linux_virtual_machine" "vm" {

  for_each = var.vms

  name                = each.value["vm_name"]
  location            = each.value["location"]
  resource_group_name = each.value["rg_name"]

  admin_username                  = each.value["admin_username"]
  admin_password                  = var.admin_password
  network_interface_ids           = var.nic_ids
  disable_password_authentication = false
  size                            = "Standard_B1s"
  os_disk {
    caching              = "ReadOnly"
    storage_account_type = "Standard_LRS"
    name                 = each.value["os_disk_name"]
    disk_size_gb         = "40"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }
}

Thanks again!

You might also think about creating multiple instances of the modules instead of creating multiple resources within a single module (type).

Try using a helper map in Calling_modules.tf which has the same keys as var.vms.

locals {
  nics_map = zipmap(keys(var.vms),module.nic.nic_ids)
}

After that pass this to the vm module in Calling_modules.tf:

nic_ids = local.nics_map #please note this changes the input type within the module

Within the module VM.tf you can then refer using:

network_interface_ids           = [var.nic_ids[each.key]]

Thanks @tbugfinder for the suggestion.
I just wanted to be sure where I make these changes.
My VM module file is called VM.tf, and my parent module that calls the VM and NIC module is called Calling_modules.tf.
All the changes you mentioned above, will be in Calling_modules.tf ?

well, no.
I’ll change my previous post.

Thanks @tbugfinder for your continued support!
I see what you are suggesting with zipmap.
Using that, our local.nics_map will look something like this, creating a one to one relation with VMs and NIC ids:

Blockquote
local.nics_map = {
vm1 = “<nic-id-1>”
vm2 = “<nic-id-2>”
}

However, the nic_ids in Calling_modules.tf refers to the network interfaces to be associated with the VM, which has to be a string, and that variable type cant be changed since its internal to Azure.

I used below, but got error:

Blockquote
nic_ids = tostring(values(local.nics_map))

Blockquote
Error: Invalid function argument
on main.tf line 52, in module “vm”:
52: nic_ids = tostring(values(local.nics_map))
|----------------
| local.nics_map is object with 2 attributes
Invalid value for “v” parameter: cannot convert tuple to string.

Is there a way around?
Thanks again!

Within the VM module, above code should only get the nic_id, no?

Yes, thats right.
I am referring to the value of nic_ids, that we are passing in Calling_module.tf
Calling_modules.tf : (I apologize for formatting)

Blockquote
locals {
tags = {
env = “test”
created_by = “xxx”
}
nics_map = zipmap(keys(var.vms),module.nic.nic_ids)
}
module “nic” {
source = “app.terraform.io/joshilabs/nic/azurerm
version = “0.2.4”
nics = var.nics
}
output “map” {
value = module.nic.map_nic_ids
}
output “nic_name” {
value = module.nic.nic_names
}
output “nic_ids” {
value = module.nic.nic_ids
}
module “vm” {
source = “app.terraform.io/joshilabs/vm/azurerm
version = “0.1.1”
admin_password = var.admin_password
vms = var.vms
nic_ids = local.nics_map
}

Are you saying the variable type cannot be changed to a map here?

Are you the owner of the module source?
zipmap could also be moved into the VM module.

Yes, thats what I believe.

Yes, I am the owner of the module

I’m sorry then I don’t get your point. You shouldn’t have to mangle around with tosting(). Just use network_interface_ids = [var.nic_ids[each.key]].
each.key should map to VM1 and VM2.