Граф зависимостей и порядок выполнения
Под всем 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.