Skip to main content

Enforce Terraform Conventions with the tflint Hook Kind

· 3 min read
Erik Osterman
Founder @ Cloud Posse

Consistent, idiomatic Terraform doesn't happen by accident — it takes a linter enforcing the same conventions on every component, every run. Atmos already runs security and cost scanners as component hooks; now it runs a linter the same way, with a built-in tflint hook kind.

The Problem

terraform validate only proves your configuration parses and is internally consistent. It says nothing about the things that keep a growing codebase healthy:

  • Code consistency & conventions — naming, formatting, and required version constraints, applied uniformly across every component and every contributor.
  • Hygiene — unused variables, outputs, and locals; deprecated interpolation syntax; provider settings that will bite you later.
  • Correctness validate misses — invalid instance types, unsupported arguments, and other provider-specific mistakes.

tflint checks all of that. But without a first-class integration, enforcing it meant a hand-rolled kind: command hook in every stack — and figuring out how to get its output to render. Worse, standards that aren't automated quietly become optional: they hold only as long as a reviewer remembers to look.

There was also a mechanical snag. The security scanners write their SARIF report to a file (--output $ATMOS_OUTPUT_FILE), which is how Atmos picks it up. tflint has no file-output flagtflint --format=sarif writes to stdout. So a first-class tflint kind needed the hook engine to learn one new trick: capture a tool's stdout.

The Solution

A built-in tflint hook kind — zero configuration. Wire it on before.terraform.init so a lint failure stops you before any init/plan work, the way a fast-failing linter should:

components:
terraform:
vpc:
hooks:
lint:
events: [before.terraform.init]
kind: tflint

For CI workflows that only run atmos terraform plan, apply, or deploy and do not call atmos terraform init explicitly, use that command's before event instead (for example before.terraform.plan) so the lint summary is written in the same job.

The kind ships sane defaults (tflint --chdir=$ATMOS_COMPONENT_PATH --format=sarif, on_failure: warn) and runs against the same directory Terraform does — including the provisioned workdir when that feature is enabled.

Under the hood, hook kinds can now opt into stdout capture: Atmos redirects the tool's stdout into its structured-output side channel, so a tool that only prints to stdout is handled identically to one that writes a file. From there, tflint rides the exact same path as the other scanners — a single markdown summary in your terminal and on the Atmos Pro run page.

And because it produces SARIF through the shared handler, tflint automatically inherits everything the scanner CI integration added: a job step summary, inline PR annotations on the diff, and a GitHub Code Scanning upload — all gated by your ci: config, no extra wiring. (See Scanner Findings as Inline PR Annotations and Code Scanning Alerts.)

How to Use It

  1. Make sure tflint is on PATH (brew install tflint), or pin it for auto-install:

    dependencies:
    tools:
    tflint: "0.59.1"
  2. Add the hook to a component and run any Terraform command:

    atmos terraform plan vpc -s test

tflint runs first — before init — and renders its findings. The builtin terraform ruleset enforces core conventions with no setup; add a .tflint.hcl and run tflint --init once for provider rulesets (aws/google/azurerm) that catch cloud-specific mistakes (a kind: command hook works for that). A working, credential-free setup lives in examples/hooks-tflint.

Findings default to on_failure: warn, so conventions surface as guidance first — flip to fail when you're ready to enforce them as a gate.