Git, CI/CD & GitOpsCIX · Terraform Delivery PipelineFormatAndValidate
fmt and validate in CI — the cheapest checks
What you'll learn
- Run terraform fmt -check -recursive -diff as the first CI gate and explain each flag
- Run terraform init -backend=false then terraform validate -json as the second gate
- Distinguish errors from warnings in the validate JSON output and surface both
- Identify the cost-per-rejection ordering that makes these the cheapest checks in the pipeline
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The first two stages of a production Terraform pipeline are the two commands built into the Terraform CLI: terraform fmt and terraform validate. They are deliberately narrow, deliberately fast, and deliberately side-effect-free. Their job is not to certify a change as safe; their job is to fail in under thirty seconds on the class of mistakes that should never reach a reviewer, a security scanner, or the state backend.
The fmt stage
terraform fmt rewrites HCL files to match the canonical HashiCorp style: aligned equals signs, two-space indentation, consistent block ordering, normalised quoting. Run with -check, it reports rather than rewrites. Run with -recursive, it walks every .tf file under the working directory. Run with -diff, it prints the changes it would have made.
The command that belongs in CI:
terraform fmt -check -recursive -diff
flowchart LR
A["Working tree HCL"] --> B["fmt -check"]
B --> C{"Well-formed?"}
C -->|"yes"| D["Pass, exit 0"]
C -->|"no"| E["Print diff, exit 1"]
E --> F["Author runs terraform fmt locally"]
F --> A
Three properties of this invocation matter:
-checkmakes fmt a reporter, not a writer. A CI job that rewrites the developer’s working tree is a CI job that disagrees with the developer about what changed. The pipeline reports; the developer fixes.-recursivecovers monorepo layouts. A repository with multiple Terraform modules needs all of them checked, not just the working directory.-diffmakes the failure self-documenting. A reviewer who sees “fmt failed” learns nothing; a reviewer who sees the proposed diff learns what to fix and where.
What fmt does not do: it does not parse HCL, it does not validate references, it does not check provider schemas, and it does not enforce team conventions that the canonical style leaves undecided. A file that passes fmt -check can still be syntactically invalid, semantically broken, or operationally dangerous.
The validate stage
terraform validate parses every .tf and .tf.json file, resolves internal references (resource addresses, variable references, output references), and checks the result against the provider schemas downloaded by terraform init. The command requires a populated .terraform/ directory, which means validate runs after init in the pipeline.
The init that belongs on the PR job:
terraform init -backend=false
The -backend=false flag prevents init from authenticating to the remote state backend. The PR job therefore needs no state credentials and cannot accidentally touch state. The validate that follows:
terraform validate -json
The -json flag emits a structured document distinguishing errors from warnings. A minimal CI integration parses valid, error_count, warning_count, and diagnostics[] (each with severity, summary, detail). Errors fail the build; warnings post as a separate check.
What fmt and validate do not catch
The two commands are deliberately scoped. The classes of mistake they miss are precisely the classes the rest of this part of the course exists to address:
- Deprecated or removed provider attributes. validate checks the schema it has downloaded; a provider upgrade that removes an attribute is caught by the next
terraform plan. - Unused variables and dead code. validate does not warn about a variable that is declared but never referenced.
- Security misconfiguration. A security group open to
0.0.0.0/0passes bothfmtandvalidatecleanly. - Cross-module coupling. validate operates on a single configuration directory. A reference to an output another module stopped exporting passes locally and fails at plan time.
- Drift between state and configuration. Neither command reads state by default. Drift is a
terraform planconcern, addressed in lessongit-cicd-gitops-cix-06.
Ordering matters
The cost of a rejection grows as the pipeline advances. A fmt rejection costs seconds. A validate rejection costs the time to download providers. A plan rejection costs the time to download providers, read remote state, and refresh attributes. A rejected apply costs a partial state mutation.
flowchart LR
A["fmt - seconds"] --> B["init + validate - tens of seconds"]
B --> C["plan - minutes"]
C --> D["policy gate - seconds"]
D --> E["human approval - hours"]
E --> F["apply - minutes, mutates state"]
Placing fmt first and validate second is the cheapest order of gates: the cheapest mistakes are caught by the cheapest stages, and the most expensive stage (apply) only runs if every preceding gate passed.
Production discipline
- fmt runs first, with
-check -recursive -diff, and exits non-zero on any formatting drift. The diff is posted to the PR as a comment so the author can apply it. - Validate runs after
init -backend=false. The PR job holds no state credentials; the validate job cannot touch state even by accident. - Validate output is JSON. The human-readable form is for terminals; the CI system parses the structured form and distinguishes errors from warnings.
- Warnings post as a separate check. They do not fail the build; they accumulate as a backlog and can be promoted to errors by deliberate decision.
- fmt and validate are not skipped on
-fastor label-based branch filters. The whole point of the gates is that they are cheap; skipping them saves nothing and loses everything.
Cross-course references
- Terraform for Production Sysadmins - Part IV (HCL Syntax) covers the language rules validate enforces.
- Terraform for Production Sysadmins - Part VII (Providers) covers the provider schemas validate resolves against.
- This course, Part L (TerraformCI) - lessons
git-cicd-gitops-l-02andgit-cicd-gitops-l-03cover fmt and validate at the intermediate level. - This course, Part XLIX (InfrastructureCI) - the general framing of format-as-first-gate.
Quiz
Knowledge check · 4 questions
Q1. Why does the PR job run `terraform init -backend=false` rather than a plain `terraform init`?
Q2. A passing `terraform validate` confirms that a change is correct, secure, and ready to apply.
Q3. List the three flags passed to `terraform fmt` in CI and explain the role of each.
Q4. Diagnose why a fmt+validate pipeline allowed a security misconfiguration to reach production.
A team runs fmt and validate as their only Terraform CI gates on pull requests. A contributor adds a security group ingress rule that exposes port 22 to `0.0.0.0/0`. The PR passes fmt and validate, the team merges it, and the apply runs. A week later, a security audit flags the rule and the team concludes that fmt and validate are unreliable.
Passing score: 75%. Answers are checked in this browser.