Terraform saved plans: bind approval to the artifact
Treat code revision, state target, binary plan digest and approval as one record; regeneration creates a new approval decision.
Quick answer
A Terraform approval should identify the saved binary plan that will be applied, its code revision and its intended state target. Render that same file for review, preserve its digest across the approval wait and apply that file directly. If it becomes stale or must be regenerated, create a new review packet rather than silently carrying the old approval forward.
On this page
In a Terraform approval pipeline, the reviewer should approve the saved plan that the runner will apply. A terminal diff from an earlier speculative plan does not establish that relationship. Save the executable plan, render that same file for review and retain its identity with the code revision and state target. Regeneration creates a new decision. [1][2]
Consider a fictional production change that replaces an application instance group. The plan job finishes, a reviewer checks the proposed replacement and the apply job waits. During that wait, another legitimate infrastructure operation changes the state. The apply job receives a stale-plan error. The dangerous repair is to run a fresh plan and assume the earlier approval covers it.
This article designs the decision record for a CLI-managed pipeline. It is not an executable deployment of infrastructure and does not change a cloud account. A managed Terraform run service can provide its own plan-and-apply lifecycle; use its native run identity and controls rather than mixing that lifecycle with an unrelated local plan file.
What did the reviewer actually approve?#
Write down the plan digest, code revision and state target that the reviewer is approving.
A useful review packet identifies the configuration commit, Terraform version, dependency lock file, module selections, input set, backend and workspace, and the saved plan artifact. The record need not expose secrets. It needs stable identifiers that let the apply job and the reviewer answer the same question: which exact intended operation is authorized for which state target?
The saved plan is central because Terraform distinguishes speculative planning from a plan saved for later execution. A normal terraform apply creates a fresh plan. Passing a saved plan file instead executes its recorded decisions. These are different interfaces to the approval boundary, even if both begin from the same configuration directory. [1][2]
An artifact digest is a useful integrity check, not a complete trust system. If an attacker or an overprivileged job can replace both the plan and the expected digest, a matching hash proves little. Keep the review decision and expected artifact identity in a control the apply job cannot silently rewrite. Limit who can replace the artifact before approval and after it.
Build the review packet from one saved plan#
The following shell sketch shows the direction of evidence. It is intended for an already initialized, authenticated working directory with the correct backend and workspace selected. It does not establish those prerequisites by itself. The commands create a local plan and review files; they are examples for a dedicated non-production rehearsal before use in a deployment system.
terraform plan -input=false -out=tfplan
terraform show -no-color tfplan > plan-review.txt
terraform show -json tfplan > plan-review.json
shasum -a 256 tfplan > tfplan.sha256Render the review from the saved binary file, not from a second plan command. A second plan could observe different remote state or inputs. The reviewer should inspect the proposed replacements, deletions, unknown values and relevant provider behavior in the packet generated from the artifact that will reach the apply job. A summary count alone does not explain which resource is being replaced. [1][3]
Restrict the binary plan and its rendered derivatives to the deployment reviewers and runner.
Terraform warns that sensitive values can be present in the plan file even when the terminal display obscures them. Its machine-readable output can also expose sensitive state values. Treat text, JSON, downloaded artifacts, build caches and diagnostic attachments as separate possible copies. Apply the retention and access policy to the packet as a whole. [1][3]
Do not upload a full state file merely to record the target. A restricted backend identifier and workspace name can identify the intended state boundary without copying every resource attribute into the approval system. If debugging needs a state snapshot, keep it in a separately controlled location and give the broader incident record a reference rather than the contents.
The saved packet should also explain the change's operational intent. In the fictional instance-group replacement, record why replacement is acceptable, what keeps traffic served during it and what observation would stop rollout. Terraform can describe resource operations, but it does not decide whether the application's availability requirement permits them. That remains part of the human review.
Which identity must survive the approval wait?#
The apply stage needs the approved artifact, its expected digest and the same intended state target. It also needs compatible tooling and access to perform the planned operation. Recreating a working directory from a different commit, upgrading providers during initialization or selecting a similarly named workspace introduces an unnecessary ambiguity before the state check even begins.
Preserve the dependency lock file with the configuration. It records provider dependency selections. It does not lock remote module selections, so pin or otherwise control those independently when reproducibility requires it. Do not describe .terraform.lock.hcl as a complete deployment lockfile. A plan artifact and a dependency lock file answer different questions. [4]
The digest check below is a minimal artifact-integrity check for the saved checksum file generated earlier. It is not the approval gate itself. In a real pipeline, obtain the expected digest from the approved review record, protect the artifact source and stop before apply if provenance or target checks fail.
# Read-only integrity check. The expected checksum must be trusted.
shasum -a 256 -c tfplan.sha256
# Consequential execution belongs after the pipeline's approval and target gates:
# terraform apply -input=false tfplanSaved-plan apply does not prompt again. Terraform treats passing the file as approval. Do not add -auto-approve and imagine it creates a new security property; the meaningful control is the gate that allowed this command to receive this file for this target. Keep that fact visible when reviewing CI changes that rearrange jobs. [2]
| Record element | Question it answers | What it does not prove |
|---|---|---|
| Configuration revision | Which reviewed source produced the packet? | Which state was targeted |
| Backend and workspace identity | Which state boundary is intended? | Artifact integrity |
| Plan digest | Are these the approved bytes? | That the plan is still applicable |
| Approval identity and time | Who approved this packet? | That later regeneration is covered |
| Apply result | What did execution report? | Application-level health |
State locking protects an operation's access to state when the backend supports it. It is not a reservation stretching across an arbitrarily long approval wait. Plan creation can finish and release its lock while another run legitimately changes state. Design the deployment queue around that possibility rather than making the approved artifact appear immune to concurrent work.
What should happen when the plan is stale?#
The HashiCorp maintainer explanation distinguishes state lineage from serial. A saved plan records both from the state snapshot it used. A serial mismatch produces the stale-plan error; a lineage mismatch indicates a different state history. This is not simply a timer expiring because approval took too long, and storage-object timestamps are not the right substitute for those identities. [5]
In the fictional case, another authorized operation changes state while approval waits. The old plan can no longer be applied against that state snapshot. Keep the failure as useful evidence. Find the intervening operation in the deployment and state history. Determine whether the target is still correct before creating another plan.
Invalidate the old review packet, generate a fresh plan and ask for a new decision before applying it.
A fresh plan can be rendered next to the old packet to help the reviewer understand the difference. It is still a new proposal. Counts such as “unchanged number of resources” are insufficient because resource identities, replacement reasons or values can differ. The new packet should name the superseded packet so responders can trace why the decision was repeated.
Do not edit state serials, replace backend metadata or release an active operation's state lock to make the stale plan pass. Those actions would attack the evidence that prevented the wrong execution. If the problem is a wrong workspace or backend, repair selection and plan again. If it is legitimate concurrency, serialize the relevant apply workflow and keep a bounded approval process.
An unchanged state serial does not certify that every remote object is unchanged outside Terraform. A saved plan represents decisions made at planning time, and remote systems can still change or reject operations. Keep the approval-to-apply interval appropriate for the risk and replan when the environment's assumptions are no longer trustworthy. A digest is about file identity, not current reality. [1]
How do you rehearse the failure paths?#
Use a disposable configuration and an isolated state target. First, create a saved plan and render it. Record its digest, replace the local artifact with different bytes in the rehearsal and verify that the artifact gate blocks execution. This proves the integrity branch. It does not prove protection if the expected digest can be rewritten by the same actor.
Next, generate a legitimate intervening state change in the disposable target, then attempt the old saved plan. The expected pipeline behavior is to preserve the stale result, invalidate the review packet and stop. Do not automatically call a fresh apply. Review the new proposal separately and verify that it receives a different artifact identity and a new approval record.
Exercise target-selection errors too. Give the apply stage an intentionally incorrect workspace identifier in the test fixture and verify the deployment gate rejects it before execution. A job name such as “production” is a label; the backend and workspace observations are the evidence. Keep the account or project identity check alongside those observations when the provider target depends on credentials.
Rehearse artifact expiry. If the approved binary disappears under retention rules, the pipeline should say that the approved artifact is unavailable. It should not quietly substitute a newly generated plan. Decide how long a review packet can remain valid and make the approval system present that lifetime to reviewers before they spend time approving an unusable proposal.
Finally, exercise partial apply failure. Some operations may complete before another fails, so the next step is inspection and a new plan against current state, not replaying an old review packet blindly. Application health also needs its own evidence after infrastructure operations finish. A successful Terraform exit does not establish that the replacement instance group serves the intended requests.
Keep the reviewer packet small enough to inspect#
Put the change's intent, affected state target, artifact identity and destructive operations at the top of the review. Link to the full restricted plan rather than pasting enormous JSON into a general chat. Preserve exact resource addresses when discussing replacements so the reviewer can connect the explanation to the recorded operations.
The approval interface should show the current packet's status. A superseded, expired or stale packet must stay visibly ineligible for apply. The history can remain useful for audit without leaving an old approval button active. That small product behavior is often where a technically sound artifact design becomes an operationally reliable workflow.
Frequently asked questions#
- Does terraform apply tfplan ask for confirmation?
- No. In saved-plan mode, passing the plan file is treated as approval and Terraform performs the recorded operations without another confirmation prompt. The pipeline must therefore establish its approval gate before invoking that command. [2]
- Can I regenerate a stale plan and keep the approval?
- Treat regeneration as a new decision. The new plan can contain different actions, inputs or state assumptions even when the headline counts look familiar. Render the new artifact, record its new digest and obtain approval for that packet. [1][5]
- Is the dependency lock file the same as the saved plan?
- No. The dependency lock file records provider selections and checksums. It does not describe the proposed resource operations, and it does not lock remote module versions. Preserve it as an input to the plan workflow, with separate version controls for modules. [4]
- Is a redacted plan display safe to share publicly?
- Do not infer that from the terminal display. The saved plan may contain sensitive values in cleartext, and machine-readable output can expose state values. Protect the binary and every derivative, including review files and CI artifacts, according to their actual contents. [1][3]