Speculative vs. Saved Terraform Plans Making CI Approval Match Execution

A source-grounded Terraform CI pattern that separates plan exit codes, saved-plan approval and execution, while retaining backend-locking qualifications.

A green check on a CI terraform plan step is not a green light for terraform apply. A useful pipeline policy should distinguish whether planning succeeded, whether it found changes, and whether those particular changes were approved. HashiCorp exposes three options that, used together, draw a defensible boundary between review and execution: -detailed-exitcode, -out=FILE, and -lock-timeout.

Speculative plans are not authorization to apply

Running terraform plan without -out=FILE produces a speculative plan: a description of the plan’s effect, with no intent to actually apply it terraform plan documentation. Speculative plans exist for code-review workflows where developers verify the effect of a change before submitting it. The same documentation warns, plainly, that intervening changes to the target system can make the final effect of a configuration change different from what an earlier speculative plan indicated, and that the final non-speculative plan should always be re-checked before applying terraform plan documentation. A speculative plan is a snapshot at one moment, not a contract for later execution.

Saved plans shift the approval boundary

Passing -out=FILE writes a non-speculative plan file that terraform apply will execute as written terraform plan documentation. When a saved plan is passed to apply, the command runs without prompting, and -auto-approve is ignored, because passing the plan file is itself the approval terraform apply documentation. The apply command also rejects any additional planning modes or options in that mode, because the plan file already encodes the final decisions terraform apply documentation. Creating the file and authorizing its execution are separate decisions for the pipeline owner; passing it to apply is the execution boundary.

When terraform apply runs without a saved plan file, it automatically creates a new execution plan as if terraform plan had been run, then prompts for approval before applying terraform apply documentation. This is the mechanism that makes apply-time re-planning possible without an explicit intermediate plan step. (Design framing; the regeneration behavior is sourced.)

What -detailed-exitcode changes for a CI gate

-detailed-exitcode returns three values: 0 for empty diff, 1 for error, and 2 for non-empty diff terraform plan documentation. This is the flag that lets a pipeline answer that question without scraping human-readable output.

Hypothetical example: a policy-enforced pipeline can branch on the exit code — 0 for no changes, 2 for changes present — and require an authorized reviewer to comment before the apply job can start. The approval is then decoupled from the plan job’s runtime. (Design judgment, not a source claim.)

State locking in a two-phase pipeline

When the selected backend supports locking, Terraform automatically locks state for operations that could write it. Routine acquisition can be silent; a longer wait can produce a status message state locking documentation. The upstream guidance is explicit: -lock=false is documented as something the maintainers do not recommend for most commands, because concurrent writers can corrupt state state locking documentation. For contended CI environments where another job may briefly hold the lock, the right knob is -lock-timeout=DURATION, which retries acquisition for a specified window before erroring terraform plan documentation. The same flag is also supported on terraform apply terraform apply documentation.

A hypothetical saved-plan approval workflow

The following is a design proposal, not a claim about a particular team’s deployment. It uses the saved-plan workflow HashiCorp describes for automation, while keeping the approval tied to the plan that will be executed.

  1. Create the plan — run terraform plan -out=tfplan -detailed-exitcode -lock-timeout=30s. The 30-second lock wait is an example policy choice, not a vendor recommendation. Handle exit code 1 as failure and 2 as a successful plan containing changes, rather than treating every nonzero exit code identically.
  2. Review and authorize — have an authorized reviewer examine the proposed changes and approve use of that saved plan. The plan documentation warns that saved plan files can contain sensitive values in cleartext; treat the file as a sensitive artifact rather than a public build log.
  3. Apply the approved file — pass that same file to terraform apply tfplan. The command does not ask again for approval when a saved plan is supplied, so the pipeline must enforce its approval policy before invoking it. Do not pass new planning options at this step; the saved plan already contains the planning decisions.

These flag and saved-file behaviors are documented in the plan reference and the apply reference. If the pipeline instead runs apply without a saved file, Terraform generates another plan and normally asks for approval. Approval of an earlier speculative result should not be represented as approval of that newly generated result. A team choosing this alternative must design its approval interaction accordingly.

The practical design question is which planning result is reviewed and how that result reaches execution. Saved plans and automatic planning offer different approval boundaries; neither removes the need to handle errors and backend capabilities. A green planning check alone does not answer those policy questions.

Law Wen Feng

Cloud and AI Architect in Malaysia. Writes about Azure infrastructure, AI agents, cloud migration, disaster recovery and Microsoft Fabric. More about Law Tech Rants