This documentation explains the complex for_each loop used to create network security rules in our Azure Network Manager module.
resource "azurerm_network_security_rule" "this" {
for_each = {
for rule in flatten([
for subnet_key, subnet in var.subnets : [
for rule_key, rule_value in subnet.security_rules : {
subnet_key = subnet_key
rule_key = rule_key
rule = rule_value
}
] if subnet.subnet_purpose == "Default"
]) : "${rule.subnet_key}_${rule.rule_key}" => rule
}
# ... resource attributes ...
}
This loop performs the following operations:
var.subnetssubnet_purpose == “Default”security_rulessubnet_key, rule_key, and rule properties“${subnet_key}_${rule_key}” as the resource map keysubnets = {
web_subnet = {
counter_subnet = "001"
prefix_len = 24
subnet_purpose = "Default"
security_rules = {
allow_http = {
counter_netsecrule = "001"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "Internet"
destination_address_prefix = "*"
description = "Allow HTTP from Internet"
},
allow_https = {
counter_netsecrule = "002"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "Internet"
destination_address_prefix = "*"
description = "Allow HTTPS from Internet"
}
}
},
bastion_subnet = {
counter_subnet = "002"
prefix_len = 27
subnet_purpose = "AzureBastionSubnet" # Not Default, will be filtered out
security_rules = {
allow_ssh = {
counter_netsecrule = "001"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "Internet"
destination_address_prefix = "*"
description = "Allow SSH from Internet"
}
}
},
db_subnet = {
counter_subnet = "003"
prefix_len = 24
subnet_purpose = "Default"
security_rules = {
allow_sql = {
counter_netsecrule = "001"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "1433"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "*"
description = "Allow SQL from VNet"
}
}
}
}
Loop through var.subnets:
web_subnet (subnet_purpose = “Default”) - INCLUDEDbastion_subnet (subnet_purpose = “AzureBastionSubnet”) - EXCLUDED due to filterdb_subnet (subnet_purpose = “Default”) - INCLUDEDFor each included subnet, loop through its security rules:
web_subnet: allow_http and allow_httpsdb_subnet: allow_sql
The flatten() function creates this list:
[
{
subnet_key = "web_subnet"
rule_key = "allow_http"
rule = { ... rule properties ... }
},
{
subnet_key = "web_subnet"
rule_key = "allow_https"
rule = { ... rule properties ... }
},
{
subnet_key = "db_subnet"
rule_key = "allow_sql"
rule = { ... rule properties ... }
]
The outer for expression creates this map:
{
"web_subnet_allow_http" = { subnet_key = "web_subnet", rule_key = "allow_http", rule = {...} }
"web_subnet_allow_https" = { subnet_key = "web_subnet", rule_key = "allow_https", rule = {...} }
"db_subnet_allow_sql" = { subnet_key = "db_subnet", rule_key = "allow_sql", rule = {...} }
}
The loop creates three NSG rules:
azurerm_network_security_rule.this[“web_subnet_allow_http”]azurerm_network_security_rule.this[“web_subnet_allow_https”]azurerm_network_security_rule.this[“db_subnet_allow_sql”]For the first rule, the resource would be:
resource "azurerm_network_security_rule" "this" {
# Key: "web_subnet_allow_http"
name = "rule-app01-dev-weu-001" # Assuming these variables are set
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "Internet"
destination_address_prefix = "*"
description = "Allow HTTP from Internet"
resource_group_name = "my-resource-group" # From var.resource_group_name
network_security_group_name = "nsg-app01-dev-weu-001" # References the NSG created for web_subnet
}
subnet_purpose == “Default”.network_security_group_name.${subnet_key}_${rule_key} to ensure uniquely named resources.try() to handle optional destination address fields.This approach separates NSG rules from NSGs, which makes managing individual rules much easier and avoids the problem of recreating all rules when any single rule changes.