====== Terraform Network Security Rule Loop Explained ======
This documentation explains the complex ''for_each'' loop used to create network security rules in our Azure Network Manager module.
===== Loop Structure =====
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:
* Iterates over each subnet in ''var.subnets''
* Filters to only include subnets with ''subnet_purpose == "Default"''
* For each qualified subnet, iterates over its ''security_rules''
* Creates a flattened list of objects with ''subnet_key'', ''rule_key'', and ''rule'' properties
* Uses ''"${subnet_key}_${rule_key}"'' as the resource map key
===== Example Input =====
subnets = {
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"
}
}
}
}
===== Step-by-Step Loop Execution =====
==== Step 1: First level of iteration ====
Loop through ''var.subnets'':
* ''web_subnet'' (subnet_purpose = "Default") - INCLUDED
* ''bastion_subnet'' (subnet_purpose = "AzureBastionSubnet") - EXCLUDED due to filter
* ''db_subnet'' (subnet_purpose = "Default") - INCLUDED
==== Step 2: Second level of iteration ====
For each included subnet, loop through its security rules:
* For ''web_subnet'': ''allow_http'' and ''allow_https''
* For ''db_subnet'': ''allow_sql''
==== Step 3: Create flattened list ====
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 ... }
]
==== Step 4: Create for_each map ====
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 = {...} }
}
==== Step 5: Create resources ====
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"]''
===== Example Resource Output =====
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
}
===== Key Benefits of This Approach =====
* **Filtered Creation**: Only creates rules for subnets with ''subnet_purpose == "Default"''.
* **Association**: Links each rule to its parent NSG using ''network_security_group_name''.
* **Unique Keys**: Uses ''${subnet_key}_${rule_key}'' to ensure uniquely named resources.
* **Optional Attributes**: Uses ''try()'' to handle optional destination address fields.
* **Independent Management**: Each rule can be added, modified, or removed without affecting other rules.
* **No Rule Recreation**: When adding a new rule, existing rules remain untouched in Terraform.
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.