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.

By the JobSupport4U engineering team · Updated · 6 min read

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:

Shell
terraform plan -refresh-only

To 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.

Shell
terraform plan -detailed-exitcode -lock=false -input=false

Deciding what to do with each change

For every drifted attribute there are three honest options:

  1. Revert reality to the code. The console change was a mistake or a temporary fix — run a normal terraform apply and Terraform puts it back.
  2. Keep the change and update the code. The change was right — edit the configuration to match, then confirm terraform plan shows no changes. This is the most common correct answer.
  3. Accept the change into state only. terraform apply -refresh-only records 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:

HCL
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:

HCL
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:

HCL
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.
Keep reading

More guides