Skip to main content
RunBook Academy

Git, CI/CD & GitOpsL · Terraform CIFormatAndValidate

fmt and validate — what Terraform built-ins catch and what they do not

Intermediate⏱ ~22 mingitterraform

What you'll learn

  • Run terraform fmt -check -recursive -diff and explain each flag
  • Run terraform validate -json and parse the structured output
  • Identify what fmt and validate catch and what they deliberately do not catch
  • Place fmt and validate in the CI pipeline as fast, side-effect-free gates

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

Not yet marked complete on this device.

The first two gates in a Terraform CI pipeline are the two commands built into the Terraform CLI itself: terraform fmt and terraform validate. They are deliberately narrow. They are not a substitute for lint, security scanning, or integration tests - they are a fast, deterministic, side-effect-free first layer that catches the cheapest class of mistakes before the heavier stages run. Putting them at the front of the pipeline saves minutes of CI time per change and surfaces the kind of mistake that wastes a reviewer’s attention.

What fmt does

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 exits non-zero when any file would be changed, without modifying anything. Run with -recursive, it walks every .tf file under the working directory. Run with -diff, it prints the changes that would be made.

The command that belongs in CI:

terraform fmt -check -recursive -diff
  • -check makes fmt a reporter, not a writer. CI should never silently rewrite a developer’s working tree.
  • -recursive ensures monorepo layouts with multiple modules are all checked.
  • -diff makes the failure self-documenting: the developer sees exactly which lines would change.
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

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.

What validate does

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 that have been downloaded into the working directory. It runs after terraform init and requires a populated .terraform/ directory. Run with -json, it emits a structured document that distinguishes “valid” from “valid with warnings”.

The command that belongs in CI:

terraform validate -json

The JSON output exposes valid (boolean), error_count, warning_count, and diagnostics[] (an array of { severity, summary, detail } records). Most CI systems parse this JSON and surface warnings as a separate check from errors. Warnings do not fail the build by default; errors do.

What fmt and validate do not catch

The two commands are deliberately scoped to cheap, deterministic checks. The classes of mistake they miss are precisely the classes that the next stages of the pipeline exist to catch:

  • Deprecated or removed provider attributes - validate only 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 used.
  • Security misconfiguration - a security group open to 0.0.0.0/0 passes both fmt and validate cleanly.
  • 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 plan concern.

Placement in the pipeline

fmt and validate belong at the front of the pipeline, before any step that downloads providers or reads state:

  1. terraform fmt -check -recursive -diff - fails in under a second, no network.
  2. terraform init -backend=false - downloads providers and modules, does not configure the state backend.
  3. terraform validate -json - parses against the downloaded schemas.
  4. … heavier stages: tflint, tfsec, checkov, terratest, plan, apply.

The -backend=false flag on init prevents the validate job from authenticating to the state backend, which means the PR job does not need state credentials. This is the cheapest way to harden the plan boundary: validate does not touch state.

Production discipline

  1. fmt and validate run on every PR, in that order, in under 30 seconds. Anything slower means the gates are doing too much.
  2. Validate uses -json output, not the human-readable form. The human-readable form is for terminals; the CI system parses the JSON.
  3. Init in the PR job uses -backend=false. The PR job is forbidden from authenticating to state.
  4. Warnings are surfaced but do not fail the build by default. A failing build on warnings teaches engineers to ignore warnings.
  5. fmt and validate are not skipped on -fast branches. The whole point of the gate is that it is cheap; skipping it saves nothing.

Cross-course references

  • Terraform for Production Sysadmins - Part IV (HCL Syntax) covers the language rules that validate enforces.
  • Terraform for Production Sysadmins - Part VII (Providers) covers the provider schemas that validate resolves against.
  • This course, Part XLIX (InfrastructureCI) - lesson git-cicd-gitops-xlix-02-format-stage is the general framing of format in the pipeline; this lesson is the Terraform-specific instantiation.
  • This course, Part XLIX (InfrastructureCI) - lesson git-cicd-gitops-xlix-05-test-and-validate is the broader pattern of test-and-validate.

Quiz

Knowledge check · 4 questions

  1. Q1. A change passes terraform fmt -check and terraform validate. Which of the following is the change still allowed to contain?

  2. Q2. Running terraform validate in a pull-request CI job requires authenticating to the remote state backend.

  3. Q3. List the three flags used in the canonical CI invocation of terraform fmt, and state what each one contributes.

  4. Q4. Diagnose why a fmt+validate pipeline failed to catch a real bug.

    A team configures fmt and validate as their only Terraform CI gates on pull requests. A contributor renames a variable in variables.tf from `cidr_block` to `vpc_cidr` but forgets to update one reference inside a module's main.tf. The PR is merged and applied; the apply fails because the reference is dangling. The team concludes that fmt and validate are unreliable and want to disable them.

Passing score: 75%. Answers are checked in this browser.