Subchapter 33.2
references/terraform-validation.mdMarkdown9 KBView on GitHub
Canonical definition of the
fmt → init → validate → policy → fix-and-retry → offline-fallbackprotocol for any phase that emits aterraform/directory. This document is : it specifies the mechanics and the report shape, but the (a migration skill’s Generate phase) owns execution, the fix-and-retry edits, the user prompt, and any run-state decisions.
References
Security Posture RulesScripts
Test Validate Terraform PolicyAlso bundled
GitignoreThis protocol is part of the read-only tf-best-practices unit. Neither this document nor the
policy checker it invokes may:
.tf files — the caller applies fixes (Stage D is a description of what the caller
does, not an action this unit performs),The protocol’s only durable output is validation-report.json. Where the caller writes it,
whether validation failure blocks the phase, and how the terminal status maps to run-state are
all caller decisions. The pseudocode below uses “the caller advances / stops” deliberately —
this unit never advances a state machine.
Any step that writes a terraform/ directory and wants to check it for format, HCL-level, and
policy defects while degrading gracefully when the provider registry is unreachable.
Working directory: the terraform/ directory under test. All commands run non-interactively
(-input=false -no-color where supported). $TERRAFORM_DIR is the caller-supplied path.
terraform fmt -recursive (auto-apply — a caller action).terraform fmt -recursive -check. If non-zero, enter the Fix-and-Retry loop targeting fmt
failures. On success, advance to Stage B.terraform init -backend=false -input=false -no-color, capturing stderr.validation_status = "passed_degraded_offline",
emit warning, SKIP Stage C, proceed to Stage F (policy still runs), then Stage E. Do
NOT enter the retry loop.terraform validate -json, capturing stdout..diagnostics[] and enter the Fix-and-Retry loop targeting validate
failures.validation_status = "passed" and advance to Stage F (Stage D remains
available for policy retries).Attempt budget: 3 attempts per batch. Hardcoded.
Per attempt:
fmt -recursive -check (list of files that would change).terraform init.terraform validate -json.violations[] from the policy checker’s --json verdict (or POLICY_FAIL
stderr lines)..tf.)(file, line, summary) reappears on consecutive attempts, emit a “same error
recurring” signal in the attempt log — warning only; continue.On the 3rd consecutive failure in a batch, the caller prompts the user:
Terraform validation failed after 3 automated fix attempts.
Last error: <one-line summary>
[retry] attempt 3 more fixes
[skip] proceed with warning (validation_status = skipped_user_continue)
[abort] stop
Choose [retry/skip/abort]:User choices (caller applies its own run-state policy on each):
attempts
is NOT reset.validation_status = "skipped_user_continue", emit warning, proceed to
Stage E. (Whether the caller then allows phase completion is a caller decision.)validation_status = "skipped_user_abort", write validation-report.json
with that status, and STOP. The caller MUST NOT record a completion signal (do not advance
run-state). This unit does not touch run-state itself.Run the read-only policy checker:
python3 "<tf-best-practices>/scripts/validate-terraform-policy.py" "$TERRAFORM_DIR" --json "$VERDICT_PATH"On non-zero exit, parse the verdict’s violations[] (each carries file, line, rule,
fix_hint) and enter Stage D Fix-and-Retry targeting policy violations.
On success (POLICY_OK), advance to Stage E.
If Stage C was skipped due to offline fallback, still run Stage F — the policy check is static and needs no provider init.
Record the policy outcome in validation-report.json policy_status (see schema). A policy
failure MUST be visible in the report — it must never be masked by a passed_degraded_offline
top-level status.
Write the report to the single canonical path the caller pins, e.g.
$MIGRATION_DIR/validation-report.json — slash-joined, never a bare $MIGRATION_DIR/
directory (a bare dir invites a missing-slash <ts>validation-report.json sibling) — per
the schema below.
FUNCTION isNetworkUnavailable(init_stderr)
INPUT: init_stderr (string, captured stderr of `terraform init`)
OUTPUT: boolean
patterns ← ["lookup", "dial tcp", "connection refused", "timeout", "no such host"]
IF init_stderr IS NULL OR trim(init_stderr) = "" THEN
RETURN false // empty stderr → treat as non-network failure (let retry loop handle)
END IF
haystack ← toLowerCase(init_stderr)
FOR EACH p IN patterns DO // first-match-wins
IF contains(haystack, toLowerCase(p)) THEN
RETURN true
END IF
END FOR
RETURN false
END FUNCTIONRules:
terraform init only.v2 adds policy_status so the policy verdict is durably recorded independently of the
fmt/init/validate outcome. A policy failure is visible even when the top-level status is
passed_degraded_offline.
{
"$schema": "validation-report/v2",
"status": "passed | passed_degraded_offline | skipped_user_continue | skipped_user_abort | policy_failed",
"policy_status": "POLICY_OK | POLICY_FAIL | not_run",
"attempts": 0,
"errors_found": [
{
"file": "string (relative to terraform/)",
"line": "integer (1-based, 0 if unknown)",
"severity": "error | warning",
"summary": "string (≤200 chars)"
}
],
"errors_fixed": [
{
"file": "string",
"line": "integer",
"severity": "error | warning",
"summary": "string",
"attempt": "integer (1-indexed)"
}
],
"policy_violations": [
{
"rule": "string (e.g. alb_http_redirect)",
"file": "string",
"line": "integer",
"severity": "error | warning",
"summary": "string",
"fix_hint": "string"
}
],
"offline_fallback_used": false,
"timestamp": "ISO 8601 UTC",
"terraform_version": "string (empty if unavailable)"
}Field rules:
status: one of the enum values; equals the terminal validation_status. policy_failed
is used when the fmt/init/validate stages passed (or offline-skipped) but policy did not and
the user did not skip/abort.policy_status: POLICY_OK / POLICY_FAIL from Stage F, or not_run if Stage F did not
execute (e.g. no terraform/ produced). MUST reflect the last policy run — never inferred
from status.policy_violations: the verdict’s violations[] when policy_status == POLICY_FAIL
(deduped); empty otherwise.attempts: total fix-and-retry attempts across all batches (never resets on retry).errors_found / errors_fixed: fmt/init/validate diagnostics, deduped by
(file, line, summary).offline_fallback_used: true iff status == "passed_degraded_offline".terraform_version: from terraform version -json; empty string if unavailable (never crash).Example (validate passed first try; policy caught an HTTP-forward ALB, caller fixed it):
{
"$schema": "validation-report/v2",
"status": "passed",
"policy_status": "POLICY_OK",
"attempts": 1,
"errors_found": [],
"errors_fixed": [],
"policy_violations": [],
"offline_fallback_used": false,
"timestamp": "2026-07-15T15:37:04Z",
"terraform_version": "1.9.5"
}