# Plan-as-artifact и automation mode в CI _CI/CD · TerraformLab Knowledge Base_ **TL;DR:** В CI plan-job создаёт `plan.tfplan`, грузит как артефакт. Apply-job скачивает артефакт и применяет ровно его, никакого нового plan на apply-стадии. Это гарантирует: то что показали reviewer'у в PR ровно то, что применилось. Все команды с `-input=false`, `-no-color`, `-lock-timeout` чтобы автоматизация не зависала. Plan-файл содержит sensitive, обращайся с ним как с секретом. ## Канонический pipeline Production-pipeline для Terraform выглядит так: ``` PR opened/updated: job-1 plan → выгружает plan.tfplan + plan.txt job-2 lint → fmt, validate, tflint, checkov job-3 policy → OPA/terraform-compliance над plan.json [reviewer читает plan.txt из PR-комментария или artifact] PR merged → main: job-4 plan-prod → plan.tfplan на prod [manual approval / required reviewers] job-5 apply → apply plan.tfplan ``` Ключевое, **apply применяет тот же plan**, что reviewer одобрил. Это достигается через сохранение plan-файла как артефакта между jobs. ## Зачем именно так Альтернатива (плохая): apply делает свой fresh plan, потом apply'ит. Между PR-plan и apply-time-plan может пройти час; drift, новый коммит, обновлённая переменная, и apply делает не то, что вы видели в ревью. Plan-файл фиксирует решение в момент времени. Apply его исполняет. Если drift произошёл, apply ловит `Saved plan is stale` и падает, заставляя сделать новый plan-cycle. ## Команды для CI ```bash # plan-job terraform init -input=false -no-color terraform plan -input=false -no-color -lock-timeout=2m -out=plan.tfplan terraform show -no-color plan.tfplan > plan.txt terraform show -json plan.tfplan > plan.json # apply-job (отдельный job, скачал plan.tfplan) terraform init -input=false -no-color terraform apply -input=false -no-color -lock-timeout=10m plan.tfplan ``` Что здесь важно: - `-input=false`, никаких интерактивных prompt'ов. По умолчанию TF может спрашивать переменные с stdin, в CI это hang. - `-no-color`, без ANSI-кодов в логах. Удобнее grep'ать. - `-lock-timeout`, апишник может уже держать lock; даём 2 мин шанса. На apply дольше, потому что apply сам держит lock и параллельный job может прийти. - `-out=plan.tfplan`, БИНАРНЫЙ plan. Нельзя редактировать руками. - `plan.txt` и `plan.json`, human-readable и machine-readable представления одного и того же. `TF_IN_AUTOMATION=true` (env-var), Terraform молчит про «next steps», убирает суггестии в CLI-выводе. Это для всего CI. ## GitHub Actions: plan-job ```yaml jobs: plan: runs-on: ubuntu-latest env: TF_IN_AUTOMATION: "true" TF_INPUT: "false" steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.9.8 # OIDC instead of access-keys; see tf-oidc-aws - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::ACCOUNT:role/tf-runner aws-region: us-east-1 - run: terraform init -no-color - run: terraform plan -no-color -lock-timeout=2m -out=plan.tfplan - run: terraform show -no-color plan.tfplan > plan.txt - run: terraform show -json plan.tfplan > plan.json - uses: actions/upload-artifact@v4 with: name: tf-plan-${{ github.run_id }} path: | plan.tfplan plan.txt plan.json retention-days: 5 ``` Артефакт включает все три формата: бинарный (для apply), txt (для ревьюера в PR-комментарии), json (для OPA/Checkov/compliance). ## Apply-job ```yaml apply: needs: plan runs-on: ubuntu-latest environment: prod # gating через GitHub environment env: TF_IN_AUTOMATION: "true" TF_INPUT: "false" steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.9.8 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::ACCOUNT:role/tf-runner aws-region: us-east-1 - uses: actions/download-artifact@v4 with: name: tf-plan-${{ github.run_id }} - run: terraform init -no-color - run: terraform apply -no-color -lock-timeout=10m plan.tfplan ``` `environment: prod` в GHA, это gate. Можно настроить required reviewers, ограничить branch, добавить wait-timer. Apply не стартует пока кто-то из approver'ов не одобрит. ## detailed-exitcode для PR-gate Иногда нужно знать «есть изменения или нет» без application: ```bash terraform plan -detailed-exitcode -out=plan.tfplan # exit 0, no changes # exit 1, error # exit 2, has changes ``` CI-логика: exit 0 = «No-op», pause pipeline; exit 2 = «есть план, apply'м после approve»; exit 1 = bug, не apply'ить. ```yaml - id: plan run: | set +e terraform plan -detailed-exitcode -out=plan.tfplan code=$? set -e echo "exitcode=$code" >> "$GITHUB_OUTPUT" test "$code" -ne 1 - if: steps.plan.outputs.exitcode == '0' run: echo "No changes, skipping apply" - if: steps.plan.outputs.exitcode == '2' uses: actions/upload-artifact@v4 with: name: tf-plan-${{ github.run_id }} path: plan.tfplan ``` ## PR-комментарий с плана Помогает ревьюеру: ```yaml - name: Post plan to PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const plan = fs.readFileSync('plan.txt', 'utf8'); const truncated = plan.length > 60000 ? plan.slice(0, 60000) + '\n... (truncated)' : plan; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: '```hcl\n' + truncated + '\n```', }); ``` Reviewer видит plan inline в PR, не надо скачивать артефакт. Лимит размера PR-комментария в GitHub, 65k символов, отсюда truncate. ## Plan-файл, секретный артефакт В `plan.tfplan` лежат: - Все sensitive значения (variable, output, locals). - Все computed-атрибуты, в том числе ARN'ы и id'шки приватных ресурсов. - Окружающие переменные, которые в plan вошли. Это значит: - **retention минимальный** (1-5 дней). После apply артефакт не нужен. - **Не публиковать публично.** Private-репо ок; public-репо, катастрофа. - **Не клонировать в неуполномоченные jobs.** Audit-job, который ради логирования читает plan и пишет в S3, должен иметь те же IAM привилегии, что плановый. См. [tf-secrets-in-state](/terraform/kb/tf-secrets-in-state.md). ## Multi-environment pipeline Один и тот же pipeline на dev / stage / prod: ```yaml jobs: plan-dev: uses: ./.github/workflows/tf-plan.yml with: env: dev workdir: envs/dev apply-dev: needs: plan-dev uses: ./.github/workflows/tf-apply.yml with: env: dev workdir: envs/dev plan-prod: needs: apply-dev uses: ./.github/workflows/tf-plan.yml with: env: prod workdir: envs/prod # ... ``` Dev сначала, prod после успешного dev. Plan-файлы, разные артефакты, не путать. ## Подводные камни - **`apply plan.tfplan` без init после download падает.** `actions/download-artifact` не приносит `.terraform/`, провайдеры и модули. Сделай init перед apply. - **terraform init требует backend.** Apply-job в другом jobs/runner не имеет конфига backend'а локально, он в HCL, init его прочитает. Если backend требует cross-account-роли, OIDC должен это покрывать. - **Plan-файл privy к версии Terraform.** `plan.tfplan` от 1.9 не применится 1.8. В CI fix-версию через `hashicorp/setup-terraform` одну для plan и apply. - **Cycle: apply создаёт что-то новое, что в plan не было.** Это бывает когда provider добавляет default-теги; они появляются «откуда-то» при apply. Plan-as-artifact это не ловит, ловит drift-detection. - **`-target` в CI = красный флаг.** Plan с `-target`, это «делаем подмножество». Если apply'ишь такой plan, остальной граф может оказаться в неконсистентном состоянии. CI не должен использовать `-target`; это инструмент аварийного recovery. - **`-refresh=false` ускоряет, но врёт.** Plan без refresh пропускает drift с облаком, apply падает позже. Использовать только когда refresh реально дорог, и осознанно. ## Команды ```bash terraform plan -input=false -no-color -lock-timeout=2m -out=plan.tfplan ``` Канонический CI-plan. Все флаги для автоматизации. ```bash terraform show -json plan.tfplan > plan.json ``` Для policy-сканеров (Checkov, OPA, terraform-compliance). ```bash terraform apply -input=false -no-color plan.tfplan ``` Apply сохранённого плана. Без fresh-plan. ```bash terraform plan -detailed-exitcode -out=plan.tfplan; echo $? ``` 0=no changes, 1=error, 2=changes. Используй для conditional jobs. ## См. также - [OIDC между GitHub Actions и AWS, без access keys](/terraform/kb/tf-oidc-aws.md) - [OPA + Rego, policy as code для Terraform plan](/terraform/kb/tf-policy-as-code.md) - [terraform plan: посмотреть, что Terraform собирается сделать](/terraform/kb/tf-plan.md)