# DAG в Terraform, как строится граф зависимостей _Advanced · TerraformLab Knowledge Base_ **TL;DR:** Terraform строит DAG (directed acyclic graph) из ресурсов и их зависимостей. На plan/apply граф «обходится» в топологическом порядке, параллельно где можно (limit, `-parallelism`, дефолт 10). Implicit dependencies, через interpolation; explicit, `depends_on`. Цикл = `Cycle: ...` ошибка. Понимание DAG объясняет почему apply иногда «застревает» и как ускорить большой граф. ## Что такое DAG в Terraform DAG = Directed Acyclic Graph. Вершины, ресурсы, провайдеры, модули, переменные, outputs. Рёбра, зависимости. Граф строится для каждой операции: init, plan, apply, destroy. Идея проста: чтобы создать ресурс B, который ссылается на ресурс A, сначала нужен A. Граф фиксирует это в виде стрелок. ## Источники зависимостей ### 1. Implicit (через interpolation) ```hcl resource "aws_iam_role" "lambda" { ... } resource "aws_lambda_function" "demo" { role = aws_iam_role.lambda.arn # ← implicit dep } ``` Terraform парсит выражения, видит ссылку, добавляет ребро: `aws_iam_role.lambda → aws_lambda_function.demo`. ### 2. Explicit (`depends_on`) ```hcl resource "aws_lambda_function" "demo" { depends_on = [aws_iam_role_policy_attachment.lambda] # ... } ``` Используется когда дeps есть, но через значение не выражены например, IAM-policy создаётся параллельно, но Lambda должна стартовать только после неё. См. [tf-depends-on](/terraform/kb/tf-depends-on.md). ### 3. Provider ```hcl provider "aws" { ... } resource "aws_s3_bucket" "x" { provider = aws.eu # ← dep на конкретный alias } ``` Каждый ресурс зависит от своего провайдера. Это implicit. ### 4. Модули ```hcl module "network" { ... } module "app" { vpc_id = module.network.vpc_id # ← dep на network } ``` Ресурсы внутри `module.app` гипотетически могут стартовать когда хотят, но Terraform учитывает explicit dep между модулями через interpolation outputs. ## terraform graph, посмотреть DAG ```bash terraform graph | dot -Tpng > graph.png ``` `dot`, Graphviz; в Alpine ставится через `apk add graphviz`. Альтернатива, `rover` или `tf-summarize`, см. [tf-rover-visualization](/terraform/kb/tf-rover-visualization.md). ## Walk-порядок Терраформ обходит граф в топологическом порядке. Ресурсы без зависимостей идут первыми, потом всё что от них зависит. Параллельно где возможно. Пример: ``` aws_iam_role.lambda ├── aws_iam_role_policy_attachment.lambda └── aws_lambda_function.demo └── aws_lambda_alias.live random_password.api_key └── aws_lambda_function.demo (через env) aws_s3_bucket.logs (нет deps на lambda) ``` Walk-порядок (упрощённо): 1. **Layer 1** (параллельно): `aws_iam_role.lambda`, `aws_s3_bucket.logs`, `random_password.api_key`. 2. **Layer 2** (после Layer 1): `aws_iam_role_policy_attachment.lambda`, `aws_lambda_function.demo`. 3. **Layer 3**: `aws_lambda_alias.live`. ## Parallelism По умолчанию Terraform делает до 10 операций параллельно. Каждый AWS-API вызов идёт в своём goroutine. ```bash terraform apply -parallelism=20 # больше параллелизма terraform apply -parallelism=1 # последовательно (debug) ``` Больше, быстрее, но рискуешь поймать AWS API throttling (`ThrottlingException`). Меньше, медленнее, но безопаснее для API с rate-limit'ами. Optimal зависит от провайдера и масштаба state'а. 10, норм по умолчанию. Для огромных стеков иногда 20-30; для AWS Organizations manipulation, 1-2. ## Циклы Если два ресурса ссылаются друг на друга, Terraform не может выбрать порядок. Пример (плохо): ```hcl resource "aws_security_group" "web" { ingress { from_port = 80 to_port = 80 protocol = "tcp" security_groups = [aws_security_group.alb.id] # ← dep на alb } } resource "aws_security_group" "alb" { ingress { from_port = 443 to_port = 443 protocol = "tcp" security_groups = [aws_security_group.web.id] # ← dep на web } } ``` `Error: Cycle: aws_security_group.web, aws_security_group.alb`. Решение, `aws_security_group_rule` resource'ы отдельно: ```hcl resource "aws_security_group" "web" {} resource "aws_security_group" "alb" {} resource "aws_security_group_rule" "web_from_alb" { type = "ingress" security_group_id = aws_security_group.web.id source_security_group_id = aws_security_group.alb.id # ... } resource "aws_security_group_rule" "alb_from_web" { type = "ingress" security_group_id = aws_security_group.alb.id source_security_group_id = aws_security_group.web.id # ... } ``` Теперь два `security_group`'а независимы, rules, отдельные ресурсы без cycle'а. Debug cycle'а: ```bash terraform graph -draw-cycles | dot -Tsvg > cycle.svg ``` Циклы подсвечены красным. См. [tf-graph](/terraform/kb/tf-graph.md). ## Walk-state vs plan vs apply | Операция | Что в DAG | |---|---| | `init` | Загрузка провайдеров и модулей. DAG строится частично. | | `validate` | Полный DAG, но без API-вызовов. | | `plan` | Полный DAG + refresh-фаза для каждого ресурса. Считает diff. | | `apply` | Walk DAG, для каждой ноды, provider call. Parallel где можно. | | `destroy` | Reverse DAG, топологический порядок наоборот. | Destroy сложнее: нужно удалить child'ов до parent'а. Reverse-walk. ## Какие зависимости НЕ ловятся автоматически - **Side effects через external script.** `data "external"` исполняет скрипт, но Terraform не знает что внутри. Если скрипт читает из S3-бакета, который только что создан тем же plan'ом может race-condition'ить. - **Ресурсы, создаваемые через provisioners.** `local-exec` / `remote-exec` запускаются после ресурса, но если они что-то создают в облаке (через CLI), Terraform этого не видит. - **Через provider-default'ы.** `default_tags { ... }`, теги применяются ко всем ресурсам, но в DAG нет ребра «всё зависит от default_tags». - **terraform_remote_state.** Зависимость на другой stack, это data source, читается на каждом plan. Граф этого не моделирует cross-stack. ## Подводные камни - **Цикл сложно вычислить визуально.** На большом графе circular-deps через 5 ресурсов руками не найдёшь. `terraform graph -draw-cycles` , единственный sane debug. - **`depends_on` создаёт «жёсткое» ребро.** Использовать только когда implicit dep нет. Лишние `depends_on` замедляют apply (parallelism падает). - **Module-level depends_on (TF 1.0+).** Можно поставить на весь модуль , но тогда **все** ресурсы модуля зависят от него. Часто overkill. - **count/for_each + dynamic, DAG строится по элементам.** Каждый `aws_s3_bucket.x[0]`, `[1]`, `[2]`, своя нода в графе. - **Большой граф = долгий plan.** Refresh каждой ноды, API call. 5000 ресурсов = 5000 GET-вызовов. См. [tf-large-scale-state](/terraform/kb/tf-large-scale-state.md) про разбиение. - **Parallelism vs API throttling.** На larger stacks `-parallelism=20` может ускорить, но AWS отвечает 429. Эмпирически подбирай. ## Команды ```bash terraform graph | dot -Tpng > graph.png ``` DAG в визуальном виде. Требует graphviz (dot). ```bash terraform graph -draw-cycles | dot -Tpng > cycles.png ``` Циклы подсвечены красным. Главный debug-инструмент. ```bash terraform apply -parallelism=20 ``` Параллелизм 20 (default 10). Быстрее, риск throttling. ```bash terraform apply -parallelism=1 ``` Последовательно, для debug, чтобы видеть точный порядок. ## См. также - [Зависимости ресурсов: явные и неявные](/terraform/kb/tf-depends-on.md) - [terraform graph: граф зависимостей ресурсов](/terraform/kb/tf-graph.md) - [Большой state, иерархия, blast-radius, naming](/terraform/kb/tf-large-scale-state.md)