Граф зависимостей и порядок выполнения

Под всем Terraform лежит одна структура — направленный ациклический граф (DAG). Каждый ресурс это узел, каждая зависимость — ребро. Любая операция (plan, apply, destroy) — это обход графа.

Terraform не выполняет ваш код последовательно. Он превращает его в граф и обходит его параллельно везде, где это безопасно. Понимание графа — это понимание Terraform.

Зависимости бывают двух видов. Неявные Terraform находит сам — из ссылок: если B использует A.id, значит B зависит от A. Это основной и предпочтительный способ. Явные задаются вручную через depends_on — когда связь есть, но не выражается ссылкой (например, IAM-политика должна примениться до запуска приложения).

Зачем граф ацикличен

Граф называется ациклическим (DAG), потому что в нём не может быть циклов: если A зависит от B, а B от A, непонятно, что создавать первым. Terraform такое отвергает с ошибкой Cycle. Узлы без связи между собой обрабатываются параллельно — по умолчанию до 10 одновременно (флаг -parallelism).

                aws_vpc.main
                /          \
               v            v
      aws_subnet.a     aws_subnet.b   <- параллельно!
           |                |
           v                v
      instance.web     instance.api   <- тоже параллельно
               \          /
                v        v
             aws_lb.main (depends_on обоих)

Здесь две ветки (subnet.a и subnet.b) независимы — Terraform создаёт их одновременно. А балансировщик ждёт оба инстанса.

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

Перед обходом Terraform обязан проверить, что цикла нет. Классический способ — попытаться сделать топологическую сортировку; если не получилось обойти все узлы, значит есть цикл. Смоделируем детектор циклов:

def has_cycle(graph):
    WHITE, GRAY, BLACK = 0, 1, 2
    color = {n: WHITE for n in graph}
    def dfs(node):
        color[node] = GRAY            # в процессе обхода
        for nxt in graph[node]:
            if color[nxt] == GRAY:    # вернулись в текущий путь
                return True
            if color[nxt] == WHITE and dfs(nxt):
                return True
        color[node] = BLACK
        return False
    return any(color[n] == WHITE and dfs(n) for n in graph)

ok = {"vpc": [], "subnet": ["vpc"], "vm": ["subnet"]}
bad = {"a": ["b"], "b": ["c"], "c": ["a"]}   # цикл!

print("Здоровый граф, цикл есть?", has_cycle(ok))
print("Битый граф, цикл есть?  ", has_cycle(bad))

«Попробуй сам ▶» — серый цвет означает «узел в текущем пути обхода»; если мы снова на него наткнулись — это цикл. Terraform делает ровно такую проверку перед планом.

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

  • Злоупотребление depends_on. Его добавляют «на всякий случай», ломая параллелизм. Используйте только когда неявной связи реально нет.
  • Случайные циклы через модули. Модуль A берёт выход B, B берёт выход A — Terraform падает с Cycle.
  • Ожидать строгой последовательности. Независимые ресурсы создаются параллельно; нельзя полагаться на «этот точно создастся раньше».

Best practices

  • Предпочитайте неявные зависимости (ссылки) явному depends_on — граф получается точнее.
  • Команда terraform graph | dot -Tsvg > graph.svg рисует граф визуально — полезно для отладки сложных проектов.
  • Если упёрлись в производительность на больших проектах — разбивайте на отдельные state, а не крутите -parallelism.

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

Граф нужен не только для создания, но и для корректного удаления. При destroy Terraform обходит тот же DAG, но в обратном топологическом порядке: сначала удаляются «листья» (то, что ни от чего не зависит), потом — узлы глубже. Если бы он удалял в прямом порядке, то попытался бы снести VPC раньше подсетей внутри неё, и облако вернуло бы ошибку «сеть не пуста». Обратный обход гарантирует, что зависимые ресурсы уходят первыми, освобождая родителей.

Команда terraform graph выводит граф в формате DOT, который можно отрисовать утилитой Graphviz в картинку. На больших проектах это незаменимый инструмент отладки: когда apply ведёт себя странно или ловится неожиданный цикл, визуализация графа сразу показывает, где проходит лишнее ребро. Стоит также помнить про параметр -parallelism (по умолчанию 10): он ограничивает число одновременно обрабатываемых узлов. Уменьшать его иногда приходится, когда облачный API упирается в rate limit и начинает возвращать ошибки при слишком агрессивной параллельной нагрузке.

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

Проверьте себя
1. Почему граф зависимостей должен быть ациклическим?
AДля красоты визуализации
BИначе непонятен порядок: при цикле A↔B неясно, что создавать первым
CЧтобы ускорить parallelism
DЭто требование лицензии
2. Когда оправдано использовать depends_on?
AВсегда, для надёжности
BКогда зависимость реальна, но не выражается ссылкой на атрибут
CЧтобы ускорить apply
DДля каждого ресурса