lesson ── terraform-intermediate ── ~15 мин ── 4 шагов
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 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
We will reuse the same audited-bucket module we wrote in the first
lesson. Copy it here:
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.
Create main.tf in /home/student/tf-foreach/:
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).{ 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.
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:
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.
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.
This is the main reason for_each beats count for modules.
Change the default in var.log_buckets, drop debug:
default = { info = { versioning = false } error = { versioning = true }}
Run plan:
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):
terraform apply -auto-approve
✓ After removing debug: info and error stayed untouched. That is key stability.
Two different techniques:
for_each over a module (this lesson):
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:
# 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:
When in doubt, for_each over a module (that is the right call more often).
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 instanceterraform plan -target='module.X["key1"]'a targeted plan for a single instanceконцепции