Skip to main content

Command Palette

Search for a command to run...

Day 16: Dynamic Blocks for Flexible Resources

Updated
13 min read
Day 16: Dynamic Blocks for Flexible Resources

Welcome to Day 16! Today we’ll master dynamic blocks - a powerful feature that allows you to dynamically generate nested configuration blocks. This makes your resources incredibly flexible and reduces repetition.

🎯 Today’s Goals

  • Understand dynamic blocks and their purpose

  • Create dynamic security group rules

  • Use for_each in dynamic blocks

  • Handle complex nested blocks

  • Build flexible, data-driven resources

🧩 What is a Dynamic Block in Terraform?

A dynamic block allows you to generate nested blocks dynamically inside a resource — especially useful when the number of nested blocks changes based on variables or lists.

Normally, in Terraform, you would define multiple ingress or egress rules like this:

resource "aws_security_group" "example" {
  name        = "example-sg"
  description = "Example security group"

  ingress {
    from_port   = 22
    to_port     = 22
    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"]
  }
}

But what if your ingress rules are dynamic, and you want to loop over them instead of writing each one manually?

That’s where dynamic blocks come in.


🚀 Syntax of a Dynamic Block

A dynamic block has this format:

dynamic "<block_label>" {
  for_each = <collection>
  content {
    # configuration for each generated block
  }
}

Explanation:

  • block_label → the nested block name (e.g., ingress, egress)

  • for_each → the list or map to iterate over

  • content → defines the inside of each block


🛠 Example: AWS Security Group with Dynamic Ingress Rules

Let’s make it practical. 👇

variables.tf

variable "ingress_rules" {
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]
}

main.tf

provider "aws" {
  region = "ap-southeast-1"
}

resource "aws_security_group" "web_sg" {
  name        = "web-sg"
  description = "Security group with dynamic ingress rules"
  vpc_id      = "vpc-1234567890abcdef0" # Replace with your VPC ID

  # Dynamic ingress rule generation
  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      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 = "web-sg"
  }
}

🧠 What’s Happening Here?

Terraform will loop through each item in var.ingress_rules and create one ingress block for each.

So the final plan will be equivalent to manually writing:

ingress {
  from_port   = 22
  to_port     = 22
  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   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

🧩 Optional — Using Maps Instead of Lists

You can also use a map, which gives you access to both the key and value.

provider "aws" {
  region = "ap-southeast-1"
}

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

  default = {
    ssh = {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
    http = {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

resource "aws_security_group" "web_sg_map" {
  name   = "web-sg-map"
  vpc_id = "vpc-1234567890abcdef0"

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

This version adds a description for each ingress rule based on the map key (e.g., "ssh", "http").


✅ Benefits of Using Dynamic Blocks

BenefitDescription
💡 ReusableYou can reuse the same code for many environments (dev, staging, prod)
🧱 CleanAvoid repeating similar nested blocks
🧮 Data-drivenEasily control rules from variables or JSON files
⚙️ FlexibleWorks great with for_each, locals, and even count

Let’s go deep into nested dynamic blocks — like how to dynamically generate tags, or even deeper structures inside a resource.


🧩 What Is a Nested Dynamic Block?

A nested dynamic block means a dynamic block inside another dynamic block or a nested configuration.

In simple words:

You use a dynamic block inside another block (static or dynamic) to generate multiple levels of configuration dynamically.


🧠 Why Do We Need Nested Dynamic Blocks?

Some Terraform resources (like aws_lb_listener, aws_ecs_task_definition, etc.) have deeply nested configurations — e.g.:

  • multiple rules

  • each rule has multiple cidr_blocks or tags

  • each tag or rule may differ depending on input

In those cases, you can nest dynamic blocks.


⚙️ Example 1: Dynamic + Nested Dynamic Block for ALB listener

Goal: Create an AWS Application Load Balancer (ALB) listener

  • with multiple actions (dynamic)

  • each action can have multiple target groups (nested dynamic).

You’ll see a real, supported nested dynamicdynamic pattern.

🗂️ variables.tf

variable "listener_actions" {
  description = "List of listener actions with nested target groups"
  type = list(object({
    type          = string
    order         = number
    target_groups = list(object({
      arn   = string
      weight = number
    }))
  }))

  # Example demo data
  default = [
    {
      type  = "forward"
      order = 1
      target_groups = [
        { arn = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app1-tg/aaaa", weight = 1 },
        { arn = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app2-tg/bbbb", weight = 2 }
      ]
    },
    {
      type  = "redirect"
      order = 2
      target_groups = []
    }
  ]
}

🗂️ main.tf

provider "aws" {
  region = "ap-southeast-1"
}

# Mock Load Balancer just for structure demo (you can skip or replace)
resource "aws_lb" "demo_lb" {
  name               = "demo-alb"
  internal           = false
  load_balancer_type = "application"
  subnets            = ["subnet-aaa111", "subnet-bbb222"] # replace with yours
}

resource "aws_lb_listener" "demo_listener" {
  load_balancer_arn = aws_lb.demo_lb.arn
  port              = 80
  protocol          = "HTTP"

  # ----------------------------------- #
  # Dynamic Block: default_action       #
  # ----------------------------------- #
  dynamic "default_action" {
    for_each = var.listener_actions
    content {
      type  = default_action.value.type
      order = default_action.value.order

      # ----------------------------------- #
      # Nested Dynamic Block: forward block #
      # ----------------------------------- #
      dynamic "forward" {
        for_each = (
          default_action.value.type == "forward" ?
          [default_action.value] : []
        )
        content {

          # ------------------------------------------------- #
          # Nested Dynamic Block inside forward: target_group #
          # ------------------------------------------------- #
          dynamic "target_group" {
            for_each = forward.value.target_groups
            content {
              arn    = target_group.value.arn
              weight = target_group.value.weight
            }
          }
        }
      }

      # Example: redirect block for second action
      dynamic "redirect" {
        for_each = (
          default_action.value.type == "redirect" ?
          [default_action.value] : []
        )
        content {
          port        = "443"
          protocol    = "HTTPS"
          status_code = "HTTP_301"
        }
      }
    }
  }
}

🗂️ outputs.tf

output "listener_actions_result" {
  value = var.listener_actions
}

Run & Test

terraform init
terraform validate
terraform plan

✅ Expected result:
Terraform will successfully validate and show dynamically generated nested blocks:

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

Changes to Outputs:
  + listener_actions_result = [
      + {
          + order         = 1
          + target_groups = [
              + {
                  + arn    = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app1-tg/aaaa"
                  + weight = 1
                },
              + {
                  + arn    = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app2-tg/bbbb"
                  + weight = 2
                },
            ]
          + type          = "forward"
        },
      + {
          + order         = 2
          + target_groups = []
          + type          = "redirect"
        },
    ]

🏗 Example 2: Simple Dynamic Tags for Any Resource

If your resource supports a tags block (not just a map), you can use:

dynamic "tags" {
  for_each = var.tags
  content {
    key   = tags.key
    value = tags.value
  }
}

But—most AWS resources already accept tags = {} maps directly, so you usually don’t need a dynamic block for tags.


✅ Key Takeaways

ConceptMeaning
Dynamic blockUsed to generate one or more nested blocks dynamically
Nested dynamic blockA dynamic block inside another dynamic or block
When to useWhen you have nested repeating structures (like network interfaces, rules, tags)
Not forSimple attributes (like cidr_blocks = []) — only for real sub-blocks
Common usageAWS launch_template, listener_rule, security_group_rule, etc.

🧪 Hands-On Lab: Dynamic Blocks Mastery

Let’s build flexible infrastructure using dynamic blocks!

Step 1: Create Project

mkdir terraform-dynamic-blocks-lab
cd terraform-dynamic-blocks-lab

Step 2: Create variables.tf

# variables.tf

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

variable "project_name" {
  type    = string
  default = "dynamic-demo"
}

variable "environment" {
  type    = string
  default = "dev"
}

variable "security_groups" {
  description = "Security group configurations"
  type = map(object({
    description = string
    ingress_rules = list(object({
      description = string
      from_port   = number
      to_port     = number
      protocol    = string
      cidr_blocks = list(string)
    }))
    egress_rules = list(object({
      description = string
      from_port   = number
      to_port     = number
      protocol    = string
      cidr_blocks = list(string)
    }))
  }))

  default = {
    web = {
      description = "Security group for web servers"
      ingress_rules = [
        {
          description = "HTTP from anywhere"
          from_port   = 80
          to_port     = 80
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
        },
        {
          description = "HTTPS from anywhere"
          from_port   = 443
          to_port     = 443
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
        }
      ]
      egress_rules = [
        {
          description = "All traffic"
          from_port   = 0
          to_port     = 0
          protocol    = "-1"
          cidr_blocks = ["0.0.0.0/0"]
        }
      ]
    }

    app = {
      description = "Security group for app servers"
      ingress_rules = [
        {
          description = "App port from VPC"
          from_port   = 8080
          to_port     = 8080
          protocol    = "tcp"
          cidr_blocks = ["10.0.0.0/16"]
        }
      ]
      egress_rules = [
        {
          description = "HTTPS to internet"
          from_port   = 443
          to_port     = 443
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
        },
        {
          description = "Database access"
          from_port   = 5432
          to_port     = 5432
          protocol    = "tcp"
          cidr_blocks = ["10.0.0.0/16"]
        }
      ]
    }

    db = {
      description = "Security group for databases"
      ingress_rules = [
        {
          description = "PostgreSQL from app tier"
          from_port   = 5432
          to_port     = 5432
          protocol    = "tcp"
          cidr_blocks = ["10.0.0.0/16"]
        }
      ]
      egress_rules = []
    }
  }
}

variable "instances" {
  description = "Instance configurations with dynamic volumes"
  type = map(object({
    instance_type = string
    ami_filter    = string
    volumes = list(object({
      device_name = string
      size        = number
      type        = string
    }))
    tags = map(string)
  }))

  default = {
    web = {
      instance_type = "t2.micro"
      ami_filter    = "amzn2-ami-hvm-*"
      volumes = [
        { device_name = "/dev/sdf", size = 30, type = "gp3" }
      ]
      tags = {
        Tier = "Web"
        Role = "Frontend"
      }
    }

    app = {
      instance_type = "t2.small"
      ami_filter    = "amzn2-ami-hvm-*"
      volumes = [
        { device_name = "/dev/sdf", size = 50, type = "gp3" },
        { device_name = "/dev/sdg", size = 100, type = "gp3" }
      ]
      tags = {
        Tier = "App"
        Role = "Backend"
      }
    }
  }
}

variable "s3_buckets" {
  description = "S3 bucket configurations with dynamic lifecycle rules"
  type = map(object({
    versioning = bool
    lifecycle_rules = list(object({
      id         = string
      enabled    = bool
      prefix     = string
      expiration_days = number
    }))
  }))

  default = {
    logs = {
      versioning = true
      lifecycle_rules = [
        {
          id              = "delete-old-logs"
          enabled         = true
          prefix          = "logs/"
          expiration_days = 90
        },
        {
          id              = "delete-temp"
          enabled         = true
          prefix          = "temp/"
          expiration_days = 7
        }
      ]
    }

    data = {
      versioning = true
      lifecycle_rules = [
        {
          id              = "archive-old-data"
          enabled         = true
          prefix          = "archive/"
          expiration_days = 365
        }
      ]
    }
  }
}

Step 3: Create main.tf

# main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# VPC
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# Subnets
resource "aws_subnet" "public" {
  count = 2

  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 1}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "${var.project_name}-public-${count.index + 1}"
  }
}

data "aws_availability_zones" "available" {
  state = "available"
}

# Security Groups with Dynamic Blocks
resource "aws_security_group" "groups" {
  for_each = var.security_groups

  name        = "${var.project_name}-${each.key}-sg"
  description = each.value.description
  vpc_id      = aws_vpc.main.id

  # Dynamic ingress rules
  dynamic "ingress" {
    for_each = each.value.ingress_rules
    iterator = rule

    content {
      description = rule.value.description
      from_port   = rule.value.from_port
      to_port     = rule.value.to_port
      protocol    = rule.value.protocol
      cidr_blocks = rule.value.cidr_blocks
    }
  }

  # Dynamic egress rules
  dynamic "egress" {
    for_each = each.value.egress_rules
    iterator = rule

    content {
      description = rule.value.description
      from_port   = rule.value.from_port
      to_port     = rule.value.to_port
      protocol    = rule.value.protocol
      cidr_blocks = rule.value.cidr_blocks
    }
  }

  tags = {
    Name = "${var.project_name}-${each.key}-sg"
    Tier = title(each.key)
  }
}

# AMI Data Source
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# EC2 Instances with Dynamic EBS Volumes
resource "aws_instance" "servers" {
  for_each = var.instances

  ami           = data.aws_ami.amazon_linux.id
  instance_type = each.value.instance_type
  subnet_id     = aws_subnet.public[0].id

  vpc_security_group_ids = [
    aws_security_group.groups[each.key].id
  ]

  # Dynamic EBS volumes
  dynamic "ebs_block_device" {
    for_each = each.value.volumes
    iterator = volume

    content {
      device_name = volume.value.device_name
      volume_size = volume.value.size
      volume_type = volume.value.type
      encrypted   = true
      tags = {
        Name = "${var.project_name}-${each.key}-volume-${volume.key}"
      }
    }
  }

  tags = merge(
    each.value.tags,
    {
      Name        = "${var.project_name}-${each.key}"
      Environment = var.environment
    }
  )
}

# S3 Buckets with Dynamic Lifecycle Rules
resource "random_id" "bucket" {
  for_each    = var.s3_buckets
  byte_length = 4
}

resource "aws_s3_bucket" "buckets" {
  for_each = var.s3_buckets

  bucket = "${var.project_name}-${each.key}-${random_id.bucket[each.key].hex}"

  tags = {
    Name = "${var.project_name}-${each.key}"
    Type = each.key
  }
}

resource "aws_s3_bucket_versioning" "buckets" {
  for_each = var.s3_buckets

  bucket = aws_s3_bucket.buckets[each.key].id

  versioning_configuration {
    status = each.value.versioning ? "Enabled" : "Disabled"
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "buckets" {
  for_each = var.s3_buckets

  bucket = aws_s3_bucket.buckets[each.key].id

  dynamic "rule" {
    for_each = each.value.lifecycle_rules
    iterator = lifecycle

    content {
      id     = lifecycle.value.id
      status = lifecycle.value.enabled ? "Enabled" : "Disabled"

      filter {
        prefix = lifecycle.value.prefix
      }

      expiration {
        days = lifecycle.value.expiration_days
      }
    }
  }
}

Step 4: Create outputs.tf

# outputs.tf

output "security_groups" {
  description = "Security group details"
  value = {
    for key, sg in aws_security_group.groups :
    key => {
      id               = sg.id
      name             = sg.name
      ingress_count    = length(var.security_groups[key].ingress_rules)
      egress_count     = length(var.security_groups[key].egress_rules)
    }
  }
}

output "instances" {
  description = "Instance details"
  value = {
    for key, instance in aws_instance.servers :
    key => {
      id          = instance.id
      private_ip  = instance.private_ip
      volume_count = length(var.instances[key].volumes)
      tags        = var.instances[key].tags
    }
  }
}

output "s3_buckets" {
  description = "S3 bucket details"
  value = {
    for key, bucket in aws_s3_bucket.buckets :
    key => {
      name                = bucket.id
      versioning          = var.s3_buckets[key].versioning
      lifecycle_rule_count = length(var.s3_buckets[key].lifecycle_rules)
    }
  }
}

output "dynamic_block_summary" {
  description = "Summary of resources created with dynamic blocks"
  value = {
    security_groups = {
      total           = length(aws_security_group.groups)
      total_ingress   = sum([for sg in var.security_groups : length(sg.ingress_rules)])
      total_egress    = sum([for sg in var.security_groups : length(sg.egress_rules)])
    }
    instances = {
      total         = length(aws_instance.servers)
      total_volumes = sum([for inst in var.instances : length(inst.volumes)])
    }
    s3_buckets = {
      total                 = length(aws_s3_bucket.buckets)
      total_lifecycle_rules = sum([for bucket in var.s3_buckets : length(bucket.lifecycle_rules)])
    }
  }
}

Step 5: Deploy and Test

# Initialize
terraform init
# Plan
terraform plan
# Apply
terraform apply -auto-approve
# View outputs
terraform output
# Check security group rules
terraform output security_groups
# Check instance volumes
terraform output instances
# Check S3 lifecycle rules
terraform output s3_buckets
# Clean up
terraform destroy -auto-approve

📝 Best Practices

DO:

  1. Use dynamic blocks for repeated nested blocks

  2. Keep dynamic blocks simple and readable

  3. Document complex dynamic structures

  4. Validate input data structure

DON’T:

  1. Don’t use dynamic blocks for single blocks

  2. Don’t nest too many levels of dynamic blocks

  3. Don’t create overly complex conditions in dynamic blocks

  4. Don’t use dynamic blocks when static blocks are clearer

📝 Summary

Today you learned:

  • ✅ Dynamic block syntax and structure

  • ✅ Creating dynamic security group rules

  • ✅ Nested dynamic blocks

  • ✅ Dynamic blocks with for_each

  • ✅ Real-world dynamic block patterns

🚀 Tomorrow’s Preview

Day 17: Introduction to Modules

Tomorrow we’ll:

  • Understand Terraform modules

  • Learn module structure

  • Use public modules

  • Pass data between modules

  • Organize code with modules


← Day 15: Built-in Functions | Day 17: Introduction to Modules →


Remember: Dynamic blocks eliminate repetition and make resources data-driven!

More from this blog

S

StackOps - Diary

33 posts

Welcome to the StackOps - Diary. We’re dedicated to empowering the tech community. We delve into cloud-native and microservices technologies, sharing knowledge to build modern, scalable solutions.