skip to content

Testing and Static Checks

Terraform now has a native test framework alongside the familiar linters and security scanners. I should be able to place each tool on the pyramid and say what it would have caught.

part ofTerraformoverview, primer and where to startread it →
on this pageshow

questions

6

In a CI job, what does `terraform fmt -check` do differently from plain `terraform fmt`, and what does it miss by default?

level: juniorimportance: must knowfreq 62%

answer

  1. the read-only variant of a formatter
  2. the exit code is the whole point
  3. scope defaults to a single directory
  4. -recursive walks the module subtree
  5. whitespace only, never semantics

basics

~20 s

terraform fmt -check reports formatting problems without rewriting anything and exits non-zero if any file would change, which is what makes it usable as a build gate. By default it looks only at the current directory, so nested modules need -recursive.

solid answer

~50 s

Plain `terraform fmt` rewrites files in place and exits zero, so running it in CI silently fixes the throwaway checkout and reports success — the repository stays unformatted. `-check` turns the command read-only and turns the answer into an exit status: zero when everything is already canonical, non-zero when at least one file would change, which is exactly what a pipeline step needs. Add `-diff` if you want the job log to show what it would have rewritten. The trap is scope: `terraform fmt` processes only the directory you point it at, not the tree beneath it, so a root-level check passes while `modules/` drifts. Use `-recursive`. Formatting is whitespace, indentation and `=` alignment only — it never reorders arguments or changes what the configuration means, so it is safe to run on the whole repo.

go deeper

for a junior

Know that plain terraform fmt rewrites files while -check only reports and fails the build, and remember to add -recursive so nested modules are covered.

for a middle

Be able to explain why running the fixer in CI produces a permanently green, permanently unformatted repo, and where fmt sits below validate and lint in the chain of checks.

for a senior

Show the judgment that formatting belongs on save or in a pre-commit hook with CI as a backstop, and that a style gate must never be confused with a correctness or security gate.

for a principal

Own the tradeoff between enforcement and friction: a formatting failure that blocks an urgent apply costs more than it saves, so decide deliberately whether style checks gate the deploy or only the review.

## What the command actually does `terraform fmt` rewrites Terraform configuration files into HashiCorp's canonical style: indentation, spacing, and vertical alignment of the `=` signs inside a block. It operates on `.tf` and `.tfvars` files. What it does **not** do is as important — it never reorders arguments, never renames anything, never adds or removes blocks, and never touches meaning. Formatting a repository cannot change a single byte of the resulting plan. That is why it is safe to run over everything and why it is a pure hygiene tool rather than a correctness tool. It does need the input to be parseable HCL. A file with a genuine syntax error is a parse failure, not a formatting suggestion, so `fmt` reports an error rather than reformatting it. ## Why `-check` exists A pipeline does not want a fixer, it wants a verdict. Two flags change the mode: - `-check` — do not write anything; exit zero if every file is already formatted, non-zero otherwise. - `-diff` — print the changes that would be made, so the failing job explains itself. The classic broken pipeline runs plain `terraform fmt`. It happily rewrites the files inside the ephemeral CI workspace, exits zero, and the workspace is then thrown away. Every build is green and the repository never gets formatted. `-check` is the difference between a gate and a no-op. ```bash terraform fmt -check -recursive -diff ``` Developers run `terraform fmt -recursive` locally to fix; CI runs the same command with `-check` to enforce. Same tool, two modes. ## The directory-scope trap By default `terraform fmt` processes the files in one directory — the current one, or a path you pass as an argument — and stops there. It does not walk subdirectories. A repository laid out as a root module plus `modules/network`, `modules/rds` and so on will pass a root-level `terraform fmt -check` while every module underneath it is unformatted, which is exactly the code reviewers spend their time complaining about. `-recursive` fixes it, and it is the single most common omission in a Terraform CI job. ## What it deliberately cannot tell you `fmt` is the shallowest check you can run. It says nothing about whether a variable is declared, whether an attribute exists on that resource type, whether an argument is required, whether the value is legal for the target cloud, or whether the resource is a security problem. Those belong to progressively deeper tools: - `terraform fmt -check` — style, no init required, milliseconds. - `terraform validate` — internal consistency against provider schemas, requires an initialized directory. - `tflint` — provider-specific value rules and best-practice lint. - `checkov` / `trivy` — security and compliance rules over HCL or plan JSON. - `terraform plan` — what would actually change, requires credentials. Each one can only see failures the previous layer let through, and only the last one talks to a cloud API. ## Practical notes `fmt` reads from stdin when you pass `-` as the target, which is how editor integrations format a buffer without touching disk. `-list=false` suppresses the filename listing when you only want the exit status. And because formatting is semantically inert, the sane team policy is to make it automatic — an editor-on-save action or a repository hook — and keep the CI check purely as a backstop for people who bypassed it, rather than as the primary mechanism for getting formatted code. One last subtlety worth saying out loud in an interview: a green `fmt -check` proves nothing about the configuration's correctness. It is worth having because inconsistent whitespace makes diffs noisy and code review harder, not because it protects production.

  • Your CI job runs `terraform fmt -check` at the repository root and passes, yet reviewers keep seeing misaligned HCL under modules/. What is happening?
    The check is only covering the root directory. `terraform fmt` does not descend into subdirectories unless you pass `-recursive`, so every nested module is invisible to the gate. Adding `-recursive` to both the local fix command and the CI check closes the gap, and the first run after that will produce a large but semantically empty diff.
  • A colleague argues the pipeline should run `terraform fmt` and commit the result instead of failing the build. What do you say?
    It can work, but it means the pipeline pushes to the branch, which needs write credentials and can fight concurrent pushes and re-trigger itself. The cheaper version of the same idea is to format on save or in a pre-commit hook so the code arrives formatted, and keep `-check` in CI purely as a backstop.
  • What does `terraform fmt` do if a file contains a genuine HCL syntax error?
    It reports a parse error rather than reformatting, because it has to build a syntax tree before it can print one back out. That is incidental, though — it is not a validator, and you should not rely on it to catch broken configuration. `terraform validate` is the command that actually checks references, argument names and types.

saying these in an interview costs you the question

  • Thinks fmt -check rewrites the files it inspects
  • Claims terraform fmt validates the configuration or catches errors
  • Assumes fmt walks the whole repository by default
  • Believes reformatting can change what a resource does
  • Runs plain terraform fmt in CI and calls it a gate

context

open as a page

What does `terraform validate` actually check, and why can a configuration pass validate and still fail during apply?

level: middleimportance: must knowfreq 70%

basics

~20 s

terraform validate checks a configuration against itself and the installed provider schemas: syntax, references, required arguments, attribute names and type consistency. It contacts no provider API and reads no state, so permissions, quotas and real-world values only surface at plan or apply.

open as a page

What does TFLint catch in Terraform code that `terraform validate` cannot, and how does it know?

level: middleimportance: should knowfreq 48%

basics

~20 s

TFLint adds opinionated and provider-specific rules on top of schema correctness: unused declarations, deprecated idioms, naming conventions, and values a provider will reject such as a nonexistent instance type. Its provider rulesets ship that knowledge as plugins, so the checks stay offline.

open as a page

How does Terraform's native `terraform test` framework work — where do the test files live, and what does a `run` block do?

level: middleimportance: should knowfreq 45%

basics

~20 s

Tests are .tftest.hcl files in the module directory or a tests/ subdirectory. Each run block executes a plan or an apply against the module and evaluates assert conditions. Apply runs create real infrastructure, which Terraform destroys when the file finishes.

open as a page

Why do teams run Checkov or Trivy against `terraform show -json` plan output instead of the raw HCL, and what does that cost them?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Plan JSON contains resolved values, so a scanner sees the attribute a resource will really have instead of an unresolved var or module reference. The cost is that it needs init, credentials and a successful plan, so it runs late and can never be a pre-commit check.

open as a page

What does declaring `mock_provider "aws"` in a Terraform test file change about how the tests execute, and what stops being tested?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

A mock_provider block replaces the real provider, so Terraform walks the full plan and apply graph while generated values stand in for computed attributes. Nothing is created, no credentials are needed, and tests run in seconds — but no API ever validates the request.

open as a page