Understanding Terraform Dynamic Blocks with Examples
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 blocks 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 name = expression
and also have nested blocks as arguments. Terraform dynamic blocks are commonly used to dynamically construct these repeated nested block arguments 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
iterator
argument is not set, this defaults to thelabel
of 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
key
andvalue
are 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.,
resource
block) must have a schema that includes a block type that expects thelabels
argument. 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
iterator
is 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 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 Terraform dynamic blocks 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_each
meta-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.settings
being 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
iterator
is used within thecontent
to access the values to be used in each dynamically generated block.
If the iterator
is not specified, the 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.key
is 0 whileingress.value
is {description = “Allow SSH access”, port = 22} - 2nd iteration:
ingress.key
is 1 whileingress.value
is {description = “Allow HTTP traffic”, port = 80} - 3rd iteration:
ingress.key
is 2 whileingress.value
is {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 given. For each iteration, we will have the following:
- 1st iteration:
ingress.key
is “Allow SSH access” whileingress.value
is {port = 22} - 2nd iteration:
ingress.key
is “Allow HTTP traffic” whileingress.value
is {port = 80} - 3rd iteration:
ingress.key
is “Allow HTTPS traffic” whileingress.value
is {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 writing the repeated nested blocks out literally.
Always keep in mind that the end goal is to have a simplified user interface that abstracts away complexity.
FAQ
Below are some of the frequently asked questions about Terraform dynamic blocks.
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.
Can a Terraform dynamic block be inside another dynamic block?
Yes! You can have multi-level nested block structures if the resource types used have multiple levels of nested block within one another.
You can generate these nested structures by having another dynamic block within the content
of a dynamic block.
Below is an example of a dynamic block inside another dynamic block:
dynamic "first_block" {
for_each = complex_value_to_iterate_over
content {
first_block_arg = first_block.key
dynamic "second_block" {
for_each = first_block.value.first_block_attribute
content {
second_block_arg = second_block.value.second_block_attribute
}
}
}
}
In the code above, we have second_block nested inside first_block within the content
portion of the dynamic block. Also, note that first_block.value.first_block_attribute refers to the current element of the outer dynamic block while second_block.value.second_block_attribute refers to the current element of the inner dynamic block.
The outer dynamic block generates repeated nested first_block, as we have seen earlier. This nested first_block has a nested block argument called second_block, and within this inner block, you can reference either the current element of the outer block or the current element of the inner block.
Please note that while this is a great feature of Terraform dynamic blocks, it could also lead to code that is difficult to understand.
Conclusion
You now understand what Terraform dynamic blocks 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.
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.
If you want to gain other Infrastructure as Code (IaC) skills, check out our IaC Learning Path.