Debugging Terraform: Logs, Errors & the TF_LOG Nuclear Option

Terraform Errors Are Usually Telling You Exactly What’s Wrong

Here’s the thing about Terraform errors: they look scary but they’re almost always specific. Wrong argument name, missing required field, provider API rejection with a reason attached. Before you reach for verbose logging, just read the error message. Slowly. Top to bottom.

I’ve sat with engineers who immediately jump to TF_LOG=TRACE when the error message literally says “The subnet ID ‘subnet-0abc123’ does not exist in us-east-1.” The answer is right there. Read it.

Reading Terraform Errors

A typical error:

│ Error: InvalidParameterValue: The subnet ID 'subnet-0abc123' does not exist
│
│   with aws_instance.web,
│   on main.tf line 15, in resource "aws_instance" "web":
│   15:   subnet_id = "subnet-0abc123"
│
│ The subnet doesn't exist in the specified region.

Read it top to bottom:

  1. Error type and message — InvalidParameterValue: The subnet ID...
  2. Resource address — aws_instance.web
  3. File and line — main.tf line 15
  4. The offending config — the actual HCL that triggered it
  5. Explanation — why it failed

Most errors are this clear. Fix the subnet ID, move on. Don’t overthink it.

TF_LOG — When You Actually Need to See Everything

For the hard stuff — unexpected behavior, provider bugs, auth failures where the error message is genuinely unhelpful — enable verbose logging with the TF_LOG environment variable.

Log levels, from the firehose to the trickle:

TRACE   — Everything. API requests, responses, internal graph traversal. Megabytes of output.
DEBUG   — Provider communication, high-level decisions.
INFO    — Operational messages.
WARN    — Non-fatal warnings.
ERROR   — Fatal errors only.
# Enable debug logging
export TF_LOG=DEBUG
terraform apply

# Enable trace logging (the nuclear option)
export TF_LOG=TRACE
terraform plan

# Turn it off when you're done
unset TF_LOG
# or
export TF_LOG=""

You can also split core and provider logs, which is genuinely useful when you know the problem is on one side:

export TF_LOG_CORE=INFO       # Terraform core messages
export TF_LOG_PROVIDER=DEBUG  # Provider plugin messages
Writing Logs to a File

TRACE logging dumps so much output to your terminal that the actual plan output becomes unreadable. Write it to a file instead:

export TF_LOG=DEBUG
export TF_LOG_PATH=/tmp/terraform-debug.log

terraform apply

Now terraform apply runs normally in your terminal, and the debug log is captured separately. Grep through it at your leisure:

# Search for errors
grep "Error" /tmp/terraform-debug.log
# Search for a specific resource
grep "aws_instance.web" /tmp/terraform-debug.log

This is especially useful in CI/CD. Capture debug logs as a build artifact on failed runs. When the pipeline fails at 3 AM, you can diagnose the issue from the logs without re-running the whole thing.


Common Error Patterns

After debugging enough Terraform issues, you start recognizing these instantly:

Authentication failures:

Error: error configuring Terraform AWS Provider: no valid credential sources found

→ AWS credentials aren’t configured. Check AWS_ACCESS_KEY_ID, ~/.aws/credentials, or your IAM role assignment. This is the #1 error people hit in CI/CD pipelines.

Provider version mismatch:

Error: Unsupported argument

→ You’re using an argument that doesn’t exist in the provider version you’ve got installed. Either upgrade the provider or check the docs for your version. This one usually happens when you copy config from a blog post that uses a newer provider version than yours.

State lock:

Error: Error acquiring the state lock

→ Someone else has the lock, or a previous apply crashed and left the lock behind. Use terraform force-unlock <LOCK_ID> but only — and I mean only — if you’re certain nobody else is actively applying.

Quota exceeded:

Error: Error creating VPC: VpcLimitExceeded: The maximum number of VPCs has been reached

→ This is a cloud problem, not a Terraform problem. Request a quota increase in the cloud console.

Circular dependency:

Error: Cycle: aws_security_group.a, aws_security_group.b

→ Two resources reference each other. Classic fix: use a aws_security_group_rule resource instead of inline ingress/egress blocks. Breaks the cycle.

terraform console — Interactive REPL

For debugging expressions and understanding what values actually look like at runtime:

terraform console

Loads the current state and config. Test expressions interactively:

> var.environment
"prod"

> aws_instance.web.public_ip
"54.123.45.67"

> join("-", [var.project, var.environment])
"myapp-prod"

> [for s in aws_subnet.public : s.id]
[
  "subnet-0abc123",
  "subnet-0def456",
]

Exit with Ctrl+D or exit.

I use this constantly. When an expression produces something unexpected, testing it in console is infinitely faster than embedding it in a resource and running a full plan cycle. Five seconds vs. five minutes.

Crash Logs

If Terraform outright crashes (as opposed to exiting with an error), it writes a crash log:

terraform.log   # or crash.log — written to current directory

This is a Go panic stack trace. You won’t debug this yourself unless you’re contributing to the Terraform codebase. What you do with it: attach it to a GitHub issue against the Terraform or provider repo. That’s it.

Debugging Pattern: The Investigation Workflow

When the error message isn’t enough and you need to dig deeper, this is the pattern I follow:

# 1. Enable debug logging to a file
export TF_LOG=DEBUG
export TF_LOG_PATH=/tmp/tf-debug.log

# 2. Run the failing command
terraform apply 2>&1 | tee /tmp/tf-output.log

# 3. Search logs for the specific resource
grep "aws_instance.web" /tmp/tf-debug.log | head -50

# 4. Look for the actual API request and response
grep -A5 "POST\|PUT\|PATCH" /tmp/tf-debug.log | grep -B2 "Error"

# 5. Clear logging once done
unset TF_LOG
unset TF_LOG_PATH

Nine times out of ten, you won’t need this workflow. terraform validate, the provider docs, and the error message itself usually have the answer. But for that tenth time — an auth issue that makes no sense, a provider returning a cryptic 500 error — this is how you crack it open.

Key Takeaways

TF_LOG=TRACE generates megabytes of output per apply. Even on a moderate config. It contains provider API responses which can include sensitive values — credentials, tokens, account IDs. Don’t leave trace logging enabled in CI unless you’re actively debugging, and make sure those CI logs aren’t retained longer than necessary.

Before reaching for TF_LOG, try the simple things: terraform validate, check the provider docs for the specific resource, verify your credentials work with aws sts get-caller-identity. The error message itself is almost always enough. Verbose logging is a last resort, not a first instinct.