Linking VMs to NICs - Complex object

Hi,

I’m building a set of VM’s in a module based on the content of a nested object that is provided to the module. I want to be able to build as many VM’s as are passed.

Because the VM and Interfaces are different resources, I’m struggling to get them associated correctly.

An example of my object and how I’m using it to build the Interfaces:

Input object:

machines = {

machine1 = {
    size = "standard"
    zone = "1"
    network = {
        nic1 = {
            subnet = "subnet_a"
            ip_addr = "10.0.0.1"
        },
        nic2 = {
            subnet = "subnet_b"
            ip_addr = "192.168.0.8"
        }
    }
},

machine2 = {
    size = "standard"
    zone = "1"
    network = {
        nic1 = {
            subnet = "subnet_b"
            ip_addr = "10.0.0.1"
        }
    }
}

Building interfaces:

locals {
  nic_config = flatten([
    for vm_key, vm in var.machines: [
      for nic_key, nic in vm.network :{
        vm = vm_key
        nic = nic_key
        subnet = nic.subnet
        ip = nic.ip_address
      }
    ]
  ])
}

# Lookup subnet ID
data "azurerm_subnet" "vm" {
  for_each             = {
    for nic in local.nic_config : "${nic.vm}.${nic.subnet}" => nic
  }
  name                 = each.value.subnet
  virtual_network_name = var.vnet
  resource_group_name  = var.resource_group
}

# Build interfaces for all VM's

resource "azurerm_network_interface" "vm" {
  for_each             = {
    for nic in local.nic_config : "${nic.vm}.${nic.nic}" => nic
  }
  name                = "${each.value.nic}-${each.value.vm}"
  location            = data.azurerm_resource_group.vm.location
  resource_group_name = data.azurerm_resource_group.vm.name

  ip_configuration {
    name                          = "conf_${each.value.nic}"
    subnet_id                     = data.azurerm_subnet.vm["${each.value.vm}.${each.value.subnet}"].id
    private_ip_address_allocation = "static"
    private_ip_address            = each.value.ip
  }
}

This all works OK but I’m not struggling when I’m building the azure_linux_virtual_machine resource to provide each VM with a list of only the relevant Interface ID’s to the attribute “network_interface_ids” which expects a list of ID’s. I need to somehow grab the ID’s from the interface resources but only those that relate to the specific VM I’m iterating over in my next loop.

Any ideas on how I could change my approach to make this easier?

Thanks

This is a complex one… I can just about see a way to a solution, using a for expression to evaluate each NIC, with a conditional expression to return either the ID, if it was in the context of the right machine or an empty string if not, and then wrap the whole thing in a call to the compact function to drop the empty strings. But it’ll be ugly.

Terraform really wants you to have one instance of your module per machine. Is this an option?

1 Like

Hi, thanks for the reply.

I think it could be an option to have one instance of the module per machine. Is that considered better practice? Would you use a for_each to loop through the map and pass each individual machine map to the module? Or would you specify the module completely multiple times?

Would I still be able to use the same object structure if I was only passing a single machine map to the module, so rather than above, I’d just pass:

machine1 = {
    size = "standard"
    zone = "1"
    network = {
        nic1 = {
            subnet = "subnet_a"
            ip_addr = "10.0.0.1"
        },
        nic2 = {
            subnet = "subnet_b"
            ip_addr = "192.168.0.8"
        }
    }

And still be able to dynamically handle different numbers of NICs etc…?

It’s not anything formally written down in a practices document, it’s just that if you structure the code that way, it works more nicely, rather than needing complex workarounds.

Either could work, it would be a matter of personal preference or which is situationally better.

If you absolutely had to, I guess you could, but I think it would be confusing to use a list or map of objects, which was only allowed to contain a single item. I think it would feel more natural to the user if you moved the machine name to be a name attribute:

{
    name = "machine1"
    size = "standard"
    zone = "1"
    network = {
...

then you’d just be passing a single object to the module.

1 Like

I did actually manage to get this to work with the original data structure in case anyone has any similar use case, here’s the relevant snip:

# Using Linux Virtual Machine Resource
resource "azurerm_linux_virtual_machine" "vm" {
  for_each            = var.machines
  name                 = each.key
  
  ...
 
  network_interface_ids = [
    for nic in local.nic_config : azurerm_network_interface.vm["${nic.vm}.${nic.nic}"].id  if nic.vm== "${each.key}"
  ]