Terraform Validation, Linting & CI
Build the gate that runs before every apply: format, validate, lint, scan, and a plan a human approves.
- Time
- 50 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Free
Before you start
You will need
- Terraform >= 1.6
- tflint
- trivy or checkov
- A GitHub repository
You will be able to
- Chain the checks that catch a bad change before it reaches AWS
- Apply exactly the plan that was reviewed
- Detect drift on a schedule rather than during an incident
Cost — Free
— everything here runs without creating infrastructure. `plan` reads AWS but changes nothing.
Success criteria
0 of 4
The scenario#
Terraform runs from someone's laptop. Reviews read the HCL, not the plan, so nobody notices the -/+ that would recreate the database until it happens.
This lab puts the checks in front of the apply.
The order, and why#
fmt → validate → tflint → security scan → plan → review → applyCheapest first. fmt takes a second; a security scan takes twenty; plan calls
AWS. Failing early means the expensive steps only run on changes that deserve
them.
1. The local checks#
terraform fmt -check -recursive # -check fails rather than rewriting
terraform init -backend=false # no state needed to validate syntax
terraform validate-backend=false matters in CI: validation needs no credentials and no state, so
it can run on a pull request from a fork.
2. Lint what validate cannot see#
tflint --init
tflint --recursivevalidate checks syntax and types. tflint catches the things that are valid
HCL and wrong anyway: a nonexistent instance type, a deprecated argument, a
missing required tag.
3. Scan for insecure defaults#
trivy config --severity HIGH,CRITICAL --exit-code 1 .Catches the classics — an unencrypted bucket, a security group open to
0.0.0.0/0, public access left unblocked. --exit-code 1 is what makes it a
gate rather than a report.
4. Plan as an artifact#
terraform plan -out=tfplan -input=false
terraform show -no-color tfplan > plan.txtThen apply exactly that:
terraform apply -input=false tfplanThis is the important habit. Re-planning at apply time means the change that runs is not the change that was reviewed — the world may have moved in between. A saved plan cannot drift between approval and execution.
5. The workflow#
name: terraform
on:
pull_request:
paths: ["infrastructure/**"]
schedule:
- cron: "0 6 * * 1" # weekly drift check
permissions:
contents: read
id-token: write
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive
- run: terraform init -backend=false
- run: terraform validate
- uses: terraform-linters/setup-tflint@v4
- run: tflint --recursive
- uses: aquasecurity/trivy-action@master
with:
scan-type: config
severity: HIGH,CRITICAL
exit-code: "1"
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_PLAN_ROLE }}
aws-region: us-east-1
- run: terraform init
- run: terraform plan -out=tfplan -no-color | tee plan.txt
- uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplanThe plan role should be read-only. A pull request from anywhere should be able to show you what it would do and never be able to do it.
6. Drift detection#
terraform plan -detailed-exitcode
# 0 = no changes, 1 = error, 2 = driftThat exit code is designed for exactly this. On a schedule, 2 means someone
changed AWS by hand — and finding that on a Monday morning is considerably
better than finding it mid-incident, when you cannot tell whether the drift is
the cause or a previous fix.
When it goes wrong#
fmt -check fails and you cannot see why
terraform fmt -diff -recursive prints the exact change it wants.
validate fails in CI, passes locally
A different Terraform version. Pin it with setup-terraform's terraform_version.
The scanner flags something you accept
Suppress it inline with a documented reason. A blanket --skip teaches everyone to ignore the tool.
The saved plan is rejected at apply
State moved since the plan. That is the protection working — re-plan and review again.
The concept behind it
Phase complete · 04 Infrastructure as Code
You can now: The AWS environment is described in version-controlled modules with remote, locked state.
Next phase
Lab 28 of 58 on the project path
Previous: Jenkins EC2 Instance, S3 Backend & AWS Backup Vault