TerraformXVIII · Troubleshooting and RecoveryProduction Terraform
The Troubleshooting Methodology
What you'll learn
- Apply a repeatable sequence from user-visible symptom to verified recovery
- Trace a Terraform failure through configuration, graph, provider, state, and backend layers
- Form one testable hypothesis before changing live infrastructure or state
- Select the appropriate investigation cadence for a workstation, CI, staging, or production incident
- Record evidence, decisions, and recovery steps for the next engineer
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
The troubleshooting methodology is a disciplined way to move from a user-visible symptom to a verified recovery. It gives the on-call engineer an order of operations: understand the symptom, trace the dependency chain, state one hypothesis, test it with the smallest safe command, fix the cause, and verify the result. Terraform makes configuration, state, providers, and remote services look like one workflow; during an incident they are separate boundaries that fail independently.
This method is not a substitute for a runbook. It is the structure inside which the runbook is used. A fast but unverified workaround can create a second incident.
The method is a control loop
Terraform’s output is the end of a chain, not the beginning:
User-visible symptom
|
v
Configuration and variables
|
v
Dependency graph
|
v
Provider API and real infrastructure
|
v
State and backend
|
v
Last known change
The arrows matter. An error from a provider may be caused by a value that was accepted in configuration but rejected by the API. An error that looks like state corruption may be a wrong resource address. The chain prevents you from treating the first error line as the whole diagnosis.
The control loop is:
- Record the symptom. Capture the exact error, command, working directory, environment, revision, and affected resource address.
- Stop unsafe actions. Do not start another apply or force-unlock a state lock while you do not know who holds it.
- Trace the dependency chain. Establish which layer owns the failure and which layers already worked.
- Form one hypothesis. Make a statement that can be disproved.
- Test the hypothesis. Prefer a read-only command or a saved plan.
- Fix the cause. Use the narrowest recovery with a reviewed rollback path.
- Verify and document. Confirm infrastructure, state, and logs, then record what the next engineer will need.
Start with the user-visible symptom
Write down what a user or monitor observed before opening the console.
Use precise observations such as “the web security group was not
updated”, “the apply stopped at aws_instance.api”, or “a second
plan says the database will be destroyed”. Avoid starting with a vague
diagnosis such as “Terraform is broken”.
Then create a compact evidence bundle. The following commands are safe to run while you establish the boundary of the incident. A saved plan is an artefact, so set a restrictive umask and treat it as sensitive.
CONFIGURATION — creates a plan artefact for review; it does not apply infrastructure.
umask 077
pwd
terraform version
terraform workspace show
terraform plan -input=false -no-color -out=/tmp/incident.tfplan
terraform show -json /tmp/incident.tfplan
A typical report can begin like this:
Error: creating EC2 Instance: InvalidAMIID.NotFound
with aws_instance.api,
on main.tf line 31, in resource "aws_instance" "api":
The image id 'ami-0123456789abcdef0' does not exist
The report already separates the address (aws_instance.api), the
provider operation (creating EC2 Instance), and the external
condition (InvalidAMIID.NotFound). The next question is not “which
command retries fastest?” It is “where did this image ID come from?”
Walk the dependency chain
Configuration references create an implicit dependency graph. For example, this fragment makes the instance depend on the security group ID. The security group is therefore upstream of the instance; if the instance fails, the group and its ingress rules belong in the first part of the chain to inspect.
resource "aws_security_group" "web" {
name = "web"
}
resource "aws_instance" "web" {
ami = var.web_ami
instance_type = "t3.small"
vpc_security_group_ids = [aws_security_group.web.id]
}
Use the graph to confirm the relationship rather than guessing from resource names.
READ-ONLY
terraform graph -type=plan | grep -E 'aws_security_group|aws_instance'
terraform plan -input=false -no-color -refresh-only
The first command shows the graph available to Terraform. The second checks drift without proposing new changes. If the graph cannot be read because of a lock, stop and investigate the lock before widening the search.
At each boundary, ask a different question:
| Boundary | Evidence to collect | Common mistake |
|---|---|---|
| Configuration | Exact file, line, variable value source, and syntax error | Editing a value to see what happens |
| Graph | Resource address and dependency edges | Assuming names imply order |
| Provider | API operation, request identifier, status, and retry detail | Treating every provider error as an outage |
| State | Address, serial, lock holder, and plan diff | Removing a state entry to make the plan quiet |
| Backend | Reachability, credentials, versioning, and lock ID | Copying a local state file over remote state |
Form one hypothesis and test it
A useful hypothesis has a measurable result. Examples include:
- “The apply is using the wrong environment variable because the
plan shows
us-east-1while the production backend is ineu-west-2.” - “The provider cannot create the resource because the AMI variable points to a retired image.”
- “The plan’s destroy is caused by a resource address change, not by infrastructure drift.”
- “The lock is stale because the lock holder’s process has exited.”
Test the hypothesis with a command that has a clear pass or fail. The following is read-only and should be used before considering a state command.
printf 'AWS_REGION=%s\n' "${AWS_REGION:-unset}"
printf 'TF_WORKSPACE=%s\n' "${TF_WORKSPACE:-unset}"
terraform state list
terraform plan -input=false -no-color -refresh-only
If the hypothesis is disproved, update the evidence and state a new one. Do not stack unrelated changes to make the output look better. If it is confirmed, record why the test was sufficient before applying the fix.
Fix, verify, and document
Once the cause is supported by evidence, choose a rollback or forward fix. A configuration rollback usually means returning to the last reviewed configuration revision and applying that plan. A state rollback is different: never replace current state merely because an old file is older. Use backend versioning, verify the resource addresses against the real environment, and run a refresh-only plan first.
SERVICE-IMPACT — applies the reviewed rollback plan and changes infrastructure and state. This example assumes the backend is healthy and the lock is not held.
terraform plan -input=false -no-color -out=/tmp/rollback.tfplan
terraform show -no-color /tmp/rollback.tfplan
terraform apply -input=false -auto-approve /tmp/rollback.tfplan
If the saved plan is stale because the configuration changed, discard it and generate a new one. Do not manually edit the state file to make a stale plan look correct.
Verification has three parts:
- Infrastructure. Check the service, resource health, traffic, and monitoring signals, not only the Terraform exit code.
- State and plan. Confirm the intended addresses, serial, and an empty or explainable plan.
- Evidence. Preserve the error, plan, revision, command output, and operator decisions with an access-controlled incident record.
The right cadence for each environment
Cadence should match blast radius and observability, not the time of day. More logging is not automatically more safety.
| Environment | Routine cadence | Incident response | Evidence to retain |
|---|---|---|---|
| Workstation | Validate and review a plan before every apply; use INFO only when diagnosing | Start with DEBUG and a protected log file; do not mutate state to experiment | Revision, plan, exact error, redacted log |
| CI | Run fmt -check, validate, policy checks, and a detailed-exitcode plan | Run one controlled failed job with scoped logging; cancel unrelated jobs if blast radius is high | Job ID, plan artefact, lock and provider events, policy result |
| Staging | Reproduce the failure and recovery on a copy or disposable account before production | Exercise provider, module, lock, and partial-apply paths at the same cadence as production | Timeline, command transcript, recovery time, drift observations |
| Production | No routine trace; review saved plans and monitor apply duration and failure class | Use INFO for high-level events, DEBUG for a bounded diagnosis, and TRACE only for a single unexplained boundary | Incident record, state serial, plan, logs, rollback result |
| Exercises | At least quarterly, or after a material workflow change | Run a game day with a named incident commander and a stop condition | Gaps, owners, due dates, and changed runbook |
Five failure modes in the method
1. Confusing the first error with the root cause
Symptom. The first line is an InvalidAMIID error, but the
revision also changed the region and provider version. The engineer
changes the AMI and retries; the next error is a credential error.
Recovery. Capture the full error and surrounding plan, compare the last known good revision, and test the image, region, credential, and provider as separate hypotheses.
2. Breaking the dependency chain
Symptom. A network rule is created after the instance that needs it. The instance is healthy in the cloud but the service cannot reach it. The engineer keeps changing the instance type.
Recovery. Inspect terraform graph, express the intended edge, and
run a plan that changes only the dependency or ordering. Do not use
-target to hide an incomplete graph in production.
3. Using a state operation to test a configuration hypothesis
Symptom. A rename causes a destroy-and-create plan. The engineer
runs state rm because the plan looks wrong.
Recovery. Add a reviewed moved block or use state mv with a
backup and a plan check. A state command is the recovery, not the
diagnostic experiment.
4. Retrying without checking the lock holder
Symptom. Every apply stops before the first resource change. The backend reports a lock held by a cancelled CI job.
Recovery. Check the lock ID, owner, timestamp, and process list.
Wait for the owner or use the approved stale-lock procedure. Never
delete lock metadata or use -lock=false as an incident shortcut.
5. Declaring success at the CLI exit code
Symptom. terraform apply returns zero while monitoring still
reports a failed health check. The engineer closes the incident because
the command completed.
Recovery. Verify the resource, its dependencies, the state address, and the service-level signal. Record the difference between a command success and a user-visible recovery.
Security, performance, and production discipline
Debugging can expose credentials, topology, and state. Keep state, plan artefacts, logs, and environment dumps in the same access boundary as production. Use short-lived credentials, never run a shell with trace output enabled for a secret-bearing apply, and remove the artefact when the incident record no longer needs it.
At scale, repeated full refreshes and unbounded provider concurrency
can increase incident time. Use -refresh-only to separate drift from
a proposed change, and reduce -parallelism when the provider is
throttling rather than increasing it blindly. Do not turn off refresh
or state locking for routine applies; the small time saving is not
worth the ambiguity during recovery.
Verification
- You can record the exact symptom, resource address, command, environment, and revision before changing anything.
- You can trace a failure through configuration, graph, provider, state, and backend without assuming the first error is the cause.
- You can state one testable hypothesis and describe its pass or fail signal.
- You can choose an investigation cadence appropriate to CI, staging, production, or a workstation.
- You can verify infrastructure, state, and evidence separately before closing the incident.
- You can identify a rollback path and the signal that proves it worked.
Knowledge check · 7 questions
Q1. What should an on-call engineer record before testing a Terraform failure?
Q2. Walking the dependency chain means following the graph Terraform builds from references, not the order the resources appear in a file.
Q3. Which action best tests one configuration hypothesis?
Q4. Which evidence should be retained for a Terraform incident? (Select all that apply.)
Q5. A staging exercise needs to test provider throttling, a stale state lock, and a partial apply. Which cadence is most appropriate?
Q6. What proves that a Terraform incident is recovered?
Q7. What is the appropriate next step after a hypothesis is confirmed?
Passing score: 75%. Answers are checked in this browser.