Hedronite · Cert Lesson · HashiCorp · Mon 2026-10-05

Terraform Associate: refresh-only plans and drift handling

Refresh-only updates state. It does not apply your configuration.

Lesson Class: Cert (T2 · Terraform Associate 003 · Pro-depth)
Vendor: HashiCorp
Verified: terraform 1.14.3 · local_file refresh-only · credential-free
Paired Ops: Azure storage management policy + Rust census
Paired Dev: serde resource_drift / relevant_attributes
Grounding: plan/apply docs · JSON format · refresh tutorial
Mode Before Diff
Read the planning mode before you trust the resource diffs.
A refresh-only delete in the UI can mean record the change, not destroy the object.

<!-- hal:authoritative:yaml -->

Refresh-only updates state to match the remote. It does not apply your configuration. Know which plan mode you ran.

§I. Frame: Associate objectives for tonight

10-02 covered encoding functions. 09-29 covered lifecycle meta-arguments. 09-26 covered type constraints. 09-23 covered data sources. 09-20 covered dynamic blocks. 09-17 covered ephemeral. 09-14 covered import/moved. 09-11 covered backends. 09-08 covered check blocks. Leave all of that.

Tonight is planning modes: terraform plan -refresh-only, terraform apply -refresh-only, why the old terraform refresh workflow is retired, and how refresh-only relates to drift in plan JSON (resource_drift, relevant_attributes). Ops ships Azure lifecycle policy. Dev parses the JSON spine.

§II. Three planning modes

ModeFlagGoal
Normal(default)Make remote match configuration
Destroy-destroyEmpty the state by destroying remotes
Refresh-only-refresh-onlyMake state (and root outputs) match remotes; do not change remotes to match config

Named technique: Mode Before Diff. Read the mode line before you read the resource diffs. A refresh-only plan that looks like a delete can be "record that the object is gone or changed," not "destroy it on apply."

HashiCorp added -refresh-only in Terraform 0.15.4+. Associate questions still bait the legacy terraform refresh subcommand.

§III. Why refresh alone was retired

Older workflows ran terraform refresh to rewrite state from remote reads, then planned separately. That command still exists on some versions as a compatibility path, but current docs point operators to refresh-only plans you can review and apply:

terraform plan  -refresh-only -out=tfplan-ro
terraform apply -refresh-only           # or: terraform apply tfplan-ro

Fact one. Refresh-only apply updates state and root outputs. It does not execute normal create/update/delete from configuration drift toward config.

Fact two. You cannot combine -refresh-only with -refresh=false. Refresh-only is the refresh.

Fact three. Exam trap: "run terraform refresh then apply" is the old story. The current story is a reviewable refresh-only plan.

§IV. Drift on the CLI (credential-free demo)

Using hashicorp/local local_file (no cloud account):

  1. Apply a file resource with content alpha.
  2. Edit the file on disk to beta-external.
  3. Run terraform plan -refresh-only.

Terraform 1.14.3 reports the object changed outside Terraform. The human UI says this is a refresh-only plan and will not undo the change by rewriting the file to match old state unless you leave refresh-only mode. terraform show -json on that saved plan carries resource_drift and an empty resource_changes list.

That JSON is the Dev spine. Cert only needs the CLI contract: refresh-only is how you choose to record drift into state after review.

§V. Drift vs normal plan

QuestionRefresh-onlyNormal plan
Reads remotes?YesYes (unless -refresh=false)
Writes remotes to match config?NoYes, after apply
Updates state to match remotes?Yes, on applyAlso refreshes during plan; apply then converges config
Plan JSON resource_driftOften populatedOften populated when refresh sees external change
Plan JSON relevant_attributesMay be emptyUsed to filter which drift may have shaped planned changes

Rule one. Seeing drift in a normal plan does not mean you must apply a config change. You may need a refresh-only apply first so state matches reality, then a second normal plan.

Rule two. relevant_attributes is the filter Dev uses. Empty index means unmarked drift, not "no drift."

Rule three. -refresh=false on a normal plan skips the remote read. Faster, and blind to external change. Never use it when you are diagnosing drift.

§VI. Exam drills

  1. Operator changed a tag in the cloud console during an incident. Which command records that tag in state without changing other attributes toward config? → terraform apply -refresh-only (after reviewing plan -refresh-only).
  2. Can refresh-only destroy a resource because it is absent from .tf? → No. Absent-from-config destruction is normal/destroy-mode behavior.
  3. Does terraform refresh remain the documented happy path on current Associate material? → No. Prefer refresh-only plan/apply.
  4. Where do machines read the drift list? → terraform show -json → resource_drift (and optionally relevant_attributes).

§VII. Close

Refresh-only is a planning mode with a reviewable plan file. It reconciles state to remotes. Normal mode reconciles remotes to configuration. Dev classifies resource_drift against relevant_attributes. Ops keeps Azure lifecycle intent declarative so fewer "fixed in portal" drifts appear in the first place.

Related