lesson ── terraform-intermediate ── ~15 мин ── 4 шагов
Один module "logs" создал один бакет. Что если нужно три бакета, для
логов разных уровней (debug/info/error), у каждого свой versioning? Можно
написать три module блока с copy-paste. Можно один module с
for_each = var.buckets. Второе, лучше.
Этот урок про for_each поверх модуля: одна декларация, N экземпляров,
стабильные ключи в state.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
Используем тот же audited-bucket модуль, что писали в первом
уроке. Скопируем его сюда:
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
Можно копи-пастить блоками. Главное, структура и контракт.
✓ Модуль на месте. Теперь: вызов с for_each.
Создай main.tf в /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 }}
Главное:
for_each = var.log_buckets, это map, не set. Ключи, это
твои имена (debug/info/error), значения, параметры.each.key, текущий ключ ("debug"), each.value, текущее значение
(object с versioning).{ for k, m in module.log : k => m.arn }: map-comprehension:
собираем {debug: <arn>, info: <arn>, error: <arn>}.✓ Декларация написана. Теперь init и apply.
cd /home/student/tf-foreach
terraform init
terraform apply -auto-approve
Plan покажет «Plan: 7 to add», три бакета, три versioning'а, один random_id.
Apply должен пройти. После, посмотри state:
terraform state list
Должно быть похожее:
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
Ключ, это твоё имя ("debug", "info", "error"). Не индекс. Это
ключевое отличие от count.
✓ Три бакета созданы. Один HCL-блок, три инстанса.
OpenTofu держит CLI и state совместимыми с Terraform по командам
этого шага: миграция обычно проходит через mv .terraform .terraform.bak; tofu init -upgrade. Но при первом переходе
сделай backup state и прогон на feature-branch - расхождения
концентрируются в новых фичах (variables в backend,
state-encryption, OCI registry-backed модули). См.
tf-opentofu-parity для полной матрицы.
Это главная причина почему for_each лучше count для модулей.
Поменяй default в var.log_buckets, убери debug:
default = { info = { versioning = false } error = { versioning = true }}
Запусти plan:
terraform plan
Plan должен показать только удаление 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 и error не трогаются. Их адреса не зависят от наличия
debug, они привязаны к ключам, не к индексам.
С count это было бы destroy на всех, recreate на двух, массовая
работа на пустом месте. См. tf-count-for-each про count vs for_each.
Apply (необязательно, можно вернуть debug через git checkout):
terraform apply -auto-approve
✓ После удаления debug: info и error остались нетронутыми. Это стабильность ключей.
Две разные техники:
for_each над модулем (этот урок):
module "log" {for_each = var.log_buckets
source = "./modules/audited-bucket"
# ...
}
N экземпляров модуля. Каждый, своя «капсула». Хорошо когда каждый instance изолирован.
for_each внутри модуля:
# modules/audited-bucket/main.tf
resource "aws_s3_bucket" "this" {for_each = var.bucket_configs # map передан в модуль
# ...
}
Один модуль управляет N бакетами. Подходит когда «гроздь» бакетов, часть единого контракта (например, у tracker'а кампании всегда 3 бакета).
Когда что:
Если не уверен, for_each над модулем (так чаще правильно).
for_each поверх module блока (TF 0.13+) принимает map или set.
Каждый ключ → отдельный экземпляр модуля. Адрес в state, module.X["key"].
Главное преимущество над count, стабильность ключей при удалении.
команды
terraform state listвидишь module.X["key"] для каждого экземпляраterraform plan -target='module.X["key1"]'точечный plan одного экземпляраконцепции