Terraform Dynamic Blocks: Advanced Use Cases and Examples
Tags: terraform, devops, infrastructureascode, sre
Published: 2024-06-17 12:15:00

Imagine a scenario where you have to create multiple similar resources, like subnets or security group rules, each with a slight variation. 

Instead of copying and pasting the same code with minor changes, dynamic blocks let you write the configuration once and dynamically generate the variations based on input values.

This blog will dive into [Terraform](https://www.env0.com/blog/what-is-terraform-cli) dynamic blocks and their components like `label`, [`for_each`](https://www.env0.com/blog/terraform-for-each-examples-tips-and-best-practices), `iterator`, and `content`. 

We will also explore various use cases and practical scenarios, such as:

1.  creating EC2 instances with specific Amazon EBS volume configurations
2.  applying dynamic blocks in resource and data blocks
3.  implementing multilevel nested dynamic blocks

> **‍_Disclaimer_**_All use cases for dynamic blocks in Terraform discussed here work similarly in_ [_OpenTofu_](https://www.env0.com/blog/opentofu-the-open-source-terraform-alternative)_, the open-source Terraform alternative. However, to keep it simple and familiar for DevOps engineers, we will refer to them as Terraform dynamic blocks throughout this discussion._

**Where to Use Dynamic Blocks**
-------------------------------

Here are some situations where dynamic blocks prove to be helpful:

*   **Creating Multiple AWS Subnets**: Suppose you need to create subnets in different availability zones. Rather than writing separate blocks for each subnet, you can use a dynamic block to iterate over a list of availability zones and create a subnet for each one.
*   **Configuring Security Group Rules**: When managing many security group rules, dynamic blocks help you define and organize them compactly. Instead of writing each rule separately, you can use a dynamic block to iterate over a list of rules, which simplifies the configuration
*   **Provisioning Multiple EC2 Instances**: If you need multiple EC2 instances with similar configurations but different attributes (like tags or instance types), dynamic blocks allow you to handle this efficiently.

**Components of Terraform Dynamic Blocks**
------------------------------------------

Dynamic blocks contain four main components - the `label`, `for_each`, `iterator`, and `content`.

Here's a detailed explanation of each:

![detailed explanation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mqiyy4efhxm6tw6qmgp0.png)

### **Basic Syntax**

To demonstrate how these work, let's use an example of a dynamic block that creates multiple configurations based on a list of input values.

Here’s what that dynamic block's syntax would look like:

    dynamic "label" {
      for_each = var.iterable_variable
      iterator = iterator_name # Optional, defaults to label
    
      content {
        # Configuration details for each iteration
        attribute = iterator_name.value
      }
    }

In this configuration:

*   The `label` specifies the type of dynamic block to create. 
*   The `for_each` statement loops through a list or map provided by `var.iterable_variable`, creating a block for each item.
*   The `iterator` is an optional name for the current item in the loop. If not specified, Terraform uses the label name by default. This iterator allows you to reference the current item being processed.
*   The `content` block contains the configuration details for each generated block, using `iterator_name.value` to insert the appropriate value for each iteration.

Let's apply this dynamic block syntax in a real-world scenario where we provision multiple EC2 instances and attach specific EBS volumes based on their instance IDs.

First, let's create a **main.tf** file. To use this, we need to retrieve existing instances using a data block and create a local variable to hold the instance IDs:

![](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/66671e3b6d92fc1868696e95_AD_4nXdHXklCiDIr35ZXJhTQLS9pj6Y9uHDV2wYspqDRWzIx2BxdXkMHi8ccbLfBVhbQT95G6y3Uu9xHEy-I3ytBDMaLxCV3dWDAiN65RFJkYl04kViEFxqQtrP_OQuHFm7BxJT8Mf_Y5KqjX1sxbhEYybmL-UKe.png)

In this configuration, the data `aws_instances`, `existing_instances` block fetches existing running EC2 instances. The `locals` block creates a local variable `instance_ids` that stores the IDs of the fetched instances.

Next, we will use a dynamic block to apply specific EBS configurations to EC2 instances based on their instance IDs.

We will dynamically attach different EBS volumes to the specified instances by iterating over the instance IDs. This approach ensures that each instance receives the appropriate EBS volume settings without redundant code. 

Here is how our resource block will look:

    resource "aws_instance" "dynamic_instance" {
      for_each = {
        for instance_id in local.instance_ids :
        instance_id => instance_id
      }
    
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = "t2.micro"
      tags = {
        Name = each.key
      }
    
      dynamic "ebs_block_device" {
        for_each = [
          for id in local.instance_ids : id
          if id == "i-0d5933a76d45a6aee"
        ]
        content {
          device_name = "/dev/sdh"
          volume_size = 10
          encrypted   = true
        }
      }
    
      dynamic "ebs_block_device" {
        for_each = [
          for id in local.instance_ids : id
          if id == "i-095aff1e2acc82958"
        ]
        content {
          device_name = "/dev/sdh"
          volume_size = 20
          encrypted   = true
        }
      }
    }

In this resource block, the `for_each` statement iterates over the `instance_ids` to create a resource for each instance.

The dynamic blocks `ebs_block_device` iterate over the instance IDs and attach specific EBS volumes to instances with `IDs i-0d5933a76d45a6aee` and `i-095aff1e2acc82958`, ensuring each instance receives the correct EBS volume settings.

**How to Use Terraform Dynamic Blocks**
---------------------------------------

Dynamic blocks are supported inside resource, data, provider, and provisioner blocks. This section will focus on applying dynamic blocks within resource and data blocks.

### **Applying Dynamic Blocks in Resource Blocks**

Dynamic blocks can be applied within resource blocks to handle configurations that repeat with slight variations. This is useful for resources that require nested blocks for repeated configurations, such as AWS security groups with multiple ingress and egress rules.

For example, you can [apply](https://www.env0.com/blog/terraform-apply-guide-command-options-and-examples) dynamic blocks within a resource block to create AWS security groups. 

We'll define variables for subnets and security group rules in our **variables.tf**. These variables will hold the configurations needed for creating subnets and security group rules in AWS. 

    variable "subnets" {
      description = "A list of maps, where each map contains subnet-specific attributes"
      type = list(object({
        cidr_block = string
        az         = string
      }))
      default = [
        {
          cidr_block = "10.0.1.0/24"
          az         = "us-west-2a"
        },
        {
          cidr_block = "10.0.2.0/24"
          az         = "us-west-2b"
        },
        {
          cidr_block = "10.0.3.0/24"
          az         = "us-west-2c"
        }
      ]
    }
    
    variable "security_group_rules" {
      description = "A list of security group rules"
      type = list(object({
        type        = string
        from_port   = number
        to_port     = number
        protocol    = string
        cidr_blocks = list(string)
      }))
      default = [
        {
          type        = "ingress"
          from_port   = 80
          to_port     = 80
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
        },
        {
          type        = "ingress"
          from_port   = 443
          to_port     = 443
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
        }
      ]
    }

Now, let's create an `aws_security_group` resource with a dynamic block configuration:

    resource "aws_security_group" "env0_security_group" {
      name        = "env0-security-group"
      vpc_id      = aws_vpc.env0_vpc.id
    
      dynamic "ingress" {
        for_each = [for rule in var.security_group_rules : rule if rule.type == "ingress"]
        content {
          from_port   = ingress.value.from_port
          to_port     = ingress.value.to_port
          protocol    = ingress.value.protocol
          cidr_blocks = ingress.value.cidr_blocks
        }
      }
    
      dynamic "egress" {
        for_each = [for rule in var.security_group_rules : rule if rule.type == "egress"]
        content {
          from_port   = egress.value.from_port
          to_port     = egress.value.to_port
          protocol    = egress.value.protocol
          cidr_blocks = egress.value.cidr_blocks
        }
      }
    
      tags = {
        Name = "env0-security-group"
      }
    }

In the Terraform code above, dynamic blocks are used to iterate over the `security_group_rules`. 

For each ingress rule, a new ingress block is created with the specified ports, protocol, and CIDR blocks. 

Similarly, for each egress rule, a new egress block is created. This ensures that all specified rules are dynamically applied to the security group, streamlining the configuration and maintaining consistency across the setup.

### **Applying Dynamic Blocks in Data Blocks**

Dynamic blocks can also be used within data blocks to retrieve information on the go. This approach is useful when you query resources based on varying criteria and dynamically generate the query filters.

For example, you need to find all EC2 instances in your AWS account that match specific criteria, such as being in a "running" state and having a specific tag. Instead of manually specifying each filter, you can use dynamic blocks to define these filters programmatically. 

    resource "aws_instance" "env0_instance" {
      ami           = "ami-09040d770ffe2224f" 
      instance_type = "t2.micro"
    
      tags = {
        Name = "env0-instance"
        Environment = "env0"
      }
    }
    
    variable "instance_filters" {
      description = "A list of filters for finding EC2 instances"
      default = [
        {
          name   = "instance-state-name"
          values = ["running"]
        },
        {
          name   = "tag:Environment"
          values = ["env0"]
        }
      ]
    }
    
    data "aws_instances" "env0_instances" {
      dynamic "filter" {
        for_each = var.instance_filters
        content {
          name   = filter.value.name
          values = filter.value.values
        }
      }
    }
    
    output "instance_ids" {
      value = data.aws_instances.env0_instances.ids
    }
    

In this configuration, the dynamic block within the data block iterates over the `instance_filters` variable.

For each filter in the list, it creates a filter block with the specified name and values, allowing you to query EC2 instances based on dynamic criteria. The resulting instance IDs are then output for further use.

By using dynamic blocks in both resource and data blocks, you can create more flexible and maintainable Terraform configurations that adapt to varying requirements and reduce redundancy in your code.

**Multilevel Nested Dynamic Blocks**
------------------------------------

Nested dynamic blocks allow you to handle more complex configurations by embedding one dynamic block inside another.

This is particularly useful when dealing with resources that have nested configurations requiring iteration over multiple levels of nested blocks – for example, when defining custom attributes with constraints for AWS Cognito User Pools.

### **How to Implement Nested Dynamic Blocks**

Implementing nested dynamic blocks involves using one dynamic block inside another, allowing each level to iterate over its own set of values.

For example, to create an AWS Cognito User Pool with nested custom attributes, we can define variables for the custom attributes and their constraints. Nested dynamic blocks are then used to generate the schema, iterating over the attributes and their constraints to build the complete configuration efficiently.

First, let us define `env0_user_pool_custom_attributes` variable for user pool custom attributes and their constraints in our **variables.tf** file, which will hold a list of custom attribute configurations for an AWS Cognito User Pool:

    variable "env0_user_pool_custom_attributes" {
      description = "List of custom attributes for the user pool"
      type = list(object({
        name                       = string
        attribute_data_type        = string
        is_required                = bool
        is_mutable                 = bool
        string_attribute_constraints = list(object({
          min_length = number
          max_length = number
        }))
      }))
      default = [
        {
          name                       = "custom-attribute"
          attribute_data_type        = "String"
          is_required                = false
          is_mutable                 = true
          string_attribute_constraints = [
            {
              min_length = 4
              max_length = 256
            }
          ]
        }
      ]
    }

Next, we will create the `aws_cognito_user_pool` resource using nested dynamic blocks to define the schema for the user pool:

    resource "aws_cognito_user_pool" "env0_production_user_pool" {
      name = "env0-production-user-pool"
    
      dynamic "schema" {
        for_each = var.env0_user_pool_custom_attributes
        content {
          name                = schema.value.name
          attribute_data_type = schema.value.attribute_data_type
          mutable             = schema.value.is_mutable
          required            = schema.value.is_required
    
          dynamic "string_attribute_constraints" {
            for_each = lookup(schema.value, "string_attribute_constraints", [])
            content {
              min_length = string_attribute_constraints.value.min_length
              max_length = string_attribute_constraints.value.max_length
            }
          }
        }
      }
    }

Here, the outer dynamic block schema iterates over the `env0_user_pool_custom_attributes` variable to create a schema for each custom attribute. Inside the schema block, another dynamic block `string_attribute_constraints` iterates over the constraints for each attribute.

This setup dynamically creates a schema entry for each custom attribute and applies the specified constraints, such as minimum and maximum lengths, ensuring a flexible and efficient configuration.

You can manage complex Terraform configurations more effectively using nested dynamic blocks to reduce redundancy and improve maintainability. This technique is beneficial for handling resources that require deep nesting and multiple levels of dynamic configurations.

**Terraform Dynamic Blocks with env0**
--------------------------------------

[env0](https://www.env0.com/) is a powerful platform designed to streamline IaC workflows, making managing and deploying cloud infrastructure easier. By integrating with tools like Terraform or OpenTofu, env0 enhances control over cloud deployments. Let’s look at an example that demonstrates how to use env0 to automate the creation of multiple AWS subnets using Terraform dynamic blocks.

‍**Setting Up env0**

1. On your env0 dashboard, create a new project. Name it something like "AWS VPC Project".

![](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/6667268628d4be3fb8d7a7b9_AD_4nXfSkuxy0IYPPIik7ldwMwp0WSMs9bTwOcsN7p-bxIJl5qy-Jc_ii2kNyayRaU-I8Mq5jddR2fmTjV_OWki347XSYlFaCZBJD5xpJo_S1Mkd0SOuCvkjADx-jmgrThG6T3LAOcPu7XDe_SbQJlL8-gQ9tUl_.png)

2. Connect your Git repository where your Terraform code is stored. If you don't have a repository, create one and push your Terraform configuration to it.

![](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/666726861b60e0e2415e0c85_AD_4nXca1IWlNWIDVEalRHzBRURu7OyjpMh8dY0unCpBok-tfWJ972VdIx5o5M4B8Qk3LYmXsi1QkELlox550bmTY61HCpXd0QSw6hh63LqbRTuxBadArs4MDgU6pOz44QWD0VP3Sgi2aGxt94z0cGhvj4XVzMLI.png)

3. Set the required variables, such as AWS credentials and any Terraform variables you’ve defined.

![](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/666726866272ad4136c982e9_AD_4nXfCda6w2B7694LHM99CZG1QBR71IOUYUdZxiTUZ97yHm-AE2FHQv63yDNFPikds7uAD1l3SVNbG5eK1bzbK8OuRyZmj2bvxz2BclnSRqbTNoLs9iEQlHSNJqh_oXT2yg0Kdp9wT7N06WiZpAU-MBkDniDbx.png)

4. Click the deploy button to start Terraform deployment. env0 will handle the execution and provide logs and outputs.

![](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/666726860d0f698a11b7660e_AD_4nXdAxTDWX4-rbCvoPFe-eco27tMu0tN4raOB8sbKLV9_yIpotZrLylIcns2BZl0G-QQG_G7g1sOrfioRUQum-dX6byRpSUQKZyoYVP21Kco2hF2QK0GAxv64rSzAXkrNgh2GE1N3qZFLov9pyXpirnP-R70z.png)

![](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/666726860ba383488721a6ea_AD_4nXe7C3fSOGfXv0AsUqShJHGX6PIE-Fq0TThUoWFZ0WUgTVg9aLmuSswC31Uta0HKZmUL-Y6BgkBzA3f64DTeHCGxH1j2NRPTFBvlEwpy0yZkmI802KzoOKLooP1Jo8eSD07prJbooIv_HkRKLMIkasSmT92d.png)

By using [env0](https://www.env0.com/) and dynamic blocks together, you can efficiently manage and automate the creation of multiple AWS subnets. 

This approach makes your Terraform code more scalable and maintainable. With env0, you benefit from streamlined deployments and centralized infrastructure management.

**Conclusion** 
---------------

In this blog, we explored how to use Terraform dynamic blocks to create AWS subnets, configure security group rules, and set up EC2 instances efficiently. We also looked at nested dynamic blocks for handling complex setups, like custom attributes for AWS Cognito User Pools.

By using [env0](https://www.env0.com/) to automate Terraform deployments, we made the process easier and more organized. This combination helps keep your infrastructure scalable, maintainable, and free from repetitive tasks, letting you focus on more important work.

**Frequently Asked Questions** 
-------------------------------

#### ‍**Q: What is a dynamic block vs. static block in Terraform?**

Terraform dynamic block allows you to generate multiple nested blocks within a resource or module based on a `for_each` expression. 

This is useful when the number of blocks is not fixed or doesn’t need to be computed. A static block is a fixed configuration written directly into the Terraform code, specifying the exact settings without iteration or computation.

#### ‍**Q: What is one disadvantage of using dynamic blocks in Terraform?**

One disadvantage of using dynamic blocks in Terraform is that they can make the configuration harder to read and understand. 

This can be particularly challenging for new team members or when the logic within the dynamic block becomes complex, potentially leading to maintenance difficulties.

#### ‍**Q: What is the difference between dynamic block and for_each in Terraform?**

The difference between a dynamic block and `for_each` in Terraform lies in their use cases. 

A dynamic block dynamically creates multiple nested blocks within a single resource or module, allowing for flexible configuration of nested elements. On the other hand, `for_each` iterates over a set of values to create multiple instances of a resource or module, enabling the creation of several independent resources based on a collection.

#### ‍**Q: What is a dynamic tag in Terraform?**

A dynamic tag in Terraform is a tag created using a dynamic block, which allows tags to be generated based on a `for_each` expression or other dynamic conditions. 

This approach enables more flexible and programmatic tagging of resources, adapting to different scenarios and requirements without hardcoding each tag.