Filter out objects in for_each loop

I’m trying to create virtual network peering resources by passing in existing resource data to a child module. Our Azure network will be a typical hub/spoke design, so I’m using two resource blocks to define the different peering configurations (one creates the peering between the spokes and the hub, one creates the peering between the hub and the spokes). I actually have the first one working already, but the second is giving me trouble.

From main.tf in our networking module (parent module)

resource "azurerm_virtual_network" "vnets" {
  for_each            = var.vnets
  name                = each.key
  location            = each.value.location
  resource_group_name = each.value.resource_group_name
  address_space       = each.value.address_space
  dns_servers         = each.value.dns_servers

  depends_on = [
    azurerm_resource_group.rg-network
  ]
}

module "firewall" {
  source = "./modules/Fortigate/Active-Active-ELB-ILB"
  vnet_hub_id = azurerm_virtual_network.vnets["${var.hub_vnet}"].id
  vnet_hub_name = var.vnet_hub_name
  subnets = var.subnets
  vnets = var.vnets
  subnet_associations = {for name,id in azurerm_subnet.subnets: name => id}
  rg_network_name = azurerm_resource_group.rg-network.name
  rg_network_location = azurerm_resource_group.rg-network.location
  rg_keyvault_name = var.rg_keyvault_name
  }

The two peering resource blocks from main.tf in firewall module (child module)

resource "azurerm_virtual_network_peering" "peer-spoke-hub-eastus" {
  for_each = { 
    for k,v in var.vnets : k => v
    if v.hub != true
    }
  name                         = "peer-${each.key}-to-hub"
  resource_group_name          = var.rg_network_name
  virtual_network_name         = each.key
  remote_virtual_network_id    = var.vnet_hub_id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = false
  depends_on = [
    var.vnets,
    var.subnets,
    azurerm_subnet_route_table_association.subnet_associations,
  ]
}

resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus" {
  for_each = { 
    for k,v in data.azurerm_virtual_network.vnet_spoke  : k => v
    }

  name                         = "peer-hub-to-${each.key}"
  resource_group_name          = var.rg_network_name
  virtual_network_name         = var.vnet_hub_name
  remote_virtual_network_id    = each.value.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = false
  depends_on = [
    var.vnets,
    var.subnets,
    azurerm_subnet_route_table_association.subnet_associations,
  ]
}

The first block was relatively easy. I’m not sure if this is the best way to do it, but I was able to just loop through all my vnet input objects using the original var.vnets variable. Then I’m using an attribute that I added to that variable to define which vnet is the hub and excluding it. This works fine.

For the second resource block I need to provide names and ids of the spoke networks. I can generate that easily enough using similar code to subnet_associatons but I haven’t figured out how to filter the hub network out.

The first thing I tried was some sort of for statement in an input variable in the firewall module block. I don’t have my code for that attempt, but something like

vnet_spoke_ids = for k,v in azurerm_virtual_network.vnets : k => v if v.hub != true

The second was trying to figure out if it might be possible to merge the ID into var.vnets. I… really struggle with some of the typing and data manipulation in Terraform. I didn’t get very far there.

Finally I’ve been trying to see if I could somehow bring in all the vnets from the data source azurerm_virtual_networks, but again I haven’t found a way to filter out the hub network. It would be nice if there were a way to filter out results based on name for example. Something like

filter {
name != var.vnet_hub
}

The only thing I have thought of but haven’t tried yet would be to tag the virtual networks as either hubs or spokes, and then filter on tag = spoke.

What would be the best way to accomplish what I’m trying to do?

So I thought the azurerm_virtual_network data source could take a filter but I guess I was wrong. I did find a data source that should be able to meet my needs, can someone help me figure out how to convert the output into something I can use in a for_each loop?

data "azurerm_resources" "spokes" {
  type = "Microsoft.Network/virtualNetworks"

  required_tags = {
    role        = "spokeNetwork"
  }
}
resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus" {
  for_each = data.azurerm_resources.spokes

  name                         = "peer-hub-to-${each.key}"
  resource_group_name          = var.rg_network_name
  virtual_network_name         = var.vnet_hub_name
  remote_virtual_network_id    = each.value.resources.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = false
  depends_on = [
    var.vnets,
    var.subnets,
    azurerm_subnet_route_table_association.subnet_associations,
  ]
}
╷
│ Error: Unsupported attribute
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 96, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   96:   remote_virtual_network_id    = each.value.resources.id
│     ├────────────────
│     │ each.value is a object, known only after apply
│
│ This object does not have an attribute named "resources".
╵
╷
│ Error: Unsupported attribute
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 96, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   96:   remote_virtual_network_id    = each.value.resources.id
│     ├────────────────
│     │ each.value is a string, known only after apply
│
│ Can't access attributes on a primitive-typed value (string).
╵
╷
│ Error: Missing map element
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 96, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   96:   remote_virtual_network_id    = each.value.resources.id
│     ├────────────────
│     │ each.value is map of string with 1 element
│
│ This map does not have an element with the key "resources".
╵
╷
│ Error: Unsupported attribute
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 96, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   96:   remote_virtual_network_id    = each.value.resources.id
│     ├────────────────
│     │ each.value is a string, known only after apply
│
│ Can't access attributes on a primitive-typed value (string).
╵
╷
│ Error: Unsupported attribute
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 96, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   96:   remote_virtual_network_id    = each.value.resources.id
│     ├────────────────
│     │ each.value is a string, known only after apply
│
│ Can't access attributes on a primitive-typed value (string).
╵
╷
│ Error: Unsupported attribute
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 96, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   96:   remote_virtual_network_id    = each.value.resources.id
│     ├────────────────
│     │ each.value is a list of object, known only after apply
│
│ Can't access attributes on a list of objects. Did you mean to access an attribute for a specific element of the list, or across all elements of the list?
╵
╷
│ Error: Unsupported attribute
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 96, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   96:   remote_virtual_network_id    = each.value.resources.id
│     ├────────────────
│     │ each.value is "Microsoft.Network/virtualNetworks"
│
│ Can't access attributes on a primitive-typed value (string).

The azurerm teams own example won’t run…

This is theirs:

# Get resources by type, create spoke vNet peerings
data "azurerm_resources" "spokes" {
  type = "Microsoft.Network/virtualNetworks"

  required_tags = {
    environment = "production"
    role        = "spokeNetwork"
  }
}

resource "azurerm_virtual_network_peering" "spoke_peers" {
  count = length(data.azurerm_resources.spokes.resources)

  name                      = "hub2${data.azurerm_resources.spokes.resources[count.index].name}"
  resource_group_name       = azurerm_resource_group.hub.name
  virtual_network_name      = azurerm_virtual_network.hub.name
  remote_virtual_network_id = data.azurerm_resources.spokes.resources[count.index].id
}

I changed the tags and name to match my environment…

data "azurerm_resources" "spokes" {
  type = "Microsoft.Network/virtualNetworks"

  required_tags = {
    role        = "spokeNetwork"
  }
}

resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus" {
  count = length(data.azurerm_resources.spokes.resources)

  name                      = "peer-hub-to-${data.azurerm_resources.spokes.resources[count.index].name}"
  resource_group_name       = var.rg_network_name
  virtual_network_name      = var.vnet_hub_name
  remote_virtual_network_id = data.azurerm_resources.spokes.resources[count.index].id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = false
}
Error: Invalid count argument
│
│   on modules\networking\modules\Fortigate\Active-Active-ELB-ILB\00-main.tf line 91, in resource "azurerm_virtual_network_peering" "peer-hub-spoke-eastus":
│   91:   count = length(data.azurerm_resources.spokes.resources)
│
│ The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.
╵

I went ahead and ran the apply -target data.azurerm_resources.spokes command which as expected changed/modified/deleted nothing, but the error persists.