Terraform Nested Loops and flatten(): A Beginner's Guide with Azure Virtual Networks

·

6 min read

Cover Image for Terraform Nested Loops and flatten(): A Beginner's Guide with Azure Virtual Networks

Introduction

If you're working with Terraform and Azure, you've probably encountered situations where you need to create multiple resources based on hierarchical data structures. For example, creating multiple Virtual Networks (VNets) and then multiple Subnets within each VNet.

In this blog, I'll explain how to use nested for loops combined with flatten() to elegantly solve this problem. We'll use Azure Virtual Networks as our reference.

The Problem: Creating VNets and Subnets

Imagine you need to create:3 Azure Virtual Networks (dev, prod, staging) Multiple Subnets within each VNet. Each subnet has its own CIDR block Doing this manually would require hardcoding each resource. But with Terraform's for loops and locals, we can automate this beautifully.

Solution Overview

Our approach has 3 main steps:

  1. Define the data structure - Organize VNets and subnets as variables

  2. Create VNets - Use for_each to loop through VNets

  3. Create Subnets - Use nested loops with flatten() to create subnets

Step 1: Define the Data Structure

First, let's define our Azure networks as a variable:

variable "azure_networks" {
  type = map(object({
    resource_group = string
    location       = string
    address_space  = list(string)
    subnets        = map(object({ 
      address_prefix = string 
    }))
  }))
  default = {
    "vnet-dev" = {
      resource_group = "rg-dev"
      location       = "East US"
      address_space  = ["10.1.0.0/16"]
      subnets = {
        "subnet-vm" = {
          address_prefix = "10.1.1.0/24"
        }
        "subnet-db" = {
          address_prefix = "10.1.2.0/24"
        }
      }
    }
    "vnet-prod" = {
      resource_group = "rg-prod"
      location       = "West US"
      address_space  = ["10.2.0.0/16"]
      subnets = {
        "subnet-web" = {
          address_prefix = "10.2.1.0/24"
        }
        "subnet-api" = {
          address_prefix = "10.2.2.0/24"
        }
        "subnet-db" = {
          address_prefix = "10.2.3.0/24"
        }
      }
    }
    "vnet-staging" = {
      resource_group = "rg-staging"
      location       = "Central US"
      address_space  = ["10.3.0.0/16"]
      subnets = {
        "subnet-test" = {
          address_prefix = "10.3.1.0/24"
        }
      }
    }
  }
}

What we have here:

  • A map of 3 Virtual Networks

  • Each VNet has subnets stored as a nested map

  • Each subnet has an address prefix (CIDR block)

Step 2: Create Azure Virtual Networks

using for_each, we iterate through each VNet:

resource "azurerm_virtual_network" "example" {
  for_each = var.azure_networks

  name                = each.key
  address_space       = each.value.address_space
  location            = each.value.location
  resource_group_name = each.value.resource_group
}

What happens:

Iterationeach.keyeach.value.locationresult
1"vnet-dev""East US"Creates VNet vnet-dev in East US
2"vnet-prod""West US"Creates VNet vnet-prod in West US
3"vnet-staging""Central US"Creates VNet vnet-staging in Central US

Terraform References:

  1. azurerm_virtual_network.example["vnet-dev"].id

  2. azurerm_virtual_network.example["vnet-prod"].id

  3. azurerm_virtual_network.example["vnet-staging"].id

Step 3: The Magic Part - Nested Loops with flatten()

Now comes the complex part: creating subnets across all VNets. We need to:

  1. Loop through each VNet

  2. Loop through each subnet

  3. within that VNet Combine the data Flatten the result into a single list

locals {
  # Create a flat list of all subnets with their parent VNet info
  azure_subnets = flatten([
    for vnet_name, vnet_config in var.azure_networks : [
      for subnet_name, subnet_config in vnet_config.subnets : {
        vnet_name      = vnet_name
        subnet_name    = subnet_name
        address_prefix = subnet_config.address_prefix
        vnet_id        = azurerm_virtual_network.example[vnet_name].id
        resource_group = vnet_config.resource_group
      }
    ]
  ])
}

Let me break this down line by line:

LineExplanation
for vnet_name, vnet_config in var.azure_networks : [Outer loop: Iterate through each VNet (dev, prod, staging)
for subnet_name, subnet_config in vnet_config.subnets : {Inner loop: Iterate through subnets in the current VNet
vnet_name = vnet_nameStore the VNet name
subnet_name = subnet_nameStore the subnet name
address_prefix = subnet_config.address_prefixGet the subnet's CIDR block
vnet_id = azurerm_virtual_network.example[vnet_name].idReference the VNet's ID
flatten([...])Convert the nested lists into one flat list

Iteration Trace with Real Data Let me show you exactly what happens in each iteration:

Iteration 1: vnet_name = "vnet-dev"

Inner loop processes subnets in vnet-dev:

Sub-iteration 1a: subnet_name = "subnet-vm"

{
  vnet_name      = "vnet-dev"
  subnet_name    = "subnet-vm"
  address_prefix = "10.1.1.0/24"
  vnet_id        = azurerm_virtual_network.example["vnet-dev"].id
  resource_group = "rg-dev"
Sub-iteration 1b: subnet_name = "subnet-db"
{
  vnet_name      = "vnet-dev"
  subnet_name    = "subnet-db"
  address_prefix = "10.1.2.0/24"
  vnet_id        = azurerm_virtual_network.example["vnet-dev"].id
  resource_group = "rg-dev"
}

Result from Iteration 1: A list with 2 objects

Iteration 2: vnet_name = "vnet-prod"

Inner loop processes subnets in vnet-prod:

Sub-iteration 2a: subnet_name = "subnet-web"

{
  vnet_name      = "vnet-prod"
  subnet_name    = "subnet-web"
  address_prefix = "10.2.1.0/24"
  vnet_id        = azurerm_virtual_network.example["vnet-prod"].id
  resource_group = "rg-prod"
}

Sub-iteration 2b: subnet_name = "subnet-api"

{
  vnet_name      = "vnet-prod"
  subnet_name    = "subnet-api"
  address_prefix = "10.2.2.0/24"
  vnet_id        = azurerm_virtual_network.example["vnet-prod"].id
  resource_group = "rg-prod"
}

Sub-iteration 2c: subnet_name = "subnet-db"

{
  vnet_name      = "vnet-prod"
  subnet_name    = "subnet-db"
  address_prefix = "10.2.3.0/24"
  vnet_id        = azurerm_virtual_network.example["vnet-prod"].id
  resource_group = "rg-prod"
}

Result from Iteration 2: A list with 3 objects

Iteration 3: vnet_name = "vnet-staging"

Inner loop processes subnets in vnet-staging:

Sub-iteration 3a: subnet_name = "subnet-test"

{
  vnet_name      = "vnet-staging"
  subnet_name    = "subnet-test"
  address_prefix = "10.3.1.0/24"
  vnet_id        = azurerm_virtual_network.example["vnet-staging"].id
  resource_group = "rg-staging"
}

Result from Iteration 3: A list with 1 object

Before flatten() - Nested Lists

After the outer loop completes, we have a list of lists:

[
  [
    { vnet_name = "vnet-dev", subnet_name = "subnet-vm", ... },
    { vnet_name = "vnet-dev", subnet_name = "subnet-db", ... }
  ],
  [
    { vnet_name = "vnet-prod", subnet_name = "subnet-web", ... },
    { vnet_name = "vnet-prod", subnet_name = "subnet-api", ... },
    { vnet_name = "vnet-prod", subnet_name = "subnet-db", ... }
  ],
  [
    { vnet_name = "vnet-staging", subnet_name = "subnet-test", ... }
  ]
]

Problem: This is hard to work with because it's nested!

After flatten() - Single Flat List. The flatten() function removes one level of nesting:

[
  { vnet_name = "vnet-dev", subnet_name = "subnet-vm", address_prefix = "10.1.1.0/24", vnet_id = "...", resource_group = "rg-dev" },
  { vnet_name = "vnet-dev", subnet_name = "subnet-db", address_prefix = "10.1.2.0/24", vnet_id = "...", resource_group = "rg-dev" },
  { vnet_name = "vnet-prod", subnet_name = "subnet-web", address_prefix = "10.2.1.0/24", vnet_id = "...", resource_group = "rg-prod" },
  { vnet_name = "vnet-prod", subnet_name = "subnet-api", address_prefix = "10.2.2.0/24", vnet_id = "...", resource_group = "rg-prod" },
  { vnet_name = "vnet-prod", subnet_name = "subnet-db", address_prefix = "10.2.3.0/24", vnet_id = "...", resource_group = "rg-prod" },
  { vnet_name = "vnet-staging", subnet_name = "subnet-test", address_prefix = "10.3.1.0/24", vnet_id = "...", resource_group = "rg-staging" }
]

Step 4: Convert List to Map and Create Subnets

for_each requires a map, not a list. So we convert the flat list to a map with unique keys:

resource "azurerm_subnet" "example" {
  for_each = {
    for subnet in local.azure_subnets : "${subnet.vnet_name}.${subnet.subnet_name}" => subnet
  }

  name                 = each.value.subnet_name
  resource_group_name  = each.value.resource_group
  virtual_network_name = each.value.vnet_name
  address_prefixes     = [each.value.address_prefix]

  depends_on = [azurerm_virtual_network.example]
}

What Gets Created After running terraform apply, you'll have 3 Vnets and 6 subnets.

Key Concepts to Remember

ConceptExplanation
Outer LoopIterates through each VNet
Inner LoopIterates through each subnet within that VNet
flatten()Converts nested lists into a single flat list
Map ConversionTransforms the flat list into a map for for_each
Unique Keys"vnet-dev.subnet-vm" ensures each subnet is unique

Why This Approach?

  1. ✅ DRY (Don't Repeat Yourself) - No hardcoded resources

  2. ✅ Scalable - Add new VNets/subnets easily by updating the variable

  3. ✅ Maintainable - All data in one place

  4. ✅ Flexible - Change naming, locations, CIDR blocks easily

  5. ✅ Reusable - Use this pattern for other hierarchical resources

Conclusion

Nested loops with flatten() are powerful Terraform patterns for managing hierarchical resources. By understanding how to:

Loop through parent resources (VNets) Loop through child resources (Subnets) Flatten nested lists Convert lists to maps You can automate complex infrastructure deployments with minimal code and maximum flexibility.