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
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-onlywrites those new attributes into state and does not change the object. A normal apply is what changes the object. Useterraform state list,show,mv,rm, andreplace-providerto inspect or rewrite bindings. Prefer amovedblock over a one-offterraform state mvwhen 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.web ↔ i-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.
| Situation | After refresh | After a normal apply |
|---|---|---|
| Console changed a tag Terraform also manages | State learns the new tag; plan wants to set the tag back to HCL | Object matches HCL again |
Console changed a tag covered by lifecycle.ignore_changes | State learns the new tag; plan stays empty for that attribute | Object keeps the console value |
| Console deleted the instance | Binding is stale; plan proposes create | New instance, new id in state |
| You want state to accept the console tag | apply -refresh-only records the tag; no object change | Only 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.
| Command | What it does | Changes remote objects? |
|---|---|---|
terraform state list [address...] | Lists resource instance addresses, optionally filtered by address or -id | No |
terraform state show ADDRESS | Prints one instance's attributes for humans (terraform show -json is the machine API) | No |
terraform state mv SOURCE DEST | Rebinds an existing remote object to a new address (same resource type) | No |
terraform state rm ADDRESS | Forgets the binding; the remote object keeps existing | No |
terraform state replace-provider FROM_FQN TO_FQN | Rewrites 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.web → module.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-onlyis how you accept outside changes into state.- Console edits are drift, not a second backend.
state rmforgets; it does not destroy.movedis the shareable rename;state mvis an immediate CLI rewrite.replace-providerchanges provider FQNs in state, not instance ids.- Do not hand-edit
terraform.tfstate.
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?
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?
What does terraform state rm 'aws_instance.legacy' do?