· 5 min read
How to Format Terraform the Canonical Way
Manesh Jayawardhana
CIO & Co-founder
A Terraform pull request shows forty changed lines. Thirty-six of them are whitespace, because someone’s editor reindented the file and the equals signs shifted. Finding the four lines that change infrastructure means reading all forty.
HCL has a canonical format precisely so this does not happen, and it is enforceable in one line of CI.
What canonical formatting does
Two rules produce most of the effect.
Two-space indentation for each nesting level.
Aligned assignments within each group of consecutive arguments. The equals signs line up, which makes a block scannable at a glance.
The alignment detail people miss is that it resets at a blank line or a nested block. A resource with three arguments, a blank line, then four more has two independent alignment groups, each aligned within itself. That is why a long block can have equals signs at several different columns and still be correctly formatted.
Understanding that rule is the difference between trusting the formatter and fighting it.
Why it matters more in infrastructure code
Formatting churn is annoying in application code. In infrastructure code it is a review hazard.
A Terraform diff is the thing standing between a plan and production. When a review has to distinguish four real changes from thirty-six cosmetic ones, the probability of missing one rises — and the missed one might be a security group rule or a deletion protection flag.
Consistent formatting is not tidiness here. It is what keeps the diff readable enough to review properly.
| Practice | Effect on review |
|---|---|
| Formatted on commit | Diff shows only real changes |
| Formatted occasionally | Periodic whole-file churn |
| Never formatted | Every diff mixes both |
terraform fmt -check in CI | Unformatted code cannot merge |
Where to enforce it
Pre-commit hook — catches it before the code leaves the machine, which is the cheapest place.
CI check — terraform fmt -check -recursive fails the build on unformatted files. This is the one that actually guarantees the repository stays formatted, because it does not depend on anyone having installed the hook.
Editor on save — convenient and per-developer, so it supplements the other two rather than replacing them.
Formatting is also worth separating from any other change. A commit that reformats and modifies infrastructure in one go is a commit nobody can review.
Formatting is not the same as linting
Two different jobs, frequently conflated.
Formatting fixes layout — indentation, alignment, line breaks. It has one correct output and no opinions about content.
Linting enforces conventions — naming patterns, required tags, deprecated arguments, provider version constraints, modules pinned to specific versions. Tools like tflint and Checkov cover this ground, and the rules are yours to choose.
A repository with formatting enforced and no linting has consistent whitespace and inconsistent everything else. Resource naming drifts, tags are missed on half the resources, and security misconfigurations pass review because nobody was looking for them specifically.
Both belong in CI, and they catch entirely different classes of problem.
Format before review, not during
A workflow detail that keeps diffs readable.
Running the formatter as part of a pre-commit hook means every commit arrives formatted and no commit contains formatting churn. Reviews then show only substantive changes.
Running it manually and occasionally produces the opposite: a large formatting commit every few weeks, mixed into whatever else was being changed at the time.
Where a repository is being formatted for the first time, doing it as a single commit that touches nothing else — and adding that commit to a blame-ignore file — keeps history usable. Most git tooling supports ignoring specific revisions in blame output, which means a repository-wide reformat does not obscure who last changed a line.
Common mistakes to avoid
- Reformatting and changing resources in the same commit.
- Fighting the alignment reset at blank lines by manually padding, which the formatter then undoes.
- Running the formatter locally and having no CI check, so the repository drifts whenever someone forgets.
- Assuming formatting validates anything — it does not check syntax, references or provider requirements.
- Formatting generated files that are overwritten on the next run.
How to do it with Terraform HCL Formatter
The Terraform HCL Formatter applies the canonical rules to a pasted snippet.
- Paste the HCL — a resource block, a module, or a whole file.
- Format to the canonical style with aligned assignment groups.
- Copy it back into your configuration.
- Add
terraform fmt -check -recursiveto CI so this stops being a manual step.
The terraform fmt documentation covers the command and its flags. Other developer tools are in the tools directory.
Frequently asked questions
Why do the equals signs align in groups rather than across the block?
Because alignment resets at a blank line or a nested block. Each run of consecutive arguments aligns within itself, which is why one block can have several alignment columns and still be canonically formatted.
Does formatting validate my configuration?
No. It only fixes layout. Use terraform validate for syntax and reference checking, and a plan to see what would actually change.
Should generated files be formatted?
Not usually — they are overwritten on the next generation, so formatting them creates churn with no benefit. Exclude them from the check rather than reformatting them each time.
Final thought
Put terraform fmt -check in CI and reformat once, on its own. After that every diff shows only what actually changes your infrastructure.