11.4 Resource Drift and State Operations

Key Takeaways

  • Drift means the real remote object no longer matches the binding and cached attributes in state; Terraform detects it when it refreshes during plan
  • terraform apply -refresh-only (or terraform plan -refresh-only) updates state and root outputs to match reality and does not change the remote objects
  • A normal apply is what changes the object back to match configuration; refresh alone never does that
  • Exam-relevant terraform state subcommands are list, show, mv, rm, and replace-provider; prefer a moved block over a one-off terraform state mv for refactors teammates must share
  • Manual console edits, terraform state rm, and a skipped refresh all create or hide drift; do not hand-edit the JSON snapshot
Last updated: August 2026

11.4 Resource Drift and State Operations

Quick Answer: Drift is when the real object differs from what state last recorded. Terraform notices during the refresh that runs at the start of plan. terraform apply -refresh-only writes those new attributes into state and does not change the object. A normal apply is what changes the object. Use terraform state list, show, mv, rm, and replace-provider to inspect or rewrite bindings. Prefer a moved block over a one-off terraform state mv when the whole team must keep the rename.

Objective 6d on Terraform Associate (004) pairs detection with surgery. Objective 7b later drills inspection; this section is the drift workflow those commands serve. Official references: Planning modes (-refresh-only), terraform state, and the moved block.

What drift is

Terraform expects a one-to-one binding: aws_instance.webi-0abc123. State also caches attributes (instance type, tags, user data). Drift is any of:

  • Someone changed the live object outside Terraform (console, CLI, another tool).
  • Someone deleted the live object and left the binding behind.
  • Someone created a look-alike object and the binding still points at the old id.
  • Someone edited state (or ran state rm / state mv) so the binding no longer matches the world.

It is not drift when you change HCL and have not applied yet. That is a pending configuration change. Drift is reality ≠ state. A later normal plan then compares refreshed state to configuration and proposes create / update / destroy.

SituationAfter refreshAfter a normal apply
Console changed a tag Terraform also managesState learns the new tag; plan wants to set the tag back to HCLObject matches HCL again
Console changed a tag covered by lifecycle.ignore_changesState learns the new tag; plan stays empty for that attributeObject keeps the console value
Console deleted the instanceBinding is stale; plan proposes createNew instance, new id in state
You want state to accept the console tagapply -refresh-only records the tag; no object changeOnly if you also change HCL, or you intended a no-op

Refresh detects; refresh does not heal the object

By default, terraform plan and terraform apply (automatic plan mode) read already-existing remote objects so state is current, then compare configuration to that prior (now refreshed) state. HashiCorp's plan introduction lists those three steps in that order.

  • Refresh updates state from the API.
  • Apply (normal mode) updates objects so they match configuration.

terraform apply -refresh-only selects refresh-only planning mode: the plan's only goal is to update Terraform state and root module output values to match changes made outside Terraform. Typical use: you changed objects during an incident and now need Terraform's records to match. Approve the refresh-only plan and state is rewritten. The instance type in AWS does not flip back.

terraform plan -refresh-only is the preview of that same mode. The two alternative modes (-destroy and -refresh-only) are mutually exclusive with each other and with normal mode.

-refresh=false skips the synchronizing read. Plans become faster and can be wrong because Terraform ignores external changes. You cannot combine -refresh=false with -refresh-only — that would disable the entire refresh-only operation.

The standalone terraform refresh command still exists as an older path. 004 / Terraform 1.12 phrasing follows current docs: prefer terraform apply -refresh-only so the state write is a reviewed plan.

Console clicks are just unmanaged writes

An on-call engineer who resizes an instance in the AWS console, attaches a security group, or deletes a disk has created drift. Terraform is not angry; it is stale. The next plan that refreshes will show it. If CI uses -refresh=false to save API calls, the drift stays hidden until someone refreshes.

Do not "fix" drift by opening terraform.tfstate in an editor. HashiCorp: do not directly edit the JSON; use terraform state so the CLI absorbs format changes. Hand-edits are how teams destroy the wrong id on the next apply.

Exam-relevant terraform state subcommands

All terraform state subcommands work with remote state the same way they work with local state (each read or write is a network round-trip). Mutating subcommands always write a backup; you cannot disable that.

CommandWhat it doesChanges remote objects?
terraform state list [address...]Lists resource instance addresses, optionally filtered by address or -idNo
terraform state show ADDRESSPrints one instance's attributes for humans (terraform show -json is the machine API)No
terraform state mv SOURCE DESTRebinds an existing remote object to a new address (same resource type)No
terraform state rm ADDRESSForgets the binding; the remote object keeps existingNo
terraform state replace-provider FROM_FQN TO_FQNRewrites the provider source on resources in state (for example hashicorp/aws → a private fork)No
terraform state list
terraform state list 'module.vpc'
terraform state list -id=sg-1234abcd
terraform state show 'aws_instance.web'
terraform state mv aws_instance.web aws_instance.app
terraform state rm 'aws_instance.legacy'
terraform state replace-provider hashicorp/aws registry.acme.corp/acme/aws

After state rm, a later normal plan wants to create a new object at that address. If the forgotten object still occupies a unique name, create fails. state rm is "stop managing," not destroy. HashiCorp now also documents removed blocks (lifecycle { destroy = false }) so the forget can go through plan/apply instead of a one-shot CLI; 004 still expects you to know terraform state rm.

replace-provider does not install a provider and does not change instance ids. It rewrites the provider FQN recorded on existing instances so the next init/plan uses a different source (a move from the public registry to an internal mirror or fork). It always writes a backup.

moved block versus terraform state mv

Renaming resource "aws_instance" "web" to "app" looks like destroy-old + create-new unless you tell Terraform it is the same object.

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

moved {
  from = aws_instance.web
  to   = aws_instance.app
}

Terraform checks state for from, renames that binding to to, and plans as an update or no-op instead of a replace. The moved block lives in configuration, so every teammate and every pipeline applies the same refactor.

terraform state mv aws_instance.web aws_instance.app does the same rebinding immediately in the current backend. Official warning: in a collaborative environment you must coordinate so nobody plans against the old address between your HCL rename and your mv. That race is why moved is the safer default on 1.12. mv is still correct for one-off repairs, moving an instance into a module (aws_instance.webmodule.worker.aws_instance.web), or shifting a count index.

You can only mv like-to-like: instance to instance, module to module, same resource type.

Scenario: the incident resize

Production is t3.micro in HCL. On-call resizes the instance to t3.large in the console so a launch survives. Next morning, terraform plan refreshes, sees t3.large in AWS, and proposes an update back to t3.micro. That proposal is not refresh-only — it is a normal plan healing configuration. If the team instead decides the larger size is now desired, they edit HCL to t3.large or they run terraform apply -refresh-only first so state records t3.large, then they update HCL to match before anyone applies a shrink. They do not state rm the instance (that would plan a second instance). They do not force-unlock. They do not edit JSON.

004 traps for objective 6d

  • Refresh updates state. Apply updates objects.
  • -refresh-only is how you accept outside changes into state.
  • Console edits are drift, not a second backend.
  • state rm forgets; it does not destroy.
  • moved is the shareable rename; state mv is an immediate CLI rewrite.
  • replace-provider changes provider FQNs in state, not instance ids.
  • Do not hand-edit terraform.tfstate.
Loading diagram...
Refresh records drift; apply is what changes the object
Test Your Knowledge

An on-call engineer changes a managed EC2 instance type in the AWS console. You want Terraform state to record the new type without changing the instance again. Which Terraform 1.12 command matches that goal?

A
B
C
D
Test Your Knowledge

You rename resource "aws_instance" "web" to "app" and want every teammate's next plan to keep the existing EC2 instance. What is the configuration-first approach on Terraform 1.12?

A
B
C
D
Test Your Knowledge

What does terraform state rm 'aws_instance.legacy' do?

A
B
C
D