Skip to main content
RunBook Academy

TerraformXXII · CI/CD for Production TerraformCI

CI for Terraform: The Production Pipeline

Intermediate⏱ ~14 minbashgithub-actionsterraform

What you'll learn

  • Build a CI pipeline for Terraform
  • Choose the right gates and the right checks
  • Use saved plans for reviewable applies
  • Recognise the production risks of skipping CI

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12

Not yet marked complete on this device.

A CI pipeline is the production control for Terraform changes. The pipeline runs the validation, the plan, the policy, and the apply. The pipeline is the safety net. The lesson teaches the production-grade CI pipeline.

The pipeline stages

A production CI pipeline has these stages:

  1. Lint. terraform fmt -check -recursive and tflint.
  2. Validate. terraform init -backend=false and terraform validate.
  3. Security scan. trivy config, tfsec, or checkov.
  4. Plan. terraform plan -out=tfplan.
  5. Review. The plan is uploaded as an artifact. The PR is reviewed.
  6. Apply. terraform apply tfplan (after manual approval).
  7. Notify. The on-call engineer is notified.

Each stage is a gate. A failure in any stage stops the pipeline.

The GitHub Actions example

name: terraform

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  terraform:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4

      # 1. Lint
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.x
      - run: terraform fmt -check -recursive
      - uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: latest
      - run: tflint --recursive

      # 2. Validate (no backend)
      - run: terraform init -backend=false
      - run: terraform validate

      # 3. Security scan
      - uses: aquasecurity/trivy-action@v0.20.0
        with:
          args: config --severity HIGH,CRITICAL
      - run: trivy config --severity HIGH,CRITICAL .

      # 4. Plan
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1
      - run: terraform init
      - run: terraform plan -out=tfplan -no-color
      - uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan
      - uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const plan = JSON.parse(
              exec('terraform show -json tfplan').stdout
            );
            const counts = plan.resource_changes.reduce((acc, r) => {
              acc[r.change.actions[0]] = (acc[r.change.actions[0]] || 0) + 1;
              return acc;
            }, {});
            const body = `### Terraform plan\n\n` +
              `| Action | Count |\n|--------|-------|\n` +
              Object.entries(counts).map(([k, v]) => `| ${k} | ${v} |`).join('\n');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body,
            });

      # 6. Apply (only on main, after manual approval)
      - if: github.ref == 'refs/heads/main'
        uses: hashicorp/tf-actions@v1
        with:
          tf-actions-version: latest
          command: apply
          working-directory: .
          terraform_version: 1.9.x
          plan_artifact: tfplan

The pipeline:

  • Runs on every PR and on every push to main.
  • Runs the lint, validate, and security scan on every PR.
  • Runs the plan on every PR.
  • Comments the plan on the PR.
  • Applies the saved plan only on main, after manual approval.

The gates

The pipeline gates (in order):

  1. Lint. Format errors. The fix is terraform fmt.
  2. Validate. Configuration errors. The fix is to fix the configuration.
  3. Security scan. Security issues. The fix is to fix the configuration.
  4. Plan. The plan is reviewed. The fix is to fix the plan or accept the plan.
  5. Apply. The apply is gated by manual approval.

Each gate is a production control. A failure in any gate stops the pipeline.

The plan review

The plan is uploaded as an artifact. The PR is reviewed by a second engineer. The review checks:

  • The summary line matches the change ticket.
  • The action types are expected.
  • The replacements are expected.
  • The destructions are expected.
  • The drift is expected.

The review is the production control for the apply.

The apply

The apply is gated by manual approval:

# GitHub Actions with environment approval
jobs:
  terraform-apply:
    runs-on: ubuntu-latest
    environment: production  # requires manual approval
    # ...

The apply uses the saved plan from the artifact:

- uses: hashicorp/tf-actions@v1
  with:
    command: apply
    plan_artifact: tfplan

The apply is the same plan that was reviewed. The audit trail is the artifact.

The notification

The on-call engineer is notified when the apply completes:

- name: Notify on apply
  if: always()
  run: |
    curl -fsSL -X POST "$SLACK_WEBHOOK" \
      -d "Terraform apply ${{ github.run_id }} ${{ job.status }}"

The notification is the production control for the post-apply monitoring.

What comes next

The next lesson is CD / Terraform apply — the production deployment patterns.

Verification

Knowledge check · 7 questions

  1. Q1. What is the role of the CI pipeline in Terraform?

  2. Q2. What is the role of plan artifacts in CI/CD?

  3. Q3. terraform apply -auto-approve in CI/CD is appropriate for production.

  4. Q4. What is the role of OIDC for CI/CD?

  5. Q5. Which of the following are part of a production CI/CD pipeline? (Select all that apply.)

  6. Q6. What is the role of environment-specific CI/CD?

  7. Q7. A CI/CD pipeline applies to production. The state is corrupted in dev. What is the impact?

Passing score: 75%. Answers are checked in this browser.