An introduction to Dynamic Blocks in Terraform

post thumb
IaC
by Jeroen Penders/ on 16 Sep 2024

An introduction to Dynamic Blocks in Terraform

Terraform, a popular Infrastructure as Code (IaC) tool, allows you to define and manage your infrastructure declaratively. One of Terraform’s powerful features is the ability to create dynamic blocks, which help make your configuration more modular, reusable, and maintainable. In this post, we’ll explore what dynamic blocks are, how they work, and provide a practical example to illustrate their usage.

What are Dynamic Blocks?

Dynamic blocks in Terraform enable you to programmatically generate nested blocks within a resource or module based on a collection of input data. This is particularly useful when you need to create a varying number of similar nested blocks based on some dynamic input, such as generating multiple security group rules, tags, or other configuration items.

Components of a Dynamic Block

A dynamic block in Terraform has three main components:

  • dynamic Keyword: This keyword is followed by the name of the block you want to generate dynamically.
  • for_each Argument: This argument specifies the collection (list, map, or set) over which the dynamic block will iterate.
  • content Block: This nested block contains the actual configuration that will be repeated for each item in the for_each collection.
dynamic Keyword

The dynamic keyword is used to declare a dynamic block. It is followed by the name of the block you wish to generate dynamically. This block name should match the type of nested block you are creating within the resource or module.

Example:

dynamic "ingress" {
  # Configuration here
}

In this example, the ingress block is what we want to create dynamically within a resource, such as a security group.

for_each Argument

The for_each argument specifies the collection that the dynamic block will iterate over. This collection can be a list, map, or set. For each item in this collection, a corresponding nested block will be generated.

Example:

dynamic "ingress" {
  for_each = var.ingress_rules
  # Content here
}
content Block

The content block contains the actual configuration that will be repeated for each item in the for_each collection. Inside the content block, you can refer to the current item from the for_each collection using block_name.value.

Example:

dynamic "ingress" {
  for_each = var.ingress_rules
  content {
    description = ingress.value.description
    from_port   = ingress.value.from_port
    to_port     = ingress.value.to_port
    protocol    = ingress.value.protocol
    cidr_blocks = ingress.value.cidr_blocks
  }
}

In this example, for each item in var.ingress_rules, a new ingress block will be created with the attributes description, from_port, to_port, protocol, and cidr_blocks set based on the values of the current item (ingress.value).

  • The dynamic “ingress” block iterates over the var.ingress_rules list.
  • For each item in the list, it generates an ingress block with the specified attributes.

Practical Example: AWS Security Group Rules

Let’s dive into a practical example to understand how dynamic blocks work. Imagine you need to create multiple ingress rules for an AWS Security Group based on a list of port ranges. Manually defining each ingress rule can be tedious and error-prone. Instead, you can use dynamic blocks to automate this process.

Static Approach

Without dynamic blocks, you might define each ingress rule manually:

resource "aws_security_group" "webserver" {
  name        = "sg for webserver"
  description = "Allow inbound traffic"
  vpc_id      =  aws_vpc.testvpc.id

  ingress {
    description      = "Allow inbound ssh traffic"
    from_port        = 22
    to_port          = 22
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
  }
  ingress {
    description      = "Allow inbound http traffic"
    from_port        = 80
    to_port          = 80
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
  }
  ingress {
    description      = "Allow inbound https traffic"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
  }

  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
  }
  tags = {
    Name = "webserver_sg"
  }
}

This approach is straightforward but doesn’t scale well if you need to manage many ingress rules or if the rules change frequently.

Dynamic Approach

Using dynamic blocks, you can define the ingress rules dynamically based on input data. First, define a variable to hold the ingress rules:

variable "ingress_rules" {
    type = list(object({
        description = string
        from_port = number
        to_port = number
        protocol = string
        cidr_blocks = list(string)
    }))
}

Next we create a file named terraform.tfvars which will contain the values for the ingress_rules variable:

ingress_rules = [
  {
    description = "Allow inbound ssh traffic"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  },
  {
    description = "Allow inbound http traffic"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  },
  {
    description = "Allow inbound https traffic"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
]

Next, use the dynamic block to generate the ingress rules based on the input data:

resource "aws_security_group" "webserver" {
  name        = "sg for webserver"
  description = "Allow inbound traffic"
  vpc_id      = aws_vpc.testvpc.id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      description = ingress.value.description
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "allow_ssh_http_https"
  }
}

In this example:

var.ingress_rules is the collection over which the dynamic block will iterate. Each item in var.ingress_rules will be used to generate a separate ingress block.

  • The dynamic “ingress” block iterates over the var.ingress_rules list.
  • For each item in the list, it generates an ingress block with the specified attributes.

Nested Dynamic Blocks in Terraform

Nested dynamic blocks in Terraform allow you to dynamically generate nested configurations within another dynamic block. This is particularly useful for resources that require multiple layers of nested configurations, such as AWS Load Balancers with listeners and their associated actions. Key Concepts of Nested Dynamic Blocks

  1. Outer Dynamic Block: This block iterates over a collection to create a set of nested blocks.
  2. Inner Dynamic Block: This block is nested within the outer dynamic block and iterates over another collection to create additional nested blocks within each item generated by the outer block.

Let’s break down the nested dynamic blocks in the provided AWS Load Balancer example.

Example:

AWS Load Balancer with Nested Dynamic Blocks

Consider the following Terraform configuration that creates an AWS Application Load Balancer with multiple listeners and their associated default actions dynamically.

resource "aws_lb" "example" {
  name               = "example-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.lb_sg.id]
  subnets            = [aws_subnet.az1.id]

  # outer Dynamic block:
  dynamic "listener" {
    for_each = var.listeners
    content {
      port     = listener.value.port
      protocol = listener.value.protocol

      # inner Dynamic block:
      dynamic "default_action" {
        for_each = listener.value.default_actions
        content {
          type             = default_action.value.type
          target_group_arn = default_action.value.target_group_arn
        }
      }
    }
  }
}

Detailed Breakdown

1. Resource Definition

The aws_lb resource creates an AWS Application Load Balancer.

resource "aws_lb" "example" {
  name               = "example-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.lb_sg.id]
  subnets            = [aws_subnet.az1.id]
  • name: Specifies the name of the load balancer.
  • internal: Determines whether the load balancer is internal or internet-facing.
  • load_balancer_type: Sets the type of load balancer to application.
  • security_groups: Associates security groups with the load balancer.
  • subnets: Specifies the subnets in which the load balancer will be deployed.
2. Outer Dynamic Block (listener)

The outer dynamic block creates multiple listener blocks based on the var.listeners variable.

dynamic "listener" {
  for_each = var.listeners
  content {
    port     = listener.value.port
    protocol = listener.value.protocol
  • for_each = var.listeners: Iterates over the listeners variable, which should be a list of objects representing listener configurations.
  • content { … }: Defines the attributes of each listener, setting port and protocol dynamically based on the current item in var.listeners.
3. Inner Dynamic Block (default_action)

Within each dynamically generated listener block, the inner dynamic block creates multiple default_action blocks.

dynamic "default_action" {
  for_each = listener.value.default_actions
  content {
    type             = default_action.value.type
    target_group_arn = default_action.value.target_group_arn
  }
}
  • for_each = listener.value.default_actions: Iterates over the default_actions for each listener, which should be a list of objects representing default actions.
  • content { … }: Defines the attributes of each default action, setting type and target_group_arn dynamically based on the current item in default_actions.

Example Input Variables

To better understand how the dynamic blocks work, let’s define example input variables for listeners and default_actions.

Variable Definitions
variable "listeners" {
  type = list(object({
    port             = number
    protocol         = string
    default_actions  = list(object({
      type             = string
      target_group_arn = string
    }))
  }))
}

Terraform.tfvars which will contain the values for the listeners variable:

listeners = [
  {
    port     = 80
    protocol = "HTTP"
    default_actions = [
      {
        type             = "forward"
        target_group_arn = "arn:aws:elasticloadbalancing:eu-central-1:123123123123:targetgroup/target-group-1"
      }
    ]
  },
  {
    port     = 443
    protocol = "HTTPS"
    default_actions = [
      {
        type             = "forward"
        target_group_arn = "arn:aws:elasticloadbalancing:eu-central-1:123123123123:targetgroup/target-group-2"
      }
    ]
  }
]

Resulting Terraform Configuration

The nested dynamic blocks enable Terraform to generate the following configuration based on the provided input variables:

resource "aws_lb" "example" {
  name               = "example-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.lb_sg.id]
  subnets            = [aws_subnet.az1.id]

  listener {
    port     = 80
    protocol = "HTTP"

    default_action {
      type             = "forward"
      target_group_arn = "arn:aws:elasticloadbalancing:eu-central-1:123123123123:targetgroup/target-group-1"
    }
  }

  listener {
    port     = 443
    protocol = "HTTPS"

    default_action {
      type             = "forward"
      target_group_arn = "arn:aws:elasticloadbalancing:eu-central-1:123123123123:targetgroup/target-group-2"
    }
  }
}

Benefits of Dynamic Blocks
  1. Reduced Code Duplication: Write the block content once and generate it multiple times based on input data.
  2. Improved Maintainability: Easier to manage and update configurations as you only need to update the input data.
  3. Flexibility: Dynamically create blocks based on varying input, making your configurations more adaptable to changes.
Use Cases for Dynamic Blocks
  • Security Group Rules: Create multiple security group rules dynamically based on a list of allowed ports.
  • Tags: Dynamically generate tags for resources based on input variables or data sources.
  • Network Interfaces: Configure multiple network interfaces dynamically for instances or other resources.
  • Auto-Scaling Policies: Define multiple auto-scaling policies dynamically based on varying input criteria.
  • IAM roles: Dynamic blocks can be used to create multiple inline_policy or assume_role_policy blocks.
  • Load Balancers: Dynamic blocks can be used to define multiple listener or listener_rule blocks.

Conclusion

Dynamic blocks in Terraform offer a powerful way to streamline and enhance your infrastructure configurations. By leveraging dynamic blocks, you can write cleaner, more maintainable, and flexible Terraform code. Whether you’re managing security group rules, tags, or any other repeatable nested blocks, dynamic blocks can help you automate and scale your infrastructure as code.

Try integrating dynamic blocks into your Terraform projects and experience the benefits of a more efficient and modular configuration. Happy coding!