Короткий ответ
Пайплайн на PR должен выполнять terraform init + terraform plan -out=plan.tfplan, сохранять читаемый вывод (terraform show -no-color plan.tfplan) и публиковать его как комментарий к PR через API системы CI/VCS; после merge в main отдельный job выполняет terraform apply с ручным approval-гейтом (environment protection rule в GitHub Actions/manual gate в GitLab CI) и обязательно используя именно тот план, что был проверен (или заново сгенерированный план с тем же содержимым); одновременный запуск двух apply предотвращается как самим state locking backend'а (второй apply будет ждать/упадёт на занятом локе), так и организационно — настройкой очереди/конкурентности на уровне CI (например, concurrency group на имя окружения), чтобы не запускать два job параллельно на один и тот же state вообще.
Как это работает подробнее
CI-система видит каждый PR и merge как отдельное событие и по умолчанию может запускать job'ы параллельно; ограничение конкурентности (named concurrency group / environment lock в CI) заставляет job'ы, относящиеся к одному и тому же окружению, выполняться строго по очереди, а backend state locking Terraform — это второй, более низкоуровневый уровень защиты, срабатывающий даже если конкурентность в CI настроена неверно.
Пример кода
# .github/workflows/terraform.yml (фрагмент)
on:
pull_request:
push:
branches: [main]
concurrency:
group: terraform-prod
cancel-in-progress: false
jobs:
plan:
if: github.event_name == 'pull_request'
steps:
- run: terraform plan -out=plan.tfplan
- run: terraform show -no-color plan.tfplan > plan.txt
- uses: actions/github-script@v7 # публикует plan.txt как комментарий к PR
apply:
if: github.ref == 'refs/heads/main'
environment: prod # требует manual approval в настройках окружения
steps:
- run: terraform apply -auto-approve plan.tfplanЧто использовать на практике
- Практика. план, применяемый в
apply, в идеале должен быть ровно тем артефактом, что проверяли люди в PR (а не новыйplan, сгенерированный в момент apply, который теоретически может отличаться, если кто-то успел смержить что-то ещё между review и apply) — для этого план сохраняют как CI-артефакт и передают в job apply; approval-гейт должен требовать подтверждения конкретным человеком/ролью, а не автоматического прохождения, особенно для prod.
Подводные камни
- Ошибка. частая ошибка — давать возможность запускать
applyвручную из любой ветки в обход пайплайна «в экстренном случае», что обходит review и approval-процесс и со временем становится обычной практикой; другая ошибка — не ограничивать конкурентность на уровне CI и полагаться только на state locking, из-за чего второй job просто падает по таймауту лока вместо аккуратной постановки в очередь, создавая ложные сбои в пайплайне.