skip to content

Plan/Apply Workflow

The operational loop around the code: init, plan, apply, workspaces, targeting, and running all of it in CI with drift detection. Interviewers ask how a plan gets reviewed and approved, because that is what separates IaC from a shared laptop.

part ofTerraformoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

In a team's Terraform pipeline, why does terraform plan run on the pull request while terraform apply runs only after the change merges to the main branch?

level: juniorimportance: must knowfreq 78%

answer

  1. review the effect, not the intent
  2. cheap veto before the irreversible half
  3. merged branch describes production
  4. read-only on the PR, write on main
  5. one commit, one audit trail

basics

~20 s

A plan on the pull request turns the proposed change into a reviewable diff before anything is touched. Apply runs from main so only merged, reviewed code ever changes real infrastructure, giving one source of truth and one audit trail.

solid answer

~50 s

The pull request is where a human still has a cheap veto, so that is where `terraform plan` belongs: it renders exactly what would be created, changed or destroyed, and the pipeline posts that output back onto the PR so the reviewer approves a diff rather than approving HCL and guessing. The plan job needs no permission to change anything. Apply is the irreversible half, so it is triggered by the merge to main and runs only from main's commit — that keeps a single branch as the description of what production should be, makes `git log` the change record, and means nobody can apply from a fork or a feature branch. In automation both commands run with `-input=false` so a missing variable fails the job instead of blocking on a prompt, and the apply job usually sits behind an approval gate as well.

go deeper

for a junior

Be able to say plainly that plan is the safe, read-only preview run on the pull request and apply is the irreversible step run after merge, and that the reviewer is approving the plan output, not just the code.

for a middle

Explain the mechanics: -input=false so the job fails instead of hanging, a pinned Terraform version and committed lock file so the plan is reproducible, and separate credentials for the plan and apply jobs.

for a senior

Show you have operated this. Talk about plan/apply skew when pull requests queue up, break-glass applies putting state ahead of main, and detecting which root modules a shared-module change actually affects.

for a principal

Own the policy: which environments get an approval gate and who holds it, how emergency access is granted and reconciled afterwards, and how the same pipeline shape stays affordable across dozens of root modules and teams.

## The pipeline in one paragraph A team Terraform repository normally has exactly two automated paths. On a pull request, CI checks out the branch, runs `terraform init`, then `terraform fmt -check`, `terraform validate` and `terraform plan`, and publishes the plan output where the reviewer can read it — usually as a comment on the PR. Nothing is created. On a merge to `main`, CI checks out `main`, initialises again and runs `terraform apply`, frequently behind a manual approval. Everything else — nightly drift runs, ad-hoc destroys of ephemeral environments — is an extra path bolted onto that spine. ## Why plan belongs on the pull request HCL is not self-evident. A three-line change to a variable default, a module version bump, or an edit to a list can expand into a plan that replaces a database. Reviewing the code alone means reviewing intent; reviewing the plan means reviewing effect. The plan makes the effect concrete: how many resources are added, changed, destroyed, and which ones. The reviewer's real job is to look for the destroys and the replacements, and for a resource count that does not match the size of the diff. Running it in CI rather than on a laptop matters for a second reason: reproducibility. The pipeline runs a pinned Terraform version, a committed provider lock file and the same backend every time, so the plan the reviewer reads is the plan the machine will act on — not one produced by whatever versions happen to be on one engineer's machine. ``` terraform init -input=false terraform plan -input=false -no-color -out=tfplan ``` `-input=false` is the flag that separates automation from interactive use. Without it, a variable with no value makes Terraform prompt on stdin, and a CI job with no stdin simply hangs until its timeout. `-no-color` strips ANSI escapes so the output is readable when pasted into a PR comment. Terraform also honours the `TF_IN_AUTOMATION` environment variable, which suppresses the "run terraform apply next" style hints that make no sense in a pipeline. ## Why apply belongs to main Apply is the half that cannot be undone by closing a browser tab. Tying it to the merge commit gives three properties: 1. **One source of truth.** Whatever is on `main` is the claim about what production looks like. If apply could run from feature branches, two branches could fight over the same state and `main` would describe nothing. 2. **An audit trail for free.** "Who changed prod" becomes "which commit was applied", answerable from Git history plus the run log, without anyone reconstructing console activity. 3. **A credential boundary.** The plan job can hold read-only cloud credentials; only the apply job, which runs on a protected branch, holds credentials that can write. A pull request from an untrusted contributor therefore cannot reach anything that can be changed. The apply job typically also requires a human approval before it proceeds, because merge approval and change approval are not always the same decision — the reviewer approved the code, and someone still has to decide that now is the time to touch production. ## The failure modes this shape has The most common one is **plan/apply skew**. The plan on the PR was computed against the base branch and the state as they were at plan time. By the time the PR merges, another PR may have merged first, or someone may have changed the same resources by hand. Whatever the pipeline does at apply time — re-plan on `main`, or carry the reviewed plan file forward as an artifact — the reviewed plan is a snapshot with an expiry date, and a long-lived PR should be re-planned before it is trusted. The second is **the escape hatch that becomes the habit**. Everyone keeps credentials for emergencies, and every emergency apply from a laptop puts state ahead of `main`, so the next pipeline plan shows a diff nobody wrote. If break-glass access exists, applying from it should be loud, logged, and followed by a commit that makes `main` match reality again. The third is **partial coverage**. A pipeline that plans only the directories a PR touched will miss the case where a change to a shared module affects a root module the PR did not touch. Either plan everything, or make the affected-directory detection follow module dependencies rather than the file paths in the diff.

  • The pull request was opened last Monday and its plan is a week old. What should the pipeline do before applying it?
    Treat the old plan as advisory and re-plan. Other pull requests may have merged and other people may have changed resources directly, so the diff computed a week ago no longer describes what apply would do. Either re-run plan on the merge commit and gate the apply on that fresh output, or require the PR to be updated against the base branch, which forces a new plan run.
  • Why do the Terraform commands in a CI job pass -input=false?
    Because a CI job has no interactive stdin. Without `-input=false`, a variable with no supplied value makes Terraform prompt for it, and the job hangs there until the runner's timeout kills it — which looks like an infrastructure problem rather than a missing variable. With the flag, Terraform fails immediately with a clear error naming the variable.
  • Where would you put the human approval in this pipeline, and what does it protect against?
    On the apply job, after the merge, showing the plan being applied. Merge approval says the code is correct; the apply approval says now is an acceptable time to change production — during a freeze, an incident, or a risky replacement those are different answers. The approver must see the actual plan, otherwise the gate only records a click.

saying these in an interview costs you the question

  • Apply straight from the feature branch to test it faster
  • The reviewer reads the HCL diff and skips the plan output
  • Plan needs write credentials because it talks to the cloud
  • A plan from last week is still valid at merge time
  • Emergency applies from a laptop need no follow-up commit

context

open as a page

In Terraform, what do `terraform plan` and `terraform apply` each do, and what does the `-auto-approve` flag change about `terraform apply`?

level: juniorimportance: must knowfreq 85%

basics

~10 s

terraform plan computes and prints the changes needed to make real infrastructure match the configuration, without changing anything. terraform apply makes those changes, pausing for an interactive confirmation first; -auto-approve skips that confirmation prompt.

open as a page

In a CI job, what does `terraform fmt -check` do differently from plain `terraform fmt`, and what does it miss by default?

level: juniorimportance: must knowfreq 62%

basics

~20 s

terraform fmt -check reports formatting problems without rewriting anything and exits non-zero if any file would change, which is what makes it usable as a build gate. By default it looks only at the current directory, so nested modules need -recursive.

open as a page

In Terraform, what does a CLI workspace give you, and what actually changes when you run `terraform workspace select dev`?

level: juniorimportance: must knowfreq 62%

basics

~10 s

A Terraform CLI workspace is a separate state file for the same configuration in the same backend. Selecting one changes only which state Terraform reads and writes; the code, backend and credentials stay identical.

open as a page

Why would a team run `terraform plan -out=tfplan` and later `terraform apply tfplan` instead of just running `terraform apply`?

level: middleimportance: must knowfreq 65%

basics

~20 s

So the changes that get applied are exactly the changes that were reviewed. A bare terraform apply computes a brand-new plan at apply time; applying a saved plan file replays the recorded decisions instead, with no re-planning and no approval prompt.

open as a page

What does Terraform do during the refresh step of `terraform plan`, and what are you trading away when you run `terraform plan -refresh=false`?

level: middleimportance: must knowfreq 68%

basics

~20 s

Refresh re-reads every resource recorded in Terraform state from the provider API, so the plan diffs real infrastructure against your config. Passing -refresh=false skips those reads — faster and easier on rate limits, but the diff then trusts possibly stale state.

open as a page

What does the `-target` option do to a `terraform plan` or `terraform apply`, and why does Terraform treat it as a tool for exceptional circumstances rather than routine use?

level: middleimportance: must knowfreq 65%

basics

~20 s

Terraform's -target restricts a run to the addresses you name plus what they depend on, skipping the rest of the configuration and most of its refresh. The apply is therefore deliberately incomplete, which is why a full untargeted plan must follow it.

open as a page

What does `terraform validate` actually check, and why can a configuration pass validate and still fail during apply?

level: middleimportance: must knowfreq 70%

basics

~20 s

terraform validate checks a configuration against itself and the installed provider schemas: syntax, references, required arguments, attribute names and type consistency. It contacts no provider API and reads no state, so permissions, quotas and real-world values only surface at plan or apply.

open as a page

Why do most teams isolate production with a separate Terraform root module and backend rather than a CLI workspace?

level: seniorimportance: must knowfreq 60%

basics

~20 s

CLI workspaces share one backend, one access policy, one credential set and one configuration, so a mistake in the selected workspace can reach production. Separate root modules give each environment its own state store, permissions and blast radius.

open as a page

In Terraform, what does `terraform apply -replace="aws_instance.web"` do, and when would you reach for it instead of editing the configuration?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Terraform's -replace option plans a destroy-and-recreate of one named resource instance without any configuration change. You reach for it when the real resource is broken or half-built while its configuration and recorded state still look correct.

open as a page

In a Terraform pipeline, what cloud credentials should the plan job hold compared with the apply job, and why is a single long-lived access key stored in CI a poor choice for both?

level: middleimportance: should knowfreq 48%

basics

~20 s

The plan job should get read-only access to the managed resources; only the apply job needs permission to create, change and destroy. Both should receive short-lived credentials issued per run through federation, not one static key sitting in CI forever.

open as a page

In Terraform, how does running `terraform destroy` differ from deleting a resource block from the configuration and running `terraform apply`?

level: middleimportance: should knowfreq 50%

basics

~20 s

terraform destroy proposes deleting everything this configuration currently manages, leaving the code untouched, so a later apply recreates it. Deleting a resource block changes the desired state permanently, so the next apply removes just that one object.

open as a page

What does `terraform init` actually do, and what ends up inside the `.terraform` directory after it runs?

level: middleimportance: should knowfreq 70%

basics

~20 s

terraform init prepares a working directory: it wires up the backend, installs the providers the configuration requires, and fetches remote modules. Plugins land in .terraform/providers and module copies in .terraform/modules, while .terraform.lock.hcl is written beside your code.

open as a page

In Terraform, what do `terraform plan -refresh-only` and `terraform apply -refresh-only` do, and when would you reach for them?

level: middleimportance: should knowfreq 52%

basics

~20 s

A refresh-only run re-reads real infrastructure and proposes updates to the Terraform state file only — never to infrastructure. Plan shows what drifted; apply writes those observed values into state, leaving the configuration and the real resources untouched.

open as a page

What does TFLint catch in Terraform code that `terraform validate` cannot, and how does it know?

level: middleimportance: should knowfreq 48%

basics

~20 s

TFLint adds opinionated and provider-specific rules on top of schema correctness: unused declarations, deprecated idioms, naming conventions, and values a provider will reject such as a nonexistent instance type. Its provider rulesets ship that knowledge as plugins, so the checks stay offline.

open as a page

How does Terraform's native `terraform test` framework work — where do the test files live, and what does a `run` block do?

level: middleimportance: should knowfreq 45%

basics

~20 s

Tests are .tftest.hcl files in the module directory or a tests/ subdirectory. Each run block executes a plan or an apply against the module and evaluates assert conditions. Apply runs create real infrastructure, which Terraform destroys when the file finishes.

open as a page

How is the `terraform.workspace` value typically used inside a Terraform configuration, and where does driving behaviour from it start to hurt?

level: middleimportance: should knowfreq 45%

basics

~20 s

terraform.workspace evaluates to the active workspace's name as a string. Configurations use it to prefix resource names or to look up per-workspace sizing, but branching real behaviour on it makes the same code mean different things in different states.

open as a page

Two pipeline runs for the same Terraform root module start at the same time and the second one fails because it cannot acquire the state lock. How should the pipeline be designed so this stops being a problem?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Serialise runs per state in the pipeline itself: one concurrency lane per root module, queueing rather than cancelling, so a second run waits its turn instead of racing. The state lock is a last-resort safety net, not a scheduler.

open as a page

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?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Passing 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.

open as a page

What does the plan file produced by `terraform plan -out=tfplan` actually contain, and why does that make it sensitive?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A saved plan is an opaque binary archive holding the configuration, a snapshot of prior state and every planned attribute value — including values marked sensitive, which are only masked in the printed output, not removed from the file. Protect it exactly like state.

open as a page

After `terraform apply -refresh-only` records a colleague's console change into Terraform state, what does the very next plain `terraform plan` propose, and why?

level: seniorimportance: should knowfreq 38%

basics

~20 s

It proposes changing the resource back to whatever the configuration says. Refresh-only updates state, never the .tf files, so the plan now compares an accurate state against an unchanged config and sees a difference it intends to correct.

open as a page

How would you run a scheduled Terraform drift-detection job in CI, and how should the job decide whether to raise an alert?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Run terraform init and a non-interactive plan on a schedule against the deployed branch, and branch on -detailed-exitcode: 0 means no changes, 2 means the world differs from code, 1 means the run itself failed. Alert on 2, page on repeated 1.

open as a page

After an incident you fixed production with `terraform apply -target=module.db`. The next untargeted `terraform plan` proposes changes to resources nobody edited. What explains that, and how do you handle the plan?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A targeted apply evaluates and refreshes only the targeted subgraph, so the first untargeted plan afterwards shows the catch-up work: dependents that never picked up new attribute values, stale outputs, and real-world drift no run had refreshed since.

open as a page

Why do teams run Checkov or Trivy against `terraform show -json` plan output instead of the raw HCL, and what does that cost them?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Plan JSON contains resolved values, so a scanner sees the attribute a resource will really have instead of an unresolved var or module reference. The cost is that it needs init, credentials and a successful plan, so it runs late and can never be a pre-commit check.

open as a page

A Terraform CI job applied against the default workspace instead of `staging`. How does Terraform decide which CLI workspace is active, and how do you make that deterministic in a pipeline?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Terraform reads the active workspace from .terraform/environment in the working directory. A fresh CI checkout has no such file, so it silently uses default. Make it explicit with TF_WORKSPACE or a terraform workspace select step after init.

open as a page

What does `terraform plan -detailed-exitcode` return, and how would you use it in a script?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

With -detailed-exitcode, terraform plan exits 0 when there are no changes, 1 on error, and 2 when the plan succeeded and changes are pending. Scripts use it to tell an empty diff apart from a real one without parsing output.

open as a page

How do you correctly retire an ephemeral Terraform CLI workspace, and what happens if you run `terraform workspace delete` while its state still tracks resources?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

Destroy inside the workspace first, then select another workspace and delete it. Terraform refuses to delete a workspace whose state still tracks resources unless you pass -force, which discards the state and orphans the real infrastructure.

open as a page

A large `terraform apply` keeps failing partway through with rate-limit errors from the cloud provider's API. What does Terraform's `-parallelism` option control, and how would you use it here?

level: seniorimportance: nice to knowfreq 35%

basics

~20 s

Terraform's -parallelism sets how many resource operations one run performs concurrently while walking the dependency graph, defaulting to 10. Lowering it thins the burst of API calls the run makes, which is the fastest way to get a throttled apply through.

open as a page

What does declaring `mock_provider "aws"` in a Terraform test file change about how the tests execute, and what stops being tested?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

A mock_provider block replaces the real provider, so Terraform walks the full plan and apply graph while generated values stand in for computed attributes. Nothing is created, no credentials are needed, and tests run in seconds — but no API ever validates the request.

open as a page

When would you run Terraform through a purpose-built run service such as Atlantis or HCP Terraform instead of hand-written plan and apply jobs in your existing CI system?

level: principalimportance: nice to knowfreq 34%

basics

~20 s

Adopt a run service when the number of Terraform repositories makes hand-rolled jobs a maintenance burden, and when you want cloud credentials, state, run history and approvals owned by one system instead of scattered across CI configurations.

open as a page

showing 1–30 of 31