Skip to content

Terraform approval workflows: apply exactly the plan you approved

An approval is only worth something if the apply runs exactly what the approver read. Here is how plans drift between review and apply, and how to design an approval workflow that prevents it.

How plans drift between review and apply

A common workflow is: run plan in a pull request, a reviewer approves, the branch merges, and the pipeline runs apply. That final apply makes a new plan. It can differ from what the reviewer read because:

  • another change merged and applied in between;
  • someone changed a resource by hand, so refresh finds new drift;
  • a module or provider version constraint resolved to a newer release; or
  • a variable changed in the pipeline or platform.

The reviewer approved one plan; a different plan ran.

Saved plans help, if you use them properly

OpenTofu and Terraform can save a plan and apply exactly that file:

tofu plan -out=tfplan
tofu show tfplan     # what the reviewer reads
tofu apply tfplan    # applies only this plan

If state changed after the plan was saved, apply refuses the stale plan instead of applying something different. To rely on this in a pipeline, the same file has to move from the plan job to the approval step to the apply job, the apply must use the same provider and module versions, and the file must be protected, because it can contain sensitive values.

The case against auto-apply

Auto-apply removes the human step for plans that pass policy. It is convenient for low-risk workspaces, but it means:

  • a policy gap becomes an outage, since nothing else stands between a merge and production;
  • a replace that destroys a database looks the same as a tag change to the pipeline; and
  • there is no named person who decided the change should happen.

A good approval workflow

  1. Plan in an isolated, clean environment, pinned to a commit and a tool version.
  2. Check policy before the reviewer looks, so findings sit next to the diff.
  3. Show a summary and the full plan: counts of creates, updates, and destroys, plus the resource-level changes.
  4. Make destroys deliberate, with an extra confirmation when the plan deletes anything.
  5. Bind the approval to the plan, its policy result, and the state it was planned against. If any of them change, the approval is void.
  6. Apply with the same inputs: the same providers and modules the plan downloaded.
  7. Expire old plans, so a week-old plan can't be approved against today's infrastructure.
  8. Record who decided, and why, in an audit log.

How StackShip does it

  • Every plan with changes waits for an owner or admin. Members can start runs but not approve them. There is no auto-apply.
  • The approval binds to the plan digest, the dependency bundle, the state version it was planned against, and the policy result. If any of these change before you approve, the approval is rejected.
  • Plans that destroy resources ask you to confirm the create, change, and destroy counts.
  • The apply reuses the exact providers and modules the plan downloaded.
  • Unapproved plans expire after seven days.
  • Approvals, discards with their reasons, and policy overrides are in the audit log.

The same rules apply to AI agents through the MCP server: an agent can only approve the exact plan it read. See runs and approvals for the full run lifecycle.

Get started

Run your first OpenTofu plan today.

Free for every organization. Sign up with email or Google.