fmt & validate
terraform fmt and terraform validate are the two lightweight checks that keep a configuration tidy and internally sound before you ever touch real infrastructure. fmt rewrites your files into Terraform’s canonical style, eliminating the bikeshedding over indentation and alignment that plagues most codebases. validate parses the configuration and verifies it is syntactically valid and internally consistent — all without making a single API call to a provider. Both are fast, offline, and ideal for pre-commit hooks and the early stages of CI. OpenTofu ships the identical tofu fmt and tofu validate commands.
terraform fmt
fmt reads your .tf and .tfvars files and reformats them to the canonical HCL style: two-space indentation, aligned = signs within a block, consistent spacing, and tidy ordering of arguments. Because the style is fixed and non-configurable, every Terraform codebase formatted with fmt looks the same, which makes diffs smaller and reviews faster.
Running it with no arguments rewrites the files in the current directory in place and prints the names of any files it changed.
terraform fmt
Output:
main.tf
variables.tf
Consider this misaligned input. The arguments are unaligned and inconsistently indented:
resource "aws_s3_bucket" "logs" {
bucket = "devcraftly-logs"
tags = {
Name="log-bucket"
Env = "prod"
}
}
After terraform fmt, the same block becomes canonical:
resource "aws_s3_bucket" "logs" {
bucket = "devcraftly-logs"
tags = {
Name = "log-bucket"
Env = "prod"
}
}
Useful fmt flags
The defaults rewrite files in place, but several flags adapt fmt for CI, scripting, and multi-module repositories.
| Flag | Behavior |
|---|---|
-check | Exit non-zero if files would change; do not write anything |
-recursive | Process subdirectories as well as the current directory |
-diff | Print a unified diff of the changes that would be made |
-list=false | Suppress the list of changed file names |
-write=false | Do not overwrite files (pairs naturally with -diff) |
In CI you almost always want -check and -recursive together. This verifies that every file in the repository is already formatted, and fails the build if any is not, without modifying anything:
terraform fmt -check -recursive -diff
Output:
modules/network/main.tf
--- old/modules/network/main.tf
+++ new/modules/network/main.tf
@@ -3,3 +3,3 @@
- cidr_block="10.0.0.0/16"
+ cidr_block = "10.0.0.0/16"
The command exits with code 3 here because changes are needed but -write=false is implied by -check. An exit code of 0 means everything is already formatted.
terraform fmt -reads from stdin and writes the formatted result to stdout. This is how editor integrations format-on-save without invoking a full directory rewrite.
terraform validate
validate checks that a configuration is syntactically valid and internally consistent. It confirms that argument names exist, that types line up, that references point at declared resources and variables, and that required arguments are present. Crucially, it never contacts a provider’s API and never reads state — it cannot tell you whether a resource already exists or whether your credentials work. It answers one question only: is this configuration well-formed?
Because validate evaluates the configuration against provider schemas, the working directory must already be initialized so those schemas are available.
terraform init -backend=false
terraform validate
Output:
Success! The configuration is valid.
When something is wrong, validate reports the exact file, line, and a description of the problem:
╷
│ Error: Reference to undeclared resource
│
│ on main.tf line 12, in resource "aws_eip" "nat":
│ 12: instance = aws_instance.gateway.id
│
│ A managed resource "aws_instance" "gateway" has not been declared in the
│ root module.
╵
validate in automation
The -json flag emits machine-readable diagnostics, which is the basis for tooling that surfaces validation errors as inline annotations in pull requests.
terraform validate -json
Output:
{
"format_version": "1.0",
"valid": true,
"error_count": 0,
"warning_count": 0,
"diagnostics": []
}
validateonly checks the configuration, not your real infrastructure. A config can validate cleanly and still fail atapplytime because of an invalid AMI ID, a missing IAM permission, or a name collision. Useterraform planto catch those.
fmt vs validate at a glance
The two commands solve different problems and run in a natural order: format first, then validate.
| Aspect | terraform fmt | terraform validate |
|---|---|---|
| Purpose | Canonical formatting | Syntax + internal consistency |
Needs init? | No | Yes (for provider schemas) |
| Network calls | None | None |
| Reads state | No | No |
| Modifies files | Yes (unless -check) | Never |
| Typical CI flag | -check -recursive | -json |
Using both in pre-commit and CI
A pre-commit hook gives developers fast feedback before code ever leaves their machine. With the pre-commit framework, the pre-commit-terraform repo provides ready-made hooks:
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.1
hooks:
- id: terraform_fmt
- id: terraform_validate
In CI you typically gate the pipeline on both checks before reaching plan. The format check is non-mutating, and validation initializes without a backend so it needs no credentials:
terraform fmt -check -recursive
terraform init -backend=false
terraform validate
Output:
Success! The configuration is valid.
Running these two cheap, offline checks first means a malformed or inconsistent configuration fails in seconds, long before the slower plan and apply stages consume time and cloud quota.
Best Practices
- Run
terraform fmton save in your editor so code is always formatted before commit. - Enforce formatting in CI with
terraform fmt -check -recursiveso unformatted code cannot merge. - Run
terraform validateafterinitand beforeplanto catch reference and type errors early. - Initialize with
-backend=falsefor validation in CI to avoid needing backend credentials. - Add
terraform_fmtandterraform_validatepre-commit hooks for instant local feedback. - Remember that
validateis offline only — rely onplanto surface provider- and credential-level problems. - Use
-jsonoutput to postfmtandvalidateresults as pull-request annotations.