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

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:
Define the data structure - Organize VNets and subnets as variables
Create VNets - Use for_each to loop through VNets
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:
| Iteration | each.key | each.value.location | result |
| 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:
azurerm_virtual_network.example["vnet-dev"].id
azurerm_virtual_network.example["vnet-prod"].id
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:
Loop through each VNet
Loop through each subnet
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:
| Line | Explanation |
| 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_name | Store the VNet name |
| subnet_name = subnet_name | Store the subnet name |
| address_prefix = subnet_config.address_prefix | Get the subnet's CIDR block |
| vnet_id = azurerm_virtual_network.example[vnet_name].id | Reference 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
| Concept | Explanation |
| Outer Loop | Iterates through each VNet |
| Inner Loop | Iterates through each subnet within that VNet |
| flatten() | Converts nested lists into a single flat list |
| Map Conversion | Transforms the flat list into a map for for_each |
| Unique Keys | "vnet-dev.subnet-vm" ensures each subnet is unique |
Why This Approach?
✅ DRY (Don't Repeat Yourself) - No hardcoded resources
✅ Scalable - Add new VNets/subnets easily by updating the variable
✅ Maintainable - All data in one place
✅ Flexible - Change naming, locations, CIDR blocks easily
✅ 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.