Terraform - How to iterate through output variable when a for_each has already been used

Hi,

I’m trying to iterate through the out variable produced by a module that will be the argument for another module.

The output variable looks like below and it should be passing 3 subnet ids in the form of a list.

The other module will use these arguments to create one appgw per subnet (3) by passing the subnet_id inside the gateway_ip block as below from the output variable:

Notice that I can’t use the for_each loop because I already made use of it inside the resource “azurerm_application_gateway” and can’t use count with for_each. The other alternative I thought about was to try with a for loop and tried it and got this error.
I’d like to have 1 appgw per subnet.

Any prompt help would be appreciated.

The appgw code looks like this:

resource "azurerm_application_gateway" "ops-manager-ag" {
  resource_group_name = var.resourceGroupName
  for_each = var.appgwOpsManVnets_and_Location
    name = "${var.appgwOpsMan_name}-${each.value}"
    location = each.value

  sku {
    name     = var.appgwOpsMan_sku["Name"]
    tier     = var.appgwOpsMan_sku["Tier"]
    capacity = var.appgwOpsMan_sku["Capacity"]
  }

  gateway_ip_configuration {
    subnet_id = [for i in var.appgwOpsManPublicSubnetId: i] #DOESN'T WORK
    name      = var.appgwOpsMan_gateway_ip_configuration
  }

#The output variable code to get the subnet_id from subnet module:
output "appgwOpsManPublicSubnetId" {
    value = values(azurerm_subnet.appgwOpsMansubnet)[*].id
}

Error: Incorrect attribute value type
var.appgwPublicSubnetId is tuple with 3 elements. Inappropriate value for attribute “subnet_id”: string required.

Aside: Could you please try to format your Terraform configuration using code blocks? It makes it much easier to read. To do so, add a line with just ``` before and after your snippets. Thanks!

I’m not quite sure what it is you’re trying to do here. Do you want one azurerm_application_gateway per subnet, or a single gateway with multiple gateway_ip_configuration blocks?

Assuming the latter, you probably want to try using dynamic blocks. Something like this:

resource "azurerm_application_gateway" "gw" {
  // …
  dynamic "gateway_ip_configuration" {
    for_each = var.appgwPublicSubnetId
    content {
      subnet_id = gateway_ip_configuration.value
      name = var.appgwname
    }
  }
}

@alisdair thanks for the edit tips, still need to work on the formatting.

I actually need 1 appgw per subnet and as you can see I already used the for_each loop inside the resource group of the appgw, so I think I can’t reuse it.

I see. Then it sounds like you need to construct a new map to use with for_each, perhaps using a locals block. Can you say more about what your existing for_each variable var.appgwOpsManVnets_and_Location looks like?

That’s a map with key-value pairs containing name => location.

You mentioned the use of locals. Could you give an example of how to use it here where a for_each is already present?

The challenge I’m facing is iterating through the output variable (contains the list of subnets, 3 elements) that I’d like to iterate through, but can’t because I already used for_each inside the resource group. If I could do the following I think it would solve my issue. Ideally I’d like to do this, but can’t:

gateway_ip_configuration {
    for_each = var.appgwOpsManPublicSubnetId
    subnet_id = each.value
    name      = var.appgwOpsMan_gateway_ip_configuration
  }```

#I'd need subnet_id to do this:
subnet_id = var.appgwOpsManPublicSubnetId[0]
#after appgw gets created do a second iteration:
subnet_id = var.appgwOpsManPublicSubnetId[1]
#then, after creating the second appgw, do a 3rd one:
subnet_id = var.appgwOpsManPublicSubnetId[2]

You said earlier that you want “one appgw per subnet”. Since you’re trying to iterate through the output variable, I think you mean “one appgw per subnet per name-location pair”.

You can create this structure using locals with for expressions and the merge and flatten functions. Here’s an isolated example:

variable "gateway_name_location" {
  type = map(string)
  default = {
    "alpha": "US West",
    "beta": "US East"
  }
}

variable "subnet_ids" {
  type = set(string)
  default = ["subnet1", "subnet2", "subnet3"]
}

locals {
  gateway_map = merge(flatten([
    for subnet in var.subnet_ids: [
      for name, location in var.gateway_name_location: {
        "${name}-${subnet}" = {
          "location": location,
          "subnet": subnet,
        }
      }
    ]
  ])...)
}

output "gateway_map" {
  value = local.gateway_map
}

The result of this looks like:

gateway_map = {
  "alpha-subnet1" = {
    "location" = "US West"
    "subnet" = "subnet1"
  }
  "alpha-subnet2" = {
    "location" = "US West"
    "subnet" = "subnet2"
  }
  "alpha-subnet3" = {
    "location" = "US West"
    "subnet" = "subnet3"
  }
  "beta-subnet1" = {
    "location" = "US East"
    "subnet" = "subnet1"
  }
  "beta-subnet2" = {
    "location" = "US East"
    "subnet" = "subnet2"
  }
  "beta-subnet3" = {
    "location" = "US East"
    "subnet" = "subnet3"
  }
}

You could then use this as your for_each variable, roughly like this:

resource "azurerm_application_gateway" "ops-manager-ag" {
  for_each = local. gateway_map
  name = "${var.appgwOpsMan_name}-${each.value.name}"
  location = each.value.location

  // …

  gateway_ip_configuration {
    subnet_id = each.value.subnet
    name      = var.appgwOpsMan_gateway_ip_configuration
  }
}

Does this make sense?

1 Like

It makes sense and I think that might be a good way to tackle my requirement.
I followed your advice on this. My locals block looks like this:

locals {
  gateway_map = merge(flatten([
    for subnet in var.appgwOpsManPublicSubnetId: [
      for name, location in var.appgwOpsManVnets_and_Location: {
        "${var.appgwOpsMan_name}-${location}" = {
          "location": location,
          "subnet": subnet,
            }
          }
        ]
      ]
    )
  )
}

Now, life can’t be easy for a Terraform novice…I’m having an issue with the data type on the variable I’m getting from the subnet module (output variable) because is not a map or object, but a tuple. I’m working on changing the output variable to a map, although I’d rather keep as is.

on Modules\appgateway\main.tf line 15, in locals:
  15:   gateway_map = merge(flatten([
  16:     for subnet in var.appgwOpsManPublicSubnetId: [
  17:       for name, location in var.appgwOpsManVnets_and_Location: {
  18:         "${var.appgwOpsMan_name}-${location}" = {
  19:           "location": location,
  20:           "subnet": subnet,
  21:             }
  22:           }
  23:         ]
  24:       ]
  25:     )
  26:   )
    |----------------
    | var.appgwOpsManPublicSubnetId is tuple with 3 elements
    | var.appgwOpsManVnets_and_Location is object with 3 attributes
    | var.appgwOpsMan_name is "ops-manager"

Call to function "merge" failed: arguments must be maps or objects, got
"tuple".

It’s hard to suggest what to do here without the full context. If you can modify my previous code example so that it matches the type and value of what you’re working with from your module, I can try to respond. But without knowing the value of all those variables, I’d just be guessing!

Thanks for the help.
I ended up using a zipmap function to link the 2 sources of data and then a for_each to iterate through them as keys and values.

1 Like