Chapter 11: Testing and Validation
Topic 63

fmt and validate

ToolingCI

These are the two cheapest quality gates Terraform has, and they catch a surprising amount for what they cost. terraform fmt rewrites your files into canonical formatting — consistent indentation, aligned arguments, one style — and terraform validate checks that the configuration is internally consistent: valid syntax, correct argument types, references that resolve. Both run in under a second and touch nothing real.

They are the floor below which no Terraform code should fall, and they belong at the very front of every pipeline. The trap is mistaking what they prove. A config that formats cleanly and validates is structurally sound; it is not guaranteed to apply. That gap — between "the code is well-formed" and "the change will work against real AWS" — is the single most misunderstood thing about these commands.

The fast gates — fail before anything real
fmt -check — is it formatted?
validate — is it consistent?
plan — will it apply?

terraform fmt

terraform fmt rewrites .tf files in place to the canonical style and ends every formatting argument a team would otherwise have in review. There is exactly one correct format, the tool produces it, and nobody debates brace placement again. For CI you do not want it rewriting files — you want it to fail when files are not already formatted. fmt -check exits non-zero if anything would change and writes nothing; -diff prints what it would have changed so the failure is actionable.

fmt in CI — fail on unformatted code, don't rewrite
# exits non-zero if any file is not canonically formatted
terraform fmt -check -recursive -diff

The -recursive flag walks subdirectories so a monorepo of modules is checked in one pass. Run this as a gate, not a fixer, in CI: the developer formats locally, and CI only confirms they did.

terraform validate

terraform validate checks a configuration for internal consistency without contacting any provider API or reading state. It catches a reference to an attribute that does not exist, an argument given a string where a number is required, a missing required argument, a malformed expression, a duplicate resource address. It needs terraform init to have run first, because it loads the providers to know each resource's schema — but it makes no API calls and creates nothing.

init then validate — schema-aware, no API calls
# init installs providers so validate knows the schemas
terraform init -backend=false

# structural and type checks only — touches no real infrastructure
terraform validate

Passing -backend=false to init lets validate run in CI without configuring or reaching the state backend, since validation does not need it. The output is a clean success or a precise pointer to the file, line, and argument that is wrong.

What validate Does and Doesn't Catch

This is the line that matters. validate verifies structure and types — that the configuration is well-formed and self-consistent. It does not verify that the plan will succeed. It cannot tell you the AMI ID does not exist in your region, that your IAM role lacks permission to create the resource, that you are over a service quota, or that the instance type is unavailable in the chosen availability zone. Those are real-world facts that only plan (which queries AWS) or apply (which tries the operation) can surface.

So validate passing is necessary, not sufficient. It guarantees Terraform can understand your code, not that AWS will accept it. Treat it as the syntax-and-types gate it is, and let plan handle feasibility and policy-as-code handle the rules.

In the Pipeline

Both commands belong in the first stage of CI, before plan, because they are fast and fail on obvious problems for a fraction of a plan's cost. A typical front gate runs fmt -check, then init -backend=false, then validate, and stops the pipeline if any of them fails. There is no reason to spend a plan's API calls and minutes on code that is not even formatted or that references a variable that does not exist.

Locally, the same gates move even earlier. Configure your editor to run terraform fmt on save, and add a pre-commit hook that runs fmt -check and validate so a malformed or unformatted commit never reaches the shared branch. The earlier the gate, the cheaper the failure — a pre-commit catch costs seconds, the same catch in CI costs a queued job and a context switch.

Common Mistakes
  • Assuming a passing validate means the apply will succeed — it does not check AWS-side validity, permissions, or quotas, so a config that validates can still fail on the first real API call.
  • Skipping fmt -check in CI and letting every contributor's formatting drift, so pull-request diffs fill with whitespace churn that buries the real change.
  • Expecting validate to catch a non-existent AMI ID or an over-budget instance type — those are plan, apply, or policy concerns, not structural ones.
  • Running these gates only on a laptop, so unformatted or invalid code still reaches the shared branch when someone forgets.
  • Running validate before init and being confused by the error — it needs the provider schemas installed first to know each resource's argument types.
Best Practices
  • Run terraform fmt -check -recursive and terraform validate as the first, fast stage of every pipeline, before any plan.
  • Format on save in your editor and add a pre-commit hook running fmt -check and validate so issues never reach CI.
  • Use init -backend=false before validate in CI so validation runs without touching the state backend.
  • Treat validate's scope as structure and types only, and rely on plan for real-world feasibility and policy-as-code for org rules.
  • Treat any formatting or validation failure as a hard stop — never let a pipeline proceed to plan past a red gate.
Comparable tools CloudFormation cfn-lint for template structure and best practices Pulumi the host language's compiler and type checker General a code formatter plus a linter in any language

Knowledge Check

terraform validate passes on your config. What has it actually proven?

  • The configuration is syntactically valid and internally consistent in types and references — nothing about apply
  • The plan will apply cleanly because AWS has already accepted and provisioned every declared resource in the config
  • Every referenced AMI, instance type, and service quota is available right now in the target region
  • The IAM credentials currently in use have permission to create all of the declared resources

Why use terraform fmt -check rather than plain terraform fmt in CI?

  • It fails the build on unformatted code without rewriting files, making formatting a gate developers satisfy locally
  • It formats the code noticeably faster by skipping the file-rewrite step entirely
  • It is the only fmt mode that descends into and handles subdirectories recursively
  • It also validates argument types and references while it reformats, fully replacing any separate validate step in CI

Why must terraform init run before terraform validate?

  • Validate loads the provider schemas to check argument names and types, so the providers must be installed first
  • Validate writes its results into the remote state backend, which the init step configures
  • Validate makes live API calls to AWS that require the credentials and the remote state backend that init sets up
  • Init runs the actual canonical format check that the validate step then reports on afterward

Which problem will fmt and validate together never catch?

  • An AMI ID that does not exist in the target region, which only plan or apply can discover
  • A reference to a resource attribute that does not exist anywhere on that resource type's schema
  • A string value passed where the argument schema requires a number instead
  • Inconsistent indentation and unaligned arguments spread across many files

You got correct