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 beingress
oregress
.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 assignsvalue
to the current element being iterated. Iffor_each
is over a map,key
is 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 theiterator
variable (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_each
argument is provided a map created from thevar.ingress_rules
list, usingrule.rule_id
as the key. This makes each generatedingress
block uniquely identified by itsrule_id
, which is crucial for Terraform’s state management withfor_each
. - The
iterator = rule_data
assigns the current key-value pair of thefor_each
map torule_data
. Insidecontent
,rule_data.value
refers 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
content
block, two moredynamic
blocks are defined:source_port_range
anddestination_port_range
. - These inner dynamic blocks iterate over the
source_port_ranges
anddestination_port_ranges
lists 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_each
iseach.value.password != "" ? [1] : []
. This creates a list containing1
if the password is not empty, and an empty list if it is. The dynamic block will only be generated whenfor_each
has 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_nics
is 2).- The
nic_index.value
then 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_group
module itself contains thedynamic "ingress"
block. - The number and content of the ingress rules are entirely driven by the
ingress_rules
variable 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_each
Properly: Thefor_each
argument is the core ofdynamic
blocks. 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
iterator
Names: Whilevalue
is the default, renaming your iterator (e.g.,rule
,nic
,item
) improves readability, especially in nesteddynamic
blocks. - Mind the
content
Block: All arguments that belong to the dynamically generated block must go inside thecontent
block. - No Dynamic Meta-Arguments: You cannot use
dynamic
blocks to generate meta-arguments likecount
,for_each
,lifecycle
, orprovisioner
blocks.dynamic
blocks are for nested configuration arguments within a resource, not for the resource itself. - Readability vs. Complexity: While powerful, excessive nesting of
dynamic
blocks or overly complexfor_each
expressions 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
dynamic
blocks. Usevariable validation
to catch errors early. null
Values 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 ofnull
to 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