Stage 4 · Provision
Validation, Policy & Secrets
terraform validate & plan
Syntax validation, plan files, detailed exit codes, JSON output, and CI gating — catching errors before apply.
terraform validate
terraform validate checks the syntax and structure of your configuration. It does not make API calls or check state. It catches typos, type errors, and invalid resource references before any plan or apply.
$ terraform validate
Success! The configuration is valid.
$ terraform validate -json
{
"valid": true,
"error_count": 0,
"warning_count": 0
}
$ terraform validate -json | jq .valid
trueterraform validate checks syntax, types, and references. The -json flag produces machine-readable output for CI integration. Always run validate before plan.
terraform plan
$ terraform plan # Show plan to stdout
$ terraform plan -out=tfplan # Save plan to file
$ terraform plan -out=tfplan -json # Save as JSON
$ terraform plan -target=aws_instance.web # Target specific resource
$ terraform plan -var="environment=prod" # Override variable
$ terraform plan -destroy # Show what would be destroyed
$ terraform plan -refresh-only # Show drift without changesterraform plan is the safety mechanism. It shows what will change without making changes. The -out flag saves the plan for later apply. The -json flag enables programmatic processing.
Exit Codes
| Exit Code | Meaning | CI Action |
|---|---|---|
| 0 | No changes — infrastructure matches config | Pass |
| 1 | Error — plan failed | Fail |
| 2 | Changes — plan shows modifications | Review or gate |
terraform plan -detailed-exitcode -no-color
EXIT_CODE=$?
case $EXIT_CODE in
0)
echo "No changes"
;;
1)
echo "Error"
exit 1
;;
2)
echo "Changes detected"
# Run policy checks, cost estimation, etc.
;;
esacThe -detailed-exitcode flag makes exit codes meaningful. Exit code 2 indicates changes exist. Use this to gate CI pipelines — fail on errors, review on changes.
JSON Output
$ terraform plan -out=tfplan -json > plan.json
# Count resource changes
$ jq '{
add: [.resource_changes[] | select(.change.actions[] == "create")] | length,
change: [.resource_changes[] | select(.change.actions[] == "update")] | length,
destroy: [.resource_changes[] | select(.change.actions[] == "delete")] | length
}' plan.json
# List destroyed resources
$ jq '.resource_changes[] | select(.change.actions[] == "delete") | .address' plan.jsonJSON output enables programmatic plan analysis. Use jq to extract specific information — counts, destroyed resources, or specific attributes. This powers CI checks and PR comments.
CI Gating
name: Terraform CI
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Format Check
run: terraform fmt -check -recursive
- name: Init
run: terraform init -backend=false
- name: Validate
run: terraform validate
- name: Plan
id: plan
run: terraform plan -out=tfplan -detailed-exitcode -no-color
continue-on-error: trueThe CI pipeline runs format check, init, validate, and plan in sequence. Each step catches different issues. continue-on-error on plan allows processing even when changes exist.
Best Practices
- Always run terraform validate before plan.
- Use -detailed-exitcode in CI for meaningful exit codes.
- Save plan files with -out for auditing and apply.
- Use JSON output for programmatic plan analysis.
- Store plan artifacts as CI artifacts for audit trail.
- Run fmt -check before validate to catch formatting issues.
terraform show -json tfplan converts an existing plan file to JSON. This avoids running the plan twice and ensures the JSON matches the plan that was approved.
terraform validate only checks the configuration. It does not verify that resources exist, that state is current, or that API calls will succeed. Always follow validate with plan.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.