Every OpenTofu user hits the same wall of errors eventually, usually at the worst possible moment — mid-deploy, in CI, with a teammate waiting. After enough tofu apply runs on real infra I've learned that most of these have a fast, deterministic fix once you recognize the message. This is the field guide I wish I'd had: the errors you'll actually see and the shortest path out of each.
The CLI is tofu (OpenTofu is the Linux Foundation fork of Terraform), but almost all of these apply identically to terraform if you're still on it.
1. State lock: "Error acquiring the state lock"
You'll see a ConditionalCheckFailedException or a lock ID dump. It means a previous run died without releasing the lock, or someone is genuinely running apply right now.
First, make sure nobody is actually applying. Then force-unlock with the ID from the error:
tofu force-unlock 1a2b3c4d-5e6f-7890-abcd-ef1234567890
Do not reach for -force flags or delete the DynamoDB lock item by hand unless force-unlock refuses. Ninety percent of the time a stale lock from a crashed CI job is the cause.
2. Provider checksum mismatch / dependency lock
Error: registered checksum for provider ... does not match
Your .terraform.lock.hcl was generated on one platform (say, macOS arm64) and CI runs on linux amd64, so the recorded hashes don't cover the platform being used. The fix is to record hashes for all the platforms your team and CI use:
tofu providers lock \
-platform=linux_amd64 \
-platform=darwin_arm64 \
-platform=linux_arm64
Commit the updated .terraform.lock.hcl. If you legitimately upgraded a provider and want to accept the new hash, tofu init -upgrade regenerates it. Never delete the lock file to "fix" this — that just moves the problem to the next person.
3. "Backend initialization required, please run tofu init"
The backend config changed, a new module was added, or you're in a fresh checkout. This is not a real error, just an uninitialized working directory:
tofu init
If you changed backends (e.g. local to S3), you'll need to migrate state:
tofu init -migrate-state
And if init complains the backend config differs from what's cached and you want to blow away the cached backend, tofu init -reconfigure. Use -migrate-state when you want to keep state, -reconfigure when you want to point at a fresh one.
4. "Invalid for_each argument" — unknown values
Error: Invalid for_each argument
The "for_each" value depends on resource attributes that cannot be
determined until apply.
This is the single most common structural error I see. for_each needs to know its keys at plan time, but you fed it a value that only exists after another resource is created — an ARN, a generated ID, a computed name.
The fix is to key the map on something static. Use the input you control, not the computed output:
# BAD: keys depend on a created resource's attribute
resource "aws_route53_record" "r" {
for_each = { for s in aws_subnet.this : s.id => s }
}
# GOOD: key on the static input you already know
resource "aws_route53_record" "r" {
for_each = var.subnet_defs # a map you defined
}
If you truly can't avoid it, a -targeted apply to create the upstream resource first, then a normal apply, is the escape hatch — but restructuring the keys is the real fix.
5. "Unsupported argument" / "Unsupported attribute"
Error: Unsupported argument
An argument named "foo" is not expected here.
Two usual causes. Either the provider version changed and renamed/removed the argument, or you're referencing an attribute that doesn't exist on that resource. Check the exact version you have and its docs:
tofu version
tofu providers
If a provider upgrade renamed things, pin the version you were on while you migrate:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
}
For "Unsupported attribute," run tofu state show <address> on the resource to see exactly which attributes it actually exposes — I've wasted real time guessing at attribute names that the provider simply renamed between versions.
6. Dependency cycle
Error: Cycle: module.a.aws_x.foo, module.b.aws_y.bar
Two resources (or modules) reference each other, directly or through a chain, so OpenTofu can't order them. Visualize it instead of squinting at HCL:
tofu graph | dot -Tsvg > graph.svg
The fix is to break the loop. Usually one of the two references can be replaced with a static value, moved into a separate resource (like an aws_security_group_rule broken out of the group), or resolved by passing a value in as a variable rather than reading it back. Self-referential security groups are the classic offender — split the ingress rule into its own resource.
7. "Provider produced inconsistent final plan"
Error: Provider produced an inconsistent final plan
... produced an invalid new value for .some_attr ...
This is a provider bug, not your HCL — the value the provider promised at plan time didn't match apply time. It's rarely something you can fix in config directly. Fastest mitigations, in order:
First, upgrade the provider — these are frequently patched:
tofu init -upgrade
If that doesn't help, tell OpenTofu to stop tracking the flapping computed attribute with ignore_changes, or drop lifecycle { ignore_changes = [some_attr] } on the resource. As a last resort, tofu apply -replace=<address> forces a clean recreate so the provider computes the attribute fresh. If it persists, it's worth an upstream issue with the provider.
8. Registry service discovery failure
Error: Failed to query available provider packages
could not connect to registry.opentofu.org
Network, proxy, or registry outage. First confirm it's reachable:
curl -sSf https://registry.opentofu.org/.well-known/terraform.json
If you're behind a corporate proxy, set HTTPS_PROXY and NO_PROXY before init. If the public registry is flaky or you need reproducible CI, configure a provider mirror and point OpenTofu at local packages:
tofu providers mirror ./tofu-mirror
Then reference that directory with a provider_installation { filesystem_mirror { ... } } block in your CLI config. A mirror also inoculates you against the next registry hiccup, which is why every serious CI pipeline I run has one.
When the message isn't in this list
Some errors are stack- or provider-specific and don't have a one-line fix. When I hit one that isn't obvious, I check my OpenTofu error library for the exact message before I start guessing, because reading the actual failure text carefully almost always beats trial-and-error re-applies.
Takeaway
Most tofu errors fall into a handful of buckets: locking (force-unlock), lock-file/checksum drift (providers lock with all platforms), uninitialized dirs (init / -migrate-state), unknown-value for_each (key on static inputs), version-renamed arguments (pin and read state show), cycles (tofu graph, then break the loop), provider plan bugs (init -upgrade / -replace), and registry outages (mirror). Learn to recognize the message and the fix is usually one command away. Keep a mirror and a pinned lock file and you'll pre-empt half of these before they ever fire.
Top comments (0)