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_changeslifecycle guards for externally-managed computed attributes, or force-unlock and surgicallyterraform state rm+ re-import the drifted resource. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your
.tfblock 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.