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 againThe 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 planThen read:
- which file and line Terraform identifies
- the resource or expression involved
- the exact operation Terraform attempted
- the provider error, if present
- 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: UnauthorizedOperationpoints 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 validateTerraform 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 consoleYou 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: AccessDeniedmeans 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:
- Is there an actual data dependency?
- Is Terraform able to see it?
- Is the dependency only operational rather than represented in configuration?
- Is
depends_ongenuinely 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 listThen inspect a specific resource:
terraform state show terraform_data.exampleThese 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 planAvailable levels include:
ERROR
WARN
INFO
DEBUG
TRACEYou can therefore increase detail gradually:
TF_LOG=WARN terraform planthen:
TF_LOG=INFO terraform planand, only when needed:
TF_LOG=DEBUG terraform planor:
TF_LOG=TRACE terraform planA useful approach is:
Normal output
↓
Error message
↓
More context
↓
DEBUG
↓
TRACE only if necessaryDo 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 validateIf 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 valueRealistic Failure: Provider Permission
Suppose terraform plan reports:
Error: AccessDeniedYour debugging path should be:
AccessDenied
↓
Provider reached the API
↓
Authentication likely succeeded
↓
Check identity
↓
Check target account/region
↓
Check required permissionIncreasing 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 listdoes 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 planwith 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:
- Reproduce the failure.
- Read the complete error.
- Classify the subsystem.
- Run the cheapest relevant diagnostic.
- Inspect the smallest amount of state/configuration necessary.
- Increase logging only if the problem remains unclear.
- Make one small change.
- Run the plan again.
- 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 consoleThe 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.