hashicorp/terraform
Debugging
How to figure out what Terraform is doing when something goes wrong.
Logs
Terraform uses the standard log package internally with severity prefixes ([TRACE], [DEBUG], [INFO], [WARN], [ERROR]). The TF_LOG environment variable controls which severities are emitted:
TF_LOG=trace terraform plan
TF_LOG=debug terraform applyOther useful log envs:
| Variable | Purpose |
|---|---|
TF_LOG |
Master switch; one of TRACE, DEBUG, INFO, WARN, ERROR. |
TF_LOG_CORE |
Same severities, applies only to Terraform Core (excludes plugin output). |
TF_LOG_PROVIDER |
Same severities, applies only to provider plugins. |
TF_LOG_PATH |
If set, all logs (regardless of severity routing) are written to this file. |
The plumbing lives in internal/logging/. The [INFO] lines emitted at startup include the Terraform version, Go runtime version, and the parsed CLI args.
Crash logs
When terraform crashes, it writes a goroutine dump to crash.log in the working directory. The handler is set up in internal/logging/panic.go. The first thing reviewers ask for in a panic-fix PR is the contents of crash.log from the user's report.
Plugin panics are captured separately (logging.PluginPanics() in main.go) and re-emitted at the end of the run with a [ERROR] line.
Reattach: debug an in-progress provider
Terraform can connect to an already-running provider process via TF_REATTACH_PROVIDERS. This is how the SDK's acceptance-test framework runs providers in-process and how Delve attaches to a provider for breakpoints.
The handshake is parsed by internal/getproviders/reattach/. Format:
export TF_REATTACH_PROVIDERS='{
"registry.terraform.io/hashicorp/aws": {
"Protocol":"grpc","ProtocolVersion":6,
"Pid":12345,"Test":true,
"Addr":{"Network":"unix","String":"/tmp/plugin1234"}
}
}'
terraform planThe provider must already be listening on the address. Practical use: run the provider under dlv exec --headless --listen=:2345 and set TF_REATTACH_PROVIDERS accordingly; you can then debug the provider while the host CLI runs against it.
Common error shapes
| Symptom | Likely cause | Where to look |
|---|---|---|
Error: Unsupported argument |
Schema parsing or required_providers mismatch |
internal/configs/, provider schema response handling |
Provider produced inconsistent final plan |
Provider's apply diverges from its plan | The provider's PlanResourceChange/ApplyResourceChange logic, plus Core's reconciliation in internal/plans/objchange/ |
Error: Inconsistent dependency lock file |
Lock file out of date | .terraform.lock.hcl, internal/depsfile/ |
Error: Backend initialization required |
The user's backend changed | internal/command/meta_backend.go migration paths |
| Panic with "splat reference" or similar | HCL evaluation error | internal/lang/eval.go, internal/lang/scope.go |
| Hang during plan | A vertex in the graph waiting on another | TF_LOG=trace, look for the Walk: ready ... lines in internal/terraform/graph_walk_*.go |
Error: Provider configuration not present |
Provider transformer didn't attach a config | internal/terraform/transform_provider.go |
Writing log statements
Use the standard log.Printf with the severity prefix; do not use fmt.Print-family for diagnostics:
log.Printf("[TRACE] PlanGraphBuilder: building graph for %s", absModuleAddr)Trace-level messages should be safe to leave on permanently — they don't render to users unless TF_LOG is set. Spammy traces inside hot loops should be guarded or sampled.
Diagnostics vs errors
Terraform Core almost never returns a bare error. The convention is tfdiags.Diagnostics (see patterns and conventions). When chasing a bug:
- For user-visible messages, look at where a
tfdiags.Diagnosticis appended. The summary string is grep-able. - For internal panics, the first frame in the stack from
internal/is usually where the issue is.
The internal/command/format package
Every diagnostic the user sees passes through internal/command/format/, which colorizes output, wraps to terminal width, and renders source snippets when given an *hcl.File cache. When debugging diagnostic output, set Disable: true on the colorstring.Colorize to get raw text.
The view layer
CLI commands write to a view, not directly to streams. Each major command has a views.Plan, views.Apply, etc., interface with Human and JSON implementations. Looking at the Human view tells you what shows up in plain text; the JSON view tells you what -json produces. Implementation lives in internal/command/views/.
Tools for binary inspection
go test -race ./internal/terraform/... # data-race detection
go test -count=10 ./internal/configs/... # repeat to surface flaky tests
go test -timeout=5m ./internal/... # default is 10m; tighter helps catch hangs
go tool pprof http://... # not enabled by default; needs a debug buildOpenTelemetry traces
Terraform emits OpenTelemetry spans for command execution (see main.go openTelemetryInit and telemetry.go). The exporter is opt-in via the TERRAFORM_OPENTELEMETRY_EXPORTER env var. Useful for tracking down where time goes inside a long plan or apply.
Quick reproductions
When filing or fixing a bug, the smallest reproduction is usually a fixture under internal/terraform/testdata/<bug-name>/ plus a one-page test in the appropriate context_*_test.go. The codebase already has hundreds; pattern-match against an existing test for a similar scenario.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.