lesson ── terraform-production ── ~14 мин ── 5 шагов
In a production pipeline, plan and apply are separate jobs. The plan job runs
plan -out=plan.tfplan and uploads it as an artifact; the apply job downloads
that exact file and applies it. The goal is for apply to roll out the same
plan the reviewer saw, not a fresh one. That guarantee holds as long as the
artifact lives in protected storage and the reviewer ran show against this
very file. In this lesson you will emulate the pipeline with shell scripting
and learn what -input=false and detailed-exitcode do.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
cd /home/student/tf-artifact
cat > main.tf <<'EOF'
resource "aws_s3_bucket" "artifact_demo" {bucket = "linuxlab-artifact-demo"
tags = {ManagedBy = "terraform"
}
}
output "name" {value = aws_s3_bucket.artifact_demo.bucket
}
EOF
terraform init -input=false -no-color > /dev/null
terraform plan \
-input=false \
-no-color \
-lock-timeout=2m \
-out=plan.tfplan
ls -la plan.tfplan
The plan.tfplan file is binary and is not edited by hand. It is the
serialized record of "what Terraform wants to do".
✓ The plan file is created. That is the artifact.
For the reviewer, human-readable:
terraform show -no-color plan.tfplan > plan.txt
head -30 plan.txt
This is what you paste into the PR comment, the +/~/- diff.
For the policy gate, machine-readable:
terraform show -json plan.tfplan > plan.json
jq '.resource_changes[].address' plan.json
The JSON holds a resource_changes structure where OPA and Checkov look
for violations. One plan.tfplan, two representations.
✓ Plan in three formats: binary for apply, txt for review, json for policy.
set +e
terraform plan -detailed-exitcode -no-color -out=plan.tfplan
code=$?
set -e
echo "exit: $code"
We have not applied yet, so it should be 2 (there are changes). Apply it:
terraform apply -input=false -no-color plan.tfplan
Now run detailed-exitcode again:
set +e
terraform plan -detailed-exitcode -no-color -out=plan.tfplan
code=$?
set -e
echo "exit: $code"
It should be 0, no changes.
The CI pattern:
✓ detailed-exitcode gives three states. CI branches on them.
Let's make a change, run plan again, and hand the plan to the "apply job" (another shell, as if it were a different VM):
sed -i 's|"linuxlab-artifact-demo"|"linuxlab-artifact-v2"|' main.tf
cat main.tf
# plan-job
terraform plan \
-input=false \
-no-color \
-out=plan.tfplan
# simulate upload-artifact / download-artifact
mkdir -p /tmp/ci-artifacts/
cp plan.tfplan /tmp/ci-artifacts/
# apply-job, imagine this is another machine in the pipeline
cp /tmp/ci-artifacts/plan.tfplan ./plan.tfplan
terraform apply -input=false -no-color plan.tfplan
Apply did exactly what was in the plan. In real GitHub Actions this step is
actions/upload-artifact + actions/download-artifact.
✓ The pipeline loop works. The bucket was renamed through the saved plan.
OpenTofu keeps the CLI and state compatible with Terraform for the
commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On a first switch, though, back up
the state and do a run on a feature branch, the differences cluster in
the newer features (variables in backend, state encryption, OCI
registry-backed modules). See tf-opentofu-parity for the full matrix.
Apply on a stale plan must fail. Let's set up the stale scenario:
# plan once
terraform plan -input=false -out=stale.tfplan
# apply through a different plan (different changes)
sed -i 's|"linuxlab-artifact-v2"|"linuxlab-artifact-v3"|' main.tf
terraform plan -input=false -out=current.tfplan
terraform apply -input=false current.tfplan
# now try to apply the OLD stale.tfplan
set +e
terraform apply -input=false stale.tfplan 2>&1 | head -20
code=$?
set -e
echo "exit: $code"
Apply failed because the stale plan references state that is now out of date. Terraform protects against a race condition between plan and apply.
This matters: it catches the case where state moved ahead between "the reviewer approved" and "CI applied". If someone pushed something to main and apply ran, your stale plan is no longer relevant, so apply fails.
✓ The stale plan protects against a race condition. It is a safety net.
plan.tfplan holds:
aws_db_instance).That means:
Minimum retention. In GitHub Actions
actions/upload-artifact retention-days: 1 is enough for a
plan-then-apply pipeline.
Do not publish it. A public repository means a public artifact, which means a secret leak.
terraform show plan.tfplan unfolds everything. Anyone with access
to the artifact effectively sees the state sections that are sensitive
in HCL.
terraform show -json is worse, a machine-readable structure with
no redaction.
For a prod pipeline: plan files are encrypted (sops, age) or use the CI's artifact-encryption feature. See tf-secrets-in-state.
terraform plan -input=false -out=plan.tfplan, a binary plan.
terraform show -json plan.tfplan > plan.json, machine-readable.
terraform apply plan.tfplan, applies exactly what is in the file.
-detailed-exitcode 0=clean, 1=err, 2=changes, a gate for CI.
команды
terraform plan -input=false -no-color -out=plan.tfplanstandard automation mode, plan to a file.terraform show -no-color plan.tfplan > plan.txthuman-readable for review.terraform show -json plan.tfplan > plan.jsonfor OPA/Checkov/terraform-compliance.terraform apply -input=false -no-color plan.tfplanapplies the saved plan. No fresh plan.концепции