Terraform is a unique and powerful tool built for developers worldwide, enabling them to provision and manage their IT infrastructure resources by writing a simple code snippet. In Terraform, we use HCL (HashiCorp Configuration Language) as the language to write these codes, which have simpler syntaxes.

Terraform provides many features that help users efficiently provision and manage their IT resources. Some of them are Terraform backends, dry runs, workspaces, count meta arguments, lookup functions, etc. Among these features, the Terraform dynamic block is valuable for writing codes in Terraform.

This writing will explore the Terraform dynamic blocks, including a well-detailed definition and its usage with some examples.

Let’s get started!!

What is a Dynamic Block in Terraform?

Imagine you have one or two IT resources to be provisioned and managed. You will write a small and clean code in HCL and execute it. But what if you have many resources to be provisioned and managed? What if you have to make copies of a particular resource, such as an Amazon EC2 instance? You have to write a big and complex code to fulfill your achievement.

But the problem is that your code can include some repetitive code chunks, making it much bigger and more complex. This makes it harder to understand or maintain your code. You’ll be having a headache!!

This is where a dynamic block can save you.

A dynamic block in Terraform is a handy feature that allows us to construct repeatable nested blocks within a resource dynamically. Dynamic blocks feature can be used within resource, provider, data, and provisioner blocks, but it is used primarily with resource blocks. We can generate these blocks based on a variable, local, or expression, and inside, we can use maps and lists.

With this feature, nested blocks can be developed dynamically within any of these four blocks.

Let’s go through the syntax before we see how dynamic blocks work.

Syntax of a dynamic block.

resource "resource_type" "resource_name" {

 # Resource block body

 dynamic  "label"  {

   for_each = value_to_iterate_over

   iterator = name_of_the_iterator

   content {

     # Generated dynamic block body

   }

 }

}

Let’s go through each component of the dynamic block.

label: Here, we specify the name/type of the repeated/nested block as the label to generate dynamically.

for_each: When dynamically constructing the nested blocks, we must iterate over a list, map, or local. The value (can be a complex value) to iterate over is specified here.

iterator (Optional): This is an optional parameter where we can define a name representing the current element of the complex value being iterated. If the iterator argument is not set, this takes the value of the label parameter as the default value.

content: Here, we define the body of each dynamically generated block, which includes the arguments of each block. The iterator is accessed within this block to get the values of each dynamic block.

We have now learned all the theoretical parts of the Terraform dynamic blocks. Now, it’s time to see a practical example of using dynamic blocks.

Example: Creating a Security Group for a web server in AWS using dynamic blocks.

Imagine we have a web server created and need to define a security group to control the inbound and outbound traffic.

Note: In AWS, a security group is a network component that acts as a virtual firewall to our instances defined in our cloud environment. It controls the inbound and outbound traffic to and from the instances. Inside a security group, we define rules to only allow specified traffic types to and from our instances.

Traditionally, when writing a Terraform configuration file to create a security group, it looks like the below.

resource "aws_security_group" "my-webserver-sg" {

 vpc_id = data.aws_vpcs.default.id

 name = "my-new-webserver"

 description = "Security Group for my new web server"

 ingress {

   protocol = "tcp"

   from_port = 80

   to_port = 80

   cidr_blocks = [ "0.0.0.0/0" ]

 }

 ingress {

   protocol = "tcp"

   from_port = 443

   to_port = 443

   cidr_blocks = [ "0.0.0.0/0" ]

 }

 ingress {

   protocol = "tcp"

   from_port = 8080

   to_port = 8080

   cidr_blocks = [ "0.0.0.0/0" ]

 }

 ingress {

   protocol = "tcp"

   from_port = 22

   to_port = 22

   cidr_blocks = [ "0.0.0.0/0" ]

 }

 egress {

   protocol = "tcp"

   from_port = 443

   to_port = 443

   cidr_blocks = [ "10.0.0.0/16" ]

 }

 egress {

   protocol = "tcp"

   from_port = 1433

   to_port = 1433

   cidr_blocks = [ "10.0.0.0/16" ]

 }

 egress {

   protocol = "tcp"

   from_port = 80

   to_port = 80

   cidr_blocks = [ "10.0.0.0/16" ]

 }

 egress {

   protocol = "tcp"

   from_port = 53

   to_port = 53

   cidr_blocks = [ "10.0.0.0/16" ]

 }

}

As in the code, there is a resource block for the security group called ‘my-webserver-sg’ where the resource type is ‘aws_security_group’. We have set a name (‘my-new-webserver’), a description for our security group, and a mandatory argument: ‘vpc_id’ where it takes the default ID value.

As the rules for our security group, we have defined four inbound (ingress) and four outbound (egress) rules to handle the traffic to the web server. But as you can see, all these rules are constructed as nested blocks with the same structure. But these vary from each other.

Since there are eight nested blocks, it displays as a large code that can be complex to understand instantly. Also, it can be prone to errors.

As a solution, we can use dynamic blocks to construct these nested blocks dynamically without writing too many code chunks.

See the below updated code.

variable "ports" {

 type    = map(list(number))

 default = {

   inbound  = [80, 443, 8080, 22],

   outbound = [443, 1433, 80, 53]

 }

}

resource "aws_security_group" "my-webserver-sg" {

 vpc_id      = data.aws_vpcs.default.id

 name        = "my-new-webserver"

 description = "Security Group for my new web server"

 dynamic "ingress" {

   for_each = var.ports["inbound"]

   content {

     from_port   = ingress.value

     to_port     = ingress.value

     protocol    = "tcp"

     cidr_blocks = ["0.0.0.0/0"]

   }

 }

 dynamic "egress" {

   for_each = var.ports["outbound"]

   content {

     from_port   = egress.value

     to_port     = egress.value

     protocol    = "tcp"

     cidr_blocks = ["10.0.0.0/16"]

   }

 }

}

Firstly, we want to create a map that stores our inbound and outbound ports for the web server. As in the code, we created a variable called ‘ports’, and inside the variable, we defined a map that included those ports.

Next, we must add dynamic blocks inside the ‘my-webserver-sg’ resource block. Here, we only make changes to the nested blocks to keep the existing changes to our first three arguments (vpc_id, name, and description).

To explain the dynamic block feature, let’s take the dynamic block for handling inbound traffic (ingress).

dynamic "ingress" {

   for_each = var.ports["inbound"]

   content {

     from_port   = ingress.value

     to_port     = ingress.value

     protocol    = "tcp"

     cidr_blocks = ["0.0.0.0/0"]

   }

 } 

We used ‘ingress’ for the label as the block type since we are dynamically creating blocks for the ingress rules.

Next, we have defined the ‘for_each’ argument. It checks the ‘ports’ map and selects the list of inbound ports using the key ‘inbound’. Since we have four elements (ports) in the inbound list, it will create four blocks for each component.

Then, we describe the content for the dynamic block with four arguments (from_port, to_port, protocol, and cidr_blocks).

The same explanation goes for the ‘egress’ dynamic block. It will check the elements in the outbound list and construct a block for each identified element.

Ultimately, we have a clean and simple code that does the same functionalities performed by the previous code.

Why do we need a feature like dynamic blocks?

The main advantage of using a dynamic block in Terraform is the code reusability and maintainability. As we saw in the previous example, our traditional code is a large and a bit complex code. We had one web server and eight rules. Imagine we have more than one web server. Then it will get complicated for us. There will be so many nested and repeated blocks for the rules.

But we can avoid such complications by dynamically building the blocks for each web server. It saves our time, and we will also have a clean code.

Moreover, we can reduce the time to deploy the infrastructure components as Terraform does not need to go through extensive and complex codes.

Conclusion

In this article, we learned about one of the outstanding features that Terraform gives to its users: Dynamic Blocks. With dynamic blocks, we can avoid writing nested and repetitive code chunks. By dynamically creating them, we can save time and have a clean and maintainable code.