linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/lessons/tf-intermediate-04-dynamic-blocks

lesson ── terraform-intermediate ── ~15 мин ── 4 шагов

dynamic blocks: repeated subblocks

Many AWS resources have subblocks that repeat: lifecycle rules in S3, ingress/egress in a security group, statement in an IAM policy. When there are two or three, you write them by hand. When the count depends on a variable, without a dynamic block you would have to duplicate resources.

In this lesson you build an S3 bucket with a variable number of lifecycle rules. One input, a list of objects, turns into N subblocks of a resource.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Describe the lifecycle rules in a variable

    Create main.tf in /home/student/tf-dynamic/:

    hcl
    resource "random_id" "suffix" {
      byte_length = 4
    }
    variable "lifecycle_rules" {
      type = list(object({
        id              = string
        enabled         = bool
        prefix          = string
        expiration_days = number
      }))
      default = [
        {
          id              = "archive-old-logs"
          enabled         = true
          prefix          = "logs/"
          expiration_days = 30
        },
        {
          id              = "remove-tmp"
          enabled         = true
          prefix          = "tmp/"
          expiration_days = 7
        },
      ]
    }

    The type is list(object(...)) with an explicit schema for each element. This lets Terraform check at the plan stage that every element has the required fields.

    ✓ The input structure is described. Now: a resource with a dynamic block.

  2. 02

    Use dynamic for the subblocks

    Add to main.tf:

    hcl
    resource "aws_s3_bucket" "demo" {
      bucket = "linuxlab-dynamic-${random_id.suffix.hex}"
    }
    resource "aws_s3_bucket_lifecycle_configuration" "demo" {
      bucket = aws_s3_bucket.demo.id
      dynamic "rule" {
        for_each = var.lifecycle_rules
        content {
          id     = rule.value.id
          status = rule.value.enabled ? "Enabled" : "Disabled"
          filter {
            prefix = rule.value.prefix
          }
          expiration {
            days = rule.value.expiration_days
          }
        }
      }
    }

    A breakdown:

    • dynamic "rule", the name of the subblock in the parent resource. Here aws_s3_bucket_lifecycle_configuration takes several rule { }.
    • for_each = var.lifecycle_rules, you iterate over the list.
    • Inside content { }, the body of one subblock.
    • rule.value, the current element. rule.value.id, rule.value.prefix.
    • rule.key exists too, but for a list it is the index (0, 1, 2); for a map, the key.

    ✓ The dynamic block is written. Init + apply.

  3. 03

    Apply: two rules in one bucket

    bash
    cd /home/student/tf-dynamic
    terraform init
    terraform plan

    In the plan you will see the two expanded rules inside aws_s3_bucket_lifecycle_configuration:

    + rule {
        + id     = "archive-old-logs"
        + status = "Enabled"
        + filter { prefix = "logs/" }
        + expiration { days = 30 }
      }
    + rule {
        + id     = "remove-tmp"
        + status = "Enabled"
        + filter { prefix = "tmp/" }
        + expiration { days = 7 }
      }

    dynamic expanded into two real subblocks. At the resource level the result is identical to writing two rule { } blocks by hand.

    bash
    terraform apply -auto-approve

    ✓ Lifecycle configuration created with two rules.

    The same thing on OpenTofu

    OpenTofu keeps its CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. But on the first switch, back up your state and run it on a feature branch, the differences concentrate in the newer features (variables in the backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  4. 04

    An empty list gives zero subblocks

    Change the default of the variable, make it empty:

    hcl
    variable "lifecycle_rules" {
      type = list(object({
        id              = string
        enabled         = bool
        prefix          = string
        expiration_days = number
      }))
      default = []
    }

    Run a plan:

    bash
    terraform plan

    This is a common pattern: dynamic with an empty list = "no rules". It is especially useful in modules: if the user of the module did not pass any rules, the resource is created without them, it does not fail.

    You can apply (apply) or restore the previous value in the editor, this is not critical right now.

    подсказка

    If you want it back the way it was: change default back to a list with two elements.

    ✓ An empty list: a valid state. dynamic survives the edge case.

    dynamic + for vs map in HCL

    Sometimes a dynamic block can be replaced outright with a for expression:

    hcl
    # the dynamic variant
    resource "aws_security_group" "web" {
      dynamic "ingress" {
        for_each = var.ports
        content {
          from_port = ingress.value
          to_port   = ingress.value
          protocol  = "tcp"
        }
      }
    }

    vs

    hcl
    # the for-inside-an-attribute variant (if the resource takes a list attribute)
    #, some resources can do this, for example aws_vpc_security_group_ingress_rule
    # takes a list directly.

    Not every AWS resource allows this. List attributes are available where there are attributes, not subblocks. If in HCL you write xxx { ... } without =, it is a subblock and you need dynamic.

    A rule of thumb:

    • A subblock with an arbitrary number of repetitions → dynamic.
    • An attribute of type list/map → an ordinary expression.

    If the resource complains "expected block, got expression", try dynamic. If "expected expression", a for / list literal.

    • → HCL data types
    • → count vs for_each (dynamic is there too)

Что ты узнал

dynamic "X" { for_each = ..., content { ... } } expands into N X blocks inside the parent resource. Use it when the number of subblocks depends on an input. Do not confuse it with for_each on the resource itself: dynamic works inside one resource, for_each creates N resources.

команды

  • terraform planyou see the expanded subblocks in the output
  • terraform consolecheck the structure of var before you plug it into dynamic

концепции

  • · A dynamic block expands at the plan stage: it is static once the variables are resolved
  • · An empty for_each (`[]`, `{}`) = zero subblocks: this is a valid state
  • · Inside content you have each.key/each.value

← предыдущий

Troubleshooting Garden: Checkov fails in the pipeline

следующий →

Mock providers, tests without the cloud

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies