Initializing Enclave...

How to Fix Terraform 'Error: provider produced inconsistent result after apply' Drift

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–30 mins

TL;DR

  • What broke: Terraform's provider returned an attribute value after apply/refresh that does not match what the schema declared it would be — Terraform treats this as a fatal inconsistency and aborts, leaving your state file stale and your pipeline dead.
  • How to fix it: Pin the offending provider to a stable version, add ignore_changes lifecycle guards for externally-managed computed attributes, or force-unlock and surgically terraform state rm + re-import the drifted resource.
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your .tf block and the full error, and get a corrected config with the right lifecycle and version constraints generated locally in your browser.

The Incident (What Does the Error Mean?)

You ran terraform apply or terraform refresh and hit a wall:

Error: Provider produced inconsistent result after apply

When applying changes to aws_ecs_service.api, provider
"registry.terraform.io/hashicorp/aws" produced an unexpected new
value: .network_configuration[0].assign_public_ip: was cty.False,
but now cty.True.

This is a bug in the provider, which should be reported in the
provider's own issue tracker.

Or the refresh variant:

Error: Provider produced inconsistent final plan

Module module.networking, resource aws_security_group.internal,
attribute "ingress": planned value cty.SetVal([...]) does not match
config value cty.SetVal([...]).

Immediate consequence: Terraform refuses to write a new state snapshot. Your terraform.tfstate reflects a partially-applied world. Any subsequent plan will either re-attempt the same broken change or report phantom diffs forever. CI/CD pipelines stall. On-call engineers start getting paged.


The Attack Vector / Blast Radius

This error sits at the intersection of three failure modes, each with its own blast radius:

1. Provider version regression (most common) A provider upgrade introduced a schema change where an attribute that was previously Computed: false is now Computed: true (or vice versa), or a default value changed. Terraform's diff engine expected one value post-apply; the provider returned another. Every resource of that type in every workspace using that provider version is now broken. In a multi-account org with a shared provider mirror, this cascades across dozens of pipelines simultaneously.

2. External drift / out-of-band mutation Someone clicked in the AWS/GCP/Azure console, a Lambda auto-remediation function fired, or an autoscaler modified a managed attribute. The live resource state no longer matches what Terraform last wrote. On the next refresh, the provider reads the real value, computes a diff, but the planned output violates the schema contract. The state file is now lying. Any apply based on it risks destroying and recreating production resources.

3. Computed attribute ordering / set hash instability Providers that use schema.TypeSet compute element hashes based on attribute values. If a nested attribute has a server-side default that wasn't captured in state (e.g., AWS normalizing a CIDR, GCP expanding a short-form resource name to a full URL), the set hash changes between plan and apply. This manifests as a perpetual diff that can trigger forced resource replacement — meaning Terraform wants to delete and recreate a running database or load balancer on every run.


How to Fix It (The Solution)

Diagnosis First

Run with full logging to isolate the exact attribute:

TF_LOG=TRACE terraform refresh 2>&1 | grep -A 10 "inconsistent"

Identify: (a) which resource, (b) which attribute, (c) planned value vs. returned value.


Fix 1 — Pin the Provider Version (Basic Fix)

If the error appeared after a provider upgrade, roll back to the last known-good version and lock it hard.

 terraform {
   required_providers {
     aws = {
       source  = "hashicorp/aws"
-      version = "~> 5.0"
+      version = "= 5.31.0"
     }
   }
 }

Then:

rm .terraform.lock.hcl
terraform init -upgrade
terraform refresh

Fix 2 — lifecycle ignore_changes for Computed/External Attributes (Enterprise Best Practice)

For attributes that are legitimately managed outside Terraform (autoscaling desired counts, server-assigned IPs, provider-normalized values), suppress the diff at the lifecycle level. Do not do this blindly — only for attributes you have explicitly decided Terraform should not own.

 resource "aws_ecs_service" "api" {
   name            = "api-service"
   cluster         = aws_ecs_cluster.main.id
   task_definition = aws_ecs_task_definition.api.arn
   desired_count   = 3

   network_configuration {
     subnets          = var.private_subnet_ids
     security_groups  = [aws_security_group.ecs.id]
-    assign_public_ip = false
+    assign_public_ip = false  # explicit, matches provider default
   }

+  lifecycle {
+    ignore_changes = [
+      # Provider normalizes this post-apply; suppress phantom diffs
+      desired_count,
+      # If task def ARN is managed by external CI pipeline
+      task_definition,
+    ]
+  }
 }

Fix 3 — Surgical State Repair for Severe Drift

When the state is too corrupted for a clean refresh:

# 1. Unlock if stuck
terraform force-unlock <LOCK_ID>

# 2. Remove the drifted resource from state
terraform state rm aws_ecs_service.api

# 3. Re-import the live resource
terraform import aws_ecs_service.api cluster-name/api-service

# 4. Verify plan is clean before applying
terraform plan -refresh-only

Do not skip step 4. A dirty import followed by a blind apply has caused more production incidents than the original drift.


💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Lock provider versions in version control, always. Every workspace must have a committed .terraform.lock.hcl. Treat provider version bumps as a dependency PR with a mandatory review.

2. Run terraform refresh -only as a pre-flight check in CI.

# .github/workflows/terraform.yml (excerpt)
- name: Drift Detection
  run: |
    terraform init
    terraform refresh -no-color 2>&1 | tee refresh.log
    if grep -q "inconsistent" refresh.log; then
      echo "::error::Provider inconsistency detected. Check refresh.log."
      exit 1
    fi

3. Enforce provider version constraints with Checkov.

checkov -d . --check CKV_TF_1  # Ensures modules use pinned versions

Add a custom Checkov policy to block ~> (pessimistic constraint) on providers in production workspaces — require exact = pins.

4. OPA/Sentinel policy to block ignore_changes = all.

# sentinel/no-ignore-all.sentinel
import "tfplan/v2" as tfplan

main = rule {
  all tfplan.resource_changes as _, changes {
    not (changes.change.after.lifecycle.ignore_changes contains "all")
  }
}

ignore_changes = all is a footgun that masks real drift permanently. Ban it in policy.

5. Atlantis / Terraform Cloud run triggers. Configure drift detection runs on a cron schedule (not just on PR). A nightly terraform plan -refresh-only across all workspaces will surface provider inconsistencies before a human triggers an apply in anger at 2 AM.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →