Skip to content

Terraform Tools for Developer Productivity Master Guide

Tools, Workflow, Security, Testing, CI/CD, Automation, AI Assistance, Governance, and Production Best Practices

Verified baseline: 5 September 2026
Audience: Terraform developers, DevOps/SRE, platform/cloud engineers, module authors, DevSecOps teams, and engineering leaders.

This handbook treats Terraform development as an engineering system, not a collection of unrelated commands. The goal is a fast local feedback loop, reproducible plans, secure authentication, reviewable changes, automated quality gates, policy enforcement, controlled production execution, and continuous drift/health feedback.


Executive standard

A strong default stack for a professional Terraform team is:

VS Code / Cursor / Claude Code
        ↓
HashiCorp Terraform Extension + terraform-ls
        ↓
Terraform MCP Server for current Registry/HCP context
        ↓
tenv
        ↓
Terraform CLI
        ↓
terraform fmt
terraform validate
terraform test
        ↓
TFLint
        ↓
Trivy
        ↓
terraform-docs
        ↓
Infracost
        ↓
pre-commit-terraform
        ↓
Git
        ↓
GitHub / GitLab
        ↓
CI quality gates + OIDC/workload identity
        ↓
HCP Terraform / Terraform Enterprise or another controlled runner
        ↓
Sentinel / OPA / Terraform Policy where appropriate
        ↓
Cloud infrastructure
        ↓
Post-deployment validation + drift/health monitoring

Tool classification

Tool / capability Default classification Why
Terraform CLI MUST HAVE Core IaC engine
Git MUST HAVE Reviewable, auditable change history
terraform fmt MUST HAVE Canonical formatting
terraform validate MUST HAVE Configuration validity
Remote state + locking MUST HAVE for teams Prevents local-state drift and concurrent writes
Provider/module version constraints MUST HAVE Reproducibility
.terraform.lock.hcl in Git MUST HAVE for root configurations Reproducible provider selection/checksums
VS Code + HashiCorp extension STRONGLY RECOMMENDED Fast authoring feedback
terraform-ls STRONGLY RECOMMENDED IDE intelligence; usually bundled
tenv STRONGLY RECOMMENDED Reproducible CLI versions
TFLint STRONGLY RECOMMENDED Terraform/provider-aware linting
Trivy STRONGLY RECOMMENDED IaC misconfiguration + optional secret scanning
terraform test STRONGLY RECOMMENDED Native module/root tests
terraform-docs STRONGLY RECOMMENDED for modules Prevents stale README interfaces
pre-commit-terraform STRONGLY RECOMMENDED Cheap local automation
CI plan workflow MUST HAVE for production teams Reproducible review gate
OIDC/workload identity MUST HAVE for CI cloud auth Avoids static cloud credentials
Infracost RECOMMENDED PR-level cost feedback
Renovate RECOMMENDED Dependency hygiene
Terraform MCP Server RECOMMENDED with AI coding Current Registry/HCP context
Checkov OPTIONAL / context dependent Strong policy/compliance scanning; can overlap Trivy
Terratest OPTIONAL Real integration/E2E testing when native tests are insufficient
Terragrunt OPTIONAL Useful for large repeated multi-account/region layouts
Atlantis OPTIONAL PR-driven execution if it fits the operating model
HCP Terraform RECOMMENDED / ENTERPRISE Remote runs, state, governance, registry, RBAC
Terraform Enterprise ENTERPRISE Self-hosted HCP Terraform distribution
Sentinel / OPA ENTERPRISE / governance Policy gates
Conftest OPTIONAL Lightweight vendor-neutral Rego execution
tfupdate OPTIONAL / niche Focused Terraform version rewriting; Renovate is broader

2026-specific guidance

  1. Terraform MCP Server is now a first-class option for AI-assisted Terraform. Use it to ground an AI agent in current provider/module documentation instead of trusting model memory.
  2. Native terraform test is the default first testing layer. Add Terratest when you truly need live system behavior, cross-service checks, retries, or non-Terraform assertions.
  3. Trivy is the normal replacement for tfsec in new workflows.
  4. For new S3 backends use native S3 state locking (use_lockfile = true). Do not design a new backend around DynamoDB locking.
  5. HCP Terraform policy choices are broader than Sentinel alone. OPA is supported and HashiCorp also has Terraform Policy in HCL; treat beta features cautiously.
  6. Terragrunt 1.x uses the streamlined CLI, e.g. terragrunt run --all plan; old run-all examples should be considered legacy.
  7. Do not build CI around static AWS/Azure/GCP keys. Use GitHub/GitLab OIDC or the cloud’s workload-identity mechanism.

1.1 The end-to-end model

Developer intent
   ↓
Editor / AI coding environment
   ↓
Language intelligence
   ↓
Version-controlled Terraform CLI
   ↓
Fast local quality gates
   ↓
Git change
   ↓
CI reproducibility gates
   ↓
Plan
   ↓
Security / cost / policy review
   ↓
Human approval
   ↓
Controlled apply
   ↓
Verification
   ↓
Drift / health feedback

Responsibility of each layer

Layer Responsibility Typical tools
Editor Write/navigate/refactor HCL VS Code, Cursor
AI assistant Boilerplate, explanation, refactoring Claude Code, Cursor, VS Code AI
Language server Schema-aware completion/diagnostics/navigation terraform-ls
AI context server Current Terraform Registry/HCP information Terraform MCP Server
Version manager Select project-approved binaries tenv, mise, asdf, tfenv
Terraform engine Init/plan/apply/state/test Terraform CLI
Formatter Canonical HCL style terraform fmt
Validator Terraform language/module validity terraform validate
Linter Terraform/provider best practices TFLint
Security scanner Misconfiguration/compliance/secret checks Trivy, Checkov
Test layer Behavior assertions terraform test, Terratest
Documentation Generate module interface docs terraform-docs
Cost analysis Estimate cost delta Infracost
Commit automation Run cheap checks before push pre-commit / pre-commit-terraform
VCS Review/audit GitHub, GitLab
CI Reproduce quality gates and create plan GitHub Actions, GitLab CI
Remote runner/orchestrator Controlled execution/state/governance HCP Terraform, TFE, Atlantis
Policy as code Organizational guardrails Terraform Policy, Sentinel, OPA
Cloud Actual infrastructure AWS, Azure, GCP, SaaS providers

Core principle: different tools answer different questions

fmt       → Is the code formatted?
validate  → Is the Terraform configuration structurally valid?
TFLint    → Is it suspicious, deprecated, inconsistent, or provider-invalid?
Trivy     → Is it insecure or dangerously configured?
test      → Does the module satisfy its intended behavior?
plan      → What will Terraform change in this real context?
Infracost → What may the cost impact be?
Policy    → Is the change allowed by organizational rules?
Review    → Should we make this change?
Apply     → Execute the approved change.
Verify    → Did the system reach the expected state?
Code language: JavaScript (javascript)

No single tool replaces the others.


What

Terraform CLI is the execution engine for Terraform configuration, dependency initialization, planning, applying, state operations, testing, and inspection.

Essential command map

Command Use it when Production note
terraform init First run, provider/module/backend changes Review lock-file changes
terraform fmt During authoring and before commit Automate
terraform validate After editing modules/config Does not validate live cloud behavior
terraform plan Before any change Review replacements/destruction
terraform apply Execute an approved plan Prefer controlled runner
terraform destroy Intentionally remove stack High-risk; require strong controls
terraform console Test expressions/functions Great for debugging locals/CIDR logic
terraform output Read root outputs Avoid exposing sensitive values
terraform show Inspect state or saved plan Plan/state can contain secrets
terraform providers Inspect provider dependencies Useful for upgrade debugging
terraform state Controlled state inspection/moves High-risk; use change procedure
terraform import Bring existing resources under management Prefer reviewable import workflows
terraform test Execute .tftest.hcl tests Default first test framework

Practical workflow

terraform version
terraform init
terraform fmt -recursive
terraform validate
terraform test
terraform plan -out=tfplan
terraform show tfplan
<em># apply only after review/approval</em>
terraform apply tfplan
Code language: HTML, XML (xml)

terraform init

What it does:

  • Initializes backend.
  • Downloads providers.
  • Downloads child modules.
  • Creates/updates .terraform.lock.hcl.
  • Prepares the working directory.

Useful patterns:

terraform init
terraform init -upgrade
terraform init -backend=false   <em># useful for validation jobs that do not need state</em>
terraform init -reconfigure
Code language: HTML, XML (xml)

Use -upgrade deliberately, not on every CI run, because it tells Terraform to re-evaluate dependency selections.

terraform fmt

terraform fmt
terraform fmt -recursive
terraform fmt -check -recursive

Local development: run write mode.
CI: run -check so CI detects unformatted code without modifying the branch.

terraform validate

terraform init -backend=false
terraform validate
Code language: JavaScript (javascript)

Validates Terraform configuration consistency and syntax in an initialized directory. It does not prove:

  • credentials are valid,
  • resource values are allowed by the cloud API,
  • IAM permissions are sufficient,
  • deployment will succeed,
  • architecture is secure,
  • cost is acceptable.

Those require lint/security/plan/test/policy/runtime checks.

terraform plan

terraform plan
terraform plan -out=tfplan
terraform show -no-color tfplan
terraform show -json tfplan > tfplan.json

Review especially:

  • -/+ replacements,
  • deletions,
  • IAM changes,
  • network exposure,
  • encryption changes,
  • database/storage replacement,
  • identity-provider changes,
  • state moves/imports,
  • count/for_each key churn.

Do not treat “Plan: 1 to add, 0 to change, 0 to destroy” as sufficient review. The kind of resource matters.

terraform apply

Recommended hierarchy:

  1. HCP Terraform/TFE or controlled CI runner.
  2. Dedicated deployment runner with OIDC/workload identity.
  3. Laptop apply only for sandbox/learning or documented break-glass workflows.

Applying a saved reviewed plan is safer than regenerating a plan immediately before apply when your workflow supports preserving the plan artifact securely.

terraform destroy

Treat as a destructive privileged operation. Prefer environment protection, explicit approval, and resource protection where appropriate.

terraform show

Saved plan JSON is excellent input for policy engines and scanners:

terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

Security: plan output can contain sensitive information. Store it only in trusted, short-lived CI artifacts.

terraform state

Useful commands:

terraform state list
terraform state show aws_instance.web
terraform state mv OLD_ADDRESS NEW_ADDRESS
terraform state rm ADDRESS
Code language: CSS (css)

Policy:

  • Never directly edit terraform.tfstate.
  • Back up/retain remote state versions.
  • Require peer review/change ticket for production state surgery.
  • Prefer Terraform language mechanisms such as moved blocks where they solve the problem cleanly.
  • state rm means “stop managing this object”; it does not delete the real object.

Why

The editor should catch low-cost mistakes before you reach a terminal or CI.

The official HashiCorp extension currently provides language-server-backed:

  • syntax highlighting,
  • IntelliSense/completion,
  • inline diagnostics,
  • formatting,
  • go-to-definition and symbols,
  • module/project navigation,
  • snippets,
  • HCP Terraform workspace/run integration,
  • support for newer Terraform language surfaces.

Installation

VS Code Marketplace extension:

HashiCorp Terraform
publisher: HashiCorp

The extension bundles a compatible terraform-ls, so most VS Code users should not separately install the language server.

Recommended settings

Example .vscode/settings.json:

{
  "[terraform]": {
    "editor.defaultFormatter": "HashiCorp.terraform",
    "editor.formatOnSave": true,
    "editor.formatOnPaste": false,
    "editor.codeActionsOnSave": {
      "source.organizeImports": "never"
    }
  },
  "[terraform-vars]": {
    "editor.defaultFormatter": "HashiCorp.terraform",
    "editor.formatOnSave": true
  },
  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": true,
  "editor.rulers": [100, 120]
}
Code language: JSON / JSON with Comments (json)

The HashiCorp extension should be the authority for Terraform formatting; avoid competing formatters for .tf.

Useful companion extensions

Keep the list small:

  • GitLens or your organization’s Git review extension.
  • YAML extension for CI/config files.
  • Markdown lint/preview for module docs.
  • EditorConfig if your organization uses it.
  • AI assistant only if approved by your security/data policy.

Avoid installing multiple Terraform format/language extensions at the same time.


What

terraform-ls is HashiCorp’s official Terraform language server. It exposes IDE features over the Language Server Protocol.

Provides

  • completions,
  • diagnostics,
  • navigation,
  • reference awareness,
  • provider/schema intelligence,
  • module awareness,
  • workspace indexing.

Installation decision

VS Code

Do not normally install manually. The HashiCorp Terraform extension bundles the compatible server.

Other LSP-compatible editors

Manual installation can be appropriate.

macOS/Linux with Homebrew:

brew install hashicorp/tap/terraform-ls
terraform-ls version

Best practice

Open the project folder/workspace, not only one .tf file. Whole-project indexing enables better module/ref navigation.


What

Terraform MCP Server connects an MCP-capable AI client to current Terraform provider/module/policy information and, when configured, HCP Terraform/TFE context.

Problem it solves

Without external grounding, an AI model can produce:

  • provider arguments removed two versions ago,
  • fictional resources,
  • invalid nesting,
  • outdated module interfaces,
  • obsolete HCP Terraform behavior.

MCP reduces that risk by giving the agent current source context.

Architecture

Developer request
      ↓
AI host / coding agent
      ↓
MCP client
      ↓
Terraform MCP Server
      ↓
Terraform Registry / HCP Terraform
      ↓
Current provider, module, policy, workspace context
      ↓
AI-generated suggestion
      ↓
fmt → validate → lint → security → test → plan
Code language: JavaScript (javascript)

Use cases

  • “Show the current schema for this AWS resource before generating it.”
  • “Find an approved module in our private registry.”
  • “Explain this provider argument using the current provider docs.”
  • “Generate a module that is compatible with our pinned provider.”
  • “Find the HCP Terraform workspace for this stack and summarize its run context.”

Example prompts

Using the Terraform MCP Server, verify the current AWS provider schema for
aws_s3_bucket and generate the smallest secure example compatible with our
required provider constraint. Do not invent attributes.
Code language: JavaScript (javascript)
Find the current Registry documentation for the module used in ./network.
Explain which inputs are required and identify any code in our wrapper that is
using removed/deprecated inputs. Do not change state addresses.
Code language: JavaScript (javascript)
Before modifying this Terraform, list the resources that would be security
sensitive (IAM, KMS, network ingress, state backend). Generate code only after
checking current provider documentation. Then run fmt/validate/test and explain
remaining assumptions.
Code language: JavaScript (javascript)

Security rules

Treat the MCP server as a privileged integration if it can access private HCP Terraform information.

  • Scope HCP tokens minimally.
  • Do not expose sensitive state to an AI tool by default.
  • Separate read/documentation use from write/run-management permissions.
  • Review generated commands before execution.
  • Never authorize autonomous production apply merely because the generated HCL validates.

Recommendation: Strongly recommended for teams already using AI-assisted Terraform, optional otherwise.


What

tenv is a Go-based version manager for Terraform, OpenTofu, Terragrunt, Terramate, and Atmos. It is the successor to tfenv/tofuenv.

Why

One globally installed Terraform version creates avoidable problems:

  • project A needs a new language feature,
  • project B is still validated on an older minor version,
  • CI and laptop silently differ,
  • developers accidentally upgrade providers/state behavior by upgrading the CLI.

Installation

macOS:

brew install tofuutils/tap/tenv

Verify:

tenv --version
tenv tf --help

Common usage

tenv tf install 1.16.0
tenv tf use 1.16.0
terraform version
Code language: CSS (css)

Project file:

# .terraform-version
1.16.0
Code language: CSS (css)

tenv can also resolve compatible Terraform versions from required_version constraints.

Team policy

For root configurations:

terraform {
  required_version = "~> 1.16.0"
}
Code language: JavaScript (javascript)

and use the same minor/patch policy in CI.

For reusable modules, avoid unnecessarily over-constraining callers. If the module only needs a feature introduced in 1.7:

terraform {
  required_version = ">= 1.7.0"
}
Code language: JavaScript (javascript)

Comparison

Tool Best fit Strength Limitation
tenv Terraform/OpenTofu/Terragrunt-centric teams Purpose-built, multi-IaC binary support Another tool to standardize
tfenv Existing Terraform-only estates Familiar and simple tenv is its modern successor
asdf Polyglot toolchains One manager for many ecosystems Plugin lifecycle/UX
mise Modern polyglot developer environments Fast, broad tooling and task support Broader than Terraform-specific need

Default recommendation: tenv for Terraform-centric teams; mise is an excellent choice if the organization already standardizes all developer tools through it.


Formatting is non-negotiable because it removes style arguments from review.

terraform fmt -recursive
terraform fmt -check -recursive

Local: write/fix.
CI: -check.

Do not build an internal HCL formatting standard that conflicts with terraform fmt.


Use it as a structural gate, not as a linter or security test.

CI-friendly:

terraform init -backend=false -input=false
terraform validate -no-color
Code language: JavaScript (javascript)

Use a real plan later when values/backend/provider runtime context matters.


What

TFLint is a pluggable Terraform linter. Its built-in Terraform rules catch language-level issues, and cloud provider rulesets add checks that Terraform’s core validation cannot know.

Problems it solves

  • deprecated HCL patterns,
  • unused declarations,
  • missing required provider/version constraints,
  • unpinned Registry modules,
  • invalid or discouraged provider-specific values,
  • invalid instance types,
  • organization naming conventions through configured/custom rules.

Installation

macOS:

brew install terraform-linters/tap/tflint

Windows:

winget install -e --id TerraformLinters.tflint
Code language: CSS (css)

Linux: use the signed/attested release artifacts or the project’s documented installer.

Production-oriented .tflint.hcl

tflint {
  required_version = ">= 0.64.0"
}

config {
  call_module_type    = "all"
  disabled_by_default = false
}

plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

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


rule "terraform_documented_variables" {
  enabled = true
}

rule "terraform_documented_outputs" {
  enabled = true
}

rule "terraform_naming_convention" {
  enabled = true
}
Code language: PHP (php)

Initialize and run:

tflint --init
tflint --recursive --format=compact

For CI systems repeatedly running tflint --init, provide an authenticated GitHub token to avoid anonymous GitHub API rate limits.

TFLint vs validate

terraform validate TFLint
Terraform-native structural validity Static lint/best-practice checks
Understands Terraform language/module configuration Extensible provider-specific rules
Not a style/security scanner Can catch deprecated/invalid provider patterns
Mandatory baseline Strongly recommended complement

What

Trivy scans Terraform/IaC for misconfiguration and can also scan repositories/filesystems for secrets and other security problems.

Common findings

  • public exposure,
  • weak encryption,
  • risky IAM,
  • permissive security groups,
  • weak storage policies,
  • Kubernetes misconfiguration,
  • plaintext secret patterns.

Commands

IaC misconfiguration:

trivy config .

Block high/critical findings:

trivy config --severity HIGH,CRITICAL --exit-code 1 .
Code language: PHP (php)

Scan repository/filesystem for both misconfiguration and secrets:

trivy fs --scanners misconfig,secret --severity HIGH,CRITICAL .

Use tfvars when appropriate:

trivy config --tf-vars environments/staging/staging.tfvars .

Policy

  • Run a fast high/critical gate locally.
  • Run the authoritative scan in CI.
  • Exceptions need a reason, owner, and preferably expiration date.
  • Treat scanner output as a risk signal, not proof of exploitability.

What

Checkov is an IaC static analysis and compliance/policy scanner with Terraform source and Terraform plan scanning.

Install in an isolated Python environment:

pipx install checkov
checkov --version

Source scan:

checkov -d .

Plan scan:

terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
checkov -f tfplan.json

Security: plan JSON may contain injected values and secrets. Create and scan it only in trusted CI and avoid publishing it as a broadly readable artifact.

Trivy vs Checkov

Situation Recommendation
Small/medium team wants one broad security tool Trivy only
Organization already standardizes Prisma/Checkov policies/compliance Checkov only can be enough
Regulated org has distinct, non-overlapping policy packs and accepts runtime cost Both, but define ownership
Same findings duplicated in both with no governance benefit Remove one

Do not add both simply to create the appearance of more security.


What

Terraform’s native test framework executes .tftest.hcl files with run, variables, assertions, plan/apply modes, helper modules, and provider/resource mocking.

Minimal example

main.tf:

variable "environment" {
  type = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment must be dev, staging, or prod"
  }
}

locals {
  name = "payments-${var.environment}"
}

output "name" {
  description = "Generated workload name."
  value       = local.name
}
Code language: JavaScript (javascript)

tests/name.tftest.hcl:

run "staging_name" {
  command = plan

  variables {
    environment = "staging"
  }

  assert {
    condition     = output.name == "payments-staging"
    error_message = "Unexpected generated name."
  }
}
Code language: JavaScript (javascript)

Run:

terraform test
terraform test -filter=tests/name.tftest.hcl

Provider mocking

For modules using real providers, Terraform 1.7+ can mock providers:

mock_provider "aws" {}

run "secure_bucket_configuration" {
  command = plan

  assert {
    condition     = aws_s3_bucket.example.bucket == var.bucket_name
    error_message = "Bucket name wiring is incorrect."
  }
}
Code language: JavaScript (javascript)

This is excellent for logic/interface tests without cloud credentials or real resources.

Test pyramid

            Live E2E / integration
          Terratest / custom tests
        -----------------------------
             terraform test
        plan/apply + mocks/assertions
      ---------------------------------
      validate + TFLint + security scan
    -------------------------------------
                  fmt

What each layer proves

Tool Proves
validate Terraform configuration can be parsed/validated
TFLint Lint/provider conventions
terraform test Declared module behavior/assertions
Real plan Concrete proposed change in target context
Terratest Deployed infrastructure actually behaves as expected

Terratest

Use when you need:

  • actual AWS/Azure/GCP API behavior,
  • HTTP/DNS/TLS checks,
  • retries/eventual consistency,
  • cross-component E2E validation,
  • deploy → validate → destroy workflows.

Costs:

  • slower,
  • cloud cost,
  • credential requirements,
  • cleanup complexity,
  • flaky external dependencies.

Default: native terraform test first; Terratest only for gaps.

Kitchen-Terraform

Still useful in teams already invested in Test Kitchen/Ruby-based infrastructure testing. It is rarely the first recommendation for a new Terraform-only test platform in 2026.


What

Generates Terraform module documentation from source:

  • requirements,
  • providers,
  • modules,
  • resources,
  • inputs,
  • outputs.

Installation

macOS:

brew install terraform-docs

Usage

terraform-docs markdown table .
terraform-docs markdown table --output-file README.md --output-mode inject .
Code language: CSS (css)

README before



Creates a VPC.
Code language: PHP (php)

README with markers

# VPC module

Creates a VPC.



Code language: HTML, XML (xml)

After terraform-docs, the marker section contains generated Requirements, Providers, Inputs, Outputs, etc.

.terraform-docs.yml

formatter: "markdown table"

output:
  file: "README.md"
  mode: "inject"
  template: |-
    
    {{ .Content }}
    

sort:
  enabled: true
  by: "required"

settings:
  anchor: true
  default: true
  description: true
  escape: true
  hide-empty: false
  html: true
  lockfile: true
  read-comments: true
  required: true
  sensitive: true
  type: true
Code language: HTML, XML (xml)

Best practice

Generated interface reference is not a replacement for authored documentation. A gold-standard module README has:

  1. purpose,
  2. architecture/behavior,
  3. security assumptions,
  4. usage example,
  5. generated inputs/outputs,
  6. upgrade notes,
  7. operational caveats.

What

Infracost estimates cloud cost and turns Terraform changes into a cost delta visible during review.

Workflow

Terraform change
      ↓
Plan/HCL analysis
      ↓
Infracost
      ↓
Monthly cost estimate + delta
      ↓
PR reviewer sees cost impact
      ↓
Budget / FinOps decision

CLI

infracost breakdown --path .
infracost breakdown --path . --format json --out-file infracost-base.json
infracost diff --path . --compare-to infracost-base.json
Code language: CSS (css)

It can also consume Terraform plan JSON when source parsing is not sufficient.

CI recommendation

For GitHub, prefer the vendor’s current GitHub App or modern diff/scan actions where allowed. Avoid building new integrations around legacy action patterns unless you need the lower-level CLI.

Policy

Cost estimation is advisory unless your organization has explicitly defined budgets/thresholds. A small cost increase may be correct; a zero-cost plan may still be dangerously destructive.


Why high value

It moves deterministic checks left:

git commit
    ↓
fmt
    ↓
validate
    ↓
TFLint
    ↓
Trivy
    ↓
terraform-docs
    ↓
commit accepted

Installation

brew install pre-commit
<em># or</em>
pipx install pre-commit

pre-commit --version
Code language: HTML, XML (xml)

Production-ready .pre-commit-config.yaml

repos:
  - repo: https:
    rev: v1.108.1
    hooks:
      - id: terraform_fmt

      - id: terraform_validate
        args:
          - --hook-config=--retry-once-with-cleanup=true

      - id: terraform_tflint

      - id: terraform_trivy
        args:
          - --args=--severity=HIGH,CRITICAL
          - --args=--exit-code=1

      - id: terraform_docs
Code language: JavaScript (javascript)

Enable:

pre-commit install
pre-commit run --all-files

Local vs CI split

Local

Run checks that are fast and deterministic:

  • fmt,
  • validate,
  • TFLint,
  • focused Trivy,
  • docs.

CI

Re-run them authoritatively and add:

  • full security policy,
  • terraform test,
  • target-context plan,
  • Infracost,
  • plan-policy checks,
  • approval/apply rules.

Never rely solely on local hooks; users can skip them and environments differ.


For a moderate monorepo:

terraform/
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── versions.tf
│   │   ├── tests/
│   │   └── README.md
│   ├── eks/
│   ├── rds/
│   └── iam/
│
├── environments/
│   ├── development/
│   ├── staging/
│   └── production/
│
├── .github/
│   └── workflows/
├── .pre-commit-config.yaml
├── .tflint.hcl
├── renovate.json
└── README.md

Root module vs child module

Root module: deployment unit. Owns backend, environment composition, concrete provider constraints, and state.

Child/reusable module: abstraction with documented inputs/outputs. It should not normally own a backend or assume one environment/account.

Files

Recommended per module:

main.tf        
variables.tf   
outputs.tf     
versions.tf    
locals.tf      
data.tf        
tests/         
README.md
Code language: PHP (php)

Split further by domain only when it improves readability; Terraform loads all .tf files in the directory together.

Environment separation

Prefer separate state per meaningful failure/ownership boundary:

production/network
production/cluster
production/database
production/app-platform

rather than one giant production state.

Reasons:

  • smaller blast radius,
  • simpler permissions,
  • faster plan,
  • clearer ownership,
  • fewer unrelated lock conflicts.

Repository strategy

Strategy Pros Cons Best fit
Monorepo Shared standards, atomic changes, discovery CI/path complexity, repo scale Central platform teams
Multi-repo by system Strong ownership boundaries Cross-repo upgrades harder Independent teams
One repo per environment Strong isolation Duplication/drift risk Strict org separation
One repo per reusable module Independent versioning/release Many repos Mature internal module ecosystem

There is no universal “one repo per environment” rule. Choose boundaries from ownership, release cadence, permissions, and blast radius.


Why it exists

Terragrunt wraps Terraform/OpenTofu to reduce repetition and orchestrate many related root modules.

Common value:

  • shared remote-state configuration,
  • generated provider configuration,
  • reusable environment/account configuration,
  • dependency outputs,
  • multi-account/multi-region layouts,
  • ordered execution across units.

Current CLI model

Modern Terragrunt 1.x:

terragrunt plan
terragrunt apply
terragrunt run --all plan
terragrunt run --all apply

Do not teach new teams to standardize on legacy run-all.

Example

live/
├── root.hcl
├── dev/
│   ├── vpc/terragrunt.hcl
│   └── app/terragrunt.hcl
└── prod/
    ├── vpc/terragrunt.hcl
    └── app/terragrunt.hcl

root.hcl:

remote_state {
  backend = "s3"

  config = {
    bucket       = "example-terraform-state"
    key          = "${path_relative_to_include()}/terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true
  }
}

generate "provider" {
  path      = "provider.generated.tf"
  if_exists = "overwrite_terragrunt"

  contents = <<EOF
provider "aws" {
  region = "ap-northeast-1"
}
EOF
}
Code language: JavaScript (javascript)

prod/app/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "../../../modules/app"
}

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  vpc_id = dependency.vpc.outputs.vpc_id
}
Code language: PHP (php)

When Terragrunt is unnecessary

Do not add it when:

  • you have a few simple root modules,
  • HCP Terraform already removes the orchestration pain you were trying to solve,
  • duplication is small and explicit,
  • the team does not need multi-unit orchestration,
  • introducing a second language/workflow would cost more than it saves.

Terraform alone is a valid production architecture.


Pipeline architecture

Pull request
   ↓
checkout
   ↓
setup pinned Terraform
   ↓
fmt -check
   ↓
init -backend=false (static/test jobs)
   ↓
validate
   ↓
TFLint
   ↓
Trivy
   ↓
terraform test
   ↓
OIDC → target read/plan role
   ↓
terraform init
   ↓
terraform plan
   ↓
Infracost / policy / review
Code language: JavaScript (javascript)

Apply should be a separate controlled phase, not an automatic side effect of every PR plan.

Example CI workflow

name: terraform-ci

on:
  pull_request:
    paths:
      - "**/*.tf"
      - "**/*.tfvars"
      - "**/*.tftest.hcl"
      - ".github/workflows/terraform-ci.yml"

permissions:
  contents: read

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.16.0"
          terraform_wrapper: false

      - name: Format
        run: terraform fmt -check -recursive

      - name: Init without backend
        run: terraform init -backend=false -input=false

      - name: Validate
        run: terraform validate -no-color

      - name: Setup TFLint
        uses: terraform-linters/setup-tflint@v6
        with:
          tflint_version: "v0.64.0"
          cache: true

      - name: Init TFLint plugins
        run: tflint --init
        env:
          GITHUB_TOKEN: ${{ github.token }}

      - name: TFLint
        run: tflint --recursive --format=compact

      - name: Trivy IaC
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: config
          scan-ref: .
          severity: HIGH,CRITICAL
          exit-code: "1"

      - name: Terraform tests
        run: terraform test
Code language: PHP (php)

Supply-chain hardening note

For a true enterprise workflow, pin reusable GitHub Actions to reviewed immutable commit SHAs or enforce an organization action allowlist. Major tags are easier to read in a handbook but are mutable references.

Plan job with AWS OIDC

  plan:
    needs: quality
    runs-on: ubuntu-latest

    permissions:
      contents: read
      id-token: write

    environment: staging

    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.16.0"
          terraform_wrapper: false

      - name: Authenticate to AWS using OIDC
        uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::123456789012:role/terraform-staging-plan
          aws-region: ap-northeast-1

      - name: Init
        working-directory: environments/staging
        run: terraform init -input=false

      - name: Plan
        working-directory: environments/staging
        run: terraform plan -input=false -no-color

OIDC design

Trust should be constrained by claims such as:

  • organization/repository identity,
  • environment,
  • branch/tag,
  • immutable repository/org IDs where supported.

The plan role and apply role should be separate when practical.

Apply model

Recommended:

PR merged to protected branch
       ↓
new plan in controlled execution context
       ↓
policy checks
       ↓
environment approval
       ↓
assume apply role using OIDC
       ↓
terraform apply
       ↓
verification
Code language: PHP (php)

Why re-plan after merge? The reviewed branch may no longer be identical to the final protected branch if multiple changes merged.

If your system securely preserves a reviewed saved plan and guarantees code/state identity, applying the preserved plan is also valid.

Secrets

Never store:

AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
long-lived Azure client secrets
long-lived GCP service-account JSON
Code language: JavaScript (javascript)

when federation is available.


Use the same gates:

validate → lint → security → test → plan → policy → approval → apply

GitLab supports OIDC ID tokens for cloud federation. Modern GitLab uses ID tokens, not the removed legacy CI_JOB_JWT_V2.

Illustrative shape:

stages:
  - quality
  - plan
  - apply

terraform-quality:
  stage: quality
  image: hashicorp/terraform:1.16.0
  script:
    - terraform fmt -check -recursive
    - terraform init -backend=false -input=false
    - terraform validate
    - terraform test

terraform-plan:
  stage: plan
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: sts.amazonaws.com
  script:
    - echo "Exchange GitLab OIDC token for short-lived cloud credentials"
    - terraform init -input=false
    - terraform plan -input=false

terraform-apply:
  stage: apply
  when: manual
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
  script:
    - terraform apply -input=false
Code language: PHP (php)

Exact AWS/Azure/GCP token-exchange steps should follow the current cloud/GitLab federation documentation and your organization’s role design.


What

Atlantis is a server that reacts to pull-request comments/events and runs Terraform plans/applies in a PR-driven workflow.

PR opened
   ↓
Atlantis autoplan
   ↓
review plan
   ↓
approval
   ↓
atlantis apply

Commands

atlantis plan
atlantis apply

Strengths

  • PR-native workflow,
  • project/workspace awareness,
  • locking,
  • plan/apply conversation in VCS,
  • works across multiple Terraform projects.

Atlantis locks a directory/workspace when a plan is active so competing PRs do not casually race the same deployment unit.

Atlantis vs GitHub Actions vs HCP Terraform

Need Better fit
Generic build/test ecosystem GitHub/GitLab CI
Comment-driven Terraform workflow you operate Atlantis
Managed state, remote runs, RBAC, registry, run tasks, policy, health HCP Terraform
Self-hosted HCP feature set Terraform Enterprise

Avoid running Atlantis and a separate CI apply system against the same state without a very explicit ownership model.


What it adds beyond CLI

  • remote state and state history,
  • stronger centralized run coordination,
  • remote runs,
  • workspaces,
  • projects,
  • variable sets,
  • self-hosted agents,
  • private module/provider registry,
  • run tasks,
  • policy enforcement,
  • VCS-driven speculative plans,
  • team/project/workspace RBAC,
  • health assessments including drift detection and continuous validation,
  • API/audit capabilities depending on edition.

Enterprise workflow

Git branch
   ↓
Pull request
   ↓
HCP speculative plan
   ↓
CI/security/cost checks
   ↓
review
   ↓
merge
   ↓
HCP run
   ↓
policy checks / run tasks
   ↓
approval
   ↓
remote worker or private agent
   ↓
cloud
   ↓
health assessment / drift detection
Code language: PHP (php)

Workspaces

An HCP Terraform workspace is a deployment/state boundary, not the same concept as a Terraform CLI workspace.

Use workspaces to separate infrastructure with different:

  • state,
  • permissions,
  • ownership,
  • credentials,
  • run cadence,
  • blast radius.

Avoid a single giant workspace for an entire company environment.

Projects

Use projects as access-control/organizational boundaries grouping workspaces and Stacks.

Good project boundaries:

  • platform,
  • analytics,
  • security,
  • business unit,
  • account/portfolio.

Variable sets

Good uses:

  • common non-secret environment settings,
  • dynamic credential configuration,
  • shared provider settings,
  • organization/project-wide standards.

Do not use variable sets as an excuse to hide every important deployment value from code review.

Agents

Use HCP Terraform Agents when the run must reach:

  • private VPC/VNet resources,
  • on-premises services,
  • private APIs,
  • network-isolated providers.

Agents need only outbound connectivity to HCP Terraform.

Do not deploy agents merely because “enterprise teams use agents.” Built-in workers are simpler when private network access/custom tooling is not required.

Private registry

Use the private registry for:

  • approved reusable modules,
  • internally developed providers,
  • versioned organization patterns.

A mature platform team should make the secure path the easy path:

developer need → approved private module → small set of inputs → tests/policy → deploy
Code language: JavaScript (javascript)

Run tasks

Useful for third-party checks at lifecycle stages such as:

  • pre-plan,
  • post-plan,
  • pre-apply,
  • post-apply.

Examples:

  • security platform,
  • cost governance,
  • custom compliance,
  • image validation.

Only integrate trusted run-task services because run-related information can be sensitive.

HCP Terraform vs Terraform Enterprise vs CLI

Capability Terraform CLI HCP Terraform Terraform Enterprise
Terraform engine Yes Yes Yes
Local execution Yes Can integrate Can integrate
Managed SaaS No Yes No
Self-hosted platform No No Yes
Remote state Via backend Built in Built in
Remote runs No platform by itself Built in Built in
Projects/RBAC No Yes Yes
Private registry No Yes Yes
Agents N/A Yes Yes
Policy/run tasks External Integrated Integrated
Drift/health External Integrated by edition Integrated

Terraform Enterprise is the self-hosted distribution of the HCP Terraform application for organizations requiring private hosting/advanced architecture/compliance controls.


What

HashiCorp’s policy-as-code framework integrated with HCP Terraform/TFE.

Enforcement styles

  • Advisory — report failure but allow run.
  • Soft mandatory / overridable — block unless authorized override.
  • Hard mandatory / mandatory — block until fixed, subject to platform policy-set semantics.

Use terms carefully because HCP Terraform’s UI/framework-specific naming can differ.

Example policy scenarios

  • approved AWS regions,
  • no public storage,
  • allowed EC2 families,
  • mandatory tags,
  • encryption required,
  • restrict resource deletion,
  • only approved modules.

Rollout model

new rule
  ↓
advisory
  ↓
observe false positives
  ↓
fix existing estate
  ↓
mandatory with controlled exception process
Code language: JavaScript (javascript)

Do not deploy dozens of untested hard-blocking policies on day one.


OPA

OPA evaluates Rego policies against structured input. Terraform plan JSON is a common input:

terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
opa exec --decision terraform/analysis/authz --bundle policy/ tfplan.json

Caveat: plan-time unknown values can limit what a policy can know.

Conftest

Conftest is a convenient CLI for executing Rego checks against structured files.

conftest test tfplan.json --policy policy/
conftest verify --policy policy/

Typical policy:

package main

deny contains msg if {
  some rc in input.resource_changes
  rc.type == "aws_security_group_rule"
  some cidr in rc.change.after.cidr_blocks
  cidr == "0.0.0.0/0"
  msg := sprintf("Public ingress is not allowed: %s", [rc.address])
}
Code language: JavaScript (javascript)

Production policies need careful handling for null/unknown/alternate resource shapes; the snippet is intentionally small.

Decision guide

Framework Best fit
Terraform Policy (HCL) Terraform-centric HCP teams wanting native HCL policy; evaluate maturity while beta
Sentinel HCP/TFE estates with HashiCorp governance investment
OPA Vendor-neutral enterprise policy platform
Conftest Lightweight local/CI Rego execution

What

Automates dependency update PRs for:

  • Terraform required_version,
  • .terraform-version,
  • providers,
  • Registry modules,
  • Git-based modules where supported,
  • GitHub Actions,
  • many surrounding build dependencies.

Recommended renovate.json

{
  "extends": ["config:recommended"],
  "labels": ["dependencies"],
  "packageRules": [
    {
      "matchManagers": ["terraform", "terraform-version"],
      "groupName": "terraform dependencies",
      "schedule": ["before 6am on monday"]
    },
    {
      "matchManagers": ["github-actions"],
      "groupName": "github actions"
    },
    {
      "matchUpdateTypes": ["major"],
      "dependencyDashboardApproval": true
    }
  ]
}
Code language: JSON / JSON with Comments (json)

Upgrade policy

Do not auto-merge major Terraform/provider/module upgrades into production infrastructure merely because CI is green.

Recommended:

patch → may auto-merge after strong test gates
minor → normal PR review
major → explicit upgrade issue, changelog review, staged rollout

tfupdate is a focused utility for rewriting Terraform/OpenTofu core/provider/module version constraints and lock files.

Example:

tfupdate terraform -v 1.16.0 -r .
tfupdate provider aws -v "~> 6.0" -r .
Code language: CSS (css)

It is still useful for scripts and specialized bulk updates.

Default recommendation: prefer Renovate for continuous dependency management because it covers Terraform, modules, providers, actions, and many other dependencies with PR workflows. Use tfupdate when you specifically need command-line rewriting/automation.


The console is one of the fastest ways to debug Terraform expressions without creating resources.

terraform console
Code language: JavaScript (javascript)

Examples:

> upper("prod")
"PROD"

> length(["a", "b", "c"])
3

> contains(["dev", "prod"], "prod")
true

> merge({a = 1}, {b = 2})
{
  "a" = 1
  "b" = 2
}

> [for x in ["api", "worker"] : "prod-${x}"]
[
  "prod-api",
  "prod-worker",
]

> { for x in ["api", "worker"] : x => upper(x) }
{
  "api" = "API"
  "worker" = "WORKER"
}

> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"

> try({a = 1}.b, "fallback")
"fallback"
Code language: JavaScript (javascript)

Use it to prototype:

  • for expressions,
  • map merging,
  • conditionals,
  • CIDR subdivision,
  • string transformations,
  • try/can,
  • regex,
  • collection conversion.

Do not use console output as a substitute for a repeatable test when the expression matters to production behavior.


Developer intent
      ↓
AI agent
      ↓
Terraform MCP / current provider docs
      ↓
Generate/refactor HCL
      ↓
terraform fmt
      ↓
terraform validate
      ↓
TFLint
      ↓
Trivy
      ↓
terraform test
      ↓
human review
      ↓
real plan

Good AI tasks

  • generate boilerplate,
  • explain resources,
  • draft variables/outputs,
  • create native tests,
  • refactor repeated HCL,
  • produce module documentation,
  • identify likely missing validation,
  • compare plan output,
  • search current provider docs through MCP.

Never blindly trust AI for

  • IAM,
  • security-group/network exposure,
  • KMS/key policy,
  • destructive lifecycle changes,
  • provider upgrade compatibility,
  • terraform state operations,
  • imports/moves without address review,
  • production apply,
  • secret handling,
  • cost assumptions.

AI prompt template

Act as a senior Terraform reviewer.

Context:
- Terraform: <constraint>
- Provider: <source + constraint>
- Execution: <HCP Terraform / CI / local>
- Environment: <dev/stage/prod>
- State boundary: <description>

Task:
<requested change>

Before writing code:
1. Use current Terraform/provider documentation or Terraform MCP.
2. Identify security, state, replacement, and cost risks.
3. Preserve resource addresses unless a move is explicitly required.
4. Do not hardcode credentials/secrets.
5. Prefer OIDC/workload identity.

After writing code:
1. terraform fmt
2. terraform validate
3. TFLint
4. Trivy
5. terraform test
6. explain expected plan and anything requiring human verification.

Never run or recommend an unreviewed production apply.
Code language: HTML, XML (xml)

  1. Pull latest codegit switch main git pull --ff-only
  2. Select project tool versiontenv tf install terraform version
  3. Create a branchgit switch -c feat/add-private-endpoint
  4. Initializeterraform init
  5. Write/change Terraform
  6. Formatterraform fmt -recursive
  7. Validateterraform validate
  8. Linttflint --init tflint --recursive
  9. Security scantrivy config --severity HIGH,CRITICAL --exit-code 1 .
  10. Run teststerraform test
  11. Generate docsterraform-docs markdown table --output-file README.md --output-mode inject .
  12. Plan in a safe target contextterraform plan
  13. Inspect cost deltainfracost breakdown --path .
  14. Review your own diffgit diff git status
  15. Commitgit add . git commit -m "feat: add private endpoint"
  16. Pre-commit hooks execute
  17. Pushgit push -u origin HEAD
  18. CI reproduces checks and plan
  19. PR review Reviewer checks security, replacement, blast radius, cost, test quality.
  20. Policy validation
  21. Controlled apply
  22. Post-deployment verification Check service health, metrics, logs, connectivity, and expected outputs.
  23. Drift/health feedback Operational system closes the loop.

Tool Problem solved Basic usage Value Limitation Classification
Terraform CLI IaC lifecycle terraform plan Core engine Needs surrounding controls MUST
VS Code Authoring open project Productivity Editor only Recommended
HashiCorp extension Terraform IDE install extension Completion/diagnostics VS Code focused Recommended
terraform-ls LSP intelligence normally bundled Schema/navigation Not security/policy Recommended
Terraform MCP AI current context configure MCP host Reduces stale AI output Trust/access design Recommended with AI
tenv Binary versions tenv tf use Reproducibility Extra dependency Recommended
terraform fmt Style fmt -recursive Deterministic format Not validation MUST
terraform validate Config validity validate Cheap errors Not provider runtime security MUST
TFLint Lint/provider checks tflint --recursive Higher code quality Not full security Recommended
Trivy Misconfig/secrets trivy config . Broad security Policy noise possible Recommended
Checkov Compliance/IaC policy checkov -d . Broad policy library Overlap Optional
terraform test Native behavior tests terraform test Fast/native Live behavior may need more Recommended
Terratest Real integration tests go test Strong E2E Slow/cost/flakiness Optional
terraform-docs Interface docs terraform-docs ... No stale input/output tables Doesn’t write architecture narrative Recommended
Infracost Cost delta infracost diff FinOps in review Estimate not invoice Recommended
pre-commit-terraform Local gates pre-commit run Fast feedback Can be bypassed Recommended
Git Version/review git diff Audit/review Needs policy MUST
GitHub/GitLab Collaboration PR/MR Review controls Platform dependency MUST for teams
GitHub Actions/GitLab CI Automation pipeline Reproducible checks Runner/security design needed MUST for prod teams
Terragrunt Multi-unit DRY/orchestration run --all plan Scale multi-env layouts Added abstraction Optional
Atlantis PR-driven Terraform PR commands Terraform-focused UX Operate server/security Optional
HCP Terraform Remote Terraform platform workspace runs State/runs/RBAC/policy Commercial/platform choice Recommended/Enterprise
Terraform Enterprise Self-hosted HCP self-host Compliance/private hosting Operational burden Enterprise
Sentinel HCP/TFE policy policy sets Integrated governance HashiCorp-specific Enterprise
OPA Vendor-neutral policy opa exec Reusable policy platform Rego learning curve Enterprise/Optional
Conftest Rego CLI conftest test Easy CI integration Not Terraform orchestrator Optional
Renovate Dependency PRs bot/app Sustainable upgrades Needs grouping/rules Recommended
tfupdate Constraint rewrites tfupdate provider Focused automation Narrower than Renovate Optional

Terraform CLI
VS Code
HashiCorp Terraform Extension
terraform fmt
terraform validate
Git

Add a remote backend once working with others.

Terraform CLI
VS Code + terraform-ls
tenv
TFLint
Trivy
terraform test
terraform-docs
pre-commit-terraform
GitHub/GitLab CI
remote state + locking
OIDC
Everything above
Terraform MCP Server
Infracost
reusable module standards
Renovate
structured state boundaries
policy tests
controlled runners
HCP Terraform where appropriate
Terragrunt only if complexity justifies it
Code language: JavaScript (javascript)
approved IDE/toolchain
private module/provider registry
central CI templates
OIDC/workload identity
remote execution
HCP Terraform/TFE
private agents where needed
RBAC
run tasks
policy-as-code
cost governance
audit logs
drift/health assessments
dependency automation
AI/MCP governance
Code language: PHP (php)

Developer
   ↓
IDE
   ↓
AI/MCP assistance
   ↓
Terraform code
   ↓
Format
   ↓
Validate
   ↓
Lint
   ↓
Security scan
   ↓
Native tests
   ↓
Documentation
   ↓
Pre-commit
   ↓
Push
   ↓
CI reproduces gates
   ↓
OIDC authentication
   ↓
Terraform plan
   ↓
Cost analysis
   ↓
Policy validation
   ↓
Code review
   ↓
Approval
   ↓
Controlled apply
   ↓
Post-deploy verification
   ↓
Monitoring / drift / continuous validation

Quality gates

Gate Failure means
Format Code not canonical
Validate Terraform config invalid
TFLint Quality/provider rule violation
Security Risk exceeds accepted threshold
Test Contract/behavior regression
Docs Module API reference stale
Plan Change differs from developer intent
Cost Material unexpected spend
Policy Organization rule violated
Review Human risk/architecture concerns
Approval Production change not authorized
Verify Deployment outcome not healthy

Homebrew-centered setup:

brew tap hashicorp/tap

brew install hashicorp/tap/terraform
brew install hashicorp/tap/terraform-ls
brew install tofuutils/tap/tenv
brew install terraform-linters/tap/tflint
brew install trivy
brew install terraform-docs
brew install infracost
brew install pre-commit
brew install opa
brew install conftest

For Checkov:

brew install pipx
pipx ensurepath
pipx install checkov

You may omit manual terraform-ls if VS Code’s HashiCorp extension is your only LSP client.

Verify:

terraform version
tenv --version
tflint --version
trivy --version
terraform-docs --version
infracost --version
pre-commit --version
opa version
conftest --version
checkov --version

Prefer official/vendor repositories or verified release artifacts rather than random curl-pipe-shell snippets.

Example base packages:

sudo apt-get update
sudo apt-get install -y git curl unzip jq pipx
Code language: JavaScript (javascript)

Then install:

  • Terraform from HashiCorp’s official package repository,
  • TFLint from verified GitHub release artifacts,
  • Trivy from Aqua’s official package repository,
  • terraform-docs, tenv, OPA, Conftest from official releases,
  • Checkov with pipx install checkov,
  • pre-commit with OS package manager or pipx.

Keep installation logic in a reproducible developer bootstrap script or dev-container image.

Good choices:

  • winget where the project publishes a verified package,
  • Chocolatey in organizations already managing it,
  • official signed release archives.

Known example:

winget install -e --id TerraformLinters.tflint
Code language: CSS (css)

Terraform itself can be installed via your organization’s package-management standard or HashiCorp’s official release archive.

Prefer WSL only if your team’s Terraform workflow intentionally standardizes on Linux semantics; do not require it just because Terraform can run there.

Setup checklist

[ ] Git
[ ] Terraform version manager
[ ] Pinned Terraform
[ ] Editor + HashiCorp Terraform extension
[ ] TFLint
[ ] Trivy
[ ] terraform-docs
[ ] pre-commit
[ ] Infracost if used
[ ] Checkov if selected
[ ] OPA/Conftest if selected
[ ] Cloud CLI
[ ] SSO/OIDC-compatible developer authentication
[ ] No long-lived cloud key in shell profile
[ ] Project hooks installed

aws-web/
├── backend.tf
├── versions.tf
├── provider.tf
├── variables.tf
├── main.tf
├── outputs.tf
├── terraform.tfvars.example
├── backend.hcl.example
├── tests/
│   └── basic.tftest.hcl
├── .tflint.hcl
├── .terraform-docs.yml
├── .pre-commit-config.yaml
├── .gitignore
└── README.md

versions.tf

terraform {
  required_version = "~> 1.16.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}
Code language: JavaScript (javascript)

backend.tf

terraform {
  backend "s3" {}
}
Code language: JavaScript (javascript)

backend.hcl.example

bucket       = "REPLACE-ME-terraform-state"
key          = "examples/aws-web/terraform.tfstate"
region       = "ap-northeast-1"
encrypt      = true
use_lockfile = true
Code language: JavaScript (javascript)

Initialize:

cp backend.hcl.example backend.hcl
<em># edit backend.hcl for your non-secret account settings</em>
terraform init -backend-config=backend.hcl
Code language: HTML, XML (xml)

Do not put access keys in backend config.

provider.tf

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      ManagedBy   = "Terraform"
      Project     = var.project_name
      Environment = var.environment
    }
  }
}
Code language: JavaScript (javascript)

Authentication comes from the AWS SDK credential chain: SSO/profile locally, OIDC/workload identity in CI.

variables.tf

variable "aws_region" {
  description = "AWS region for this deployment."
  type        = string
  default     = "ap-northeast-1"
}

variable "project_name" {
  description = "Short project identifier."
  type        = string
  default     = "tf-web"
}

variable "environment" {
  description = "Deployment environment."
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment must be dev, staging, or prod."
  }
}

variable "vpc_cidr" {
  description = "CIDR for the VPC."
  type        = string
  default     = "10.42.0.0/16"
}

variable "azs" {
  description = "Availability Zones for public subnets."
  type        = list(string)
  default     = ["ap-northeast-1a", "ap-northeast-1c"]

  validation {
    condition     = length(var.azs) == 2
    error_message = "This example expects exactly two AZs."
  }
}

variable "public_subnet_cidrs" {
  description = "CIDRs for public subnets, one per AZ."
  type        = list(string)
  default     = ["10.42.1.0/24", "10.42.2.0/24"]

  validation {
    condition     = length(var.public_subnet_cidrs) == 2
    error_message = "This example expects exactly two public subnet CIDRs."
  }
}

variable "ami_id" {
  description = "Approved AMI ID for the EC2 instance."
  type        = string
}

variable "instance_type" {
  description = "EC2 instance type."
  type        = string
  default     = "t3.micro"
}

variable "allowed_http_cidrs" {
  description = "CIDRs permitted to access TCP/80. Do not use 0.0.0.0/0 unless public exposure is intended and approved."
  type        = list(string)
}
Code language: PHP (php)

main.tf

locals {
  subnet_map = {
    for idx, az in var.azs :
    az => var.public_subnet_cidrs[idx]
  }
}

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-${var.environment}"
  }
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = {
    Name = "${var.project_name}-${var.environment}"
  }
}

resource "aws_subnet" "public" {
  for_each = local.subnet_map

  vpc_id                  = aws_vpc.this.id
  availability_zone       = each.key
  cidr_block              = each.value
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-${var.environment}-${each.key}"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-public"
  }
}

resource "aws_route_table_association" "public" {
  for_each = aws_subnet.public

  subnet_id      = each.value.id
  route_table_id = aws_route_table.public.id
}

resource "aws_security_group" "web" {
  name_prefix = "${var.project_name}-${var.environment}-web-"
  description = "HTTP access for example web instance"
  vpc_id      = aws_vpc.this.id

  ingress {
    description = "Approved HTTP clients"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = var.allowed_http_cidrs
  }

  egress {
    description = "Outbound access"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami                         = var.ami_id
  instance_type               = var.instance_type
  subnet_id                   = values(aws_subnet.public)[0].id
  vpc_security_group_ids      = [aws_security_group.web.id]
  associate_public_ip_address = true

  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }

  root_block_device {
    encrypted = true
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-web"
  }
}
Code language: PHP (php)

This deliberately exposes no SSH port. In a production design, use SSM/session tooling or another approved administration path rather than opening port 22 globally.

outputs.tf

output "vpc_id" {
  description = "VPC ID."
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "Public subnet IDs keyed by availability zone."
  value       = { for az, subnet in aws_subnet.public : az => subnet.id }
}

output "instance_id" {
  description = "EC2 instance ID."
  value       = aws_instance.web.id
}

output "public_ip" {
  description = "Public IPv4 address of the example instance."
  value       = aws_instance.web.public_ip
}
Code language: JavaScript (javascript)

terraform.tfvars.example

environment        = "dev"
ami_id             = "ami-REPLACE-WITH-AN-APPROVED-AMI"
allowed_http_cidrs = ["203.0.113.0/24"]
Code language: JavaScript (javascript)

tests/basic.tftest.hcl

mock_provider "aws" {}

run "basic_plan" {
  command = plan

  variables {
    environment        = "dev"
    ami_id             = "ami-0123456789abcdef0"
    allowed_http_cidrs = ["203.0.113.0/24"]
  }

  assert {
    condition     = aws_vpc.this.cidr_block == "10.42.0.0/16"
    error_message = "Unexpected VPC CIDR."
  }

  assert {
    condition     = length(aws_subnet.public) == 2
    error_message = "Expected two public subnets."
  }

  assert {
    condition     = aws_instance.web.metadata_options[0].http_tokens == "required"
    error_message = "IMDSv2 must be required."
  }

  assert {
    condition     = aws_instance.web.root_block_device[0].encrypted == true
    error_message = "Root volume must be encrypted."
  }
}
Code language: PHP (php)

.gitignore

.terraform/
*.tfstate
*.tfstate.*
*.tfplan
tfplan*
crash.log
crash.*.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
backend.hcl
*.auto.tfvars

Do commit:

.terraform.lock.hcl
Terraform source
non-secret examples
tests
tool configuration
Code language: CSS (css)

Run locally

terraform init -backend=false
terraform fmt -recursive
terraform validate
tflint --init
tflint --recursive
trivy config --severity HIGH,CRITICAL .
terraform test
Code language: JavaScript (javascript)

For a real plan, authenticate through AWS SSO/profile and initialize the real backend:

terraform init -backend-config=backend.hcl
terraform plan -var-file=terraform.tfvars
Code language: JavaScript (javascript)

Mistake Why dangerous Correct alternative
Hardcoded cloud keys Credential leak/rotation burden OIDC, SSO, workload identity
Hardcoded secrets HCL/plan/state leak Secret manager + ephemeral/write-only features where provider supports
Commit .tfstate Secrets + no safe state locking Remote state
Commit .terraform/ Large, platform-specific, unreviewable Re-run init
Ignore .terraform.lock.hcl Provider drift Commit root lock file
No provider constraints Unexpected upgrades Declare constraints
No module versions Module drift Pin Registry module versions
Apply from laptops Weak audit/reproducibility Controlled runner
No review High blast radius Protected PR flow
No lint/security tests Preventable defects Automated gates
One giant state Huge blast radius/lock contention Decompose by ownership/failure boundary
Overuse CLI workspaces Hidden environment coupling Separate roots/states/workspaces where environments differ
Huge root module Slow/unowned/unreviewable Compose smaller deployment units
Copy/paste infrastructure Drift Reusable modules/templates where repetition is stable
Weak variable design Invalid combinations Types, validation, object schemas
Excessive -target Partial graph surprises Normal full plan/apply; use target for exceptional recovery
Direct state edits Corruption Terraform state/moved/import mechanisms
Casual state rm Terraform loses management Controlled state procedure
Blind AI-generated code Hallucination/security bugs MCP/docs + automated gates + review
Ignore cost Surprise spend Infracost/FinOps review
Auto-apply PRs to prod Unreviewed change Merge + protected approval
Long-lived CI credentials High-value secret OIDC federation

Identity
[ ] No hardcoded cloud credentials
[ ] CI uses OIDC/workload identity
[ ] Developer auth uses SSO/short-lived credentials
[ ] Plan and apply roles follow least privilege
[ ] Production permissions separated from non-production

State
[ ] State is remote
[ ] State is encrypted
[ ] State access is restricted
[ ] State locking enabled
[ ] State versioning/history enabled where supported
[ ] State access/audit is monitored
[ ] State never committed to Git

Code/dependencies
[ ] Terraform version constrained
[ ] Providers constrained
[ ] .terraform.lock.hcl committed for root configurations
[ ] Modules explicitly versioned
[ ] Dependency updates automated but reviewed
[ ] No secrets in tfvars/source

Review
[ ] Branch protection enabled
[ ] Required reviewers defined
[ ] Security scan required
[ ] Tests required
[ ] Plan required
[ ] Destruction/replacement explicitly reviewed
[ ] Production approval protected

Runtime
[ ] Policy-as-code for critical invariants where justified
[ ] Audit logs retained
[ ] Drift/health monitoring enabled
[ ] Break-glass process documented
Code language: PHP (php)

State is a security asset

Terraform state may contain:

  • database passwords,
  • generated tokens,
  • private endpoint metadata,
  • identifiers,
  • provider-returned secrets.

Marking an output sensitive = true primarily controls display; it does not magically remove the value from all state.

Prefer Terraform/provider features that avoid persistence when possible, including ephemeral values and write-only arguments where supported.


Intent

[ ] Ticket/goal is clear
[ ] Change is minimal for the intended outcome
[ ] Correct environment/account/region is targeted

Code quality

[ ] terraform fmt passes
[ ] terraform validate passes
[ ] TFLint passes
[ ] Naming is consistent
[ ] Variables are typed and validated
[ ] Inputs/outputs are documented
[ ] No unnecessary abstraction

Dependencies

[ ] Terraform constraint appropriate
[ ] Provider constraints appropriate
[ ] Lock-file changes expected
[ ] Reusable module versions pinned
[ ] Major dependency upgrades reviewed separately
Code language: CSS (css)

Security

[ ] No credentials/secrets
[ ] IAM is least privilege
[ ] No accidental 0.0.0.0/0 or ::/0
[ ] Encryption settings correct
[ ] Security scanner result reviewed
[ ] State/backend permissions unaffected or intentionally changed

Plan/blast radius

[ ] Adds/changes/destroys match intent
[ ] Every replacement (`-/+`) understood
[ ] Database/storage replacement scrutinized
[ ] IAM/KMS/networking changes scrutinized
[ ] for_each/count key changes understood
[ ] Moved/imported resource addresses correct
[ ] No unexpected state churn
Code language: JavaScript (javascript)

Reliability/cost

[ ] HA assumptions still valid
[ ] Capacity/quotas considered
[ ] Cost delta acceptable
[ ] Tests cover important behavior
[ ] Rollback/recovery approach understood

Documentation/operations

[ ] terraform-docs output current
[ ] README/runbook updated if behavior changed
[ ] Monitoring/alerts adjusted if needed
[ ] Post-deploy verification defined

1. Standard repository template

Pre-create:

versions.tf
variables.tf
outputs.tf
tests/
.tflint.hcl
.terraform-docs.yml
.pre-commit-config.yaml
CI workflow
Renovate config
README markers

2. Reusable modules, but not “module everything”

Create a module when you have:

  • repeated stable pattern,
  • meaningful abstraction,
  • security defaults worth centralizing,
  • clear consumer interface.

Do not wrap one resource in a module merely to claim standardization.

3. Fast local path

Target:

fmt + validate + lint + focused security + mock tests

in a developer-friendly feedback loop.

Put slow live integration tests in CI/nightly pipelines.

4. Pre-approved module catalog

Make secure defaults easy:

Need private S3 bucket
  ↓
approved module
  ↓
3–5 meaningful inputs
  ↓
encryption/logging/policy defaults already included
Code language: JavaScript (javascript)

5. Central CI templates

Do not copy 100 slightly different Terraform workflows. Provide versioned reusable CI workflows/components.

6. Automated documentation

Generated docs prevent “variable exists in code but not README.”

7. Automated dependency updates

Small continuous upgrades are usually safer than a two-year provider jump.

8. Cost feedback in PRs

Shift cost discussion before apply.

9. Remote execution

A consistent execution environment eliminates “worked on my laptop” differences and improves auditability.

10. Use AI for mechanical work

AI is excellent at:

  • repetitive HCL,
  • tests,
  • docs,
  • refactors.

Humans remain accountable for:

  • intent,
  • architecture,
  • security,
  • blast radius,
  • production approval.

terraform validate vs TFLint

Use both. They answer different questions.

Trivy vs Checkov

Start with one. Add the second only for distinct policies/compliance.

Sentinel vs OPA

  • HCP-first, integrated governance: Sentinel is natural.
  • Cross-platform policy strategy: OPA is natural.
  • HCP Terraform can support OPA too.
  • Consider Terraform Policy as it matures if HCL-native policy is attractive.

tfenv vs tenv

For new Terraform-centric standardization, choose tenv unless existing tfenv investment outweighs migration benefit.

Terraform vs Terragrunt

Start with Terraform. Add Terragrunt to solve actual repetition/orchestration pain.

GitHub Actions vs Atlantis

  • CI ecosystem + custom workflows: GitHub Actions.
  • Terraform-specific PR command workflow: Atlantis.
  • You can combine them if responsibilities are unambiguous: CI validates, Atlantis owns plan/apply.

Atlantis vs HCP Terraform

HCP Terraform is a broader platform: state, runs, RBAC, registry, agents, policy, health. Atlantis is a focused PR automation server.

terraform test vs Terratest

Start native. Add Terratest for real behavioral integration.

terraform-docs vs manual README

Use both:

Manual sections → why/how/architecture
Generated section → exact interface reference
Code language: PHP (php)

pre-commit vs CI

Use both:

pre-commit = developer speed
CI         = enforcement

MUST HAVE

Terraform CLI
Git
terraform fmt
terraform validate
version constraints
.terraform.lock.hcl for root configs
remote state + locking for teams
code review
CI for production
OIDC/workload identity
plan review

STRONGLY RECOMMENDED

VS Code + HashiCorp Terraform Extension
terraform-ls
tenv
TFLint
Trivy
terraform test
terraform-docs
pre-commit-terraform
Infracost
Renovate
standard repository templates
reusable CI templates
post-deployment verification

OPTIONAL

Terraform MCP Server (recommended if AI is used)
Checkov
Terratest
Terragrunt
Atlantis
Conftest
tfupdate

ENTERPRISE / PLATFORM

HCP Terraform
Terraform Enterprise
private module/provider registry
private agents
RBAC
run tasks
policy sets
Sentinel / OPA / Terraform Policy
audit logging
drift detection / continuous validation
central cost/security governance
Code language: PHP (php)

SCOPE
[ ] I can explain exactly what this PR changes and why.
[ ] I changed only the intended state boundary/environment.

CODE
[ ] terraform fmt -check passes.
[ ] terraform validate passes.
[ ] TFLint passes.
[ ] No duplicate/copy-paste architecture was introduced unnecessarily.
[ ] Variables are typed, described, and validated where useful.
[ ] Outputs are intentional and documented.

VERSIONS
[ ] Terraform version policy is satisfied.
[ ] Provider constraints are explicit.
[ ] .terraform.lock.hcl changes are expected and reviewed.
[ ] Registry module versions are explicit.

SECURITY
[ ] No cloud credentials, passwords, API keys, tokens, or private keys are committed.
[ ] CI uses OIDC/workload identity.
[ ] Trivy/selected IaC security scan passes or exceptions are approved.
[ ] IAM follows least privilege.
[ ] Public ingress/egress is intentional.
[ ] Encryption is enabled where required.
[ ] State remains protected.

TESTING
[ ] terraform test passes.
[ ] Important module behavior is asserted.
[ ] Live integration testing is added when native tests cannot prove the requirement.

PLAN
[ ] A plan exists for the correct target.
[ ] I reviewed additions, changes, deletions, and replacements.
[ ] I understand every destructive/replacement action.
[ ] Resource-address changes are intentional.
[ ] No unexpected IAM/KMS/network/database changes exist.
[ ] Blast radius is acceptable.

COST
[ ] Cost delta has been reviewed when material.
[ ] New always-on/large resources are intentional.

DOCS
[ ] terraform-docs output is current.
[ ] Architecture/runbooks are updated if behavior changed.

DEPLOYMENT
[ ] Apply will run from the approved controlled environment.
[ ] Production approval requirements are met.
[ ] Rollback/recovery is understood.
[ ] Post-deployment verification is defined.

AFTER
[ ] Monitoring/alerts will detect regressions.
[ ] Drift/continuous validation is enabled where appropriate.
Code language: JavaScript (javascript)

For most modern platform teams, the cleanest production model is:

Developer laptop
  ├─ SSO / short-lived local cloud auth
  ├─ VS Code + HashiCorp extension
  ├─ tenv
  ├─ fmt / validate / TFLint / Trivy / terraform test
  └─ pre-commit
            ↓
Protected Git repository
            ↓
CI
  ├─ reproduce local checks
  ├─ dependency/supply-chain controls
  ├─ OIDC
  ├─ speculative/real plan
  └─ cost/security feedback
            ↓
HCP Terraform / TFE or dedicated controlled execution
  ├─ remote state
  ├─ locking
  ├─ RBAC
  ├─ private agents only when network access requires them
  ├─ run tasks
  ├─ policy
  └─ approvals
            ↓
Cloud
            ↓
Verification + observability + drift/health
Code language: PHP (php)

This is intentionally boring. Boring infrastructure delivery is good: deterministic, reviewable, repeatable, auditable, and difficult to bypass accidentally.


This handbook was checked against current vendor/project documentation available on 5 September 2026, including:

  • HashiCorp Terraform CLI, testing, state, backend, policy, MCP Server, HCP Terraform, and Terraform Enterprise documentation.
  • HashiCorp Terraform VS Code extension and terraform-ls.
  • tofuutils tenv.
  • terraform-linters TFLint and AWS ruleset.
  • Aqua Security Trivy.
  • Checkov.
  • terraform-docs.
  • Infracost.
  • antonbabenko/pre-commit-terraform.
  • Gruntwork Terragrunt and Terratest.
  • Atlantis.
  • Open Policy Agent and Conftest.
  • Renovate.
  • tfupdate.
  • GitHub Actions / AWS OIDC and GitLab OIDC documentation.

Selected official/reference URLs:

https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
https:
Code language: JavaScript (javascript)

Final operating principle

Terraform quality is not produced by Terraform alone.

A production Terraform system combines:

Correct code
+ reproducible versions
+ secure identities
+ protected state
+ automated lint/security/test gates
+ visible cost
+ reviewable plan
+ policy where it adds real value
+ controlled execution
+ post-deployment verification
Code language: PHP (php)

That combination turns Terraform from a command-line tool into an engineering platform.

Find Trusted Cardiac Hospitals

Compare heart hospitals by city and services — all in one place.

Explore Hospitals

Tags: , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,