How to Detect and Fix Terraform Drift
Drift happens when real infrastructure diverges from your Terraform config, and here is how to spot it and put it back in sync.
Key takeaways
- Terraform drift is when real infrastructure no longer matches your config and state because something changed it outside Terraform.
- Detect it with terraform plan -refresh-only, then decide intent: apply to revert, edit config to accept the change, import a replaced resource, or state rm a deleted one.
On this page
To fix Terraform drift, run terraform plan -refresh-only to see what changed outside Terraform, then pick a resolution by intent: terraform apply to revert reality to your config, edit the .tf files to accept the change, terraform import a resource rebuilt by hand, or terraform state rm one deleted on purpose.
Verified against
Terraform CLI 1.17 (1.17.1, released 2026-10-28), against the developer.hashicorp.com docs for plan, refresh, import blocks and the lifecycle meta-argument. Checked October 2026.
You run terraform plan expecting "No changes." Instead Terraform wants to modify a security group you never touched, or recreate an instance that looks fine. That gap between what your .tf files say and what actually exists in the cloud is drift.
Drift is not a Terraform bug. It means something changed your infrastructure outside of Terraform. Terraform tracks the world through its state file, and state only updates when Terraform runs. Anything that edits the real resource without going through Terraform leaves state stale and config out of sync with reality.
What actually causes drift
The usual suspects are boringly human:
- Someone opened the AWS console and clicked a setting to fix an incident at 2am.
- Another tool (an autoscaler, a controller, a backup job) changed a tag or a rule.
- A teammate ran an
awsCLI command as a hotfix and never brought it back into code. - A provider default changed, or the cloud mutated a computed attribute.
None of these are malicious. They are just changes Terraform did not make, so it has no idea they happened until it next compares config against the live API.
How drift shows up
Drift surfaces as an unexpected diff in terraform plan. You did not edit the config, but Terraform still proposes changes. A plan that reads like this is the tell:
$ terraform plan
# aws_security_group.web will be updated in-place
~ resource "aws_security_group" "web" {
id = "sg-0abc123"
~ ingress = [
- {
- cidr_blocks = ["0.0.0.0/0"]
- from_port = 22
- to_port = 22
- protocol = "tcp"
},
]
}
Terraform wants to remove an SSH rule you never added in code. That rule exists because someone opened port 22 in the console. Config says "no such rule," reality says "rule present," and the plan is Terraform offering to reconcile the two.
Detecting drift deliberately
Discovering drift during a real change is a bad time to find it. Detect it on purpose instead.
A plain terraform plan already refreshes state against the live API and shows any drift mixed in with your intended changes. To see drift on its own, with no pending config changes in the way, use a refresh-only plan:
$ terraform plan -refresh-only
This asks a single question: has anything changed in the real world since state was last written? It reports differences between state and reality without proposing to apply your config. It is the cleanest read on "what drifted."
A note on terraform refresh: the standalone command still exists, but the docs mark it deprecated because it writes to state with no plan to review first. Prefer terraform plan -refresh-only (and its apply counterpart below) so you always see the change before it is written.
Reading the plan diff
Get comfortable with the symbols, because they tell you what Terraform intends:
~in-place update-/+destroy and recreate (replacement)+create-destroy
Drift caused by a small attribute change usually shows as ~. Drift where the resource was rebuilt with a new identity often shows as -/+. The symbol tells you which resolution path you are on.
Resolving drift by intent
There is no single fix. The right move depends on what you want the end state to be.
Revert reality to match config. The console change was wrong or unauthorized, and code is the source of truth. Just apply. Terraform will undo the out-of-band change and bring the resource back to what your .tf files declare:
$ terraform apply
In the security group example above, this deletes the rogue port 22 rule.
Accept the real change into config. The manual change was correct and you want to keep it. Edit the .tf to describe the new reality, then plan until it is clean. Add the ingress block that someone created by hand, run terraform plan, and confirm it now reports no changes. Nothing is applied because config and reality already agree.
Update state without touching infrastructure. Sometimes the live change is fine and you only need state to catch up (for example a computed value the provider adjusted). A refresh-only apply writes the observed values into state without changing any real resource:
$ terraform apply -refresh-only
The resource was recreated or replaced outside Terraform. Someone deleted the old instance and built a new one by hand. State still points at the dead resource, so Terraform wants to create yet another. Bring the existing resource under management with an import instead:
$ terraform import aws_instance.web i-0new123456
You can also declare an import block in config and review it in a plan before applying, which is safer for anything you do more than once. Running terraform plan -generate-config-out=generated.tf writes the matching resource block for you:
# imports.tf
import {
to = aws_instance.web
id = "i-0new123456"
}
Something was deleted outside Terraform. A resource in state no longer exists. If you want it back, terraform apply recreates it from config. If it was meant to be gone, drop it from state so Terraform stops tracking it:
$ terraform state rm aws_instance.web
state rm only forgets the resource; it never deletes anything real.
Two blocks that keep drift quiet
Some drift is expected and repeating. lifecycle { ignore_changes } tells Terraform to stop fighting over attributes that another system legitimately owns, like a tag an autoscaler stamps on:
# asg.tf
resource "aws_autoscaling_group" "app" {
# ...
lifecycle {
ignore_changes = [tag, desired_capacity]
}
}
And when you refactor or rename resources, a moved block preserves the mapping so Terraform updates addresses in state instead of proposing a destroy and recreate that reads like drift:
# moved.tf
moved {
from = aws_instance.web
to = aws_instance.web_server
}
Preventing drift
Fixing drift one plan at a time does not scale. Cut it off upstream:
- Least privilege: humans should not be able to click-change prod. Restrict console write access so changes have to go through code and review.
- Policy: enforce that infrastructure changes land as pull requests, with break-glass access logged and reconciled afterward.
- Continuous detection: run refresh-only plans on a schedule so drift is caught in hours, not during the next deploy. Add
-detailed-exitcode, which exits2when the plan has changes, so the job can alert. That is exactly what a pipeline is for, and we cover the setup in drift detection in CI.
If drift is constant because many hands touch the same cloud, a continuously reconciling control plane is the other answer; the tradeoffs are in Crossplane vs Terraform.
Questions people ask
What causes Terraform drift?
Any change made outside Terraform: a console edit during an incident, an aws CLI hotfix, an autoscaler or controller rewriting tags, or a provider default that changed. Terraform only updates state when it runs, so it notices none of these until the next plan compares state against the live API. Drift is a process problem, not a Terraform bug.
Is terraform refresh deprecated?
Yes. HashiCorp's docs mark terraform refresh deprecated and recommend terraform plan -refresh-only and terraform apply -refresh-only instead. The old command writes detected changes straight to state with nothing to review. The refresh-only plan shows the same differences first, and the refresh-only apply writes them only after you approve, so a surprise deletion cannot silently disappear from state.
Does terraform apply -refresh-only change my infrastructure?
No. A refresh-only apply updates the state file to match what currently exists and leaves every real resource untouched. Use it when the live change is correct and you only want state to catch up. If you also want config to match, edit the .tf files afterwards, otherwise the next normal plan will propose reverting the change.
The call we'd make
Detect drift on a schedule with terraform plan -refresh-only so you are never surprised by it mid-change. When drift appears, decide intent first: revert with apply, absorb it into config, import a replaced resource, or reconcile state with state rm. Reach for ignore_changes only on attributes another system genuinely owns, and lock down console access so drift becomes the exception instead of the routine.
For wiring detection into a scheduled pipeline, see Terraform drift detection in CI; for other failure modes, the Terraform error troubleshooting guide.
Sources
Cite this article
Kiril Urbonas (2026). How to Detect and Fix Terraform Drift. DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/fix-terraform-drift
[How to Detect and Fix Terraform Drift](https://www.devopsness.com/blog/fix-terraform-drift) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)
More from DevOps
Explore more articles in this category
15 SRE Interview Questions With Answers, From SLOs to On-Call
SRE interviews test numbers and judgment. Fifteen questions on SLIs, error budgets, incident command, toil, capacity, on-call and postmortems, plus a systems-design scenario, each with the answer that scores and the one that loses the room.
Sign Container Images With Cosign: Keyless GitHub Actions Workflow, SBOM and In-Cluster Checks
One workflow that builds, signs keylessly with GitHub OIDC, attaches an SBOM and SLSA provenance, plus the Kyverno policy that refuses anything unsigned. Copy it, change three names, ship it.
Fix Prometheus High Cardinality: 6 Commands to Find and Drop Bad Series
When Prometheus runs out of memory, one label is almost always to blame. Six commands to find it, the relabel rules that drop it, and the scrape limits that stop it coming back.
You might have missed
Evergreen posts worth revisiting.