DEV Community

Cover image for CI/CD Quality Gates for Terraform: tflint, Checkov, Conftest, Terratest
beefed.ai
beefed.ai

Posted on Originally published at beefed.ai

CI/CD Quality Gates for Terraform: tflint, Checkov, Conftest, Terratest

  • Why staged CI/CD quality gates stop dangerous Terraform merges
  • Make fast checks fast: integrating tflint for deterministic linting
  • Shift-left security scanning: Checkov for Terraform and plan analysis
  • Write enforcement in code: Conftest (OPA/Rego) policy patterns
  • Prove it deploys: Terratest for ephemeral infrastructure validation
  • Practical checklist: concrete CI/CD quality gate with GitHub Actions and GitLab CI

Quality gates are the automated firewall that keeps misconfigured Terraform from becoming an incident. Combining fast linting, static security scanning, policy-as-code, and targeted dynamic tests gives you predictable, enforceable gates that fail merges — not production.

You recognize the symptoms: noisy PRs full of trivial lint warnings, high-severity policy failures slipping past reviewers, and flaky integration tests that either run forever or never run at PR-time. That friction creates either slow reviews or risky exceptions — both erode the guardrails that keep IaC safe.

Why staged CI/CD quality gates stop dangerous Terraform merges

A quality gate is a sequence of checks arranged by speed and confidence. Run the cheapest, deterministic checks first so developers get immediate feedback; escalate to richer analysis only for changes that pass the first filter. The canonical stages are:

  • Quick formatting & syntax: terraform fmt and terraform validate (fast, deterministic). Use terraform validate for config-level sanity checks.
  • Lint: tflint for Terraform best-practices and provider-aware rules (quick, rule-based).
  • Static security & policy scanning: Checkov runs a broad set of security/compliance checks and can scan plan output (graph/attribute checks).
  • Policy-as-code enforcement: Conftest (OPA/Rego) for organization-specific governance that Checkov doesn't encode.
  • Dynamic verification: Terratest for end-to-end behavior validation against ephemeral resources (run selectively).
Gate Tool examples Purpose Typical runtime (PR-friendly)
Syntax & fmt terraform fmt, terraform validate Catch syntax, type errors < 30s
Lint tflint Enforce best-practices, catch common mistakes 30s–2m
Static security Checkov Find insecure defaults, policy violations, plan analysis 1–5m (varies)
Policy-as-code Conftest (Rego) Enforce org policies (tags, ownership, wide open SGs) 30s–2m
Dynamic tests Terratest Verify real-world behavior (connectivity, endpoints) 2–15m (use sparingly)

Important: put fast deterministic checks early. A PR that fails lint should never get to expensive plan or dynamic tests.

Make fast checks fast: integrating tflint for deterministic linting

Use tflint to catch Terraform-language mistakes, provider-specific issues, and style violations before the plan stage. TFLint is plugin-based, configurable via .tflint.hcl, and supports outputs consumable by CI (including SARIF), and severity thresholds to control when the job should fail. Use the official GitHub Action terraform-linters/setup-tflint to install and run tflint reliably in GitHub Actions.

Example .tflint.hcl:

# .tflint.hcl
config {
  terraform_version = "1.5.0"
  deep_check = false
}

plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

plugin "aws" {
  enabled = true
  version = "0.28.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

rule "aws_instance_invalid_type" {
  enabled = true
}
Enter fullscreen mode Exit fullscreen mode

Run tflint in CI (GitHub Actions step example):

- uses: terraform-linters/setup-tflint@v6
  with:
    tflint_version: v0.58.0

- name: Init TFLint
  run: tflint --init

- name: Run TFLint (SARIF + fail on errors)
  run: tflint -f sarif --minimum-failure-severity=error --recursive > tflint.sarif
Enter fullscreen mode Exit fullscreen mode

Notes and practitioner tips:

  • Use --minimum-failure-severity to promote warnings to informational vs blocking categories.
  • Use tflint --init early so provider-aware rulesets download correctly (and avoid API rate-limits by supplying a GH token if needed).
  • Emit SARIF when possible and upload it to your code-scanning dashboard for annotating PRs.

Shift-left security scanning: Checkov for Terraform and plan analysis

Checkov runs hundreds of security and compliance checks against Terraform sources and terraform plan JSON output; it can produce SARIF, JSON, JUnit, and other outputs suitable for CI integration. Use Checkov to block insecure defaults (public S3s, overly permissive IAM, unencrypted storage) and to centralize enforcement.

A robust PR-time pattern:

  1. Run terraform init (with -backend=false if you need to avoid remote state).
  2. Create a binary plan and convert it to JSON:
    • terraform plan -out=tfplan
    • terraform show -json tfplan > tfplan.json
  3. Scan the JSON with Checkov:
    • checkov -f tfplan.json --framework terraform_plan -o sarif --output-file-path reports/checkov.sarif

Example GitHub Actions integration using the official action:

- name: Terraform Init & Plan
  run: |
    terraform init -upgrade
    terraform plan -out=tfplan

- name: Convert plan to JSON
  run: terraform show -json tfplan > tfplan.json

- name: Run Checkov (SARIF + CLI)
  uses: bridgecrewio/checkov-action@v12
  with:
    directory: .
    framework: terraform
    output_format: cli,sarif
    output_file_path: console,reports/checkov.sarif
    soft_fail: false
Enter fullscreen mode Exit fullscreen mode

Operational controls:

  • Use --soft-fail / --soft-fail-on / --hard-fail-on to stage adoption (allow low-severity issues to be informational during rollout).
  • Maintain a centralized checkov policy repo for organization-specific rules and use --external-checks-git or --external-checks-dir to pull them at runtime.
  • Upload SARIF artifacts to GitHub Code Scanning to get PR annotations. Use the upload-sarif action with security-events: write permission.

Write enforcement in code: Conftest (OPA/Rego) policy patterns

When your governance needs go beyond out-of-the-box checks, codify your rules in Rego and run them with Conftest as part of the pipeline. Conftest is a lightweight wrapper over OPA that works against HCL/JSON/YAML/plan JSON and plays well with CI. Use Conftest where you need custom logic such as mandatory tagging, environment-scoped resources, or disallowing specific cross-account bindings.

Sample Rego policy policy/s3_public.rego (deny public S3 ACLs):

package terraform.iac

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  attrs := resource.change.after
  (attrs.acl == "public-read" or attrs.acl == "public-read-write")
  msg = sprintf("S3 bucket %s has public ACL: %s", [resource.address, attrs.acl])
}
Enter fullscreen mode Exit fullscreen mode

Run Conftest against a plan JSON:

# install conftest (or use setup-conftest action)
conftest test tfplan.json --policy ./policy
Enter fullscreen mode Exit fullscreen mode

Integration notes:

  • Conftest policies are versioned and testable (conftest verify), enabling CI regression tests for policies.
  • Share policies via OCI/Git bundles (conftest pull) so teams reuse a vetted policy library.
  • Install conftest in CI via the official releases or a setup action and run tests on the plan JSON to get precise line/file feedback. [14search0] [14search1]

Prove it deploys: Terratest for ephemeral infrastructure validation

Static checks are necessary but not sufficient. Use Terratest to deploy small, focused infra changes into ephemeral test accounts and validate real behavior — then tear everything down. Terratest is a Go library that calls terraform init/apply/destroy programmatically, provides helpers for retries and idempotency, and encourages staging tests (setup → validate → teardown).

Minimal Terratest example (test/example_test.go):

package test

import (
  "testing"
  "github.com/gruntwork-io/terratest/modules/terraform"
)

func TestExampleModule(t *testing.T) {
  t.Parallel()

  terraformOptions := &terraform.Options{
    TerraformDir: "../examples/simple",
    Vars: map[string]interface{}{
      "region": "us-west-2",
    },
  }

  defer terraform.Destroy(t, terraformOptions)
  terraform.InitAndApply(t, terraformOptions)

  // Validate outputs
  output := terraform.Output(t, terraformOptions, "endpoint")
  if output == "" {
    t.Fatal("expected endpoint output")
  }
}
Enter fullscreen mode Exit fullscreen mode

Practical constraints and patterns:

  • Keep tests small and focused; test behavior not internal implementation.
  • Use defer terraform.Destroy to guarantee cleanup and keep cost bounded.
  • Run Terratest selectively: for critical modules at PR-time or on a nightly matrix for cross-account integration. Balance cost vs confidence.
  • Required runtime: Terratest needs Go (check docs for minimum Go version) and the cloud provider CLI/credentials in the runner.

Practical checklist: concrete CI/CD quality gate with GitHub Actions and GitLab CI

Below is a compact, copy-pasteable pipeline blueprint and checklist you can adapt. Each step includes the exact commands to run.

High-level PR workflow (order matters):

  1. terraform fmt -check → fail fast.
  2. terraform init -backend=false + terraform validate → basic correctness.
  3. tflint --init + tflint -f sarif --minimum-failure-severity=error → lint.
  4. terraform plan -out=tfplan + terraform show -json tfplan > tfplan.json → plan export.
  5. checkov -f tfplan.json -o sarif --output-file-path=reports/checkov.sarif → static security scan.
  6. conftest test tfplan.json --policy ./policy → policy-as-code enforcement.
  7. (Optional/conditional) go test -v ./test → Terratest E2E for critical modules.
  8. Upload SARIFs to the code-scanning dashboard and fail PR on any blocking findings.

Complete GitHub Actions minimal example (abridged):

name: Terraform Quality Gates
on: [pull_request]

permissions:
  contents: read
  security-events: write

jobs:
  fmt-validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: Terraform fmt & validate
        run: |
          terraform init -backend=false
          terraform fmt -check -recursive
          terraform validate -no-color

  tflint:
    runs-on: ubuntu-latest
    needs: fmt-validate
    steps:
      - uses: actions/checkout@v4
      - uses: terraform-linters/setup-tflint@v6
        with: { tflint_version: 'v0.58.0' }
      - name: Init TFLint
        run: tflint --init
      - name: Run TFLint (SARIF)
        run: tflint -f sarif --minimum-failure-severity=error --recursive > reports/tflint.sarif
      - uses: github/codeql-action/upload-sarif@v4
        with: sarif_file: reports/tflint.sarif

  checkov-conftest:
    runs-on: ubuntu-latest
    needs: tflint
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: Terraform plan
        run: |
          terraform init
          terraform plan -out=tfplan
          terraform show -json tfplan > tfplan.json
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          framework: terraform
          output_format: cli,sarif
          output_file_path: console,reports/checkov.sarif
          soft_fail: false
      - name: Setup Conftest
        uses: princespaghetti/setup-conftest@v1
      - name: Run Conftest policies
        run: conftest test tfplan.json --policy ./policy || exit 1
      - uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: reports/checkov.sarif
Enter fullscreen mode Exit fullscreen mode

GitLab CI integration: mirror the same stages in .gitlab-ci.yml with stages fmt, lint, security, plan, test and use cached containers for faster runs. The to-be-continuous/terraform template shows a pragmatic example integrating tflint and checkov jobs you can include or adapt.

Final operational checklist (exact commands to put into CI):

  • terraform fmt -check -recursive
  • terraform init -backend=false && terraform validate -no-color
  • tflint --init && tflint -f sarif --minimum-failure-severity=error --recursive
  • terraform plan -out=tfplan && terraform show -json tfplan > tfplan.json
  • checkov -f tfplan.json -o sarif --output-file-path=reports/checkov.sarif
  • conftest test tfplan.json --policy ./policy
  • go test -v ./test (Terratest; run conditionally)
  • Upload any *.sarif with github/codeql-action/upload-sarif@v4 to surface PR annotations.

Sources

Terraform CLI: validate / show - HashiCorp Developer - Documentation for terraform validate and notes on terraform show -json used to produce machine-readable plan/state output.

terraform-linters/setup-tflint - GitHub - Official GitHub Action to install and initialize tflint in workflows; demonstrates --init, caching, and wrapper options.

TFLint: Installation and Usage (docs / README) - TFLint configuration, .tflint.hcl semantics, --minimum-failure-severity and output formats (including SARIF).

Checkov (checkov.io) — Documentation home & CLI reference - Checkov feature overview and CLI options (frameworks, outputs, outputs to SARIF).

Static analysis of Terraform with Checkov (example: plan -> tfplan.json -> checkov) - Concrete example showing terraform plan -> terraform show -json -> checkov -f tfplan.json usage for plan scanning.

Conftest documentation (conftest.dev) - Conftest usage, Rego policy patterns, conftest test and conftest verify, and policy sharing/pull semantics.

Terratest documentation (terratest.gruntwork.io) - Terratest quick start, patterns for InitAndApply/Destroy, test_structure, and testing best practices for ephemeral infra.

Uploading a SARIF file to GitHub (GitHub Docs) - How to upload SARIF to GitHub to get code scanning PR annotations and required permissions (security-events: write).

Open Policy Agent (OPA) documentation - Rego policy language - Background on Rego and why policy-as-code provides a single source of truth for governance.

to-be-continuous/terraform GitLab CI template (example with tflint & checkov jobs) - A practical GitLab CI template showing tf-tflint and tf-checkov job patterns and artifact handling.

Top comments (0)