Skip to content

Debugging and Troubleshooting


Terraform debugging is most effective when you treat an error as evidence about a subsystem rather than immediately reaching for random commands.

A good debugging process moves from the highest-level symptom toward the smallest component that can explain it:

Configuration
    ↓
Validation
    ↓
Plan
    ↓
Error message
    ↓
Identify subsystem
    ↓
Inspect variables / expressions / dependencies / state
    ↓
Increase logging only when necessary
    ↓
Test the smallest change
    ↓
Plan again

The goal is not to memorize every Terraform command. It is to develop a repeatable way of narrowing the problem.

Start With The Error And The Plan

The first mistake in debugging Terraform is often ignoring the actual error message.

Run:

terraform plan

Then read:

  1. which file and line Terraform identifies
  2. the resource or expression involved
  3. the exact operation Terraform attempted
  4. the provider error, if present
  5. whether the problem is configuration, authentication, state, or an external API

Terraform errors often tell you which layer failed.

For example:

Error: Invalid value for variable

The given value is not suitable for var.instance_count:
a number is required.

That points toward variables and types.

A different error such as:

Error: UnauthorizedOperation

points toward provider authentication or authorization rather than Terraform expression syntax.

This distinction prevents wasted effort.

Configuration And Expression Problems

Configuration errors usually fail before Terraform can meaningfully interact with infrastructure.

Suppose:

resource "terraform_data" "example" {
  input = var.message
}

but the variable is not defined.

Run:

terraform validate

Terraform can identify the invalid reference without contacting your infrastructure.

Expression errors are similar.

For example, you might accidentally access an attribute that does not exist or produce an incompatible type.

When the problem is an expression, terraform console is particularly useful.

Run:

terraform console

You can evaluate expressions interactively.

For example:

> length(["a", "b", "c"])
3
> upper("terraform")
"Terraform"

The console is useful when you need to understand what an expression actually evaluates to rather than guessing.

Variable And Type Problems

When a variable behaves unexpectedly, inspect:

  • its declared type
  • its default value
  • the value passed by the environment
  • validation rules
  • expressions consuming the value

For example:

variable "ports" {
  type = list(number)
}

is different from:

variable "ports" {
  type = list(string)
}

A problem may therefore originate at the input boundary rather than the resource using it.

terraform console can help inspect known values during an appropriate Terraform execution context.

The goal is to determine:

What value did I expect?
        ↓
What value did Terraform receive?
        ↓
What type does Terraform see?
        ↓
Where did that value change meaning?

Provider Errors

Provider failures are different from Terraform language errors.

For example:

Error: creating resource: AccessDenied

means Terraform understood enough of the configuration to ask the provider to perform an operation. The provider or remote API rejected it.

Now investigate:

  • provider configuration
  • selected credentials
  • account/project/subscription
  • region
  • permissions
  • resource constraints
  • service availability
  • API-specific requirements

Do not start rewriting Terraform expressions if the cloud API is returning AccessDenied.

Likewise, an authentication error is not usually fixed by changing resource dependencies.

Authentication And Authorization Problems

A common debugging trap is assuming that “Terraform has credentials” means “Terraform has the required permissions.”

These are separate questions:

Can Terraform authenticate?
        ↓
Can the identity perform this specific operation?

For CI, also ask:

Which identity is the CI job actually using?
Which environment variables are present?
Which role or workload identity was selected?
Which account is the provider targeting?

The same Terraform code can work locally and fail in CI because the execution identity is different.

Dependency Problems

Terraform normally builds a dependency graph automatically from references.

For example:

resource "terraform_data" "network" {
  input = "network"
}

resource "terraform_data" "application" {
  input = terraform_data.network.output
}

The reference creates an implicit dependency.

When something happens in the wrong order, ask:

  1. Is there an actual data dependency?
  2. Is Terraform able to see it?
  3. Is the dependency only operational rather than represented in configuration?
  4. Is depends_on genuinely necessary?

Do not add depends_on blindly. First understand why Terraform’s existing graph does not represent the required relationship.

State-Related Problems

When the configuration looks correct but Terraform reports something surprising, state is one of the first places to inspect.

List managed resources:

terraform state list

Then inspect a specific resource:

terraform state show terraform_data.example

These commands answer:

What does this state currently contain?

That can reveal that:

  • the resource is not in the state you expected
  • an address changed
  • an attribute differs from your assumption
  • a module path is different
  • Terraform is managing a different object than you thought

State inspection should come before manually editing state.

Unexpected Replacement

One of the most important plan-reading skills is understanding why Terraform wants to replace a resource.

A plan may show something like:

-/+ resource "..." "example" {
      # forces replacement
}

Do not focus only on the fact that replacement is happening. Find the attribute causing it.

Ask:

What changed?
        ↓
Is that attribute ForceNew / replacement-sensitive?
        ↓
Was the change intentional?
        ↓
Is the configuration wrong?
        ↓
Is state stale?
        ↓
Did the provider change behavior?

This is much more useful than trying to suppress the replacement without understanding it.

Unexpected Diffs

When Terraform continually proposes the same change, consider:

  • a value is being changed outside Terraform
  • the provider normalizes values differently
  • an optional/default value is being interpreted differently
  • configuration does not represent the actual desired state
  • a provider bug or behavior change is involved

A repeated diff is evidence. Do not automatically add lifecycle rules to hide it.

If the infrastructure is intentionally managed elsewhere, decide explicitly whether Terraform should own that attribute or whether the architecture needs to change.

Debugging With TF_LOG

Terraform provides logging through TF_LOG.

For example:

TF_LOG=ERROR terraform plan

Available levels include:

ERROR
WARN
INFO
DEBUG
TRACE

You can therefore increase detail gradually:

TF_LOG=WARN terraform plan

then:

TF_LOG=INFO terraform plan

and, only when needed:

TF_LOG=DEBUG terraform plan

or:

TF_LOG=TRACE terraform plan

A useful approach is:

Normal output
    ↓
Error message
    ↓
More context
    ↓
DEBUG
    ↓
TRACE only if necessary

Do not begin every investigation at TRACE. The amount of output can become enormous and make the original problem harder to see.

Warning

Debug and trace logs can contain sensitive information. Never paste Terraform debug logs into a public issue or chat without reviewing them for credentials, tokens, infrastructure details, and other sensitive data.

Realistic Failure: Wrong Variable Type

Suppose a module expects:

variable "replicas" {
  type = number
}

but the caller supplies an incompatible value.

The first step is not logging.

Run:

terraform validate

If the issue is still unclear, inspect the expression or input that produces the value.

The reasoning is:

Terraform language problem
        ↓
Validate configuration
        ↓
Inspect variable declaration
        ↓
Inspect caller value
        ↓
Inspect expression producing value

Realistic Failure: Provider Permission

Suppose terraform plan reports:

Error: AccessDenied

Your debugging path should be:

AccessDenied
    ↓
Provider reached the API
    ↓
Authentication likely succeeded
    ↓
Check identity
    ↓
Check target account/region
    ↓
Check required permission

Increasing TF_LOG may help confirm which identity or endpoint Terraform is using, but the root cause is an authorization boundary rather than a Terraform expression.

Realistic Failure: State Does Not Match Your Mental Model

Suppose the configuration contains a resource you believe Terraform manages, but:

terraform state list

does not show it.

That immediately changes the investigation.

Possible causes include:

  • it was never imported
  • it was removed from state
  • the resource address changed during refactoring
  • you are using a different workspace or state
  • you initialized against a different backend

Now compare:

terraform state list
terraform plan

with the current configuration and execution context.

This is why state inspection is often more valuable than changing configuration blindly.

A Repeatable Debugging Mindset

A disciplined investigation usually follows this sequence:

  1. Reproduce the failure.
  2. Read the complete error.
  3. Classify the subsystem.
  4. Run the cheapest relevant diagnostic.
  5. Inspect the smallest amount of state/configuration necessary.
  6. Increase logging only if the problem remains unclear.
  7. Make one small change.
  8. Run the plan again.
  9. Confirm that the new result explains the original symptom.

Useful commands from the course include:

terraform fmt
terraform validate
terraform plan
terraform state list
terraform state show terraform_data.example
terraform console

The commands are less important than the reasoning behind them.

A good Terraform operator gradually turns:

"Terraform failed."

into:

"The configuration is valid, the provider authenticated successfully,
but the API rejected this operation because the execution identity
does not have the required permission."

That is debugging.

Last updated on