Static Analysis and Linting

Static analysis catches errors before you even run Terraform, identifying syntax issues, security problems, and style inconsistencies that could cause problems later. Unlike application code, infrastructure code mistakes can be expensive—literally. A misconfigured security group or an oversized instance type can cost money and create security vulnerabilities.

The tools and practices in this part form the first line of defense against infrastructure code problems, catching issues in your editor and CI pipeline before they reach cloud resources.

Terraform Built-in Validation

Terraform includes several built-in validation commands that should be part of every workflow:

# Format code consistently
terraform fmt -recursive

# Check for syntax errors and validate configuration
terraform validate

# Generate and review execution plans
terraform plan -out=tfplan

# Show plan in human-readable format
terraform show tfplan

# Show plan in JSON for automated analysis
terraform show -json tfplan | jq '.planned_values'

Automated formatting ensures consistent code style:

# Check if files need formatting (exits with code 3 if changes needed)
terraform fmt -check -recursive

# Format all files in current directory and subdirectories
terraform fmt -recursive

# Show what would be formatted without making changes
terraform fmt -diff -check

Configuration validation catches syntax and logic errors:

# Validate configuration syntax
terraform validate

# Validate with specific variable values
terraform validate -var="environment=prod"

# Validate without initializing providers
terraform validate -backend=false

TFLint for Advanced Linting

TFLint provides deeper analysis than Terraform’s built-in validation:

# Install TFLint
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

# Initialize TFLint with plugins
tflint --init

# Run linting
tflint

# Run with specific ruleset
tflint --enable-rule=terraform_unused_declarations

TFLint configuration (.tflint.hcl):

config {
  module = true
  force = false
}

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

rule "terraform_deprecated_interpolation" {
  enabled = true
}

rule "terraform_unused_declarations" {
  enabled = true
}

rule "terraform_comment_syntax" {
  enabled = true
}

rule "terraform_documented_outputs" {
  enabled = true
}

rule "terraform_documented_variables" {
  enabled = true
}

rule "terraform_typed_variables" {
  enabled = true
}

rule "terraform_module_pinned_source" {
  enabled = true
}

rule "terraform_naming_convention" {
  enabled = true
  format  = "snake_case"
}

rule "terraform_standard_module_structure" {
  enabled = true
}

AWS-specific rules catch cloud-specific issues:

# Check for deprecated instance types
tflint --enable-rule=aws_instance_previous_type

# Validate security group rules
tflint --enable-rule=aws_security_group_rule_description

# Check for invalid AMI IDs
tflint --enable-rule=aws_instance_invalid_ami

Checkov for Security Scanning

Checkov scans for security and compliance issues:

# Install Checkov
pip install checkov

# Scan Terraform files
checkov -f main.tf

# Scan entire directory
checkov -d .

# Output in different formats
checkov -d . --output json
checkov -d . --output sarif

# Skip specific checks
checkov -d . --skip-check CKV_AWS_23

# Run only specific frameworks
checkov -d . --framework terraform

Custom Checkov policies for organization-specific rules:

# custom_checks/RequireOwnerTag.py
from checkov.common.models.enums import TRUE_VALUES
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck

class RequireOwnerTag(BaseResourceCheck):
    def __init__(self):
        name = "Ensure all resources have Owner tag"
        id = "CKV_CUSTOM_1"
        supported_resources = ['*']
        categories = [CheckCategories.GENERAL_SECURITY]
        super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        """
        Looks for Owner tag in resource configuration
        """
        if 'tags' in conf:
            tags = conf['tags'][0]
            if isinstance(tags, dict) and 'Owner' in tags:
                return CheckResult.PASSED
        return CheckResult.FAILED

check = RequireOwnerTag()

Terraform Docs for Documentation

Terraform-docs generates documentation from your code:

# Install terraform-docs
curl -sSLo ./terraform-docs.tar.gz https://terraform-docs.io/dl/v0.16.0/terraform-docs-v0.16.0-$(uname)-amd64.tar.gz
tar -xzf terraform-docs.tar.gz
chmod +x terraform-docs
sudo mv terraform-docs /usr/local/bin/terraform-docs

# Generate documentation
terraform-docs markdown table . > README.md

# Generate with custom template
terraform-docs markdown table --output-file README.md .

Configuration file (.terraform-docs.yml):

formatter: "markdown table"
header-from: main.tf
footer-from: ""
recursive:
  enabled: false
  path: modules
sections:
  hide: []
  show: []
content: |-
  # {{ .Header }}
  
  {{ .Requirements }}
  
  {{ .Providers }}
  
  {{ .Modules }}
  
  {{ .Resources }}
  
  {{ .Inputs }}
  
  {{ .Outputs }}
output:
  file: "README.md"
  mode: inject
  template: |-
    <!-- BEGIN_TF_DOCS -->
    {{ .Content }}
    <!-- END_TF_DOCS -->
sort:
  enabled: true
  by: name
settings:
  anchor: true
  color: true
  default: true
  description: false
  escape: true
  hide-empty: false
  html: true
  indent: 2
  lockfile: true
  read-comments: true
  required: true
  sensitive: true
  type: true

Pre-commit Hooks

Pre-commit hooks run validation automatically before commits:

# Install pre-commit
pip install pre-commit

# Install hooks
pre-commit install

Pre-commit configuration (.pre-commit-config.yaml):

repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.81.0
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_docs
        args:
          - --hook-config=--path-to-file=README.md
          - --hook-config=--add-to-existing-file=true
          - --hook-config=--create-file-if-not-exist=true
      - id: terraform_tflint
        args:
          - --args=--only=terraform_deprecated_interpolation
          - --args=--only=terraform_unused_declarations
          - --args=--only=terraform_comment_syntax
          - --args=--only=terraform_documented_outputs
          - --args=--only=terraform_documented_variables
          - --args=--only=terraform_typed_variables
          - --args=--only=terraform_module_pinned_source
          - --args=--only=terraform_naming_convention
          - --args=--only=terraform_standard_module_structure
      - id: terraform_tfsec
      - id: terraform_checkov
        args:
          - --args=--skip-check CKV2_AWS_6
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files

TFSec for Security Analysis

TFSec focuses specifically on security issues:

# Install tfsec
curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash

# Run security scan
tfsec .

# Output in different formats
tfsec --format json .
tfsec --format sarif .

# Exclude specific checks
tfsec --exclude aws-s3-enable-logging .

# Run with custom checks
tfsec --custom-check-dir ./custom-checks .

Custom TFSec rules:

package custom

import (
    "github.com/aquasecurity/tfsec/pkg/result"
    "github.com/aquasecurity/tfsec/pkg/severity"
    "github.com/aquasecurity/tfsec/pkg/state"
    "github.com/aquasecurity/tfsec/pkg/rule"
)

var RequireOwnerTag = rule.Rule{
    LegacyID: "CUS001",
    BadExample: []string{`
resource "aws_instance" "bad_example" {
  ami           = "ami-12345678"
  instance_type = "t2.micro"
}
`},
    GoodExample: []string{`
resource "aws_instance" "good_example" {
  ami           = "ami-12345678"
  instance_type = "t2.micro"
  
  tags = {
    Owner = "team-name"
  }
}
`},
    Links: []string{
        "https://example.com/tagging-policy",
    },
    RequiredTypes: []string{"resource"},
    RequiredLabels: []string{"aws_instance"},
    Base: rule.Base{
        Rule: result.Rule{
            AVDID:       "AVD-CUS-0001",
            Provider:    "aws",
            Service:     "ec2",
            ShortCode:   "require-owner-tag",
            Summary:     "Resource should have Owner tag",
            Impact:      "Resources without Owner tag cannot be tracked for cost allocation",
            Resolution:  "Add Owner tag to resource",
            Explanation: "All resources should have an Owner tag for cost allocation and management purposes",
            Severity:    severity.Medium,
        },
    },
}

Automated Quality Gates

Integrate static analysis into CI/CD pipelines:

# GitHub Actions workflow
name: Terraform Quality Gates
on:
  pull_request:
    paths: ['**.tf', '**.tfvars']

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.6.0
      
      - name: Terraform Format Check
        run: terraform fmt -check -recursive
      
      - name: Terraform Validate
        run: |
          terraform init -backend=false
          terraform validate
      
      - name: Run TFLint
        uses: terraform-linters/setup-tflint@v3
        with:
          tflint_version: v0.47.0
      - run: tflint --init
      - run: tflint -f compact
      
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform
          output_format: sarif
          output_file_path: checkov.sarif
      
      - name: Run TFSec
        uses: aquasecurity/[email protected]
        with:
          sarif_file: tfsec.sarif
      
      - name: Upload SARIF files
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: |
            checkov.sarif
            tfsec.sarif

Quality Metrics and Reporting

Track code quality metrics over time:

#!/bin/bash
# quality-report.sh

echo "=== Terraform Quality Report ==="
echo "Generated: $(date)"
echo

echo "=== Format Check ==="
terraform fmt -check -recursive
FORMAT_EXIT=$?

echo "=== Validation ==="
terraform validate
VALIDATE_EXIT=$?

echo "=== TFLint Results ==="
tflint --format compact
TFLINT_EXIT=$?

echo "=== Security Scan ==="
tfsec --format table
TFSEC_EXIT=$?

echo "=== Documentation Check ==="
terraform-docs markdown table . > /tmp/docs.md
if diff -q README.md /tmp/docs.md > /dev/null; then
    echo "Documentation is up to date"
    DOCS_EXIT=0
else
    echo "Documentation needs updating"
    DOCS_EXIT=1
fi

echo
echo "=== Summary ==="
echo "Format: $([ $FORMAT_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")"
echo "Validation: $([ $VALIDATE_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")"
echo "Linting: $([ $TFLINT_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")"
echo "Security: $([ $TFSEC_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")"
echo "Documentation: $([ $DOCS_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")"

OVERALL_EXIT=$((FORMAT_EXIT + VALIDATE_EXIT + TFLINT_EXIT + TFSEC_EXIT + DOCS_EXIT))
exit $OVERALL_EXIT

What’s Next

Static analysis provides the foundation for infrastructure code quality, but it can only catch certain types of issues. To validate that your infrastructure actually works as intended, you need testing strategies that go beyond syntax checking.

In the next part, we’ll explore unit testing strategies for Terraform modules, including techniques for testing logic without creating real cloud resources.