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-03-module-for-each

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

for_each over a module: N instances from one block

One module "logs" created one bucket. What if you need three buckets, for logs of different levels (debug/info/error), each with its own versioning? You can write three module blocks by copy-paste. Or you can write one module with for_each = var.buckets. The second way is better.

This lesson is about for_each over a module: one declaration, N instances, stable keys in state.

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

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

запустить sandbox →

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

Шаги

  1. 01

    Take the module from lesson 1

    We will reuse the same audited-bucket module we wrote in the first lesson. Copy it here:

    bash
    cd /home/student/tf-foreach
    mkdir -p modules/audited-bucket
    cat > modules/audited-bucket/variables.tf <<'EOF'
    variable "name" {
      type = string
    }
    variable "versioning_enabled" {
      type    = bool
      default = false
    }
    variable "tags" {
      type    = map(string)
      default = {}
    }
    EOF
    cat > modules/audited-bucket/main.tf <<'EOF'
    resource "aws_s3_bucket" "this" {
      bucket = var.name
      tags   = merge({ Module = "audited-bucket" }, var.tags)
    }
    resource "aws_s3_bucket_versioning" "this" {
      bucket = aws_s3_bucket.this.id
      versioning_configuration {
        status = var.versioning_enabled ? "Enabled" : "Suspended"
      }
    }
    EOF
    cat > modules/audited-bucket/outputs.tf <<'EOF'
    output "arn" {
      value = aws_s3_bucket.this.arn
    }
    EOF

    You can copy-paste in blocks. What matters is the structure and the contract.

    ✓ The module is in place. Now: the call with for_each.

  2. 02

    Describe a map of parameters for each bucket

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

    hcl
    resource "random_id" "suffix" {
      byte_length = 4
    }
    variable "log_buckets" {
      type = map(object({
        versioning = bool
      }))
      default = {
        debug = { versioning = false }
        info  = { versioning = false }
        error = { versioning = true }
      }
    }
    module "log" {
      source   = "./modules/audited-bucket"
      for_each = var.log_buckets
      name               = "linuxlab-logs-${each.key}-${random_id.suffix.hex}"
      versioning_enabled = each.value.versioning
      tags = {
        Owner = "student"
        Level = each.key
      }
    }
    output "log_arns" {
      value = { for k, m in module.log : k => m.arn }
    }

    Key points:

    • for_each = var.log_buckets is a map, not a set. The keys are your names (debug/info/error), the values are parameters.
    • each.key is the current key ("debug"), each.value is the current value (an object with versioning).
    • In the output { for k, m in module.log : k => m.arn } is a map comprehension: it builds {debug: <arn>, info: <arn>, error: <arn>}.

    ✓ The declaration is written. Now init and apply.

  3. 03

    init + apply: this should create 3 instances

    bash
    cd /home/student/tf-foreach
    terraform init
    terraform apply -auto-approve

    The plan will show "Plan: 7 to add", three buckets, three versionings, one random_id.

    Apply should succeed. After that, look at the state:

    bash
    terraform state list

    You should see something like this:

    module.log["debug"].aws_s3_bucket.this
    module.log["debug"].aws_s3_bucket_versioning.this
    module.log["error"].aws_s3_bucket.this
    module.log["error"].aws_s3_bucket_versioning.this
    module.log["info"].aws_s3_bucket.this
    module.log["info"].aws_s3_bucket_versioning.this
    random_id.suffix

    The key is your name ("debug", "info", "error"). Not an index. This is the key difference from count.

    ✓ Three buckets created. One HCL block, three instances.

    The same thing on OpenTofu

    OpenTofu keeps the CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On a first switch, though, make a backup of the state and do a run on a feature branch, the differences cluster in the newer features (variables in backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  4. 04

    Check key stability: remove one level

    This is the main reason for_each beats count for modules.

    Change the default in var.log_buckets, drop debug:

    hcl
    default = {
      info  = { versioning = false }
      error = { versioning = true }
    }

    Run plan:

    bash
    terraform plan

    The plan should show only the removal of module.log["debug"]:

    # module.log["debug"].aws_s3_bucket.this will be destroyed
    # module.log["debug"].aws_s3_bucket_versioning.this will be destroyed

    info and error are left alone. Their addresses do not depend on whether debug exists, they are tied to keys, not to indexes.

    With count this would be a destroy on all of them, a recreate on two, a pile of work for nothing. See tf-count-for-each on count vs for_each.

    Apply (optional, you can bring debug back with git checkout):

    bash
    terraform apply -auto-approve

    ✓ After removing debug: info and error stayed untouched. That is key stability.

    for_each over a module vs inside a module

    Two different techniques:

    for_each over a module (this lesson):

    hcl
    module "log" {
      for_each = var.log_buckets
      source   = "./modules/audited-bucket"
      # ...
    }

    N module instances. Each one is its own "capsule". Good when every instance is isolated.

    for_each inside a module:

    hcl
    # modules/audited-bucket/main.tf
    resource "aws_s3_bucket" "this" {
      for_each = var.bucket_configs   # map passed into the module
      # ...
    }

    One module manages N buckets. This fits when a "cluster" of buckets is part of a single contract (for example, a campaign tracker always has 3 buckets).

    When to use which:

    • Independent things go to for_each over a module.
    • Tightly coupled group goes to for_each inside a module.

    When in doubt, for_each over a module (that is the right call more often).

    • → Module composition
    • → count vs for_each

Что ты узнал

for_each over a module block (TF 0.13+) takes a map or a set. Each key becomes a separate module instance. The address in state is module.X["key"]. Its main advantage over count is stable keys when you delete something.

команды

  • terraform state listyou see module.X["key"] for each instance
  • terraform plan -target='module.X["key1"]'a targeted plan for a single instance

концепции

  • · for_each over a map gives stable keys; deleting one does not recreate the rest
  • · Inside the module there is no access to each.key: it stays in root
  • · All instances use the same source: you cannot have one from git and another local

← предыдущий

Troubleshooting Garden: import points at nothing

следующий →

Native tests, .tftest.hcl and assert

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