Imagine this, You want to create a re-usable terraform module, which can be used in multiple environments, to create any number of VNets and subnets.
-
- In Development environment, you need to create two virtual networks (vNets), and inside each vNet there will be 3 subnets.
- In production environment, you need to create 5 vNets, and within each vNet, there will be 4 subnets.
Using reusable Terraform code — how will you do this?
The number of vNets and number of subnets are not the same — so a single for_each loop will not solve this. You need something that can iterate inside each vNet, independently.
Let’s take another example.
You added a ddos_protection_plan block in your reusable vNet module, but from Azure’s point of view, this block is optional. Some environments may skip it, while some environment might need it.
But how do you tell Terraform that this block is optional?
If you use a static block, Terraform will expect this block to be always present — which is not what you want.
Well….if you have faced any of these scenarios, you are in the right place.
In this article, we will learn what a dynamic block is, why it exists, and exactly how to use it to solve such practical challenges — with real Azure examples.
🚀 What is a dynamic block?
A dynamic block allows you to generate nested blocks programmatically inside a resource. This is useful when:
-
- The nested block needs independent iteration from the parent.
- The nested block is optional.
- You want to make your code reusable and flexible.
📌 Why Use dynamic Block? (Key Use Cases)
| Use Case | Description |
|---|---|
| Independent nested iteration | Nested block needs different iteration than parent |
| Optional nested blocks | Nested block should be conditionally included |
| Complex nested structures | Nested blocks inside nested blocks |
| Module reusability | Module should accept optional nested configs |
1️⃣ Independent Iteration within Parent Blocks
Let’s go back to the scenario where you had to create multiple VNets, each having a different number of subnets .
In such cases, a single for_each at the VNet level is not enough — because subnets inside each VNet need their own separate iteration.
This is exactly where dynamic block helps.
With dynamic, you can define a nested block (subnet in this case) and make it iterate independently using its own for_each.
This allows each VNet to generate exactly the number of subnets it needs — no more, no less.
variable "vnet_config" {}
resource "azurerm_virtual_network" "example" {
for_each = var.vnet_config
name = each.value.name
location = each.value.location
resource_group_name = each.value.resource_group_name
address_space = each.value.address_space
dynamic "subnet" {
for_each = lookup(each.value, "subnets", [])
content {
name = subnet.value.name
address_prefix = subnet.value.address_prefix
}
}
}
✅ Subnets can now be iterated independently per vNet.
2️⃣ Optional Nested Blocks (Recommended Pattern)
❗ The Problem with Static Nested Blocks (Important!)
Some Azure nested blocks are optional — for example, ddos_protection_plan in Virtual Network or delegation in subnets.
You can easily create the resource without defining these blocks in Azure Portal or CLI.
However, when you define them statically in Terraform like below example, Terraform will now expect this block to exist always, every time you run terraform plan or apply.
resource "azurerm_virtual_network" "example" {
name = "my-vnet"
location = "eastus"
resource_group_name = "my-rg"
address_space = ["10.0.0.0/16"]
ddos_protection_plan {
id = var.ddos_protection_plan_id
}
}
And if the block (being mandatory) is missing, you will get error message like this :
Error: Missing required argument
on main.tf line 7, in resource "azurerm_virtual_network" "example":
7: ddos_protection_plan {
The argument "id" is required, but no definition was found.
✅ This means — even though Azure allows this block to be optional, Terraform does not know that.
✅ In Terraform logic → Since you defined the block statically, it becomes mandatory during plan and apply.
But Why Do We Skip A Block ?
When you start building reusable Terraform modules or multi-environment infrastructure (Dev, Test, Prod, DR), this becomes a big headache:
Example:
-
- In Production → DDOS protection is mandatory → You have DDOS plan → Works fine.
- In Development → DDOS protection is not needed → No DDOS plan → Plan fails because
idis missing.
You cannot skip the block → because it is defined statically.
You cannot pass null → because id is a required field.
✅ Dynamic nested blocks + conditional for_each → Optional → Can skip if input is absent
dynamic block for optional blocks. This keeps your module flexible, safe, and environment agonistic.✅ Dynamic Block makes it optional
To overcome the problem with the static (mandatory) block, Use dynamic block + conditional for_each → So in this way you are telling Terraform: ” Hey, this block is optional. If you find this block in tfvars, take the values ; if not, don’t worry, please ignore and proceed”
variable "vnet_config" {}
data "azurerm_network_ddos_protection_plan" "vnet" {
for_each = var.vnet_config
name = lookup(each.value, "ddos_protection_plan_name", null)
resource_group_name = lookup(each.value, "ddos_protection_plan_rg", null)
}
resource "azurerm_virtual_network" "example" {
for_each = var.vnet_config
name = each.value.name
location = each.value.location
resource_group_name = each.value.resource_group_name
address_space = each.value.address_space
dynamic "ddos_protection_plan" {
for_each = lookup(each.value, "ddos_protection_plan_name", null) != null ? [1] : []
content {
id = data.azurerm_network_ddos_protection_plan.vnet[each.key].id
}
}
dynamic "subnet" {
for_each = lookup(each.value, "subnets", [])
content {
name = subnet.value.name
address_prefix = subnet.value.address_prefix
}
}
}
✅ If ddos_protection_plan_name is present → Block is created
✅ If absent → Block is skipped → No plan error
📦 Example tfvars
Block present:
vnet_config = {
vnet1 = {
name = "vnet1"
location = "eastus"
resource_group_name = "my-rg"
address_space = ["10.0.0.0/16"]
ddos_protection_plan_name = "ddos-plan-1"
ddos_protection_plan_rg = "network-rg"
subnets = [
{ name = "subnet1", address_prefix = "10.0.1.0/24" },
{ name = "subnet2", address_prefix = "10.0.2.0/24" }
]
}
}
Block absent:
vnet_config = {
vnet2 = {
name = "vnet2"
location = "eastus2"
resource_group_name = "my-rg"
address_space = ["10.1.0.0/16"]
subnets = [
{ name = "subnetA", address_prefix = "10.1.1.0/24" }
]
}
}
data block resolves the ID → making configuration simpler and tfvars user-friendly.each.key for data lookup?each.key refers to the parent resource iteration (vnet key).each.value would not work here because data block loops over the parent resource, not inside dynamic blocks.🔎 Best Practice Decision Matrix
| Block Type | Block Optionality | Separate Iteration Required | Recommended |
|---|---|---|---|
| Optional (Azure optional) | Yes | No | Dynamic block + conditional for_each |
| Optional (Azure optional) | Yes | Yes | Dynamic block + independent for_each |
| Mandatory (Azure mandatory) | No | No | Static nested block |
| Mandatory (Azure mandatory) | Yes | Yes | Dynamic block + simple for_each |
🚦 Final Rule of Thumb
-
Use for Independent Iteration
-
Ideal when you need nested loops (e.g., subnets per VNet, rules per NSG).
-
Ensures child blocks iterate independently of the parent resource.
-
-
Use for Optional Blocks
-
Helps conditionally include/exclude nested configurations (e.g., optional tags, backup policies).
-
Controlled via
for_eachwith empty collections ([]or{}) to skip.
-
-
Dynamic Blocks Aren’t Automatically Optional
-
Must explicitly manage skipping via:
-
Empty
for_eachcollections, or -
Conditional logic (
var.enable_feature ? { ... } : {}).
-
-
-
Avoid Overuse & Deep Nesting
-
Prefer flat structures where possible.
-
Break deeply nested logic into separate modules for clarity.
-
Max 2 levels of nesting recommended (e.g., VNet → Subnet).
-
-
Test with
terraform plan-
Verify:
-
Expected iterations appear.
-
Conditions (e.g., empty
for_each) skip blocks correctly.
-
-
Test edge cases (empty inputs, null values).
-
-
Validate with
terraform applyin Sandbox-
Confirm:
-
All resources deploy correctly.
-
Nested blocks (e.g., subnets) align with iterations.
-
-
Use
terraform state listto audit dynamic resources.
-
When to Avoid Dynamic Blocks
-
-
Simple, fixed nested blocks → Hardcode instead.
-
Single-level iterations → Prefer
for_eachat resource level. -
Overly complex logic → Refactor into modules.
-
Key Principle
“Dynamic blocks are powerful but should simplify—not complicate—your code.” Always prioritize readability and test rigorously.