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
Where this fits in the platform
Already built
This lab adds
- A plan that runs in CI, so drift is caught before apply
Which lets you
—
Before you start
You will need
- Terraform >= 1.6
- tflint
- trivy or checkov
- A GitHub repository
You do not need these already — the lab environment below provides them.
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.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
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.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
You will need:
- terraform
- docker
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The local checks
Step 1 of 6
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.
What you are proving: You can run the checks locally that CI will run, before pushing
Marking this settles success criterion 1.
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.
What you are proving: You can catch the mistakes validate cannot see
Marking this settles success criterion 1.
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.
What you are proving: You can scan configuration for insecure defaults rather than reviewing for them by eye
Marking this settles success criterion 1.
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.
What you are proving: You can apply a saved plan rather than re-planning at apply time
Marking this settles success criterion 3.
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.
What you are proving: You can make a misformatted or insecure change fail the pipeline, proven deliberately
Marking this settles success criteria 1 and 2.
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.
What you are proving: You can schedule a run that reports drift when something changes outside Terraform
Marking this settles success criterion 4.
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.
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.
Success criteria
0 of 4
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 59 on the project path
Previous: Jenkins EC2 Instance, S3 Backend & AWS Backup Vault