Skip to content
EgyKode
Guided labterraform

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

CostFree

— 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#

text
fmt  →  validate  →  tflint  →  security scan  →  plan  →  review  →  apply

Cheapest 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#

Terminal
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#

Terminal
tflint --init
tflint --recursive

validate 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#

Terminal
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#

Terminal
terraform plan -out=tfplan -input=false
terraform show -no-color tfplan > plan.txt

Then apply exactly that:

Terminal
terraform apply -input=false tfplan

This 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#

yaml
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: tfplan

The 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#

Terminal
terraform plan -detailed-exitcode
# 0 = no changes, 1 = error, 2 = drift

That 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

Ready to try it without help?Do the challenge

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

05 · Configuration managementAnsible Architecture, Configuration & Automated InventoryHave Ansible discover your EC2 instances by tag instead of maintaining a host list nobody remembers to update.39 minIntermediate

Previous: Jenkins EC2 Instance, S3 Backend & AWS Backup Vault