Table of Contents

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:

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:

Step 2: Second level of iteration

For each included subnet, loop through its security rules:

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:

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

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.