| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/local 2.9.0kreuzwerker/docker 3.9.0 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform and Docker — Terraform lab environment on Ubuntu |
| Privilege | Normal user with access to the Docker socket; sudo for the one lab step that writes under /etc |
| Scope | A stage-by-stage method for diagnosing Terraform failures — triage commands, init errors, plan errors, partial apply recovery, provider and authentication failures, state lock and backend problems, debug logging, and the habits that make an outage worse. Does not cover a full error-message encyclopedia, cloud vendor credential setup, or manual state file editing. |
| Related guides | Terraform debug logging with TF_LOG terraform validate Terraform state locking Terraform drift detection Terraform Associate certification course |
Terraform errors are usually specific, but they land in the middle of a run and it is not always obvious which part of the system produced them. A provider that will not download, a variable that was never set, a lock left behind by a colleague, and an API that rejects your credentials all look like "Terraform is broken" until you know which stage failed.
The fastest way to fix a Terraform failure is to identify the stage that produced it, because the stage tells you which command gives you evidence:
Installation? terraform version
Configuration? terraform fmt / terraform validate
Initialization? terraform init
Planning? terraform plan
Applying? terraform apply
State or backend? state list, lock information, backend settings
Need more detail? TF_LOGEvery error in this guide was reproduced on a lab host, one failure per directory, under ~/terraform-labs/terraform-troubleshooting/. Copy the broken configuration, watch the same message appear on your machine, then fix it.
Start with these Terraform checks
Before reading a stack of error text, run the cheap checks. Each one eliminates a whole category of cause, and together they take about ten seconds.
Start with the binary itself, because version-specific syntax and features are a common surprise on a shared machine:
terraform versionSample output:
Terraform v1.15.8
on linux_amd64That rules out "this feature does not exist in my version" and tells you which documentation applies. Next, check formatting, which also catches files that will not parse:
terraform fmt -checkSample output:
messy.tffmt -check lists files that are not canonically formatted and exits with status 3 rather than 0. Formatting is cosmetic on its own, but a file that fails to parse shows up here first, and a diff full of whitespace makes real errors harder to see. The terraform fmt lesson covers the flags in detail.
Formatting says nothing about whether the configuration makes sense, so validate it:
terraform validateSample output:
Success! The configuration is valid.Validation checks syntax, argument names, types, and references inside the module without contacting any provider API. A pass here means your problem is not a typo in a resource argument, which is worth knowing before you blame credentials. What validate can and cannot see is covered in the terraform validate lesson.
When the failure mentions a provider, ask Terraform which providers this configuration and state actually require:
terraform providersSample output:
Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/local] ~> 2.5
└── provider[terraform.io/builtin/terraform]
Providers required by state:
provider[registry.terraform.io/hashicorp/local]The source addresses are the important part. A wrong namespace here explains most "provider not found" failures, and a provider listed under state but not configuration explains resources Terraform can no longer manage.
Finally, when the directory is initialized and has state, list what Terraform believes it manages:
terraform state listSample output:
local_file.config
terraform_data.checkpointAn empty or missing state answers a different question than a full one: Terraform is not failing to update something, it never tracked it. Use only the checks that fit the situation. Running state list in an uninitialized directory just reports that no state file exists.
Troubleshoot terraform init errors
Init prepares the working directory before planning or applying infrastructure changes. It reads your configuration to resolve the backend, child modules, and provider dependencies it declares. Fixing an init failure never creates or changes the resources you declared, so nothing in your infrastructure is at risk while you work through this stage.
The most common one is a provider source address that does not exist. This configuration asks for hashicorp/docker, which is not where the Docker provider lives:
terraform initSample output:
Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider
hashicorp/docker: provider registry registry.terraform.io does not have a
provider named registry.terraform.io/hashicorp/docker
Did you intend to use kreuzwerker/docker? If so, you must specify that source
address in each module which requires that provider.Terraform even suggests the right namespace. The phrase "in each module" matters: a child module needs its own required_providers entry, because it inherits provider configuration but not source addresses. Correct the source value and run init again.
A version constraint that nothing satisfies produces a similar heading with a different reason:
terraform initSample output:
Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider
hashicorp/local: no available releases match the given constraints ~> 99.0Here the namespace is fine and the constraint is impossible. Read the constraint out loud before editing it. In this lab ~> 99.0 was a typo for ~> 2.9, and the fix belongs in required_providers, not in a deleted lock file. Constraint syntax and the lock file's role are covered in the provider version and lock file lesson.
Module sources fail at the same stage. This root module points at a local path that was never created:
terraform initSample output:
Error: Unreadable module directory
Unable to evaluate directory symlink: lstat modules: no such file or
directory
Error: Unreadable module directory
The directory could not be read for module "application" at main.tf:1.Local module errors are almost always a path typo or a directory that exists only on your machine. Remote sources fail differently. A bad Git ref or registry version reports the source it tried to fetch, which the module sources and versions lesson walks through.
The lock file produces its own family of init complaints. Adding a provider to the configuration without re-running init gives this:
terraform planSample output:
Error: Inconsistent dependency lock file
The following dependency selections recorded in the lock file are
inconsistent with the current configuration:
- provider registry.terraform.io/hashicorp/random: required by this configuration but no version is selected
To update the locked dependency selections to match a changed configuration,
run:
terraform init -upgradeDo not delete .terraform.lock.hcl just because dependency requirements changed. Deleting it makes the error go away while throwing out the recorded versions and checksums your team relies on. Terraform suggests -upgrade here, but a newly added provider does not need it. Plain init records a selection for the provider that is missing one:
terraform initSample output:
- Reusing previous version of hashicorp/local from the dependency lock file
- Finding hashicorp/random versions matching "~> 3.6"...
- Using previously-installed hashicorp/local v2.9.0
- Installing hashicorp/random v3.9.0...
- Installed hashicorp/random v3.9.0 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file.Read the first line as carefully as the last: the existing hashicorp/local selection was reused, and only the new provider was added to the lock file. That distinction decides which command you want:
| Situation | What to run |
|---|---|
| A newly added provider has no entry in the lock file | terraform init |
| You deliberately changed a constraint, or you want newer versions within the existing ones | Review the constraint, then terraform init -upgrade |
| The lock file looks wrong and you are tempted to remove it | Neither; find out which dependency changed first |
-upgrade tells Terraform to disregard current selections and reconsider every provider and module that its constraints allow, so it can move dependencies you never intended to touch. The same inconsistency error appears in a directory that was never initialized at all, so run terraform init first when you see it on a fresh clone.
Backends are the last thing init handles. Adding a backend block to a configuration that was already initialized stops the next command immediately:
terraform planSample output:
Error: Backend initialization required, please run "terraform init"
Reason: Initial configuration of the requested backend "local"Terraform refuses to run because it does not know whether you want the existing state copied into the new backend. Re-running init prompts you to migrate, and -migrate-state or -reconfigure answers that question non-interactively. Backend configuration and migration are covered in the backends and remote state lesson.
| Symptom at init | What to check | Usual cause |
|---|---|---|
does not have a provider named … |
source in every required_providers block |
Wrong namespace, often hashicorp/ on a community provider |
no available releases match the given constraints |
The version constraint you wrote | Typo, or a constraint pinned to a version that was never published |
| Network or TLS failure during download | Registry reachability and proxy settings | Firewall, proxy, or air-gapped host without a mirror |
Unreadable module directory |
The source path and whether it is committed |
Local path typo or a directory missing from version control |
Inconsistent dependency lock file |
Whether a provider was added or a constraint changed on purpose | Recorded selections no longer match requirements; which init flag you need depends on which happened |
Backend initialization required |
Whether a backend block was added or edited |
Backend changed and state migration has not been decided yet |
Troubleshoot terraform plan errors
Plan is where Terraform evaluates variables and references, builds the dependency graph, and normally refreshes existing managed objects through their providers. A plan does not execute its proposed create, update, or delete actions, but it can still fail on provider authentication, connectivity, or read permissions while refreshing existing infrastructure. The failures below are the configuration-side ones; the provider-side ones come in the next section.
A required variable with no value is the classic one. Add -input=false so Terraform fails instead of prompting, which is also how it behaves in CI:
terraform plan -input=falseSample output:
Error: No value for required variable
on main.tf line 1:
1: variable "environment" {
The root module input variable "environment" is not set, and has no default
value. Use a -var or -var-file command line argument to provide a value for
this variable.The fix is a -var, a .tfvars file, a TF_VAR_ environment variable, or a default in the declaration. The Terraform variables lesson explains which one wins when several are present. If this only fails in automation, the value probably comes from a shell profile that the pipeline does not load.
Misspelled references fail the same way whether you run validate or plan, so use validate because it is faster:
terraform validateSample output:
Error: Reference to undeclared resource
on main.tf line 6, in resource "terraform_data" "second":
6: input = terraform_data.frist.output
A managed resource "terraform_data" "frist" has not been declared in the root
module.Terraform quotes the exact line, and the typo is usually visible in it. When the name looks correct, check that the resource is in the same module. References do not cross module boundaries, and a child module value has to come out through an output.
Wrong argument names produce a similar diagnostic with a suggestion attached:
terraform validateSample output:
Error: Unsupported argument
on main.tf line 11, in resource "local_file" "config":
11: contents = "wrong argument name\n"
An argument named "contents" is not expected here. Did you mean "content"?This is a provider schema mismatch, not a Terraform bug. It also appears after a provider upgrade removes or renames an argument, in which case the provider's changelog is the place to look.
Dependency cycles are the plan error people find hardest to read, because nothing is misspelled. These two resources each want the other to exist first:
resource "terraform_data" "alpha" {
input = terraform_data.beta.output
}
resource "terraform_data" "beta" {
input = terraform_data.alpha.output
}Terraform cannot put those in any order, and it says so as soon as it builds the graph:
terraform planSample output:
Error: Cycle: terraform_data.beta, terraform_data.alphaTerraform lists every resource in the loop. Break it by removing one direction of the reference, usually by passing a static value or moving the shared value into a local. Implicit dependencies created by references and explicit ones created by depends_on are covered in the resource dependencies lesson, and terraform graph helps when the cycle spans more nodes than you can hold in your head.
Refresh is the part of plan that talks to providers, so you can take it out of the equation while you isolate a failure. In another lab directory a local_file object was deleted outside Terraform, and a normal plan proposes to create it again. Ask the same question without reading the real object:
terraform plan -refresh=falseSample output:
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.Terraform compared the configuration against state alone and reported nothing to do, even though the file is gone. That makes -refresh=false useful for narrowing a failure to the refresh step or reducing refresh-related API calls while you diagnose provider connectivity. It is a diagnostic tool, not a guarantee that planning will succeed when the provider or remote service is unavailable. Treat it as a diagnostic rather than a fix, because the answer it gives you skips exactly the reality check you wanted.
One plan behavior surprises people more than any error message: a plan over an empty state has almost nothing to refresh, so it can pass without exercising the API operations that apply will need. That is why the next section separates configuration failures from provider failures.
Troubleshoot terraform apply failures
The single most important fact about apply is that it is not a database transaction. When it fails halfway, the resources it already created still exist and are already recorded in state. Nothing rolls back.
This lab configuration creates a file, then runs a provisioner that checks for a file that does not exist:
resource "local_file" "first" {
content = "created before the failure\n"
filename = "${path.module}/first.txt"
}
resource "terraform_data" "second" {
input = "depends on the file above"
provisioner "local-exec" {
command = "test -f /etc/tf-troubleshoot-required.conf"
}
depends_on = [local_file.first]
}The depends_on guarantees the file resource is created first, so the run gets halfway before it breaks:
terraform apply -auto-approveSample output:
local_file.first: Creating...
local_file.first: Creation complete after 0s [id=1e71a7df09aa81a7a57bce8c734a02379e6c201f]
terraform_data.second: Creating...
terraform_data.second: Provisioning with 'local-exec'...
Error: local-exec provisioner error
with terraform_data.second,
on main.tf line 18, in resource "terraform_data" "second":
Error running command 'test -f /etc/tf-troubleshoot-required.conf': exit
status 1. Output:Terraform names the resource, the line, and the command that failed. Before changing anything, ask what actually happened to your infrastructure:
terraform state listSample output:
local_file.first
terraform_data.secondBoth resources are in state even though the run failed. The first was created successfully; the second exists but its provisioning did not complete. Plan again to see how Terraform describes that difference:
terraform planSample output:
# terraform_data.second is tainted, so must be replaced
-/+ resource "terraform_data" "second" {
~ id = "5a1b7737-45dd-c944-04f2-db29bbdbe662" -> (known after apply)
~ output = "depends on the file above" -> (known after apply)
}
Plan: 1 to add, 0 to change, 1 to destroy.The word to notice is tainted. Terraform marks a resource tainted when creation succeeded but provisioning failed, and the next apply replaces it. The file created before the failure is not mentioned, because it is correct and needs no action.
Now fix the underlying cause rather than the symptom. Here the provisioner needed a file that did not exist:
sudo touch /etc/tf-troubleshoot-required.confWith the real problem solved, apply again and let Terraform finish only the work that remains:
terraform apply -auto-approveSample output:
terraform_data.second: Provisioning with 'local-exec'...
terraform_data.second: Creation complete after 0s [id=fe7d030e-cf96-95ba-f087-bd6503353185]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.One resource replaced, nothing else disturbed. Read the error, list state, plan, fix the cause, then apply. That sequence is the whole recovery procedure for a failed apply.
-auto-approve in a loop when an apply fails partway. State is the only record of what was already created; deleting it turns a one-resource problem into orphaned infrastructure that Terraform will try to create a second time.
Troubleshoot provider and authentication problems
Provider failures are easy to mistake for configuration failures because both print as Terraform errors. The difference is who generated the message: Terraform Core complains about your files, while a provider complains about an API call.
Start by confirming which plugins are actually in play:
terraform providersSample output:
Providers required by configuration:
.
└── provider[registry.terraform.io/kreuzwerker/docker] ~> 3.0That confirms the source and constraint Terraform resolved, which is the first thing to check when a provider behaves unlike its documentation. This lab points the Docker provider at a socket path that does not exist, standing in for a wrong endpoint or expired credentials in a cloud provider:
terraform planSample output:
Plan: 1 to add, 0 to change, 0 to destroy.The plan is clean. Nothing in it warns you that the endpoint is unreachable, because this configuration has nothing in state to refresh, so the plan never exercises the API operation that creation needs. The failure appears when Terraform tries to do the work:
terraform apply -auto-approveSample output:
docker_volume.cache: Creating...
Error: failed to create Docker client: Error pinging Docker server, please
make sure that unix:///var/run/tf-troubleshoot-missing.sock is reachable and
has a '_ping' endpoint. Error: Cannot connect to the Docker daemon at
unix:///var/run/tf-troubleshoot-missing.sock. Is the docker daemon running?
with docker_volume.cache,
on main.tf line 14, in resource "docker_volume" "cache"Read the shape of that message rather than the Docker specifics. It names an endpoint, a connection attempt, and a service, none of which are Terraform concepts. Compare the two categories:
- Terraform Core errors quote a file and line number, and use language about arguments, references, variables, and modules
- Provider errors quote an API endpoint, a status code, a permission, or a service name, and often repeat the provider's own wording verbatim
- Both attach a
with <resource address>block, so the address alone does not tell you which layer failed
When Terraform identifies a provider-generated error, inspect both the provider configuration and the external system. The cause may be a wrong endpoint or region in the provider block, missing environment credentials, insufficient API permissions, an unavailable service, or a quota or resource conflict. This lab is a good reminder that the provider block itself can be the culprit: the unreachable socket was written in main.tf, not in the environment. If authentication is involved, confirming the same identity with the vendor's own CLI can help separate credential problems from Terraform provider configuration problems. Provider blocks, aliases, and version pinning are covered in the Terraform providers lesson.
Troubleshoot state and backend problems
State problems announce themselves clearly, and the right response is almost always to slow down rather than to reach for a forceful command.
A run that cannot take the lock reports exactly who is holding it. This output came from a plan started while another apply was still running in the same directory:
terraform planSample output:
Error: Error acquiring the state lock
Error message: resource temporarily unavailable
Lock Info:
ID: 885264e3-5e26-1423-f39c-3ba407efb964
Path: terraform.tfstate
Operation: OperationTypeApply
Who: root@golinuxcloud
Version: 1.15.8
Created: 2026-08-12 05:47:47.873004789 +0000 UTCEvery field is a clue. Operation says an apply holds the lock, Who names the user and host, and Created tells you how long it has been held. In this case another run was genuinely in progress, so the answer was to wait for it:
terraform planSample output:
No changes. Your infrastructure matches the configuration.The lock released itself when the apply finished. That is the normal outcome, and it is why force-unlock should never be your first move. Breaking a lock that a live run still holds can corrupt state. Reach for it only after confirming that no process is running against that state anywhere, as described in the state locking lesson.
Backend problems fall into three shapes worth recognizing:
- Backend not initialized — a
backendblock was added and Terraform stops until init decides what to do with existing state - Backend configuration changed — the path, bucket, key, or workspace differs from the last init, so Terraform asks you to migrate or reconfigure
- Backend unreachable — credentials, network, or permissions fail on the storage side, which the error attributes to the backend rather than to a resource
Drift is the last state-related surprise, and it usually looks like an error even though it is not one. Deleting the file behind a local_file resource out of band leaves state claiming it exists:
terraform planSample output:
local_file.config: Refreshing state... [id=d521316470633385988611b1296715364598fce2]
# local_file.config will be created
+ resource "local_file" "config" {
+ filename = "./app.conf"
}
Plan: 1 to add, 0 to change, 0 to destroy.Terraform refreshed, found nothing, and planned to create the object again. A plan proposing changes you did not write is the signal to investigate reality before applying, which the drift detection and refresh-only lesson covers properly. When you need to inspect or repair specific entries, the read-only subcommands in the terraform state commands lesson come first. Treat state rm and state push as repair tools, not diagnostics.
Use Terraform logs when the error is not enough
Sometimes the error summarizes away the detail you need: a provider swallows an API response, or a request goes somewhere you did not expect. The debug stream sits underneath the human-readable output and is off by default.
Turn it on for a single command rather than exporting it, and filter for the plugin that is failing:
TF_LOG=DEBUG terraform apply -auto-approve -no-color 2>&1 | grep 'provider.terraform-provider-docker' | tail -2Sample output:
2026-08-12T11:22:24.091+0530 [DEBUG] provider.terraform-provider-docker_v3.9.0: plugin address: network=unix address=/tmp/plugin685174577
2026-08-12T11:22:24.115+0530 [ERROR] provider.terraform-provider-docker_v3.9.0: Response contains error diagnostic: @caller=...tfprotov5/internal/diag/diagnostics.goNow you can watch the plugin start, take the request, and return a diagnostic. That is the layer the summarized error hides. For anything longer than a few seconds, write the stream to a file so you can search it repeatedly:
TF_LOG=DEBUG TF_LOG_PATH=terraform-debug.log terraform apply -auto-approveThe command behaves normally on screen while the diagnostics go to the file. Check how much you captured before you start reading:
wc -l terraform-debug.logSample output:
102 terraform-debug.logA hundred lines for one failed resource is typical, and almost all of it is routine startup noise. Jump straight to the severity that matters:
grep ERROR terraform-debug.logSample output:
2026-08-12T11:22:24.582+0530 [ERROR] provider.terraform-provider-docker_v3.9.0: Response contains error diagnostic: diagnostic_severity=ERROR tf_proto_version=5.10
2026-08-12T11:22:24.587+0530 [ERROR] vertex "docker_volume.cache" error: failed to create Docker client: Error pinging Docker server, please make sure that unix:///var/run/tf-troubleshoot-missing.sock is reachableTwo lines out of a hundred carry the diagnosis: one where the provider returned the error and one where Terraform's graph recorded which node failed. Log levels, splitting Core from provider output, and scanning a log for secrets before you share it are covered in the Terraform debug logging lesson.
Common mistakes that make troubleshooting worse
Most Terraform incidents get worse in the fifteen minutes after the first error, when someone starts trying things. These are the ones that turn a small failure into a long afternoon:
- Deleting
.terraform.lock.hclbecause init complained, instead of reading which dependency changed - Deleting state to "start clean", which orphans every object Terraform was managing
- Running
force-unlockon a lock that a live run still holds - Using
-targetas routine recovery, which leaves the rest of the graph unconverged - Approving a plan without reading it because the previous plan looked fine
- Pasting a debug log into a ticket without checking it for credentials
- Changing three things at once, so the next run cannot tell you which change helped
Work in the opposite direction instead. Escalate one step at a time and stop as soon as the evidence identifies the cause:
Read the error message in full
→ reproduce it deliberately
→ validate the configuration
→ isolate the failing stage
→ inspect state and provider requirements
→ enable TF_LOG=DEBUG
→ consult the provider or backend documentationThe discipline that matters most is changing one thing per run. Terraform gives you a fresh, complete diagnosis on every command, and that feedback is only useful if you know which edit produced it.
Clean up the lab
Three of the lab directories created real objects, and one scenario touched a file outside the lab tree. Destroy the managed resources first:
for d in baseline apply-partial state-lock; do (cd ~/terraform-labs/terraform-troubleshooting/$d && terraform destroy -auto-approve); doneRemove the file the provisioner scenario required, since nothing else on the host uses it:
sudo rm -f /etc/tf-troubleshoot-required.confConfirm no Docker objects were left behind by the provider scenario, which never managed to connect:
docker volume ls --filter name=tf-troubleshoot --format '{{.Name}}'Empty output means the failed apply created nothing, which is what you expect when the provider could not reach its endpoint. The remaining directories hold only broken configuration files, so delete the tree whenever you are finished experimenting with them.
References
- Terraform CLI documentation
- Debugging Terraform
- terraform init command
- terraform validate command
- terraform force-unlock command
- Provider requirements
Summary
Terraform troubleshooting is a sorting problem before it is a fixing problem. Each stage owns a category of failure: init resolves providers, modules, and backends; validate checks configuration consistency; plan evaluates the graph and normally refreshes existing infrastructure; apply executes the planned infrastructure operations; and state and backend errors are about coordination rather than code. Once you know which stage produced the message, you know which command gives you evidence and which files are worth reading.
Two behaviors account for a large share of confusing sessions. A plan normally refreshes existing objects and can fail on credentials or connectivity, but a plan over an empty state has almost nothing to refresh, so it may not exercise the operations apply needs. The endpoint failure in this lab appeared only at apply. And a failed apply is not rolled back: the file created before the provisioner failed stayed on disk and in state, while the failed resource was marked tainted for replacement. Reading terraform state list and one more plan told the whole story in two commands.
Resist the forceful shortcuts. Deleting the lock file, deleting state, and breaking a live lock all remove information you were about to need, and -target hides the dependency problem you are trying to find. Escalate gently instead: read the error, reproduce it, validate, isolate the stage, inspect state and providers, and only then turn on TF_LOG=DEBUG and take the evidence to the provider's documentation. Change one thing per run so the next error means something.

