====== 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.