Terraform Dynamic

Terraform Dynamic Block

Building a complex environment using Terraform requires us to be aware of many features that support us in making our infrastructure as code more dynamic. Thinking about that, we will learn how to design a configuration that contains repeatable blocks of a specific setting.

If you have already used Terraform Count before, you know that there are some limitations. So, let’s jump to our topic.

What is Terraform Dynamic?

As we learned before, the Terraform Dynamic could be compared with a FOR expression and the Count keyword.

Dynamic blocks are a less used block from all Terraform expressions, and many people aren’t aware that they exist, but they are powerful. They were created to help us with some problems with creating dynamic nested configuration blocks in Terraform

Where use Terraform Dynamic?

Dynamic blocks can only be employed within other blocks and only when repeatable configuration blocks are supported (surprisingly, they are not that common). However, Terraform Dynamic blocks are situationally helpful, such as when we are creating tags or rules for a Security Group. 

So, the Dynamic block helps us in scenarios where we need to repeat a nested block given a List, Set, or Map.

Terraform Dynamic Syntax

Let’s see the Syntax that we should use with Dynamic Block:

dynamic "my_setting" {
	for_each = VARIABLE_NAME  # set | map | list
	content = {
		key = my_setting.value
	}
}

The my_setting is the name for the variable that will contain the value of each “iteration”. 

The VARIABLE_NAME could be a Set, Map, or List to iterate over.

Inside the content, the block is what will be generated from each iteration. From the content block, you can utilize my_setting.key and my_setting.value to retrieve the key and value of the current item from the iteration in the VARIABLE_NAME

The difference between List and Map

Remember, when we use for_each with a list, the key will be the index, and the value will be the item in the list. However, if we use the map with for_each, it’s different: the key and value will be one of the key-value pairs in the map.

So using Terraform Dynamic from the Syntax above, we can generate inline blocks. Let’s analyze some real examples and how to apply them.

Terraform Dynamic Block Example

For example, we need to configure some tags for an EC2 instance. We would like to make it dynamic according to our VARIABLE_NAME.

data "aws_ami" "os" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

resource "aws_instance" "server" {
  ami           = data.aws_ami.os.id
  instance_type = "t3.large"

  tags = {
    Project = "Hello BitsLovers!"
  }

  dynamic "tag" {
    for_each = var.append_custom_tags

    content {
      key                 = tag.key
      value               = tag.value
    }
  }
}

So, if we run the Terraform Plan, we will be able to see the output below:

Terraform will perform the following actions:

 # aws_instance.server will be updated in-place
  ~ resource "aws_instance" "server" {
        tag {
            key                 = "Project"
            value               = "Hello BitsLovers!"
        }

      + tag {
          + key                 = "Environment"
          + value               = "Production"
        }

      + tag {
          + key                 = "Terraform"
          + value               = "true"
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

 If the append_custom_tags is empty in our example above, Terraform will add only one tag, “Project = Hello BitsLovers!”.

Combine for_each with the “for” expression

Inside the Terraform Dynamic Block, we can combine for_each with for.

dynamic "tag" {
    for_each = {
      for key, value in var.append_custom_tags:
      key => lower(value)
      if key != "Project"
    }

    content {
      key                 = tag.key
      value               = tag.value
    }
  }

The nested for expression loops over append_custom_tags transforms each value to lowercase and, at the same time, evaluates a conditional in the for expression to skip the key equal Project. But you can implement any logic here to meet your requirements.

Let’s another example where we need to create a kubernetes_deployment and give all environment variables (var.env_vars) using a single variable (list).

resource "kubernetes_deployment" "bits_server" {
  metadata {
    name = "Terraform Tutorial - How to use Terraform Dynamic"
    labels = {
      test = "BitsLoversApp"
    }
  }

  spec {
    replicas = 2

    selector {
      match_labels = {
        test = "BitsLoversApp"
      }
    }

    template {
      metadata {
        labels = {
          test = "BitsLoversApp"
        }
      }

      spec {
        container {
          image = "nginx:1.21.6"
          name  = "example"
          

          dynamic "env" {
            for_each = var.env_vars
            content {
              name  = env.key
              value = env.value
            }
          }

          resources {
            limits = {
              cpu    = "0.5"
              memory = "512Mi"
            }
            requests = {
              cpu    = "250m"
              memory = "50Mi"
            }
          }

          liveness_probe {
            http_get {
              path = "/"
              port = 443

              http_header {
                name  = "X-Custom-Header"
                value = "Awesome Example!"
              }
            }
          }
        }
      }
    }
  }
}

Classical Example: Security Group

Let’s see how we define a Security without any FOR:

resource "aws_security_group" "demo_sg" {
  name        = "sample-sg"

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 21
    to_port     = 21
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

It looks like we have too many lines, right? However, the example above turns simple once we know how to implement the same code using Dynamic block.

variable "sg_ports" {
  type        = list(number)
  description = "list of ingress ports"
  default     = [8080, 80,21, 22, 443]
}

resource "aws_security_group" "dynamicsg" {
  name        = "dynamic-sg"
  description = "Ingress for Vault"

  dynamic "ingress" {
    for_each = var.sg_ports
    iterator = port
    content {
      from_port   = port.value
      to_port     = port.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }

  dynamic "egress" {
    for_each = var.sg_ports
    content {
      from_port   = egress.value
      to_port     = egress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

Looks better now! 

When not using Dynamic blocks

The Dynamic blocks help us to resolve a lot of issues. However, do not overuse them because they make our code more challenging to understand.

Also, a dynamic block can solely create arguments that belong to the identical resource type, data source, provider, or provisioner being defined. Meta-argument blocks, such as lifecycle and provisioner blocks, cannot be created because Terraform needs to process them before it is ready to evaluate expressions.

Leave a Comment

Your email address will not be published. Required fields are marked *

Free PDF with a useful Mind Map that illustrates everything you should know about AWS VPC in a single view.