Terraform · AWS
Terraform state drift: how to detect and fix it
Detect Terraform drift with refresh-only plans and CI checks, decide whether to revert or adopt each change, and handle imports, moved blocks and ignore_changes.
Someone fixed a production issue in the AWS console, and now terraform plan wants to change things nobody asked it to. That is drift. This guide covers how to detect it, how to decide what to do about each change, and how to stop it happening again.
What drift is
Terraform compares three things: your configuration (the code), the state (what Terraform last recorded), and the real infrastructure. Drift is when the real infrastructure no longer matches what Terraform expects — because of console changes, scripts, other tools, autoscaling, or the cloud provider setting values itself.
Detecting drift
Every normal terraform plan refreshes state first and reports “Objects have changed outside of Terraform” when it finds drift. To see only the drift, without any proposed config changes mixed in:
terraform plan -refresh-onlyTo detect drift automatically, run a scheduled plan in CI and use the exit code: -detailed-exitcode returns 0 for no changes, 1 for errors and 2 when there are changes — alert on 2.
terraform plan -detailed-exitcode -lock=false -input=falseDeciding what to do with each change
For every drifted attribute there are three honest options:
- Revert reality to the code. The console change was a mistake or a temporary fix — run a normal
terraform applyand Terraform puts it back. - Keep the change and update the code. The change was right — edit the configuration to match, then confirm
terraform planshows no changes. This is the most common correct answer. - Accept the change into state only.
terraform apply -refresh-onlyrecords reality in state without touching infrastructure. On its own it’s rarely enough: if the code still says otherwise, the next plan will try to revert it.
Special cases
Resources created outside Terraform
Bring them under management with an import block (Terraform 1.5+), then write matching config:
import {
to = aws_sqs_queue.orders
id = "https://sqs.eu-west-1.amazonaws.com/123456789012/orders"
}terraform plan -generate-config-out=generated.tf can draft the configuration for you — review it before committing.
Resources deleted outside Terraform
The plan will propose recreating them. If the deletion was intentional, remove the resource from your code; if you only want Terraform to stop managing it, use terraform state rm.
Renamed or moved resources
Moving a resource into a module or renaming it looks like “destroy and create” unless you tell Terraform. Use a moved block:
moved {
from = aws_sqs_queue.orders
to = module.queues.aws_sqs_queue.orders
}Values another system is supposed to change
Some attributes are meant to change outside Terraform — an Auto Scaling group’s desired capacity, for example. Tell Terraform to ignore them rather than fighting the autoscaler on every apply:
resource "aws_autoscaling_group" "workers" {
# ...
lifecycle {
ignore_changes = [desired_capacity]
}
}Preventing drift
- Make the pipeline the only way to change infrastructure: limit write access in the console with IAM, and keep break-glass access for emergencies.
- Run drift detection on a schedule and treat drift as a ticket, not a surprise at the next deploy.
- Tag managed resources (for example
managed-by = terraform) so people know not to edit them by hand. - After every emergency console fix, follow up with a code change the same day.
- Use remote state with locking so two applies never race each other.