Open-Source Wikis

/

Terraform

/

How to contribute

/

Debugging

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 apply

Other 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 plan

The 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.Diagnostic is 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 build

OpenTelemetry 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.

Debugging – Terraform wiki | Factory