Home > OS >  How to pass output of two for_each loops in terraform as an input to other tf resource
How to pass output of two for_each loops in terraform as an input to other tf resource

Time:06-10

I am trying to create a few Azure resources like VNET, Subnet and an NSG. I am making use of for_each meta argument to create multiple subnets and NSG's. But, I am not able to figure out how to associate them using "azurerm_subnet_network_security_group_association". I am creating the output of subnet id's and NSG id's as a map, but I am not able to figure out how to create a association between subnet ID and NSG ID. For ex: I have a subnet created called "public_subnet" and I want to associate "public_nsg" to public subnet and likewise for private. For now, I just want the assignment to be driven by just the names.

child module main.tf:

resource "azurerm_virtual_network" "vnet" {
  name                = format("%s-%s-vnet", var.owner_custom, var.purpose_custom)
  location            = var.location
  resource_group_name = format("rg-%s-%s", var.owner_custom, var.purpose_custom)
  address_space       = var.address_space

}


resource "azurerm_subnet" "subnet" {
  for_each = var.subnets
  name = each.value["name"]
  address_prefixes = each.value["address_space"]
  resource_group_name = format("rg-%s-%s", var.owner_custom, var.purpose_custom)
  virtual_network_name = azurerm_virtual_network.vnet.name
}


resource "azurerm_network_security_group" "nsg" {
  for_each = var.nsg
  name = each.value["name"]
  location = var.location
  resource_group_name = format("rg-%s-%s", var.owner_custom, var.purpose_custom)
}


resource "azurerm_subnet_network_security_group_association" "nsg_association" {
  subnet_id = #need help here
  network_security_group_id = #need help here
}

child module variables.tf:

variable "owner_custom" {
    description = "Short name of owner"
}

variable "purpose_custom" {
    description = "Custom purpose"
}
variable "location" {
  description = "Location where resource is to be created"
  
}
variable "address_space" {
  type = list
  description = "VNET CIDR Range"
}

variable "subnets" {
  description = "A map to create multiple subnets"
  type = map(object({
    name = string
    address_space = list(string)
  })) 
}

variable "nsg" {
  description = "A map of NSGs"
  type = map(object({
    name = string
  }))
  
}

child module output.tf:

output "vnet_id" {
    value = azurerm_virtual_network.vnet.id
}

output "subnet_id" {
    value = tomap({
        for k, s in azurerm_subnet.subnet : k => s.id
    })
  
}

output "nsg_id" {
    value = tomap({
        for k,s in azurerm_network_security_group.nsg: k => s.id
    })
  
}

tfvars:

#Referenced common across modules
owner_custom = "raghav"
purpose_custom = "demo"

#Referenced in resource-group module
owner = "[email protected]"
purpose = "test"
location = "australiaeast"
org = "org"

#Referenced in network module
address_space = ["10.10.0.0/21"]

subnets = {
    subnet1 = {
        name = "public_subnet"
        address_space = ["10.10.1.0/26"]
        }

    subnet2 = {
        name = "private_subnet"
        address_space = ["10.10.1.64/26"]
        }

    subnet3 = {
        name = "privatelink_subnet"
        address_space = ["10.10.1.128/26"]
        }
    
    subnet4 = {
        name = "AzureFirewallSubnet"
        address_space = ["10.10.1.192/26"]
        }
}

nsg = {
    public_nsg = {
        name = "public_nsg"
        }

    private_nsg = {
        name = "private_nsg"
        }
    }

CodePudding user response:

From what you've described, it seems like your rule is that you want to use the part of the names before the first underscore as a sort of implied correlation key, which would mean that:

  • public_subnet is associated with public_nsg
  • private_subnet is associated with private_nsg
  • privatelink_subnet is not associated with anything
  • AzureFirewallSubnet is not associated with anything

The main task here then is to express that rule using some Terraform language expressions in order to create a lookup table from subnet to security group.

I'd typically start this by deriving some new data structures that capture the keys we'll use for matching, so that we can use those in the rest of the solution to look up these results concisely:

locals {
  subnet_types = tomap({
    for k, s in var.subnets : k => split("_", s.name)[0]
  })
  nsg_types = tomap({
    for k, s in var.nsg : split("_", s.name)[0] => k
  })
}

This uses the split function to find all of the parts separated by underscores, and then [0] to select the first part. This should therefore produce the following mappings:

local.subnet_types = {
  subnet1 = "public"
  subnet2 = "private"
  subnet3 = "privatelink"
  subnet4 = "AzureFirewallSubnet"
}
local.nsg_types = {
  public  = "public_nsg"
  private = "private_nsg
}

Notice that the values in local.subnet_ids correlate with the keys in local.nsg_types, which therefore gives enough information to reduce this into a mapping directly from subnet key to NSG key:

locals {
  subnet_nsgs = {
    for k, ty in local.subnet_ids :
    k => try(local.nsg_types[ty], null)
  }
}

This uses try to handle the situation where there isn't an NSG defined for a particular "type" keyword. The result of this should therefore be:

local.subnet_nsgs = {
  subnet1 = "public_nsg"
  subnet2 = "private_nsg"
  subnet3 = null
  subnet4 = null
}

This now has all of the information required to decide which security group associations to declare, but we will need one more transform to filter out the null elements (since we don't want to declare an association at all for those) and to pull in the other data we'll need to populate the resource arguments:

resource "azurerm_subnet_network_security_group_association" "nsg_association" {
  for_each = {
    for subnet_key, nsg_key in local.subnet_nsgs : subnet_key => {
      subnet_id = azurerm_subnet.subnet[subnet_key].id
      nsg_id    = azurerm_network_security_group.nsg[nsg_key].id
    }
    if nsg_key != null
  }

  subnet_id                 = each.value.subnet_id
  network_security_group_id = each.value.nsg_id
}

This last for expression is swapping the subnet and NSG keys for the IDs that the remote system is using to track them, which is what we'll need for the resource arguments, and also includes the clause if nsg_key != null to filter out the ones that have no associated NSG.

With all of this done, you should end up with two associations with the following addresses and the appropriate corresponding subnet and NSG IDs:

  • azurerm_subnet_network_security_group_association.nsg_association["subnet1"]
  • azurerm_subnet_network_security_group_association.nsg_association["subnet2"]
  • Related