In today’s blog post, we will learn terraform dynamic block. The dynamic block was Introduced to solve the challenge of dynamically generating a nested configuration blocks, the dynamic block is a powerful Terraform construct that allows you to build repeatable blocks based on complex data structures.
What is the Terraform dynamic Block?
The dynamic block allows you to construct repeatable nested blocks within a resource, data source, provider, or provisioner configuration. It’s essentially a for_each loop specifically designed for generating these internal blocks, whose number and content are not fixed but are derived from a given collection (list, map, or set).
Think of it as a programmatic way to generate configuration blocks, similar to how you use for_each on a resource to create multiple instances of that resource.
Why Use dynamic Blocks? (The DRY Principle)
- Reduce Repetition: Avoid copying and pasting identical or very similar nested blocks multiple times.
- Increase Flexibility: Create a variable number of blocks based on input variables or computed data.
- Improve Readability: Incorporate complex logic for generating blocks into a clean, iterable structure.
- Dynamic Configurations: Drive parts of your resource configuration directly from data.
Syntax of the dynamic Block
A dynamic block has a distinct structure:
dynamic "<BLOCK_LABEL>" {
for_each = <COLLECTION> # A list, map, or set to iterate over
# Optional: Define the iteration variable names
iterator = <NAME_FOR_EACH_ITEM> # Default is "value"
content {
# Arguments for the <BLOCK_LABEL> go here.
# You can reference <NAME_FOR_EACH_ITEM>.<ATTRIBUTE> or <NAME_FOR_EACH_ITEM>.key and <NAME_FOR_EACH_ITEM>.value
}
}
<BLOCK_LABEL>: This is the name of the nested block that you want to generate dynamically. For example, if you’re defining security group rules, this would beingressoregress.for_each: (Required) This is the collection (list, map, or set) that Terraform will iterate over. For each element in this collection, a new instance of the<BLOCK_LABEL>will be generated.iterator: (Optional) By default, Terraform assignsvalueto the current element being iterated. Iffor_eachis over a map,keyis also available. You can rename this variable for clarity (e.g.,rule,nic,item).content: (Required) This block defines the arguments that belong inside each generated instance of the<BLOCK_LABEL>. You will typically reference theiteratorvariable (e.g.,rule.port,nic.ip_address) to set values dynamically.
Common Use Cases and Examples
Let us explore the power of the dynamic block with practical scenarios.
Terraform dynamic block with for_each and List of Objects/Maps
This is the most common and powerful use case, combining dynamic with for_each to iterate over a list of complex objects (which are essentially maps of attributes).
Scenario: Create a security group that allows specific ingress rules defined by an input variable.
# variables.tf
variable "ingress_rules" {
description = "A list of ingress rule objects for the security group."
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
# Optional: Add a 'rule_id' for explicit identification if needed
rule_id = string
}))
default = [
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP"
rule_id = "http_any"
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTPS"
rule_id = "https_any"
},
{
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["192.168.1.0/24"]
description = "Allow SSH from internal network"
rule_id = "ssh_internal"
}
]
}
# main.tf
resource "aws_security_group" "web_sg" {
name = "dynamic-web-sg"
description = "Security group with dynamic ingress rules"
vpc_id = "vpc-0abcdef1234567890" # Replace with your VPC ID
dynamic "ingress" { # The BLOCK_LABEL is "ingress"
# Use a 'for' expression to convert the list of objects into a map
# where the 'rule_id' becomes the map key. This is robust for for_each.
for_each = { for rule in var.ingress_rules : rule.rule_id => rule }
iterator = rule_data # Name the current element "rule_data" for clarity
content { # Define the arguments for each "ingress" block
from_port = rule_data.value.from_port
to_port = rule_data.value.to_port
protocol = rule_data.value.protocol
cidr_blocks = rule_data.value.cidr_blocks
description = rule_data.value.description
}
}
egress { # Can still mix with static blocks
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Explanation:
- The
for_eachargument is provided a map created from thevar.ingress_ruleslist, usingrule.rule_idas the key. This makes each generatedingressblock uniquely identified by itsrule_id, which is crucial for Terraform’s state management withfor_each. - The
iterator = rule_dataassigns the current key-value pair of thefor_eachmap torule_data. Insidecontent,rule_data.valuerefers to the original rule object (e.g.,{from_port=80, ...}).
Terraform dynamic block with Nested Loops
Generate deeply nested configurations where a list of items each has its own list of sub-items.
Scenario: Create an Azure Network Security Group (NSG) with multiple security rules, and each rule can have multiple source/destination port ranges.
# variables.tf
variable "nsg_rules_config" {
description = "Configuration for NSG rules, including multiple port ranges."
type = list(object({
name = string
priority = number
direction = string
access = string
protocol = string
source_address_prefix = string
destination_address_prefix = string
source_port_ranges = list(string)
destination_port_ranges = list(string)
}))
default = [
{
name = "Allow-SSH"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_address_prefix = "*"
destination_address_prefix = "*"
source_port_ranges = ["*"]
destination_port_ranges = ["22"]
},
{
name = "Allow-Web"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_address_prefix = "Internet"
destination_address_prefix = "*"
source_port_ranges = ["*"]
destination_port_ranges = ["80", "443"] # Multiple ports for this rule
}
]
}
# main.tf
resource "azurerm_network_security_group" "main" {
name = "my-dynamic-nsg"
location = "eastus" # Replace with your location
resource_group_name = "my-resource-group" # Replace with your RG name
dynamic "security_rule" { # Outer dynamic loop for each security rule
for_each = var.nsg_rules_config
iterator = rule_data # Current rule object
content {
name = rule_data.value.name
priority = rule_data.value.priority
direction = rule_data.value.direction
access = rule_data.value.access
protocol = rule_data.value.protocol
source_address_prefix = rule_data.value.source_address_prefix
destination_address_prefix = rule_data.value.destination_address_prefix
# Nested dynamic block for source_port_ranges
dynamic "source_port_range" {
for_each = rule_data.value.source_port_ranges # Iterate over source port ranges of *this* rule
iterator = src_port
content {
source_port_range = src_port.value
}
}
# Nested dynamic block for destination_port_ranges
dynamic "destination_port_range" {
for_each = rule_data.value.destination_port_ranges # Iterate over dest port ranges of *this* rule
iterator = dest_port
content {
destination_port_range = dest_port.value
}
}
}
}
}
Explanation:
- The outer
dynamic "security_rule"iterates over the list of rule configurations. - Inside its
contentblock, two moredynamicblocks are defined:source_port_rangeanddestination_port_range. - These inner dynamic blocks iterate over the
source_port_rangesanddestination_port_rangeslists of the current outer rule. This creates a highly flexible way to define NSG rules with multiple port ranges per rule.
Terraform dynamic block with Conditional Logic
Conditionally include or exclude attributes or entire blocks based on specific conditions.
Scenario: Create a database user with an optional password, and conditionally apply a description based on whether it is for production.
# variables.tf
variable "db_users" {
description = "A list of database user configurations."
type = list(object({
name = string
password = string # Can be empty for non-prod
is_prod_user = bool
}))
default = [
{
name = "app_user_dev"
password = ""
is_prod_user = false
},
{
name = "app_user_prod"
password = "SecurePassword!"
is_prod_user = true
}
]
}
# main.tf (Conceptual database user resource)
resource "null_resource" "db_users" { # Using null_resource for demonstration
for_each = { for user in var.db_users : user.name => user }
triggers = {
user_name = each.value.name
}
dynamic "auth_details" { # Assume 'auth_details' is a nested block for credentials
for_each = each.value.password != "" ? [1] : [] # If password is not empty, generate this block
content {
password = each.value.password # This attribute is only present if the block is generated
}
}
dynamic "description_block" { # Assume 'description_block' is an optional nested block
for_each = each.value.is_prod_user ? [1] : [] # Only generate if it's a prod user
content {
text = "This user is for production environment use."
# You could add other attributes specific to production users here
}
}
provisioner "local-exec" {
command = "echo 'Configuring user ${each.value.name}. Password set: ${each.value.password != ""}. Prod user: ${each.value.is_prod_user}'"
}
}
Explanation:
- Conditional Block Generation:
dynamic "auth_details": Thefor_eachiseach.value.password != "" ? [1] : []. This creates a list containing1if the password is not empty, and an empty list if it is. The dynamic block will only be generated whenfor_eachhas at least one element.dynamic "description_block": Similarly,each.value.is_prod_user ? [1] : []ensures this block is only created for production users.
- This allows you to omit entire nested blocks from the generated configuration based on runtime conditions.
Terraform dynamic block with count
While for_each is generally preferred for dynamic blocks, count can be used when you need to generate a fixed or numerically indexed set of blocks.
Scenario: Create a set of virtual network interfaces, where the number is driven by count, and each interface has a specific configuration based on its index.
# variables.tf
variable "num_nics" {
description = "Number of network interfaces to create for a VM."
type = number
default = 2
}
# main.tf (Conceptual VM resource with dynamic NICs)
resource "null_resource" "my_vm_with_nics" {
count = 1 # Just one VM for this example
triggers = {
nic_count = var.num_nics
}
dynamic "network_interface" { # Assume 'network_interface' is a nested block
for_each = range(var.num_nics) # Iterate from 0 to num_nics-1
iterator = nic_index # The current index (0, 1, 2...)
content {
name = "nic-${nic_index.value}"
ip_address = "10.0.0.${10 + nic_index.value}" # IP based on index
is_primary = nic_index.value == 0
# ... other NIC specific properties
}
}
provisioner "local-exec" {
command = "echo 'VM with ${var.num_nics} interfaces configured.'"
}
}
Explanation:
for_each = range(var.num_nics)generates a list of numbers (e.g.,[0, 1]ifvar.num_nicsis 2).- The
nic_index.valuethen provides the current numerical index for configuring each dynamic network interface block.
Terraform dynamic block in a Module
Dynamically generate configuration blocks within a module, allowing the module’s behavior to be customized by its inputs.
Scenario: Create a reusable module to define EC2 security groups that can accept a variable number of ingress rules.
modules/security_group/variables.tf:
# modules/security_group/variables.tf
variable "name" {
description = "Name of the security group."
type = string
}
variable "vpc_id" {
description = "The VPC ID to associate the security group with."
type = string
}
variable "ingress_rules" {
description = "A list of ingress rule objects for the security group."
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
rule_id = string # Important for for_each keys
}))
default = [] # Default to empty list if no rules are passed
}
modules/security_group/main.tf:
# modules/security_group/main.tf
resource "aws_security_group" "this" {
name = var.name
description = "Managed by module: ${var.name}"
vpc_id = var.vpc_id
dynamic "ingress" {
# Use a 'for' expression to convert the list of objects into a map
# where the 'rule_id' becomes the map key. This is robust for for_each.
for_each = { for rule in var.ingress_rules : rule.rule_id => rule }
iterator = rule_data
content {
from_port = rule_data.value.from_port
to_port = rule_data.value.to_port
protocol = rule_data.value.protocol
cidr_blocks = rule_data.value.cidr_blocks
description = rule_data.value.description
}
}
egress { # Default egress rule for the module
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
output "security_group_id" {
value = aws_security_group.this.id
}
root/main.tf (Consuming the Module):
# root/main.tf
module "web_app_sg" {
source = "./modules/security_group" # Path to your module
name = "web-app-sg"
vpc_id = "vpc-0abcdef1234567890" # Your VPC ID
ingress_rules = [
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP to web app"
rule_id = "http_in"
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTPS to web app"
rule_id = "https_in"
}
]
}
module "db_sg" {
source = "./modules/security_group"
name = "database-sg"
vpc_id = "vpc-0abcdef1234567890"
ingress_rules = [ # Fewer, more restrictive rules for DB
{
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [module.web_app_sg.security_group_id] # Reference other SG
description = "Allow PostgreSQL from web app SG"
rule_id = "db_from_web"
}
]
}
Explanation:
- The
security_groupmodule itself contains thedynamic "ingress"block. - The number and content of the ingress rules are entirely driven by the
ingress_rulesvariable passed into the module from the root configuration. - This makes the module highly flexible, as it can be reused for different types of security groups requiring varying numbers of rules.
Best Practices for dynamic Blocks
- Use
for_eachProperly: Thefor_eachargument is the core ofdynamicblocks. Ensure the collection you are iterating over provides distinct elements that can serve as keys for the generated blocks if order matters for the underlying resource. - Meaningful
iteratorNames: Whilevalueis the default, renaming your iterator (e.g.,rule,nic,item) improves readability, especially in nesteddynamicblocks. - Mind the
contentBlock: All arguments that belong to the dynamically generated block must go inside thecontentblock. - No Dynamic Meta-Arguments: You cannot use
dynamicblocks to generate meta-arguments likecount,for_each,lifecycle, orprovisionerblocks.dynamicblocks are for nested configuration arguments within a resource, not for the resource itself. - Readability vs. Complexity: While powerful, excessive nesting of
dynamicblocks or overly complexfor_eachexpressions can make your HCL harder to read and debug. - Validate Input Data: Always validate the structure and content of the input variables passed to your
dynamicblocks. Usevariable validationto catch errors early. nullValues for Optional Arguments: If an argument within a dynamic block is optional and might sometimes be absent from your input data, usetry()orlookup()with a default ofnullto ensure Terraform does not throw an error when the key is missing.
# Example: conditionally include a description
content {
from_port = rule.value.from_port
to_port = rule.value.to_port
# Only include description if it exists in the input object
description = try(rule.value.description, null)
}
Conclusion
The Terraform dynamic block is an essential feature for writing flexible, reusable, and maintainable Terraform configuration blocks. By enabling the dynamic generation of nested configuration blocks, it addresses the challenges of repetition and fixed structures, particularly when dealing with variable lists of settings like security group rules, network interfaces, or policy statements.
Mastering the dynamic block, especially usage of for_each, conditional statements, handling different data types like list or maps will enable you to create fully optimized and reusable terraform code.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com