Command Palette

Search for a command to run...

Hectal

Mission 7.2 · Stage 7 — Team Workflow

Test and Lint Before Anyone Reviews

Goal: The PR pipeline fails automatically on invalid AWS values (tflint), insecure configuration (trivy), and broken module behaviour (terraform test with mocked providers) — before a human reads the plan.

50 min Free — tests use mocked providers, nothing is created 4 steps 2 break-it drills

By the end of this mission

  • Catch AWS-specific mistakes with tflint and its AWS ruleset
  • Scan configuration for security issues and document justified exceptions
  • Write terraform test files with plan-mode assertions and mock providers
  • Test that invalid inputs fail with expect_failures

Part 1

Understand it first

Layers of automated checking

fmt and validate (already in CI) catch syntax and schema errors. TFLINT with the AWS ruleset knows AWS specifics: invalid instance types, deprecated arguments, previous-generation resources. SECURITY SCANNERS (trivy, checkov) flag risky configuration: public buckets, open ingress, unencrypted storage. terraform test checks YOUR logic, such as 'a worker never gets a load balancer' or 'invalid CPU sizes are rejected'. Each layer is cheap and runs in seconds, so humans review design, not typos.

terraform test and mock providers

Test files (*.tftest.hcl) contain run blocks. Each one plans (or applies) the module with given variables, then evaluates assert conditions against the result. With mock_provider "aws" (Terraform 1.7+), the AWS provider is replaced by a fake that returns generated values, so tests need no credentials, create nothing, and take seconds. That makes them ideal for module logic.

expect_failures = [var.cpu] asserts that a validation rule rejects bad input. Testing that your guardrails actually guard is as important as testing the happy path.

Findings are decisions, not noise

Scanners will flag things you've chosen deliberately. For example, ShopLite's app security group allows all egress because tasks call external APIs through NAT. The right response is an inline ignore with a written justification, reviewed like code. Silencing whole rules globally, or ignoring the scanner, means the next real finding gets lost too.

What each check catchesdiagram
Rendering diagram…

Part 2

Your project after this mission · 4 files change

shoplite/
  • .github/
    • workflows/
      • terraform.ymlmodified
  • bootstrap/
    • main.tf
  • infra/
    • envs/
      • dev.s3.tfbackend
      • dev.tfvars
      • prod.s3.tfbackend
      • prod.tfvars
    • database.tf
    • ecs.tf
    • main.tf
    • providers.tf
    • security.tfmodified
    • tf
  • modules/
    • ecs-service/
      • tests/
        • service.tftest.hclnew
      • main.tf
      • variables.tf
    • network/
      • main.tf
  • .tflint.hclnew

Part 3

Build it, step by step

  1. 1

    tflint with the AWS ruleset

    The plugin block pins the ruleset version. tflint --init downloads it; --recursive lints every module.

    .tflint.hclwhole filehcl
    plugin "terraform" {
      enabled = true
      preset  = "recommended"
    }
    
    plugin "aws" {
      enabled = true
      version = "0.40.0"
      source  = "github.com/terraform-linters/tflint-ruleset-aws"
    }
    terminal
    $ tflint --init && tflint --recursive
    ── expected output ──
    Installing "aws" plugin...
    Installed "aws" (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.40.0)
    (no output = no issues)
  2. 2

    Security scan, and a justified exception

    trivy flags the app tier's unrestricted egress. It's deliberate, so record why, right where the resource is defined. The comment must sit directly above the resource. Everything else trivy reports should be fixed, not ignored.

    infra/security.tfadd to filehcl
    # trivy:ignore:AVD-AWS-0104 App tasks call external payment/email APIs via NAT; egress
    # is limited at the network edge (NAT) and reviewed in security review SR-112.
    resource "aws_vpc_security_group_egress_rule" "app_all" {
      security_group_id = aws_security_group.app.id
      cidr_ipv4         = "0.0.0.0/0"
      ip_protocol       = "-1"
    }
    terminal
    $ trivy config --severity HIGH,CRITICAL .
    ── expected output ──
    infra/security.tf (terraform)
    Tests: 64 (SUCCESSES: 63, FAILURES: 1)
    Failures: 1 (HIGH: 0, CRITICAL: 1)
     
    AVD-AWS-0104 (CRITICAL): Security group rule allows unrestricted egress to any IP address.
    ══════════════════════════════════════════
    infra/security.tf:51-55
    ──────────────────────────────────────────
    51 ┌ resource "aws_vpc_security_group_egress_rule" "app_all" {
    52 │ security_group_id = aws_security_group.app.id
    53 │ cidr_ipv4 = "0.0.0.0/0"
    54 │ ip_protocol = "-1"
    55 └ }
  3. 3

    Write tests for the ecs-service module

    Three tests: a worker (no load balancer) gets no load-balancer block and no request-count scaling; an API with a load balancer gets exactly one of each; an invalid CPU size is rejected by validation. The mock provider needs one hint, a realistic region for the aws_region data source, because the log configuration uses it.

    modules/ecs-service/tests/service.tftest.hclwhole filehcl
    mock_provider "aws" {
      mock_data "aws_region" {
        defaults = { region = "ap-south-1" }
      }
    }
    
    variables {
      name               = "test-svc"
      container_name     = "app"
      cluster_id         = "arn:aws:ecs:ap-south-1:123456789012:cluster/test"
      image              = "123456789012.dkr.ecr.ap-south-1.amazonaws.com/test:1.0.0"
      execution_role_arn = "arn:aws:iam::123456789012:role/exec"
      task_role_arn      = "arn:aws:iam::123456789012:role/task"
      subnet_ids         = ["subnet-aaa", "subnet-bbb"]
      security_group_ids = ["sg-123"]
      log_group_name     = "/test/app"
    }
    
    run "worker_has_no_load_balancer" {
      command = plan
    
      assert {
        condition     = length(aws_ecs_service.this.load_balancer) == 0
        error_message = "A service without load_balancer must not attach to a target group."
      }
    
      assert {
        condition     = length(aws_appautoscaling_policy.requests) == 0
        error_message = "Workers without a load balancer must not get request-count scaling."
      }
    }
    
    run "api_gets_lb_and_request_scaling" {
      command = plan
    
      variables {
        load_balancer = {
          target_group_arn = "arn:aws:elasticloadbalancing:ap-south-1:123456789012:targetgroup/api/abc"
          container_port   = 8080
          resource_label   = "app/alb/123/targetgroup/api/abc"
        }
        autoscaling = { min = 2, max = 6, requests_per_target = 500 }
      }
    
      assert {
        condition     = length(aws_ecs_service.this.load_balancer) == 1
        error_message = "Expected exactly one load_balancer block."
      }
    
      assert {
        condition     = length(aws_appautoscaling_policy.requests) == 1
        error_message = "Expected request-count scaling when resource_label and requests_per_target are set."
      }
    
      assert {
        condition     = aws_appautoscaling_target.this.min_capacity == 2
        error_message = "min_capacity should follow autoscaling.min."
      }
    }
    
    run "rejects_invalid_fargate_cpu" {
      command = plan
    
      variables {
        cpu = 300
      }
    
      expect_failures = [var.cpu]
    }
    terminal
    $ cd modules/ecs-service && terraform init -backend=false >/dev/null && terraform test
    ── expected output ──
    tests/service.tftest.hcl... in progress
    run "worker_has_no_load_balancer"... pass
    run "api_gets_lb_and_request_scaling"... pass
    run "rejects_invalid_fargate_cpu"... pass
    tests/service.tftest.hcl... tearing down
    tests/service.tftest.hcl... pass
     
    Success! 3 passed, 0 failed.
  4. 4

    Add the checks to the PR job

    Insert these steps before plan in the plan job. They need no AWS credentials, so failures show up in seconds. terraform-linters/setup-tflint and aquasecurity/trivy-action install the tools.

    .github/workflows/terraform.ymladd to fileyaml
          - uses: terraform-linters/setup-tflint@v4
          - name: tflint
            run: tflint --init && tflint --recursive
          - name: trivy
            uses: aquasecurity/trivy-action@0.28.0
            with:
              scan-type: config
              severity: HIGH,CRITICAL
              exit-code: "1"
          - name: module tests
            run: |
              for m in modules/*/; do
                if ls "$m"tests/*.tftest.hcl >/dev/null 2>&1; then
                  (cd "$m" && terraform init -backend=false -input=false >/dev/null && terraform test)
                fi
              done

Checkpoint — you should now have

  • ✓tflint --recursive passes with the AWS ruleset.
  • ✓trivy config passes; the one accepted finding has an inline justification.
  • ✓terraform test passes 3 tests for ecs-service with no AWS credentials.
  • ✓All three run in the PR job before plan.

Part 4

Break it on purpose

Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.

Break #1

Typo an instance type

In infra/probe.tf, set instance_type = "t4g.nanoo" and run tflint --recursive. (terraform validate would pass this.)

terminal
$ tflint --recursive
── what you'll see ──
1 issue(s) found:
 
Error: "t4g.nanoo" is an invalid value as instance_type (aws_instance_invalid_type)
 
on infra/probe.tf line 44:
44: instance_type = "t4g.nanoo"

Break #2

Change module behaviour and let the tests catch it

In the module, change the request policy's count condition to just var.autoscaling.requests_per_target != null ? 1 : 0 (dropping the load-balancer check), and give the worker test autoscaling = { requests_per_target = 100 }. Run terraform test.

terminal
$ terraform test
── what you'll see ──
tests/service.tftest.hcl... in progress
run "worker_has_no_load_balancer"... fail
╷
│ Error: Test assertion failed
│
│ on tests/service.tftest.hcl line 27, in run "worker_has_no_load_balancer":
│ 27: condition = length(aws_appautoscaling_policy.requests) == 0
│ ├────────────────
│ │ Diff:
│ │ - 0
│ │ + 1
│
│ Workers without a load balancer must not get request-count scaling.
╵
...
Failure! 2 passed, 1 failed.

Part 5

Interview questions from this mission

01

How do you test Terraform code?

02

What do mock providers give you in terraform test?

03

A security scanner flags something you intentionally configured. What do you do?

0/4 · 0%