Search code examples
terraformterraform-provider-azureterraform-modules

How to loop two different maps using for_each loop in Terraform


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.

Solution

  • 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
    }