terraform-troubleshooting
Skill0 starsChanged 2 months agoApache-2.0View on GitHub
Fix Terraform and OpenTofu errors - state lock timeouts, "resource already exists", provider authentication failures, cycle errors, "Invalid for_each argument", inconsistent final plan, failed applies that left state half-converged, and plans that will not converge. Use when a terraform command fails, hangs, or produces a diff that makes no sense.
In this repository26 other files it gives its agents.
- AGENTS.mdAGENTS.md
- aws-production-debuggingSkill
- github-actions-debuggingSkill
- postgresql-troubleshootingSkill
- container-image-reviewSkill
- docker-debuggingSkill
- incident-triageSkill
- postmortem-analysisSkill
- production-debuggingSkill
- kubernetes-networkingSkill
- kubernetes-production-readinessSkill
- kubernetes-resource-debuggingSkill
- kubernetes-troubleshootingSkill
- linux-network-debuggingSkill
- linux-performanceSkill
- linux-troubleshootingSkill
- dns-troubleshootingSkill
- http-debuggingSkill
- tls-debuggingSkill
- observability-debuggingSkill
- prometheus-troubleshootingSkill
- slo-designSkill
- secrets-management-reviewSkill
- technology-jobSkill
- terraform-reviewSkill
- terraform-safe-changesSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 4 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 101
Wait first. Most locks are a colleague's running apply, and force-unlocking one produces two concurrent writers and a corrupt state file — …
- Distinctive: line 104
If nothing is running, confirm it before unlocking.
- Distinctive: line 115
Check whether that CI job is still running.
- Distinctive: line 121
Always pass the specific ID.
- Very specific: line 274
Bad: Add ignore_changes to make a perpetual diff go away.
- Very specific: line 278
Bad: Delete .terraform and the lockfile when anything goes wrong.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Terraform Troubleshooting
Diagnose Terraform errors by working out which of the four layers is wrong: configuration, state, provider, or the real world.
Purpose
Use this when:
terraform planorapplyfails with an error.- An apply hangs, or fails with
Error acquiring the state lock. - The plan shows changes that make no sense, or never converges.
- An apply failed partway and you do not know what state is in.
Error: Invalid for_each argument/Provider produced inconsistent final plan/Cycle:.
Do not use this when:
- The plan is correct and you are deciding whether to apply it — use
terraform-review. - You are executing a normal change — use
terraform-safe-changes.
What this skill assumes: the exact error text. Terraform errors are specific and the message usually names the resource and the attribute; paraphrased errors are not diagnosable.
Operating Procedure
- Capture the exact error. Full text, resource address, and the command that produced it.
- Identify the layer. Configuration (HCL is wrong), state (Terraform's belief is wrong), provider (the plugin or its credentials are wrong), or reality (the cloud disagrees).
- Reproduce read-only.
terraform validate, thenplan, before touching anything. - Check the obvious environmental causes first: wrong workspace, expired credentials, stale provider cache, a concurrent run.
- Fix at the right layer. Configuration problems are fixed in code. State problems are fixed with state operations — and only after a state backup.
- Re-plan to confirm convergence.
- Record the cause; most Terraform errors recur across a team.
TF_LOG=DEBUG output is Observed evidence; your reading of an error message is an
Assumption until the log or the provider API confirms it.
Investigation
Step 1 - establish the basics
terraform version
terraform workspace show
terraform validate
terraform providersvalidate checks syntax and internal consistency without contacting any provider. If it fails, the
problem is entirely in the configuration and nothing else matters yet.
terraform providers shows which provider versions are actually selected — frequently different
from what the code appears to request, because the lockfile wins.
Step 2 - the error catalogue
| Error | Layer | Cause | Fix |
Error acquiring the state lock | State | Another run holds the lock, or a previous run died holding it | Wait; then verify no run is active and force-unlock the specific ID |
resource already exists / AlreadyExists | Reality | The resource exists but is not in state | import block, not a rename |
Provider produced inconsistent final plan | Provider | Provider bug: computed value differed between plan and apply | Upgrade or pin the provider; often fixed upstream |
Cycle: a -> b -> a | Configuration | Circular dependency, often via depends_on | Break the cycle; split the resource or use a data source |
Invalid for_each argument | Configuration | The map keys depend on values not known until apply | Key by static input, not by a computed attribute |
Invalid count argument | Configuration | Same, for count | Same |
NoCredentialProviders / InvalidClientTokenId | Provider | Missing or expired credentials | Re-authenticate; confirm the profile and region |
Error: Unsupported argument | Configuration | Argument removed or renamed in a newer provider | Read the provider changelog for the version in the lockfile |
Saved plan is stale | State | State changed since the plan was made | Re-plan; find out who else applied |
Backend configuration changed | State | Backend block edited | terraform init -reconfigure, or -migrate-state if moving |
context deadline exceeded on apply | Reality | The provider API is slow or rate limiting | Retry; check provider retry settings and API quotas |
| Objects have changed outside of Terraform | Reality | Drift | -refresh-only; decide adopt or revert |
Step 3 - state locks specifically
terraform plan -lock-timeout=10mWait first. Most locks are a colleague's running apply, and force-unlocking one produces two concurrent writers and a corrupt state file — a far worse problem than waiting.
If nothing is running, confirm it before unlocking. The error message prints the lock ID, who created it, and when:
Lock Info:
ID: 7a4e9c1e-...
Operation: OperationTypeApply
Who: ci-runner@build-4821
Created: 2026-09-11 09:14:02 UTCCheck whether that CI job is still running. Only when you have positively confirmed it is not:
terraform force-unlock 7a4e9c1e-...Always pass the specific ID. There is no safe "unlock whatever is stuck".
Step 4 - when the plan will not converge
A plan that shows the same change every time, even immediately after a successful apply, means something rewrites the attribute after Terraform sets it.
terraform apply
terraform plan -detailed-exitcode # exit 2 means it still wants to change something
terraform state show <address> | grep <attribute>Causes, in order of frequency:
- Another system owns the attribute — an autoscaler changes
desired_count, a controller adds tags, a service mesh mutates annotations. Fix:lifecycle { ignore_changes = [...] }on that specific attribute. - Provider normalisation — the API returns a canonical form (JSON key order, ARN case, a
trailing newline in a policy) that differs textually from your input. Fix: use the provider's
normalising function, for example
jsonencodeon IAM policy documents. - A genuine provider bug — check the provider's issue tracker for your exact version.
ignore_changes is the right answer only for case 1. Using it to silence case 2 or 3 hides real
drift.
Step 5 - recovering a failed apply
A failed apply leaves state reflecting whatever succeeded before the failure. Terraform is designed for this: it is not corruption, and the correct first move is to look, not to repair.
terraform state pull > "state-backup-$(date -u +%Y%m%dT%H%M%SZ).json"
terraform planThe new plan shows what remains to be done. In most cases, fixing the underlying cause and re-running apply is the complete answer. Reach for state surgery only when the plan itself proves state and reality disagree — for example, Terraform wants to create something that already exists.
Step 6 - provider-level debugging
TF_LOG=DEBUG TF_LOG_PATH=./tf-debug.log terraform plan
grep -E 'HTTP Response|StatusCode|error' tf-debug.log | head -40This shows the actual API calls and responses. Use it when the error message is generic
(context deadline exceeded, unexpected status 400) and you need to know what the provider asked
for and what the cloud said back.
The debug log contains request bodies and may contain credentials and secret values. Treat
tf-debug.log as sensitive: do not attach it to a public issue without redacting it.
Decision Tree
terraform command failed
├── terraform validate fails → configuration only; nothing else is relevant
├── Credential or auth error → provider layer; re-auth, check profile/region/role
├── "state lock" → wait; confirm no active run; force-unlock the named ID only
├── "already exists" → reality has it, state does not → import
├── "inconsistent final plan" / panic → provider bug → check version, upgrade or pin
├── Cycle / for_each / count errors → configuration; dependencies or unknown values
├── Apply failed partway
│ ├── Re-plan shows the remaining work → fix the cause, apply again
│ └── Re-plan wants to create existing resources → import them
└── Plan never converges
├── Another system mutates the attribute → ignore_changes on that attribute
├── Formatting/normalisation difference → use the canonical encoding
└── Neither → provider bugCommon Causes
| Cause | Signal | Fix | Risk |
| Concurrent CI run | Lock held by a build ID | Wait for it | read-only |
| Expired SSO session | InvalidClientTokenId | Re-authenticate | read-only |
| Provider upgraded silently | New replacements, no code change | Pin in required_providers and commit the lockfile | mutating |
| Manual console change | Drift in -refresh-only | Adopt into code | mutating |
Computed for_each key | Invalid for_each argument | Key by a static value | mutating |
| Autoscaler owns the count | Perpetual diff on desired_count | ignore_changes | mutating |
| Resource created outside Terraform | already exists | import block | mutating |
Safety
Read-only
terraform version, validate, providers, plan (takes the lock), show, state list,
state show, state pull, and TF_LOG debugging. None of these change infrastructure.
Potentially disruptive
terraform init -reconfigure/-migrate-state— changes where state lives.-migrate-statecopies state to a new backend; if it is interrupted you can end up with two partial states. Back up first.terraform applyto recover a failed apply — real changes. Re-review the plan; the situation has changed since the original review.lifecycle { ignore_changes }— silences a real diff permanently. If the attribute is not genuinely owned by another system, you have hidden drift rather than fixed it.terraform force-unlock <id>— see below; it is only safe with positive confirmation.
Destructive
terraform force-unlock <id>run while another apply is genuinely in progress produces two concurrent writers and can corrupt the state file, which is the hardest Terraform failure to recover from. Required first: positively confirm the holding process is dead (check the CI job, ask the named user), and use the exact ID from the error.terraform state rm <address>— removes the resource from state and leaves it running, unmanaged. Required first:terraform state pull > backup.json, and a concrete plan to re-import it. Never use this to make an error message go away.terraform state push— overwrites remote state wholesale. Required first: a backup of the current remote state and certainty that no one else has applied since your copy was taken. A bad push silently orphans every resource created since.terraform apply -replace=<address>on a stateful resource — destroys and recreates it, losing the data. Required first: a verified restore.
None of these should ever be the first response to an error. Every one of them is recovery, and every one needs the state backup taken before it runs.
Verification
Before: apply fails with "Error acquiring the state lock", 3 resources pending
After: apply completes; terraform plan -detailed-exitcode returns 0terraform plan -detailed-exitcode
terraform state list | wc -lExit code 0 from -detailed-exitcode is the strongest single signal that the problem is resolved:
configuration, state and reality agree. Exit 2 means something still differs — the fix is
incomplete, even if the error is gone.
For state operations, additionally confirm in the provider API that the resource count matches what state claims, and that nothing was orphaned.
Anti-Patterns
Bad: force-unlock because the lock has been there a few minutes.
Better: Find the holder. The error prints who and when. A CI apply that takes 20 minutes is
normal; breaking its lock corrupts state.
Bad: terraform state rm to clear an error.
Better: Understand the error. Removing from state abandons live infrastructure that will still
be billed, will not be managed, and will collide with the next apply.
Bad: Add ignore_changes to make a perpetual diff go away.
Better: Find out what rewrites the attribute. If nothing does, ignore_changes is hiding real
drift and you will not see the next unauthorised change either.
Bad: Delete .terraform and the lockfile when anything goes wrong.
Better: .terraform is a cache and deleting it is usually harmless; deleting
.terraform.lock.hcl silently upgrades every provider, which can turn a small error into a plan
full of replacements.
Bad: Re-run apply repeatedly hoping it converges. Better: Each failed apply can leave more partial state. Read the error, fix the cause once.
Composition
terraform-review— once the error is fixed, review the resulting plan before applying.terraform-safe-changes— for the apply itself, imports and moved blocks.aws-production-debugging— when the provider error reflects a real cloud-side problem such as quotas, IAM or service limits.
References
- Terraform: State — locking and recovery
- Terraform: Debugging —
TF_LOGlevels and what each contains. - Terraform:
terraform statecommands
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

