linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/CI/CD/tf-plan-apply-ci

kb/cicd ── CI/CD ── intermediate

Plan-as-artifact и automation mode в CI

В CI plan-job создаёт `plan.tfplan`, грузит как артефакт. Apply-job скачивает артефакт и применяет ровно его, никакого нового plan на apply-стадии. Это гарантирует: то что показали reviewer'у в PR ровно то, что применилось. Все команды с `-input=false`, `-no-color`, `-lock-timeout` чтобы автоматизация не зависала. Plan-файл содержит sensitive, обращайся с ним как с секретом.

view as markdownaka: terraform-ci-plan-apply, terraform-plan-artifact, terraform-automation-mode

Канонический 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.

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.

§ см. также

  • tf-oidc-awsOIDC между GitHub Actions и AWS, без access keysРаньше CI-runner ходил в AWS с долгоживущим access key. OIDC переворачивает это: GitHub выдаёт workflow signed JWT, STS обменивает на временные credentials через AssumeRoleWithWebIdentity. Никаких secrets, scope роли сужается до repo+branch+env. Три артефакта: IAM OIDC-провайдер, IAM-роль с trust policy, workflow с `id-token: write`.
  • tf-policy-as-codeOPA + Rego, policy as code для Terraform planPolicy-as-code = правила («все S3 шифрованы», «никаких IAM с *») написаны кодом, гонятся в CI, fail'ят PR. OPA, стандарт, Rego, язык. conftest, обёртка с CLI-friendly выводом; читает plan.json, прогоняет правила, exit 0/1. Зрелее и дороже на старте, чем Checkov, но позволяет cross-resource правила любой сложности.
  • tf-planterraform plan: посмотреть, что Terraform собирается сделатьplan, это сухая прогонка: Terraform читает ваш HCL, читает state, и показывает diff между ними. Ничего не меняет в облаке. Главный инструмент, чтобы не сломать прод по ошибке.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки