Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 046 / 100

Terraform formatting and validation: a step-by-step guide

Keep code clean automatically.

Expona Admin7 min read

Terraform formatting and validation: a step-by-step guide

As infrastructure code scales across teams and environments, inconsistent formatting and silent configuration errors become expensive problems. Terraform provides built-in tools to enforce code standards automatically and catch mistakes before they reach production. This guide walks through Terraform formatting and validation: a step-by-step guide, showing how to keep code clean automatically while integrating validation into your CI/CD pipeline.

Why formatting and validation matter

We've all seen Terraform repositories where indentation varies, variable naming conventions drift, and subtle errors slip through code review. The cost isn't just aesthetic—it's cognitive load during debugging, longer review cycles, and increased risk of deploying broken infrastructure. A team using consistent formatting and validation catches these issues before they propagate.

Terraform's native tools—fmt and validate—plus the community-driven TFLint handle different layers of code quality. Together, they form a safety net that scales with your codebase.

terraform fmt -recursive: enforcing consistent style

terraform fmt rewrites Terraform configuration files to a canonical format. It normalizes indentation, aligns attributes, and sorts arguments in a predictable order. The -recursive flag applies this across entire directory trees, making it ideal for monorepos and large deployments.

Run this once to see what would change without modifying files:

hcl
terraform fmt -recursive -check .

The -check flag returns a non-zero exit code if reformatting is needed, perfect for CI checks. To apply formatting in place across all Terraform files in the current directory and subdirectories:

hcl
terraform fmt -recursive .

A practical example: imagine a Terraform module for RDS clusters with inconsistent spacing:

hcl
resource "aws_rds_cluster" "main" {
cluster_identifier = "prod-db"
engine = "aurora-postgresql"
  master_username       = var.db_user
master_password=var.db_password
    backup_retention_period = 7
}

After running terraform fmt -recursive, it becomes:

hcl
resource "aws_rds_cluster" "main" {
  cluster_identifier      = "prod-db"
  engine                  = "aurora-postgresql"
  master_username         = var.db_user
  master_password         = var.db_password
  backup_retention_period = 7
}

Notice the consistent indentation, aligned equals signs, and logical grouping. Teams can enforce this in pre-commit hooks or CI pipelines, removing the need for style debates in pull request reviews.

terraform validate: catching syntax and configuration errors

terraform validate checks whether configuration files are syntactically valid and internally consistent. It verifies that referenced resources, variables, and outputs exist, and that block types are recognized. Unlike fmt, validate doesn't modify files—it only reports problems.

Run validation in any Terraform working directory:

hcl
terraform validate

A successful run outputs:

hcl
Success! The configuration is valid.

When issues are found, validate reports line numbers and descriptions:

hcl
Error: Reference to undefined variable

  on main.tf line 12, in resource "aws_instance" "app":
   12:   ami = var.nonexistent_ami

An input variable "nonexistent_ami" has not been declared.

Consider a scenario where a developer adds a new security group but forgets to define the required variable:

hcl
resource "aws_security_group" "app" {
  name = var.sg_name  # This variable is not defined
}

variable "vpc_id" {
  type = string
}

Running terraform validate catches this before the code reaches a pipeline or review stage. The validation also checks for circular dependencies, duplicate resource names, and type mismatches in expressions.

Important note: validate does not verify that your AWS account has sufficient permissions, that resources don't already exist, or that your variable values are sensible. It's a syntax and structural check, not a full deployment simulation.

TFLint for provider-specific rules and best practices

While validate catches syntax errors, TFLint enforces style conventions, security best practices, and provider-specific rules that Terraform's native tools don't cover. It's community-maintained and highly configurable, supporting AWS, Azure, GCP, Kubernetes, and dozens of other providers.

Install TFLint (macOS example with Homebrew):

hcl
brew install tflint

Run TFLint in a directory:

hcl
tflint

A sample output showing common issues:

hcl
1 issue(s) found:

Notice: Missing version constraint for provider "aws" (terraform_required_providers)
  on main.tf line 1

Warning: [aws_instance_invalid_type] instance_type is invalid
  on main.tf line 8 in resource "aws_instance" "web"
   8:   instance_type = "t2.ultrasmall"

Error: [aws_security_group_invalid_ingress] Ingress rule should not allow unrestricted access
  on main.tf line 15 in resource "aws_security_group" "allow_all"
   15:   cidr_blocks = ["0.0.0.0/0"]

Configure TFLint with a .tflint.hcl file in your repository root to customize rules and set provider-specific behavior:

hcl
plugin "aws" {
  enabled = true
  version = "0.24.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

rule "terraform_required_version" {
  enabled = true
}

rule "terraform_required_providers" {
  enabled = true
}

rule "aws_instance_invalid_type" {
  enabled = true
}

rule "aws_security_group_invalid_ingress" {
  enabled = true
}

A real-world example: a team writing an ECS task definition module runs TFLint and discovers that their log retention period is too short and their container image doesn't specify a tag (risking unexpected updates):

hcl
resource "aws_cloudwatch_log_group" "ecs" {
  name              = "/ecs/my-service"
  retention_in_days = 1  # TFLint warns: too short for production
}

resource "aws_ecs_task_definition" "app" {
  container_definitions = jsonencode([
    {
      image = "123456789.dkr.ecr.us-east-1.amazonaws.com/my-app"  # TFLint warns: missing tag
    }
  ])
}

With these issues caught before deployment, the team avoids production surprises and security gaps.

Run validation in pre-commit and CI

Manual validation only works if developers remember to run it. Integrate validation into pre-commit hooks and CI pipelines to make it automatic and non-negotiable.

Pre-commit setup

Create a .pre-commit-config.yaml in your repository root:

hcl
repos:
  - repo: https://github.com/terraform-linters/pre-commit-terraform
    rev: v1.85.0
    hooks:
      - id: terraform_fmt
        args: ['-recursive']
      - id: terraform_validate
      - id: tflint

Install pre-commit on your machine:

hcl
pip install pre-commit

Set up the git hooks:

hcl
pre-commit install

Now whenever a developer runs git commit, these checks run automatically. If any fail, the commit is blocked and output shows the issue.

CI pipeline integration

Add validation to your GitHub Actions, GitLab CI, or other CI system. A GitHub Actions example:

hcl
name: Terraform Validation

on:
  pull_request:
    paths:
      - 'terraform/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.7.0
      
      - name: Terraform Format Check
        run: terraform fmt -recursive -check terraform/
      
      - name: Terraform Validate
        run: terraform -chdir=terraform validate
      
      - name: TFLint
        uses: terraform-linters/setup-tflint@v4
      - run: tflint --init
      - run: tflint terraform/

With this workflow, every pull request automatically validates formatting and correctness. Failed checks prevent merging, ensuring code quality standards are enforced consistently across the team.

Common mistakes

  • Running fmt but not validate: Formatting only checks style, not correctness. A file can be perfectly formatted and still fail at plan time due to missing variables or invalid references.
  • Ignoring TFLint warnings: Many teams treat TFLint as optional, but its security and best-practice rules often catch subtle issues—like hardcoded credentials or overly permissive IAM policies—that validate misses.
  • Not committing .tflint.hcl: If TFLint configuration varies across developer machines, the same code might pass locally but fail in CI, creating frustration and delays.
  • Skipping validation for generated code: Terraform files generated by tools, Terragrunt, or CDK still need validation. Many automation frameworks can skip validation steps, leading to hidden errors.
  • Validating without a backend or vars file: terraform validate works without remote state or variable files, but it won't catch type mismatches between variable definitions and actual values used in resources.

Key takeaways

  • terraform fmt -recursive normalizes formatting across your codebase, removing style inconsistencies and reducing cognitive load in code reviews.
  • terraform validate catches syntax errors and structural problems—missing variables, invalid references, circular dependencies—before they reach production.
  • TFLint enforces provider-specific best practices, security rules, and conventions that native Terraform tools don't cover, catching issues like unrestricted ingress rules or missing resource tags.
  • Pre-commit hooks and CI pipelines make validation automatic and non-optional, scaling code quality enforcement across your entire team.
  • Combine all three tools in a cohesive workflow: fmt for style, validate for syntax, and TFLint for best practices.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles