Creating a resource via a for_each

Hey all

To save having to do a lot of copy and paste code for each subnet in an Azure VNet I want to deploy I’m hoping I can use a single ‘azurerm_subnet’ resource and loop round a variable to populate values such as name and range.

I have read a few posts on the forum and have come up with this but it’s still not working. Am I a million miles off or missing something small and obvious?

In my main.tf I have:

> resource "azurerm_subnet" "subnet" {
> for_each = { for n in var.subnets : n.name => n.range }
>   name                 = upper("SN-${var.subnets[each.key].name}")
>   resource_group_name  = azurerm_resource_group.rg_networking.name
>   virtual_network_name = module.prod_virtual_network.vnet_name
>   address_prefixes     = "${var.subnets[each.key].range}"
> }

In my variables.tf I have:

variable "subnets" {
  type = list(object({
    name = string
    range  = list(string)
  }))
  default = [
    { name = "subnet1", range = ["10.0.1.0/24"] },
    { name = "subnet2", range = ["10.0.2.0/24"] },
  ]
}

The error I’m getting when I run a plan is:

> │ Error: Invalid index
> │
> │   on main.tf line 47, in resource "azurerm_subnet" "subnet":
> │   47:   name                 = upper("SN-${var.subnets[each.key].name}")
> │     ├────────────────
> │     │ each.key is "subnet2"
> │     │ var.subnets is list of object with 2 elements
> │   
> │ The given key does not identify an element in this collection value: a number is required.
> ╵
> ╷
> │ Error: Invalid index
> │ 
> │   on main.tf line 47, in resource "azurerm_subnet" "subnet":
> │   47:   name                 = upper("SN-${var.subnets[each.key].name}")
> │     ├────────────────
> │     │ each.key is "subnet1"
> │     │ var.subnets is list of object with 2 elements
> │ 
> │ The given key does not identify an element in this collection value: a number is required.
> ╵
> ╷
> │ Error: Invalid index
> │ 
> │   on main.tf line 50, in resource "azurerm_subnet" "subnet":
> │   50:   address_prefixes     = "${var.subnets[each.key].range}"
> │     ├────────────────
> │     │ each.key is "subnet2"
> │     │ var.subnets is list of object with 2 elements
> │ 
> │ The given key does not identify an element in this collection value: a number is required.
> ╵
> ╷
> │ Error: Invalid index
> │ 
> │   on main.tf line 50, in resource "azurerm_subnet" "subnet":
> │   50:   address_prefixes     = "${var.subnets[each.key].range}"
> │     ├────────────────
> │     │ each.key is "subnet1"
> │     │ var.subnets is list of object with 2 elements
> │ 
> │ The given key does not identify an element in this collection value: a number is required.

Any help would be massively appreciated!

Thanks :slight_smile:

You can create a locals.tf file and have something like this

locals {
subnets = {
"subnet1" = {
name = "subnet1"
address_prefixes = ["10.0.1.0/24"]
}
"subnet2" = {
name = "subnet2"
address_prefixes = ["10.0.2.0/24"]
}

}

}

Then in your resource block in your main.tf

> resource "azurerm_subnet" "subnet" {
 for_each = local.subnets
   name                 =  each.value.name
 resource_group_name  = azurerm_resource_group.rg_networking.name
   virtual_network_name = module.prod_virtual_network.vnet_name
   address_prefixes     = each.value.address_prefixes
 }

Hi @BuildingYourCloud!

It looks like you are close to a working solution. The problem here is that you are trying to use var.subnets as a map, but it’s a list.

One option would be to change var.subnets to be a map of objects instead of a list of objects, like this:

variable "subnets" {
  type = map(object({
    range  = list(string)
  }))
  default = [
    "subnet1" = { range = ["10.0.1.0/24"] },
    "subnet2" = { range = ["10.0.2.0/24"] },
  ]
}

resource "azurerm_subnet" "subnet" {
  for_each = var.subnets

  name                 = upper("SN-${each.key}")
  resource_group_name  = azurerm_resource_group.rg_networking.name
  virtual_network_name = module.prod_virtual_network.vnet_name
  address_prefixes     = each.value.range
}

Alternative, if you need to preserve the existing definition of this variable as a list then you can construct an equivalent map inside the for_each argument using a for expression and then access the object attributes using each.value instead of var.subnets, like this:

variable "subnets" {
  type = list(object({
    name = string
    range  = list(string)
  }))
  default = [
    { name = "subnet1", range = ["10.0.1.0/24"] },
    { name = "subnet2", range = ["10.0.2.0/24"] },
  ]
}

resource "azurerm_subnet" "subnet" {
  for_each = { for n in var.subnets : n.name => n }

  name                 = upper("SN-${each.key}")
  resource_group_name  = azurerm_resource_group.rg_networking.name
  virtual_network_name = module.prod_virtual_network.vnet_name
  address_prefixes     = each.value.range
}

This is similar to the example you shared but notice that address_prefixes is set to each.value.range instead of var.subnets[each.key].range. This means it will select the range attribute of the current element of the for_each value, and so there’s no need to separately look up the element in var.subnets (which would require knowing the index to look up by).