Зачем нужны
Допустим, нужно создать пять S3-бакетов: logs-dev, logs-staging, logs-prod, data-dev, data-prod. Можно написать пять resource-блоков. Можно, один с count или for_each.
Это про повторяющиеся ресурсы. Если что-то отличается только индексом или ключом, выносим в count/for_each и не дублируем код.
count. Это самый простой способ
resource "aws_s3_bucket" "logs" {count = 3
bucket = "logs-bucket-${count.index}"}
Это создаст три бакета:
aws_s3_bucket.logs[0]с именемlogs-bucket-0aws_s3_bucket.logs[1]с именемlogs-bucket-1aws_s3_bucket.logs[2]с именемlogs-bucket-2
Внутри блока доступна переменная count.index, текущий номер от 0 до N-1.
Чтобы поменять количество, поменяйте число. Terraform увидит «было 3, надо 5» и создаст ещё два.
for_each, когда у каждого свой ключ
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "eu", "ap"])
bucket = "data-bucket-${each.key}"}
Создастся три бакета:
aws_s3_bucket.regional["us"]с именемdata-bucket-usaws_s3_bucket.regional["eu"]с именемdata-bucket-euaws_s3_bucket.regional["ap"]с именемdata-bucket-ap
Доступно: each.key (ключ) и each.value (значение): for_each принимает set или map:
# set, простые ключи, each.key == each.value
for_each = toset(["us", "eu", "ap"])
# map, пара ключ-значение, each.key и each.value разные
for_each = { us = { region = "us-east-1", tier = "primary" } eu = { region = "eu-central-1", tier = "secondary" }}
# then: each.key, "us"/"eu", each.value.region, each.value.tier
Главное различие: что происходит при изменении списка
Скажем, у вас три ресурса. Уберите средний.
С count:
count = 3 → count = 2
Terraform думает так: «было 3 элемента по индексам 0, 1, 2. Теперь надо 2». Удаляется элемент с индексом 2. Если вы хотели удалить элемент 1. Terraform этого не понимает; он просто сдвинет: индекс 2 пропадёт, элементы 0 и 1 останутся теми же.
Проблема: если убрать элемент из середины списка через count. Terraform пересоздаст всё, что после него. Потому что индексы сдвинутся.
С for_each:
for_each = toset(["us", "eu", "ap"]) → for_each = toset(["us", "ap"])
Terraform увидит «был ключ eu, его больше нет», удалит только aws_s3_bucket.regional["eu"]. Остальные не тронет.
Правило большого пальца: если список может меняться в середине, берите for_each. count, только для случаев когда количество растёт/убывает с конца или когда элементы реально взаимозаменяемы.
Когда что использовать
- count, три одинаковые виртуалки в auto-scaling-like подходе, отказоустойчивость через дублирование, ресурсы где порядок не важен.
- for_each, почти всегда остальное. Бакеты для разных окружений, IAM-роли для разных сервисов, sg-rules с разными портами.
Если не уверены, берите for_each. Он более явный и безопасный.
Ссылки и аутпуты
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "eu", "ap"])
bucket = "data-${each.key}"}
# одна конкретная
output "us_bucket_arn" {value = aws_s3_bucket.regional["us"].arn
}
# все ARN'ы списком
output "all_arns" {value = [for k, b in aws_s3_bucket.regional : b.arn]
}
# все ARN'ы по ключу
output "arns_by_region" { value = { for k, b in aws_s3_bucket.regional : k => b.arn }}
Подводные камни
-
count и for_each нельзя одновременно. Только один из них в одном ресурсе.
-
count = 0илиfor_each = [], валидно. Терраформ создаст ноль ресурсов. Это легальный способ условно отключить блок:count = var.create ? 1 : 0. -
for_each требует statically-knowable ключей. Ключи должны быть известны на этапе
plan. Нельзяfor_each = data.aws_some_thing.dynamicесли эти данные становятся известны только после apply другого ресурса. Лечится через зависимости или статические значения. -
При миграции с count на for_each, пересоздание. Терраформ считает это другим ресурсом. Адреса меняются (
x[0]→x["key"]). Лечится черезmoved {}блок (advanced). -
Map ключи всегда строки.
for_each = { 1 = "a", 2 = "b" }ругнётся: числа надо в строки конвертить:{ "1" = "a", "2" = "b" }. -
Большие for_each = медленный plan. Если у вас for_each по 500 элементам, plan будет дёргать API 500 раз. Думайте о масштабе.