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.