Skip to main content

Command Palette

Search for a command to run...

Terraform Dynamic Blocks

Updated
4 min read
Terraform Dynamic Blocks

Introduction

In this part, we’ll explore some advanced features of Terraform. You’ll learn about variable types, constructs like for_each, and how to use dynamic blocks to make your Terraform code cleaner and less verbose.


Introduction to Variables

Just like other programming languages, Terraform’s HCL (HashiCorp Configuration Language) supports variables and data types.

Modules and resource blocks often accept arbitrary user inputs. The best practice is to define variables in a terraform.tfvars file, which supplies values to Terraform during runtime.

Let’s start with a simple example.


Example Usage

We’ll create a Terraform project that provisions an S3 bucket. This project will include the following files:

.
├── backend.tf
├── main.tf
├── terraform.tfvars
└── variables.tf

main.tf

This file defines the S3 bucket and uses variables via the syntax var.variable_name.

provider "aws" {
  profile = var.profile
  region  = var.region
}

resource "aws_s3_bucket" "testbucket" {
  bucket = var.bucket_name
}

variables.tf

Here we declare the variables used in the project:

variable "region" {
  type    = string
  default = "us-east-1"
}

variable "profile" {
  type    = string
  default = "profile-name-to-use"
}

variable "bucket_name" {
  type        = string
  description = "Name of S3 bucket to create"
}

You can also mark variables as sensitive or add validation rules for stricter control.

terraform.tfvars

This file assigns values to the declared variables:

bucket_name = "test_bucket"

Terraform Commands

Here’s a typical Terraform workflow:

terraform init       # Initialize backend and providers
terraform fmt        # Format code
terraform validate   # Validate for syntax errors
terraform plan       # Preview changes
terraform apply      # Apply changes
terraform destroy    # Destroy resources

Types of Variables

Terraform supports two broad categories of variables:

1. Primitives:string, boolean, number

2. Complex:set, map, object, tuple, list, any

Here’s an example of a complex variable definition:

variable "igress_params" {
  type = map(object({
    port = number
    proto = string
    cidr  = list(string)
  }))

  default = {
    "rule1" = {
      port  = 22
      proto = "tcp"
      cidr  = ["0.0.0.0/0"]
    },
    "rule2" = {
      port  = 80
      proto = "tcp"
      cidr  = ["1.2.3.4/32"]
    }
  }
}

This map(object) variable defines ingress rules that you can later reference while creating security groups or VPCs.


Introduction to for_each

The for_each construct is useful when you want to create multiple resources of the same type with varying values. For example, creating three S3 buckets for dev, uat, and prod.

provider "aws" {
  profile = "aws-profile-to-use"
  region  = "us-east-1"
}

variable "s3_buckets" {
  type        = set(string)
  description = "Name of S3 buckets to create"
  default     = ["test-dev", "test-uat", "test-prod"]
}

resource "aws_s3_bucket" "testbucket" {
  for_each = var.s3_buckets
  bucket   = each.value
}

Terraform will iterate through s3_buckets and create three separate S3 buckets.


The dynamic Keyword

When defining resources like security groups, you might need multiple ingress or egress rules. Manually declaring them can get verbose and hard to maintain.

Without Dynamic Blocks

resource "aws_security_group" "my-sg" {
  vpc_id = aws_vpc.my-vpc.id

  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"]
  }
}

This approach quickly becomes messy for many rules.


With Dynamic Blocks

We can make this clean and reusable using the dynamic keyword.

variable "igress_params" {
  type = map(object({
    port  = number
    proto = string
    cidr  = list(string)
  }))

  default = {
    "rule1" = {
      port  = 22
      proto = "tcp"
      cidr  = ["0.0.0.0/0"]
    },
    "rule2" = {
      port  = 80
      proto = "tcp"
      cidr  = ["1.2.3.4/32"]
    }
  }
}

resource "aws_vpc" "my-vpc" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "my-sg" {
  vpc_id = aws_vpc.my-vpc.id

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

  dynamic "ingress" {
    for_each = var.igress_params
    content {
      from_port   = ingress.value["port"]
      to_port     = ingress.value["port"]
      protocol    = ingress.value["proto"]
      cidr_blocks = ingress.value["cidr"]
    }
  }
}

Terraform will iterate through each map entry in igress_params and dynamically create multiple ingress blocks — one for each rule.

Key benefit: Your Terraform files stay concise, and you can easily scale your configuration by just editing variable definitions.


In Summary

Dynamic blocks and constructs like for_each bring programmability and flexibility into Terraform — reducing code repetition while improving readability and scalability.

More from this blog

B

Beyond Backfills

12 posts

Beyond Backfills is a space to explore the art and craft of data engineering. The goal isn’t just moving bytes—it’s understanding the systems that shape them, and the human curiosity that follows.