Terraform Dynamic Blocks: Advanced Use Cases and Examples

Terraform Dynamic Blocks: Advanced Use Cases and Examples

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 dynamic blocks and their components like label, for_each, iterator, and content. 

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

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

DisclaimerAll use cases for dynamic blocks in Terraform discussed here work similarly in OpenTofu, 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:

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:

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 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 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

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

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.

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

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

By using env0 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 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.