count и for_each: множественные ресурсы

Чтобы не копировать один блок десять раз, Terraform даёт два meta-аргумента: count создаёт N одинаковых копий по индексу, for_each — по ключам из набора. Выбор между ними влияет на стабильность state.

Правило простое: если ресурсы по-настоящему одинаковы и их различает только число — count. Если у каждого своя идентичность — for_each. Перепутаете — получите лишние пересоздания.

count принимает число и создаёт столько экземпляров, адресуя их по индексу: aws_instance.web[0], [1], [2]. Внутри блока доступен count.index. for_each принимает мапу или множество и создаёт по экземпляру на каждый ключ, адресуя их по ключу: aws_instance.web["api"]. Внутри доступны each.key и each.value.

Почему for_each стабильнее

# count: адресация по индексу
resource "aws_instance" "srv" {
  count         = 3
  instance_type = "t2.micro"
  tags = { Name = "srv-${count.index}" }
}

# for_each: адресация по ключу
resource "aws_instance" "srv" {
  for_each      = toset(["web", "api", "worker"])
  instance_type = "t2.micro"
  tags = { Name = each.key }
}

Опасность count: если из списка ["web","api","worker"] удалить средний элемент, индексы сдвинутся — worker с индекса 2 переедет на 1, и Terraform пересоздаст ресурсы, которые трогать не собирался. У for_each такого нет: ключ "worker" остаётся ключом независимо от соседей.

Как работает под капотом

Terraform хранит экземпляры в state по их адресу. Для count адрес — это позиция (индекс), для for_each — ключ. Смоделируем катастрофу сдвига индексов: удаляем «api» из середины и смотрим, что Terraform «думает» о каждой позиции.

before = ["web", "api", "worker"]
after  = ["web", "worker"]            # удалили api из середины

print("=== COUNT (по индексу) ===")
for i in range(max(len(before), len(after))):
    b = before[i] if i < len(before) else None
    a = after[i]  if i < len(after)  else None
    verdict = "ok" if b == a else f"ПЕРЕСОЗДАТЬ {b} -> {a}"
    print(f"  [{i}] было={b}, стало={a}  -> {verdict}")

print("\n=== FOR_EACH (по ключу) ===")
for key in set(before) | set(after):
    if key in before and key in after:
        print(f"  [{key}] остаётся (ok)")
    elif key in before:
        print(f"  [{key}] удаляется")
    else:
        print(f"  [{key}] создаётся")

«Попробуй сам ▶» — у count сдвиг индексов задевает worker, хотя его не трогали. У for_each удаляется ровно api, остальное цело.

Частые ошибки

  • count для разнородных ресурсов. Список, который меняется в середине, постоянно вызывает лишние пересоздания.
  • for_each по списку без toset. for_each требует множество или мапу; список нужно обернуть в toset(...).
  • Значения, неизвестные на этапе plan. Если ключи for_each зависят от ещё не созданных ресурсов, Terraform не сможет построить план.

Best practices

  • По умолчанию выбирайте for_each — стабильные адреса по ключу избавляют от случайных пересозданий.
  • count оставьте для двух случаев: фича-флаг (count = var.enabled ? 1 : 0) и реально N одинаковых копий.
  • Ключи for_each должны быть статичными строками, известными на этапе plan.

Разбор глубже

Есть тонкость, о которую спотыкаются почти все: значения, передаваемые в for_each, должны быть известны на этапе plan. Если ключи зависят от атрибутов ресурсов, которые ещё не созданы (например, ID, генерируемый облаком), Terraform не сможет построить план и выдаст ошибку вроде «Invalid for_each argument». Лечится это либо использованием статически известных ключей, либо разбиением на два apply, либо переходом на count там, где число известно. Понимание границы «известно на plan / известно только на apply» — один из ключевых навыков опытного инженера Terraform.

Отдельно стоит запомнить идиому условного ресурса. Конструкция count = var.create_lb ? 1 : 0 — это стандартный способ включать и выключать ресурс по флагу: при false создаётся ноль копий, то есть ресурса нет вовсе. Аналогично делают и с модулями. Но есть подвох: к такому ресурсу обращаются через индекс aws_lb.this[0], и если флаг выключен, обращение к индексу 0 упадёт. Поэтому ссылки на условные ресурсы оборачивают в проверки или используют функции вроде one(...), аккуратно обрабатывающие пустой список.

Итог: count адресует по индексу и страдает от сдвигов, for_each — по ключу и стабилен. Для уникальных ресурсов почти всегда нужен for_each. Дальше — функции и выражения, делающие код выразительнее.

Проверьте себя
1. Почему for_each стабильнее count при удалении элемента из середины?
Afor_each работает быстрее
Bfor_each адресует по ключу, а count по индексу — при сдвиге индексов count пересоздаёт лишнее
Ccount вообще не поддерживается
DРазницы в стабильности нет
2. В каком случае count предпочтительнее for_each?
AКогда ресурсы уникальны
BДля фича-флага count = var.enabled ? 1 : 0 или N действительно одинаковых копий
CВсегда
DКогда нужны строковые ключи