This lesson is not about a security tool, and it runs before the security tools.
terraform fmt and terraform validate ship in the binary you already have. Together they take about a second and catch a class of problem that Checkov and Trivy will never look for — because those tools scan for misconfiguration, and these catch broken and unreviewable.
Verified against Terraform 1.15.4, against the Range Terraform.
Step 1 — fmt, as a check rather than a fixer
git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range
terraform -chdir=terraform fmt -check -diff
echo "exit: $?"exit: 0Clean, because the Range is committed formatted. -check makes it report rather than rewrite, and -diff prints what it would change. Without -check, fmt silently reformats your files — fine locally, wrong in CI, where a job that mutates the checkout and exits zero has told you nothing.
Why a formatter belongs in a security pipeline. Formatting is not cosmetic when the artifact is infrastructure. A pull request where half the diff is whitespace is a pull request nobody reads properly, and an unread Terraform diff is how an over-permissioned IAM policy gets approved. fmt -check keeps the diff to the change, so the reviewer sees the change.
Step 2 — validate, and what it actually checks
terraform -chdir=terraform init -backend=false
terraform -chdir=terraform validateSuccess! The configuration is valid.-backend=false matters: it initialises providers without touching remote state, so this runs in CI with no credentials and no lock contention.
validate checks syntax, argument names and types, and that references resolve. It does not talk to AWS and it does not check whether a value is sensible — the Range validates cleanly while declaring a public bucket, a wildcard IAM policy and a security group open to the world. Valid and safe are different questions.
Why validate must come before the scanners. Checkov and Trivy parse HCL. Give them a file that does not parse and they can return zero findings and exit zero — a green scan on a file nothing understood. Ordering
validatefirst means a broken configuration fails as broken, rather than passing as clean.
Step 3 — Gate order
# .github/workflows/terraform.yml
name: terraform
on: [pull_request]
permissions:
contents: read
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
# 1. Is the diff reviewable?
- run: terraform fmt -check -recursive -diff
# 2. Does it parse and resolve? Providers only, no state.
- run: terraform init -backend=false
- run: terraform validate
# 3. Only now is it worth asking whether it is safe.
- run: trivy config . --exit-code 1 --severity HIGH,CRITICALCheapest and most fundamental first. A pull request that fails fmt never reaches the scanner, which keeps the feedback specific — "your formatting is off" rather than a wall of policy findings that were never the point.
-recursive on fmt, because without it only the top directory is checked and modules go unformatted.
Step 4 — The one that catches real bugs
validate earns its place on the failures it catches early:
resource "aws_s3_bucket" "data" {
bucket = var.bucket_nmae # typo — undeclared variable
}Error: Reference to undeclared input variableWithout validate this surfaces during plan — after the runner has assumed a role and hit the AWS API. With it, the pull request fails in two seconds with an exact line number and no credentials involved.
Stretch: add the third native check
terraform plan -detailed-exitcodeExit 0 means no changes, 2 means changes are pending, 1 means it errored. On a pull request that is how you post "this change touches 14 resources, 3 of them destructive" as a review comment. It needs credentials, so it belongs in a job with a scoped role — not alongside fmt and validate, which need none.
What you learned
fmt -check -diff, never barefmt, in CI. A job that rewrites the checkout and exits zero has told you nothing.- Formatting is review hygiene, and unread Terraform diffs are how bad IAM gets merged.
validatewith-backend=falseruns without credentials or state.- Valid is not safe. The Range validates cleanly while declaring a public bucket and a wildcard admin role.
- Order matters: parse before policy. A file the scanner cannot parse produces zero findings and a green tick.