In a CI pipeline, why is the plan job's saved plan file handed to a separate apply job as a build artifact, and what problems does storing that artifact introduce?
answer
- approval must bind to what actually runs
- no fresh decisions at apply time
- tied to the state it was built from
- not encrypted, not redacted
- artifact retention is a secret-handling decision
basics
~20 sPassing the saved plan forward guarantees the apply performs exactly the change a human approved, with no re-plan in between. The costs are staleness once state moves on, and an artifact that contains sensitive values in the clear.
solid answer
~50 sIf the apply job re-runs `terraform plan` itself, the approval a human gave applies to output nobody will ever act on — anything that changed in between silently rides along. Saving the plan with `-out` in the plan job and passing that file to the apply job closes the gap: applying a saved plan makes Terraform execute precisely the recorded actions with no fresh decisions. Two things come with it. First, the plan is tied to the state it was computed from, so if another run changes state in the meantime Terraform refuses to apply it as stale — which is the correct behaviour, but the pipeline must handle that failure by re-planning and re-approving rather than by forcing it through. Second, a saved plan is not encrypted and embeds resource attributes and variable values, including sensitive ones, so the artifact store now holds secrets and needs the same access control and short retention as state.
go deeper
Know that a plan can be written to a file and that applying that file replays the reviewed actions instead of computing new ones. Recognise the name of the flag that saves it.
Explain what the apply job must match for the file to be usable — Terraform version, locked providers, same backend — and why applying a saved plan takes no new decisions.
Demonstrate the operational judgment: handling a stale plan by re-planning rather than forcing it, sizing artifact retention, and recognising that the artifact carries sensitive values in the clear.
Decide policy across the estate: whether plan artifacts may live in CI storage at all, whether a run service that keeps plans server-side is worth adopting, and what approval binding you require for regulated environments.
## What the handoff is for Split a pipeline into a plan job and an apply job and you have created a window: time passes, and possibly an approval happens, between deciding what to do and doing it. If the apply job starts by re-planning, whatever entered the world during that window is applied too — a colleague's merge, a console edit, a provider version that floated. Nobody reviewed that. This is the plan/apply skew problem, and the saved plan file is the standard answer to it. ``` # plan job terraform plan -input=false -out=tfplan # apply job, after approval terraform apply -input=false tfplan ``` When `terraform apply` is given a plan file rather than a directory, it takes no new decisions: it does not prompt, it does not re-diff, it executes the recorded action for each recorded resource address. That is the whole value — the artifact is the unit of approval, and what runs is a byte-for-byte match for what was reviewed. ## Crossing the job boundary Jobs typically run on different machines with empty working directories, so the file has to travel through the CI system's artifact storage, and it does not travel alone. The apply job must reproduce enough of the plan job's environment for the plan to be usable: - **The same Terraform version.** A plan file is an internal format; a different minor version may refuse to read it. - **The same providers.** The apply job runs `init` again, and the committed lock file is what makes it resolve the identical provider builds rather than newer ones. - **The same backend and credentials**, so the state the plan was computed against is the state being written. Some teams pass the whole working directory, or a cached `.terraform` directory, alongside the plan for exactly this reason. ## Problem one: the plan goes stale A saved plan records which state snapshot it was built from. If another run — another pipeline, a colleague on a laptop, a drift-remediation job — changes that state before the apply runs, Terraform rejects the plan as stale instead of applying it. That is a safety property, not a bug: the recorded actions were computed against a world that no longer exists, and blindly executing them could destroy or clobber something. The operational consequence is that a plan file has a shelf life, and the length of it is a function of how busy the state is. Pipelines that queue an approval for hours against a shared state will hit staleness routinely. The correct handling is always: re-plan, re-review, re-approve. The failure mode to avoid is a pipeline (or an engineer) that reacts to staleness by falling back to `terraform apply -auto-approve` on the directory, which throws away the guarantee the artifact existed to provide. ## Problem two: the artifact is sensitive A plan file is a compressed archive of the planned actions, and to describe those actions it embeds resource attributes and the variable values behind them. Values marked `sensitive` are redacted from the *rendered human output*, not removed from the file — Terraform's own documentation warns that a saved plan contains information from configuration and state, including sensitive data, and should be treated accordingly. It is not encrypted. So the moment you upload it as a build artifact you have put a secret-bearing blob into a store whose access rules are usually laxer than your state backend's. The mitigations are ordinary but must be deliberate: keep retention to hours rather than the default weeks, restrict who can download artifacts from the repository, avoid making the artifact available on public or fork-triggered runs, and encrypt it at rest if the CI system does not do so already. Some organisations sidestep this entirely by using a run service that keeps the plan server-side and never lets it into CI storage. A related trap: a JSON rendering of the plan, produced with `terraform show -json tfplan` for policy checks or a diff comment, has exactly the same exposure and is easier to leak because it is plain text that people paste into tickets. ## When you would not bother For a small repository where the same job plans and applies within seconds, with no approval in between and a state only one pipeline writes, re-planning at apply time is a defensible simplification and avoids the artifact-handling burden entirely. The saved-plan handoff earns its complexity as soon as a human approval sits in the middle, or several people merge to the same state.
- The apply job reports the saved plan is stale. What is the correct response, and what is the tempting wrong one?The correct response is to re-plan on the current state, put the new diff in front of a reviewer, and apply that. State moved, so the recorded actions describe a world that no longer exists. The tempting wrong one is to fall back to applying the directory with `-auto-approve` to get the pipeline green — that applies whatever the world looks like now, unreviewed, which is exactly what the saved plan was protecting against.
- What else must the apply job reproduce from the plan job for the saved plan to be usable?The same Terraform version, since the plan file is an internal format a different version may refuse to read; the same providers, which the committed `.terraform.lock.hcl` guarantees when the apply job re-runs `init`; and the same backend and credentials, so it writes the state the plan was computed against. Mismatch any of these and the apply fails or, worse, resolves different provider code.
- Marking a variable sensitive hides it from plan output. Does that protect the saved plan file?No. `sensitive` controls rendering — it redacts values in the human-readable diff and in outputs. The saved plan file still contains the underlying values, uncompressed and unencrypted, because Terraform needs them to perform the actions. Treat the artifact with the same care as the state file: restricted download, short retention, encryption at rest.
saying these in an interview costs you the question
- Re-planning in the apply job is equivalent and simpler
- A saved plan can be applied any time it is still on disk
- Sensitive variables are stripped from the plan file
- Force the apply through when the plan reports stale
- Plan artifacts can sit in CI storage for the default retention