Back to Blog

Debugging Terraform with TF_LOG for the Terraform Associate Exam: Log Levels, crash.log, Provider Logs & Reading Common Errors

When plan and apply aren't enough, you reach for logs. A practitioner's guide to debugging Terraform — the TF_LOG log levels, splitting core and provider logs with TF_LOG_CORE and TF_LOG_PROVIDER, persisting output with TF_LOG_PATH, what crash.log contains, and how to read the errors the Terraform Associate (003/004) exam keeps asking about.

By Sailor Team , September 22, 2026

Most of the time, terraform plan and terraform apply tell you everything you need: what will change, what changed, and what failed. But sooner or later you hit the run where the error message is cryptic, the provider does something you can’t explain, or Terraform simply crashes. That’s when you stop reading the pretty output and start reading the logs — the raw, verbose trace of what Terraform’s core and the provider plugins are actually doing under the hood.

Debugging is a small but very real part of operating Terraform, and the HashiCorp Terraform Associate exam expects you to know how to turn logging on, where the output goes, and how to interpret the handful of errors every practitioner eventually meets. This guide covers exactly that: the TF_LOG family of environment variables, the meaning of each log level, crash.log, and a field guide to the most common Terraform errors and how to fix them.

This post is the diagnostics companion to the everyday CLI. For the commands themselves — fmt, validate, console, state — see the Terraform commands cheat sheet; for evaluating expressions interactively, the functions and expressions guide covers terraform console in depth. Here we focus on what those commands don’t show you: the logs.

When Normal Output Isn’t Enough

Terraform’s standard output is deliberately human-friendly. It hides the machinery: the gRPC calls between Terraform core and each provider, the DAG (dependency graph) walk, the state refresh requests, the retries. Ninety-nine percent of the time you want it hidden. The other one percent, you need to see it — typically when:

  • A provider returns an error that doesn’t match anything in your configuration (“why is it calling that API?”).
  • plan and apply disagree, or an apply fails partway through and you need to know exactly which resource and which request broke.
  • A run hangs, and you want to see the last request before it stalled.
  • Terraform crashes with a Go panic and a backtrace.
  • You’re filing a bug report and the maintainers ask for a debug log.

The tool for all of these is the TF_LOG environment variable. It is not a command-line flag — it’s an environment variable you export before running Terraform, which means it applies to whatever terraform command you run next.

Turning On Logging with TF_LOG

Set TF_LOG to a log level and run any Terraform command. The logs stream to stderr:

export TF_LOG=TRACE
terraform apply

To turn logging back off, unset it:

unset TF_LOG

For a one-off run without changing your shell environment, prefix the command:

TF_LOG=DEBUG terraform plan

That single-command form is the one to reach for most of the time — it captures logs for exactly one run and leaves your shell clean afterward, so you don’t accidentally generate gigabytes of TRACE output on every subsequent command.

The Log Levels, From Loudest to Quietest

TF_LOG accepts five levels. They are ordered by verbosity — each level includes everything more severe than itself:

LevelVerbosityWhat it surfaces
TRACEHighestEverything: every gRPC call to providers, graph walking, state operations, internal decisions. The default when TF_LOG is set to an unrecognized value.
DEBUGHighDetailed operational messages — useful without the full RPC firehose.
INFOMediumHigh-level informational messages about what Terraform is doing.
WARNLowWarnings only.
ERRORLowestErrors only.

A few things worth committing to memory:

  • TRACE is the most complete and the one HashiCorp treats as fully supported for debugging. The other levels are best-effort and their exact output can change between versions. If you’re chasing a real bug, use TRACE.
  • If you set TF_LOG to any value Terraform doesn’t recognize — say TF_LOG=1 or TF_LOG=yes — it falls back to TRACE. So there’s no way to “accidentally” get a quiet log by typo; you get the loudest one.
  • Logs are not part of Terraform’s stable, machine-readable interface. Don’t parse them in automation. For structured, parseable output use the -json flag on plan/apply instead — that’s a separate, supported feature.

Splitting Core and Provider Logs

By default TF_LOG sets the level for both Terraform core and every provider plugin, which is a lot of noise. Since Terraform 0.15 you can target them separately:

VariableControlsTypical use
TF_LOGBoth core and providersBroad debugging
TF_LOG_CORETerraform core only”Is this a graph/state/core problem?”
TF_LOG_PROVIDERProvider plugins only”Is this the AWS/Azure/GCP provider misbehaving?”

This split is the single biggest quality-of-life improvement when debugging. If a resource behaves strangely, set TF_LOG_PROVIDER=TRACE and leave core quiet — now you see only the provider’s API conversation:

TF_LOG_PROVIDER=TRACE terraform apply

Conversely, if plan produces a dependency cycle or a strange ordering, TF_LOG_CORE=TRACE shows you core’s graph walk without the provider’s HTTP chatter drowning it out. When both TF_LOG and one of the specific variables are set, the more specific one wins for its component.

Persisting Logs to a File with TF_LOG_PATH

TRACE output scrolls off the terminal in seconds. To keep it, point TF_LOG_PATH at a file. It requires TF_LOG (or one of the core/provider variants) to also be set — the path variable only tells Terraform where to write, not whether to write:

export TF_LOG=TRACE
export TF_LOG_PATH=./terraform-debug.log
terraform apply

Now the human-readable summary still appears in your terminal, and the full trace lands in terraform-debug.log for you to grep, diff, or attach to a bug report. This is the standard recipe when you need to reproduce an issue and share it: set the level, set the path, reproduce, then read the file.

# Find the failing RPC call quickly
grep -i "error" terraform-debug.log | head

crash.log: When Terraform Panics

There’s a difference between Terraform reporting an error and Terraform crashing. An error is expected and handled — bad credentials, an invalid argument, a locked state. A crash is an unexpected Go panic inside Terraform core or a provider, and it’s a bug, not a misconfiguration.

When Terraform core panics, it writes a file called crash.log to the current working directory. That file contains the panic message and the full Go stack trace (backtrace) at the moment of the crash. You are not expected to fix the crash yourself — you’re expected to:

  1. Capture more context by re-running with TF_LOG=TRACE and TF_LOG_PATH set, if you can reproduce it.
  2. File an issue with the Terraform (or provider) maintainers.
  3. Attach crash.log and the trace log to the report.

Provider crashes are handled similarly — a provider plugin that panics produces its own crash output. The key exam-relevant fact: crash.log is generated automatically on a panic, it lives in the current directory, and it exists so maintainers can diagnose the bug. It is not something you commit or keep; treat it as a bug-report artifact.

A Field Guide to Common Terraform Errors

Most “why did Terraform do that?” moments never need TRACE logs — they need you to recognize a familiar error. Here are the ones you’ll see most, and the exam loves to describe as scenarios.

”Error acquiring the state lock”

Terraform locks state during any operation that could write to it, so two applies can’t corrupt the file. If a previous run was killed (Ctrl-C at the wrong moment, a crashed CI job, a dropped network connection to the backend), the lock can be left behind:

Error: Error acquiring the state lock
Lock Info:
  ID:        4d1f...
  Operation: OperationTypeApply

The fix is terraform force-unlock <LOCK_ID>, using the ID from the message — but only after you’re certain no other apply is genuinely running. Force-unlocking a live operation is how you corrupt state. When it’s a stale lock, it’s safe and routine. (The commands cheat sheet lists this under recovery.)

”Cycle: …”

A dependency cycle means resource A depends on B and B depends (directly or transitively) on A, so Terraform can’t order the graph:

Error: Cycle: aws_security_group.a, aws_security_group.b

Visualize it with terraform graph, then break the loop — often by splitting a rule out of a resource (for example, using a standalone aws_security_group_rule instead of inline rules that reference each other), or by removing an unnecessary depends_on.

”value depends on resource attributes that cannot be determined until apply”

This is the classic count/for_each trap. The number of instances (count) or the set of keys (for_each) must be known at plan time. If you derive them from an attribute that only exists after another resource is created — an ID, an ARN, a computed value — Terraform can’t plan:

Error: Invalid count argument
  The "count" value depends on resource attributes that cannot be
  determined until apply.

Fixes, in order of preference: base count/for_each on input variables or locals that are known up front rather than computed attributes; or, when you genuinely must create the dependency first, do a two-stage apply with -target to build the upstream resource, then apply normally. The count vs for_each guide covers how to choose the meta-argument in the first place.

”Provider produced inconsistent result after apply”

This one is usually not your fault. It means the provider told Terraform it would set a value one way during plan, then set it differently after apply — a provider bug. Note the resource and attribute, check whether upgrading the provider fixes it, and if not, report it upstream (this is a good time to capture a TF_LOG_PROVIDER=TRACE log).

”Unsupported argument” / “Unsupported block type”

Almost always a version mismatch: your configuration uses an argument that doesn’t exist in the provider version you actually have installed. Check your required_providers version constraints and the lock file, and confirm what got installed. The providers guide explains version constraints and how the provider block resolves them.

Backend and init errors

When you change a backend configuration, terraform init may refuse to proceed and ask you to reconcile:

  • terraform init -reconfigure — ignore the existing backend config and start fresh (don’t migrate state).
  • terraform init -migrate-state — move the existing state to the new backend.

Reach for these when init complains that “Backend configuration changed.” The state management guide covers how backends and state fit together.

A Debugging Workflow You Can Reuse

Put it together into a repeatable routine rather than flailing at TRACE logs every time:

SymptomFirst move
Cryptic provider errorTF_LOG_PROVIDER=TRACE, reproduce, read the last API call
Strange ordering / cycleterraform graph, then TF_LOG_CORE=TRACE if needed
Run hangsTF_LOG=TRACE + TF_LOG_PATH, look at the final request
Terraform panicsGrab crash.log, re-run with TRACE, file a bug
count/for_each won’t planMove the value to a variable/local, or two-stage with -target
Stale state lockterraform force-unlock <ID> (verify nothing is running)
An expression returns the wrong valueterraform console to test it in isolation

The general principle: narrow before you widen. Use the most specific log variable (TF_LOG_PROVIDER or TF_LOG_CORE) before the broad TF_LOG, persist with TF_LOG_PATH so you can read at leisure, and always try to reproduce with logging on rather than guessing after the fact.

Debugging in CI/CD Pipelines

In automation, the same variables apply — you just set them in the pipeline environment. A useful pattern is to keep runs quiet by default and flip on debug logging only when a job fails and you re-run it:

# Enable verbose logs for a single re-run by setting a pipeline variable
env:
  TF_LOG: ${{ vars.TF_DEBUG == 'true' && 'TRACE' || '' }}
  TF_LOG_PATH: ${{ vars.TF_DEBUG == 'true' && 'terraform.log' || '' }}

Then archive terraform.log (and crash.log, if present) as a build artifact so you can inspect a failure without shell access to the runner. Never leave TF_LOG=TRACE on for every pipeline run — trace logs are enormous, slow down runs, and can leak sensitive values that appear in provider requests into your CI logs.

Security note: debug logs can contain sensitive data — tokens in request headers, secrets in resource payloads. Treat any TF_LOG output like a secret: don’t paste it into public issues without scrubbing, and don’t store it in world-readable CI artifacts.

How the Exam Tests This

The Terraform Associate exam won’t ask you to read a raw TRACE dump, but it does test the surrounding knowledge in recognizable ways:

  • Enabling logs. “How do you enable verbose logging for a single Terraform run?” → set the TF_LOG environment variable (and know it’s an env var, not a flag).
  • Levels. Recognize that TRACE is the most verbose/complete level.
  • Persisting output. Know TF_LOG_PATH writes logs to a file and requires TF_LOG to be set.
  • Crashes vs errors. Know that a panic produces crash.log for bug reporting, distinct from an ordinary error.
  • Scenario recovery. “A teammate’s apply was interrupted and now every operation fails with a lock error — what do you run?” → terraform force-unlock.
  • The count/for_each unknown-value trap shows up constantly.

Frequently Asked Questions

Is TF_LOG a command-line flag or an environment variable?

An environment variable. You export TF_LOG=TRACE (or prefix a single command with TF_LOG=TRACE terraform ...). There is no --log-level flag on the Terraform CLI.

What’s the difference between TF_LOG, TF_LOG_CORE, and TF_LOG_PROVIDER?

TF_LOG sets the level for both Terraform core and provider plugins at once. TF_LOG_CORE targets only core; TF_LOG_PROVIDER targets only the providers. Use the specific ones to cut noise when you already suspect which component is misbehaving.

Why is nothing written to my TF_LOG_PATH file?

TF_LOG_PATH only sets the destination — you must also set TF_LOG (or TF_LOG_CORE/TF_LOG_PROVIDER) to a level. With no level set, there’s nothing to write.

What should I do when I see crash.log?

Don’t try to fix the panic yourself. If you can, reproduce it with TF_LOG=TRACE and TF_LOG_PATH set, then open an issue with the Terraform or provider maintainers and attach crash.log along with the trace log. crash.log is a bug-report artifact, not something to commit.

How do I fix “The count value depends on resource attributes that cannot be determined until apply”?

Base count (or for_each) on values known at plan time — input variables or locals — instead of attributes computed by another resource. When the dependency is unavoidable, apply the upstream resource first with -target, then run a normal apply.

Can debug logs leak secrets?

Yes. TRACE logs can include request payloads and headers containing credentials or secret values. Scrub logs before sharing them publicly and keep CI log artifacts access-controlled.

Conclusion

Debugging Terraform is less about memorizing a tool and more about knowing which layer to look at. TF_LOG and its TF_LOG_CORE/TF_LOG_PROVIDER variants let you dial verbosity for exactly the component you suspect; TF_LOG_PATH keeps the output so you can actually read it; crash.log captures the rare true panic for the maintainers. Around those tools sits a small library of common errors — lock conflicts, cycles, the count/for_each unknown-value trap, provider version mismatches — that you learn to recognize on sight.

The fastest way to make that recognition automatic is scenario practice: questions that hand you a symptom and ask for the command or the cause. The Sailor.sh HashiCorp Terraform Associate mock exams include troubleshooting and CLI scenarios with explanations of why each answer is right, so the difference between force-unlock and -reconfigure, or between an error and a crash, becomes reflexive. Warm up with the free Terraform Associate practice questions, and round out your CLI fluency with the commands cheat sheet and the core workflow guide. For the full objective map, see the Terraform Associate exam guide for 2026.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now