How to loop two different maps using for_each loop in Terraform

245 Views Asked by At

I am trying to attach network interface to backendend pool as per the below requirement

app-poc-1-nic & app-poc-2-nic to app-lb backendpool

db-nic-1-1-nic & db-nic-1-2-nic to db-lb backendpool

Below is the local block with nested map of objects

locals {
  vms = {
    nodes = {
      app_node1 = {
        "vm_name" = "app-poc"
        "vm_num"  = "1"
        networks = {
          nic1 = {
            "vm_name" = "app-poc"
            "subnet"  = "/subscriptions/*****/subnets/app"
          },
        }
      },
      app_node2 = {
        "vm_name" = "app-poc"
        "vm_num"  = "2"
        networks = {
          nic1 = {
            "vm_name" = "app-poc"
            "subnet"  = "/subscriptions/*****/subnets/app"
          },
        }
      },
      service_node1 = {
        "vm_name" = "service-poc"
        "vm_num"  = "1"
        networks = {
          nic1 = {
            "vm_name" = "service-poc"
            "subnet"  = "/subscriptions/*****/subnets/app"
          },
        }
      },
      service_node2 = {
        "vm_name" = "service-poc"
        "vm_num"  = "2"
        networks = {
          nic1 = {
            "vm_name" = "service-poc"
            "subnet"  = "/subscriptions/*****/subnets/app"
          },
        }
      },
      db_node1 = {
        "vm_name" = "db-poc"
        "vm_num"  = "1"
        networks = {
          nic1 = {
            "vm_name" = "db-nic-1"
            "subnet"  = "/subscriptions/*****/subnets/app"
          },
          nic2 = {
            "vm_name" = "db-nic-2"
            "subnet"  = "/subscriptions/*****/subnets/db"
          }
        }
      },
      db_node2 = {
        "vm_name" = "db-poc"
        "vm_num"  = "2"
        networks = {
          nic1 = {
            "vm_name" = "db-nic-1"
            "subnet"  = "/subscriptions/*****/subnets/app"
          },
          nic2 = {
            "vm_name" = "db-nic-2"
            "subnet"  = "/subscriptions/*****/subnets/app"
          },
        }
      },
    },
  }
  lbs = {
    tiers = {
        app-lb = {
          lb_name           = "app-lb"
          fip_name          = "app-fip"
          subnet_id         = "/subscriptions/*****/subnets/app"
          private_ip_type   = "Dynamic"
          address_pool_name = "app-address-pool"
          lb_probes = {
            ssh_probe = {
              protocol = "Tcp"
              port     = "22"
            }
          }
          lb_rules = {
            ssh_rule = {
              frontend_port           = "22"
              protocol                = "Tcp"
              backend_port            = "22"
              enable_floating_ip      = true
              frontend_ip_config_name = "app-fip"
            }
          }
        },
        db-lb = {
          lb_name           = "db-lb"
          fip_name          = "db-fip"
          subnet_id         = "/subscriptions/*****/subnets/app"
          private_ip_type   = "Dynamic"
          address_pool_name = "db-address-pool"
          lb_probes = {
            ssh_probe = {
              protocol = "Tcp"
              port     = "22"
            }
          }
          lb_rules = {
            ssh_rule = {
              frontend_port           = "22"
              protocol                = "Tcp"
              backend_port            = "22"
              enable_floating_ip      = false
              frontend_ip_config_name = "db-fip"
            }
          }
        }
    }
  }
}

Below are terraform resources that creates Network Interfaces, Virtual Machines, load balancer, probe, lb rules

data "azurerm_resource_group" "rg" {
  name = "test-rg"
}
resource "azurerm_network_interface" "nic-poc" {
  for_each = {
    for vm in flatten([
      for vm_name, vm in local.vms.nodes : [
        for nic_name, nic in vm.networks : {
          vm_number    = vm.vm_num,
          vm_name      = vm_name,
          nic_value    = nic.vm_name,
          subnet_value = nic.subnet
          nic_name     = nic_name
        }
      ]
      ]
    ) : "${vm.vm_name}-${vm.nic_name}" => vm
  }
  name                = "${each.value.nic_value}-${each.value.vm_number}-nic"
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name
  ip_configuration {
    name                          = "${each.value.nic_name}-${each.value.vm_number}-ipconfig"
    subnet_id                     = each.value.subnet_value
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_linux_virtual_machine" "vm-poc" {
  depends_on                      = [ azurerm_network_interface.nic-poc]
  for_each                        = local.vms.nodes
  name                            = "${each.value.vm_name}-${each.value.vm_num}"
  admin_username                  = "test-admin"
  admin_password                  = "password@29"
  disable_password_authentication = false
  location                        = data.azurerm_resource_group.rg.location
  resource_group_name             = data.azurerm_resource_group.rg.name
  network_interface_ids           = [for nic_key, nic in azurerm_network_interface.nic-poc : nic.id if startswith(nic_key, "${each.key}-")]

  size                = "Standard_B2ms"
  identity {
    type = "SystemAssigned"
  }
  os_disk {
    name                 = "${each.value.vm_name}-${each.value.vm_num}-OSdisk"
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "RedHat"
    offer     = "RHEL"
    sku       = "82gen2"
    version   = "latest"
  }
}

resource "azurerm_lb" "lb" {
  for_each            = local.lbs.tiers
  name                = each.value.lb_name
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name
  sku                 = "Standard"

  frontend_ip_configuration {
    name                          = each.value.fip_name
    subnet_id                     = each.value.subnet_id
    private_ip_address_allocation = each.value.private_ip_type
  }
}

resource "azurerm_lb_backend_address_pool" "bepool" {
  for_each        = local.lbs.tiers
  loadbalancer_id = azurerm_lb.lb[each.key].id
  name            = each.value.address_pool_name
}


resource "azurerm_lb_probe" "probe" {
  for_each = { for lb, details in local.lbs.tiers : lb => details.lb_probes }

  loadbalancer_id = azurerm_lb.lb[each.key].id
  name            = "ssh-probe"
  protocol        = each.value["ssh_probe"].protocol
  port            = each.value["ssh_probe"].port

}


resource "azurerm_lb_rule" "rule" {
  for_each = { for lb, details in local.lbs.tiers : lb => details.lb_rules }

  loadbalancer_id                = azurerm_lb.lb[each.key].id
  name                           = "ssh-rule"
  protocol                       = each.value["ssh_rule"].protocol
  frontend_port                  = each.value["ssh_rule"].frontend_port
  backend_port                   = each.value["ssh_rule"].backend_port
  frontend_ip_configuration_name = azurerm_lb.lb[each.key].frontend_ip_configuration[0].name
  enable_floating_ip             = each.value["ssh_rule"].enable_floating_ip
  backend_address_pool_ids       = [azurerm_lb_backend_address_pool.bepool[each.key].id]
  probe_id                       = azurerm_lb_probe.probe[each.key].id
}

Below is the code I have problem to handle the two different maps using for_loop

   resource "azurerm_network_interface_backend_address_pool_association" "pool1-1" {
      for_each                    = local.vms.nodes
      network_interface_id        = [for nic_key, nic in azurerm_network_interface.nic-poc : nic.id if startswith(nic_key, "${each.key}-")]
      ip_configuration_name       = [for nic_key, nic in azurerm_network_interface.nic-poc : nic.ip_configuration.name if startswith(nic_key, "${each.key}-")]
      backend_address_pool_id     = azurerm_lb_backend_address_pool.bepool[each.key].id
    }

Error :

Error: Invalid index
│
│   on main.tf line 238, in resource "azurerm_network_interface_backend_address_pool_association" "pool1-1":
│  238:   backend_address_pool_id     = azurerm_lb_backend_address_pool.bepool[each.key].id
│     ├────────────────
│     │ azurerm_lb_backend_address_pool.bepool is object with 2 attributes
│     │ each.key is "db_node2"
│
│ The given key does not identify an element in this collection value.

Other errors for the azurerm_network_interface_backend_address_pool_association resource

  Error: Incorrect attribute value type
│
│   on main.tf line 340, in resource "azurerm_network_interface_backend_address_pool_association" "pool1-1":
│  340:   network_interface_id        = [for nic_key, nic in azurerm_network_interface.nic-poc : nic.id if startswith(nic_key, "${each.key}-")]       
│     ├────────────────
│     │ azurerm_network_interface.nic-poc is object with 8 attributes
│     │ each.key is "service_node1"
│
│ Inappropriate value for attribute "network_interface_id": string required.

Error: Incorrect attribute value type
│
│   on main.tf line 341, in resource "azurerm_network_interface_backend_address_pool_association" "pool1-1":
│  341:   ip_configuration_name       = [for nic_key, nic in azurerm_network_interface.nic-poc : nic.ip_configuration.name if startswith(nic_key, "${each.key}-")]
│     ├────────────────
│     │ azurerm_network_interface.nic-poc is object with 8 attributes
│     │ each.key is "service_node2"
│
│ Inappropriate value for attribute "ip_configuration_name": string required.

Could someone throw someone light on this ? Thank you in advance.

Helder Sepulveda suggestion error :

│ Error: Invalid index
│
│   on main.tf line 199, in resource "azurerm_network_interface_backend_address_pool_association" "nic_lb_association":
│  199:   ip_configuration_name   = azurerm_network_interface.nic-poc[each.key].ip_configuration[0].name
│     ├────────────────
│     │ azurerm_network_interface.nic-poc is object with 8 attributes
│     │ each.key is "db_node1"
│
│ The given key does not identify an element in this collection value.
3

There are 3 best solutions below

4
Helder Sepulveda On BEST ANSWER

From the first line in your question you have:

... requirement

app-poc-1-nic & app-poc-2-nic to app-lb backendpool

db-nic-1-1-nic & db-nic-1-2-nic to db-lb backendpool

as suggested by @VonC you need a map that creates that relation, I like his approach but I do not like hardcoding that map, instead we can get it dynamically ...

We can see that the vm nodes have app and db as a prefix same with the lb tiers, I'm going to assume that pattern remains the same in larger dataset, so we can use that prefix to build the map, see my sample code below:

locals {
  vms = {
    nodes = {
      app_node1 = {},
      app_node2 = {},
      service_node1 = {},
      service_node2 = {},
      db_node1 = {},
      db_node2 = {},
    },
  }
  lbs = {
    tiers = {
        app-lb = {},
        db-lb = {}
    }
  }

  net_backend = {
    for vm in keys(local.vms.nodes) :
        vm => [
            for lb in keys(local.lbs.tiers) : lb
            if split("-", lb)[0] == split("_", vm)[0]
        ]
  }
  vm_to_lb_map = {
    for k, v in local.net_backend : k => v[0]
    if length(v) > 0
  }
}

output "vm_to_lb_map" {
  value = local.vm_to_lb_map
} 

I'm simplifying your data to keep the code short, for our purposes we really do not care about the values so I used just {} hopefully that does not confuse anyone.

Let's break it down...
I added two new local variables

  net_backend = {
    for vm in keys(local.vms.nodes) :
        vm => [
            for lb in keys(local.lbs.tiers) : lb
            if split("-", lb)[0] == split("_", vm)[0]
        ]
  }

The net_backend loops over the vm nodes and the lb tiers looking for matches in the prefix, (notice you are splitting by underscore in one but dashes on the other, would be nice to stick to one and keep it standard) at the end of this we end up with some records that do not have a match...

  vm_to_lb_map = {
    for k, v in local.net_backend : k => v[0]
    if length(v) > 0
  }

The vm_to_lb_map cleans up those that did not not get a match


...and a terraform plan on that code will give us:

terraform plan

Changes to Outputs:
  + vm_to_lb_map = {
      + app_node1 = "app-lb"
      + app_node2 = "app-lb"
      + db_node1  = "db-lb"
      + db_node2  = "db-lb"
    }

then you can do the same as suggested by @VonC

resource "azurerm_network_interface_backend_address_pool_association" "nic_lb_association" {
  for_each = local.vm_to_lb_map

  network_interface_id    = azurerm_network_interface.nic-poc[each.key].id
  ip_configuration_name   = azurerm_network_interface.nic-poc[each.key].ip_configuration[0].name
  backend_address_pool_id = azurerm_lb_backend_address_pool.bepool[each.value].id
}
3
lxop On

The issue is exactly what the error message says - that key doesn't exist in that collection.

To be more specific, you are taking a key from the for_each of the block, which is local.vms.nodes, and trying to use it to look up a value in the azurerm_lb_backend_address_pool.bepool resource set. But that resource set is keyed by a different for_each collection, namely local.lbs.tiers. So you're trying to look up, for example, local.vms.nodes["app-lb"], which doesn't exist.

1
VonC On

I am looking for how to write another for_each loop to iterate local.lb.tiers

You want to iterate over local.lbs.tiers and local.vms.nodes together, and:

  • local.lbs.tiers contains load balancer information.
  • local.vms.nodes contains VM and NIC information.

You would need to create a mapping that relates VMs/NICs to their respective load balancers. Tha means defining a map in locals that links VMs/NICs to the correct LB pool, requiring a manual mapping based on your application logic.

Then you would update azurerm_network_interface_backend_address_pool_association, using this new mapping to correctly associate NICs with LB backend pools.

So, first, that map needs to reflect how your VMs/NICs relate to LB pools. For example:

locals {
  vm_to_lb_map = {
    "app_node1" = "app-lb",
    "app_node2" = "app-lb",
    "db_node1"  = "db-lb",
    "db_node2"  = "db-lb",
    // add other mappings as needed
  }
}

With the mapping in place, update your resource to use this map for association.

resource "azurerm_network_interface_backend_address_pool_association" "nic_lb_association" {
  for_each = local.vm_to_lb_map

  network_interface_id    = azurerm_network_interface.nic-poc[each.key].id
  ip_configuration_name   = azurerm_network_interface.nic-poc[each.key].ip_configuration[0].name
  backend_address_pool_id = azurerm_lb_backend_address_pool.bepool[each.value].id
}

The each.key in the for_each refers to a VM/NIC, and each.value refers to the corresponding LB pool. That approach should align the resources correctly based on your mappings in vm_to_lb_map.

Make sure the keys in vm_to_lb_map correspond to the keys used in your azurerm_network_interface.nic-poc resources, and the values match the keys in local.lbs.tiers.