Highlights
- DRY Principle: Dynamic blocks help you avoid code repetition (Don't Repeat Yourself) by programmatically generating nested blocks like
ingressrules ortags. - Resource Simplification: They are most commonly used within resource blocks to handle complex configurations cleanly.
- Iterator Control: You can customize the temporary variable name using the
iteratorargument, making nested loops easier to read. - Collection Support: Dynamic blocks iterate over lists or maps using the
for_eachmeta-argument. - Maintainability: While powerful, they should be used judiciously to avoid making your HCL code overly complex or hard to debug.
Ever get a headache when going through a long Terraform configuration file filled with repetitive code? You may even find it difficult to understand what exactly is being provisioned and managed by Terraform. Your team may have ignored some best practices, probably due to tight deadlines, but now, the Terraform configuration files are becoming too difficult to understand and hard to maintain.
Well, you are in luck because dynamic blocks in Terraform can be helpful in such a scenario.
In this article, we'll explore what terraform dynamic block structures are, how to use them, and the best practices to follow.
Key Takeaways
- Terraform dynamic blocks help you dynamically construct repeated nested blocks.
- Dynamic blocks in Terraform are commonly used in the resource block. It is also supported inside the data, provider, and provisioner blocks
- Dynamic blocks should not be overused as they will make your Terraform configuration hard to read and maintain.
Try the Terraform Provider Lab for free

What are Terraform Dynamic Blocks?
Some top-level block types, like the resource block, have arguments in the form of a name = expression and also have nested blocks as arguments. Terraform dynamic blocks are commonly used to construct these repeated nested block arguments dynamically in resource-type blocks.
Terraform dynamic block type is supported inside resource, data, provider, and provisioner blocks. But it is commonly used in Resource blocks. Below is the syntax for creating Terraform dynamic blocks:
resource "resource_type" "resource_name" {
# body of the resource block
dynamic "label" {
for_each = complex_value_to_iterate_over
iterator = iterator_name
content {
# body of the dynamic block generated
}
}
}
Based on the syntax above, let’s explore what makes up the Terraform dynamic block:
- label specifies the kind of repeated nested block to generate.
- for_each specifies the complex value (common collections used are either list or map) to iterate over.
- Iterator is optional. It specifies a name that represents the current element of the complex value being iterated over. if the
iteratorargument is not set, this defaults to thelabelof the dynamic block. Also, the iterator object has 2 properties:- key for a list is the list element index of the current element (e.g., 0, 1, or 2). For a map, it is the map’s key (e.g. {map_key: “map_value” }). Please note that for a set, the
keyandvalueare identical and should not be used - Terraform dynamic blocks are commonly used to iterate over lists or maps. - value for a list or map is the value of the current element.
- key for a list is the list element index of the current element (e.g., 0, 1, or 2). For a map, it is the map’s key (e.g. {map_key: “map_value” }). Please note that for a set, the
- labels is optional. It is used to specify additional labels for the generated blocks. Please note that for a dynamic block to have a labels argument, the block type in which the dynamic block is being used (e.g.,
resourceblock) must have a schema that includes a block type that expects thelabelsargument. It is rarely used in practice as most providers do not support this. - content is the body of each generated block. This contains the arguments of each dynamically constructed block. The
iteratoris accessed within this block to get the values of each dynamically generated block.
Want to learn more about Terraform's basic concepts? Check out this video:
Why We Need Terraform Dynamic Blocks?
Consider the trimmed-down code snippet below:
# main.tf
resource "aws_vpc" "sandbox_vpc" {
cidr_block = "10.0.0.0/16"
instance_tenancy = "default"
tags = {
Name = "sandbox_vpc"
}
}
resource "aws_subnet" "sandbox_subnet" {
vpc_id = aws_vpc.sandbox_vpc.id
cidr_block = "10.0.1.0/24"
tags = {
Name = "sandbox_subnet"
}
}
resource "aws_security_group" "sandbox_sg" {
name = "sandbox_sg"
vpc_id = aws_vpc.sandbox_vpc.id
ingress {
description = "Allows SSH access"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [aws_vpc.sandbox_vpc.cidr_block]
}
ingress {
description = "Allows HTTP traffic"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [aws_vpc.sandbox_vpc.cidr_block]
}
ingress {
description = "Allows HTTPS traffic"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.sandbox_vpc.cidr_block]
}
# more ingress rules here...
tags = {
Name = "sandbox_sg"
}
}In this code, a Virtual Private Cloud, which is a private network in AWS, is provisioned. Also, within a VPC, there is a subnet that divides the VPC into subnetworks where there can be other resources like EC2 instances, for example. There is also a Security Group that serves as a firewall for the VPC. But, within the Security Group resource (i.e., aws_security_group.sandbox_sg), there are lots of repeated ingress blocks that represent ingress rules. So, how can we refactor these into clean, organized, and maintainable code?
As seen in the trimmed-down code snippet above, the ingress blocks are the repeated nested blocks that would need to be dynamically produced using Terraform dynamic blocks for a cleaner and simpler codebase.
Now, let's dive into some practical terraform examples in the following sections. We’ll start out by refactoring the code above that has repeated nested ingress blocks in the security group resource (i.e., aws_security_group.sandbox_sg).
Example Usages of Terraform Dynamic Blocks
The examples in this section demonstrate how to use Terraform dynamic blocks.
Using Terraform Dynamic Block in Resource Blocks
The example below uses a terraform dynamic block to dynamically generate the repetitive ingress blocks instead of having several individual ingress blocks. It is a cleaner version of the code in the previous section.
# variables.tf
variable "settings" {
type = list(object({
description = string
port = number
}))
default = [
{
description = "Allows SSH access"
port = 22
},
{
description = "Allows HTTP traffic"
port = 80
},
{
description = "Allows HTTPS traffic"
port = 443
}
]
}
# main.tf
resource "aws_vpc" "sandbox_vpc" {
cidr_block = "10.0.0.0/16"
instance_tenancy = "default"
tags = {
Name = "sandbox_vpc"
}
}
resource "aws_subnet" "sandbox_subnet" {
vpc_id = aws_vpc.sandbox_vpc.id
cidr_block = "10.0.1.0/24"
tags = {
Name = "sandbox_subnet"
}
}
resource "aws_security_group" "sandbox_sg" {
name = "sandbox_sg"
vpc_id = aws_vpc.sandbox_vpc.id
dynamic "ingress" {
for_each = var.settings
iterator = sandbox_sg_ingress
content {
description = sandbox_sg_ingress.value["description"]
from_port = sandbox_sg_ingress.value["port"]
to_port = sandbox_sg_ingress.value["port"]
protocol = "tcp"
cidr_blocks = [aws_vpc.sandbox_vpc.cidr_block]
}
}
tags = {
Name = "sandbox_sg"
}
}Let’s take an in-depth look at the refactored code above:
- The label of the dynamic block here is
ingress. The ingress block is the repeated nested block that is now dynamically generated. - The complex value being iterated over using the
for_eachmeta-argument is var.settings. This variable is a list of objects that contains the description and port for each dynamic block to be generated. - The iterator name is
sandbox_sg_ingress. This is used to represent the current element ofvar.settingsbeing iterated over. If the iterator name (i.e., sandbox_sg_ingress) is not specified, then the label of the dynamic block (i.e., ingress) will be used as the iterator. - The content contains the arguments of the generated ingress block. Please note that these arguments are attributes supported by the provider for the resource types. Also, note that the
iteratoris used within thecontentto access the values to be used in each dynamically generated block.
If the iterator is not specified, the terraform for_each dynamic block will be specified as shown below:
# main.tf
resource "aws_security_group" "sandbox_sg" {
name = "sandbox_sg"
vpc_id = aws_vpc.sandbox_vpc.id
dynamic "ingress" {
for_each = var.settings
content {
description = ingress.value["description"]
from_port = ingress.value["port"]
to_port = ingress.value["port"]
protocol = "tcp"
cidr_blocks = [aws_vpc.sandbox_vpc.cidr_block]
}
}
tags = {
Name = "sandbox_sg"
}
}Also, note that the content block can be rewritten as:
# main.tf
resource "aws_security_group" "sandbox_sg" {
name = "sandbox_sg"
vpc_id = aws_vpc.sandbox_vpc.id
dynamic "ingress" {
for_each = var.settings
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = [aws_vpc.sandbox_vpc.cidr_block]
}
}
tags = {
Name = "sandbox_sg"
}
}Furthermore, note that the complex value used in the examples above is a list of objects (i.e., var.settings). For each iteration, we will have the following:
- 1st iteration:
ingress.keyis 0 whileingress.valueis {description = “Allow SSH access”, port = 22} - 2nd iteration:
ingress.keyis 1 whileingress.valueis {description = “Allow HTTP traffic”, port = 80} - 3rd iteration:
ingress.keyis 2 whileingress.valueis {description = “Allow HTTPS traffic”, port = 443}
The complex value iterated over by for_each in a Terraform dynamic block can also be a map of objects. Let’s see this in the example below:
# variables.tf
variable "settings" {
type = map(object({
port = number
}))
default = {
"Allows SSH access" = {
port = 22
},
"Allows HTTP traffic" = {
port = 80
},
"Allows HTTPS traffic" = {
port = 443
}
}
}
# main.tf
resource "aws_vpc" "sandbox_vpc" {
cidr_block = "10.0.0.0/16"
instance_tenancy = "default"
tags = {
Name = "sandbox_vpc"
}
}
resource "aws_subnet" "sandbox_subnet" {
vpc_id = aws_vpc.sandbox_vpc.id
cidr_block = "10.0.1.0/24"
tags = {
Name = "sandbox_subnet"
}
}
resource "aws_security_group" "sandbox_sg" {
name = "sandbox_sg"
vpc_id = aws_vpc.sandbox_vpc.id
dynamic "ingress" {
for_each = var.settings
content {
description = ingress.key
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = [aws_vpc.sandbox_vpc.cidr_block]
}
}
tags = {
Name = "sandbox_sg"
}
}In the code above, we iterate over a map of objects, where the map keys represent the description and the value is port in the example. For each iteration, we will have the following:
- 1st iteration:
ingress.keyis “Allow SSH access” whileingress.valueis {port = 22} - 2nd iteration:
ingress.keyis “Allow HTTP traffic” whileingress.valueis {port = 80} - 3rd iteration:
ingress.keyis “Allow HTTPS traffic” whileingress.valueis {port = 443}
Please note that a map is an unordered collection; hence, the position of elements is not relevant.
Now, we have code that is organized, clean, and easy to maintain.
Using Terraform Dynamic Block in Data Blocks
Dynamic blocks are also supported in other blocks like data, for example:
# variables.tf
variable "instance_ids" {
type = set(string)
default = ["i-0a279d13cce5fc103", "i-01be1b2f8dbce4a26", "i-07a5f9e413eabb5f1"]
}
variable "instance_tags" {
type = list(object({
name = string
values = list(string)
}))
default = [
{
name = "tag:Environment"
values = ["test"]
},
{
name = "instance-state-name"
values = ["running"]
}
]
}
variable "drive_size" {
type = number
default = 6
}
# main.tf
data "aws_instance" "sandbox_server" {
for_each = var.instance_ids
instance_id = each.value
dynamic "filter" {
for_each = var.instance_tags
iterator = sandbox_server_filter
content {
name = sandbox_server_filter.value["name"]
values = sandbox_server_filter.value["values"]
}
}
}
resource "aws_ebs_volume" "sandbox_drive" {
for_each = data.aws_instance.sandbox_server
availability_zone = data.aws_instance.sandbox_server[each.key].availability_zone
size = var.drive_size
}
resource "aws_volume_attachment" "sandbox_drive_attach" {
for_each = data.aws_instance.sandbox_server
device_name = "/dev/sdb"
volume_id = aws_ebs_volume.sandbox_drive[each.key].id
instance_id = data.aws_instance.sandbox_server[each.key].id
}In the code above, we fetch EC2 instance details based on the requirements specified in the data block. We also use the Terraform dynamic block to dynamically generate the repeated nested filter block in the data block. Instead of having several repeated individual filter blocks written out literally based on all your desired requirements, we use dynamic blocks to keep the code clean and easy to read.
Please note that Terraform dynamic blocks cannot be used to generate meta-argument blocks like lifecycle and provisioner.
Best practices for Terraform Dynamic Blocks
It is recommended to use these blocks to abstract away repetitive and hard-to-read configurations when building a reusable module. It aims to provide a clean and simple user interface.
If using Terraform dynamic blocks makes your configuration files harder to comprehend, then you are better off literally writing the repeated nested blocks out.
Always keep in mind that the end goal is to have a simplified user interface that abstracts away complexity.

Conclusion
You now understand what terraform dynamic block structures are and have seen practical examples of how to use them. You still need to ensure they are used appropriately to avoid making your code hard to understand.
If you are looking to master IaC concepts, understanding why learn terraform in 2026 is crucial for staying ahead in the DevOps landscape.
Are you looking to polish your Terraform skills in a real-world environment? Enroll in our Terraform for Beginners Course, which covers all of Terraform fundamentals. It includes video lectures, interactive exercises, and hands-on labs to help you internalize concepts and commands.
More on Terraform:
- How to Manage Terraform State with Examples
- How to create an AWS S3 bucket using Terraform
- Terraform Modules - Tutorials and Examples
- How To Use Terraform depends_on Meta-Argument
- Terraform Template: Concepts, Use Cases and Examples
- Terraform Input and Output Variables Explained
- Terraform Variables: Types & Use Cases for Beginners
- How to use Terraform Count Index Meta-Argument? (with Examples)
- Terraform for_each: A simple Tutorial with Examples
FAQs
Q1: Can Terraform dynamic blocks be used for name = expression arguments?
Terraform dynamic blocks generate repeated nested blocks. They are used to only construct nested block arguments that are supported in a resource block type by that provider.
If you use it for a name = expression argument, you will get the error below:
Error: Unsupported block type
Hence, you can’t use Terraform dynamic blocks for name = expression arguments.
Q2: Can a Terraform dynamic block be inside another dynamic block?
Yes! You can generate terraform dynamic nested blocks if the resource type supports multiple levels of nesting. You achieve this by placing another dynamic block within the content block of the outer dynamic block.
Q3: What is the difference between for_each on a resource vs. for_each in a dynamic block?
for_each on a resource creates multiple distinct resources (e.g., 3 separate EC2 instances). for_each within a dynamic block creates multiple nested configuration blocks within a single resource (e.g., 3 ingress rules inside one Security Group).
Q4: Is it mandatory to use an iterator?
No, it is optional. If you do not specify an iterator name, Terraform defaults to using the label of the dynamic block (e.g., ingress.value). However, using a custom iterator name can improve readability, especially with nested blocks.
Q5: Where can I learn terraform deeply?
To master these concepts and prepare for the future of infrastructure as code (specifically regarding terraform 2026 trends), you should enroll in comprehensive courses that cover both the fundamentals and advanced topics like custom modules and dynamic block usage.

Discussion