The cheapest bug is the one caught before plan. Static checks read the code only: no credentials, no state, seconds to run.
flowchart LR ED["Edit .tf"] --> FMT["terraform fmt"] FMT --> VAL["terraform validate"] VAL --> LINT["tflint"] LINT --> SEC["trivy config / checkov"] SEC --> PR["Pull request"] %% step ED: You save a change in a .tf file. %% step FMT: fmt rewrites the file into the canonical style, so diffs only show real changes. %% step VAL: validate checks syntax, references and types without contacting any API. %% step LINT: TFLint knows provider rules, such as invalid instance types or unused variables. %% step SEC: A misconfiguration scanner flags insecure settings such as public buckets or privileged containers. %% step PR: Only code that passes all checks reaches review.
The four layers
| Tool | Command | Catches |
|---|---|---|
| fmt | terraform fmt -check -recursive |
Style differences (exit 3 if files need formatting, CI fails) |
| validate | terraform init -backend=false && terraform validate |
Syntax errors, unknown arguments, wrong types, bad references |
| TFLint | tflint --init && tflint --recursive |
Deprecated syntax, unused declarations, provider-specific errors (with a ruleset plugin) |
| Trivy / Checkov | trivy config . / checkov -d . |
Security misconfigurations against hundreds of built-in checks |
validate needs providers but not a backend, so init -backend=false is the trick in CI. Note it cannot catch errors that only the API knows about (an image that does not exist, a name already taken): that is what plan and tests are for.
TFLint configuration
# .tflint.hcl
plugin "terraform" {
enabled = true
preset = "recommended"
}
plugin "aws" {
enabled = true
version = "0.38.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
Suppressing a finding (with a reason)
resource "docker_container" "debug" {
#trivy:ignore:AVD-DS-0002 debug tool, dev only, removed after the incident
privileged = true
}
A suppression without a reason is a smell; reviewers should ask for one.
pre-commit
The pre-commit-terraform hooks run these tools on every commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.99.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- id: terraform_trivy
Run the same checks again in CI: hooks can be skipped with git commit --no-verify.
Traps: terraform fmt without -check rewrites files, which in CI hides the problem instead of failing; and a clean validate says nothing about whether apply will succeed.