CI/CD Workflows

Understand how the automated CI/CD pipeline validates, tests, and deploys your Terraform infrastructure.


Overview

The starter kit's CI/CD pipeline automates infrastructure validation, security scanning, and deployment through GitHub Actions. The workflow uses OpenID Connect (OIDC) for secure, keyless authentication to AWS.

Note: All workflows discussed on this page are automatically generated by the make setup command. For installation instructions, see the Install guide.

Pipeline architecture

The starter kit includes four workflow files in .github/workflows/:

.github/workflows/
├── tflint-scan.yml                    # Reusable linting workflow
├── checkov-scan.yml                   # Reusable security scan workflow
├── terraform-plan-pr-comment.yml      # Reusable plan comment workflow
└── terraform-deploy-staging.yml       # Generated environment-specific workflow

Reusable workflows

Three workflows are designed to be called from environment-specific workflows:

  1. tflint-scan.yml - Validates Terraform code quality and best practices
  2. checkov-scan.yml - Scans for security misconfigurations
  3. terraform-plan-pr-comment.yml - Posts Terraform plan output to PR comments

Environment-specific workflows

When you run make setup, the wizard generates a deployment workflow for each environment (e.g., terraform-deploy-staging.yml, terraform-deploy-production.yml). These workflows:

  • Call the three reusable workflows
  • Contain environment-specific configuration (AWS account ID, region, state bucket)
  • Handle the complete deployment lifecycle

Environment variables

Each generated workflow includes environment variables with defaults from your setup:

env:
  AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID || '123456789012' }}
  AWS_REGION: ${{ vars.AWS_REGION || 'eu-west-1' }}
  GITHUB_ACTIONS_ROLE_NAME: ${{ vars.GITHUB_ACTIONS_ROLE_NAME || 'GitHubActionsServiceRole-Terraform' }}
  ENVIRONMENT: staging
  TF_WORKING_DIR: environments/staging
  TF_STATE_BUCKET: ${{ vars.TF_STATE_BUCKET || 'terraform-state-123456789012-eu-west-1' }}

Note: Values are embedded as defaults during setup. You can override these by creating GitHub repository variables.

Workflow triggers

The environment-specific workflows (e.g., terraform-deploy-staging.yml) trigger on three events:

on:
  push:
    branches:
      - main
    paths:
      - "environments/staging/**"
      - "modules/**"
      - ".github/workflows/terraform-deploy-staging.yml"
  pull_request_target:
    branches:
      - main
  workflow_dispatch:

Push to main

Triggers when changes are merged to the main branch that affect:

  • The environment directory (environments/staging/**)
  • Shared modules (modules/**)
  • The workflow file itself

Jobs executed:

  1. TFLint scan
  2. Checkov security scan
  3. Terraform check (format, init, validate)
  4. Terraform apply

Pull request

Triggers on pull requests targeting the main branch (no path filtering for PRs):

Jobs executed:

  1. TFLint scan
  2. Checkov security scan
  3. Terraform check (format, init, validate)
  4. Terraform plan
  5. Plan comment posted to PR

Manual dispatch

Allows manual workflow execution from GitHub Actions UI or CLI:

gh workflow run terraform-deploy-staging.yml

Job breakdown

The pipeline consists of six jobs that run sequentially:

tflint → checkov → terraform-check → terraform-plan/apply → plan-comment

1. TFLint scan

Calls the reusable tflint-scan.yml workflow to validate Terraform code quality:

tflint:
  name: TFLint Scan
  uses: ./.github/workflows/tflint-scan.yml

What it does:

  • Checks out code
  • Caches TFLint plugin directory for faster runs
  • Installs TFLint
  • Initializes TFLint with plugins from .tflint.hcl
  • Runs tflint -f compact --recursive on all Terraform files
  • Posts results to GitHub Step Summary

Configuration: .tflint.hcl in the repository root

Local testing: Run make lint to execute the same checks locally. See the Makefile reference for details.

2. Checkov security scan

Calls the reusable checkov-scan.yml workflow to scan for security misconfigurations:

checkov:
  name: Checkov Security Scan
  uses: ./.github/workflows/checkov-scan.yml
  with:
    working_directory: "environments/staging"
    soft_fail: true

What it does:

  • Checks out code
  • Runs Checkov with framework set to terraform
  • Downloads external modules for comprehensive scanning
  • Uses .checkov.yml for configuration
  • Posts security scan results to GitHub Step Summary

Key settings:

  • soft_fail: true - Workflow continues even if security issues are found
  • download_external_modules: true - Scans module dependencies

Configuration: .checkov.yml in the repository root

Local testing: Run make security-scan to execute the same checks locally. See the Makefile reference for details.

3. Terraform check

Validates Terraform configuration and formatting:

terraform-check:
  name: Terraform Check
  runs-on: ubuntu-latest
  needs: [tflint, checkov]

  defaults:
    run:
      working-directory: "environments/staging"

  steps:
    - name: Checkout code
      uses: actions/checkout@v5
      with:
        ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    - name: Terraform Format Check
      run: terraform fmt -check -recursive

    - name: Terraform Init
      run: terraform init -backend=false

    - name: Terraform Validate
      run: terraform validate

What it does:

  • Checks out code (using PR head SHA for pull requests)
  • Installs Terraform
  • Validates code formatting with terraform fmt -check -recursive
  • Initializes Terraform without backend (-backend=false for validation only)
  • Validates configuration syntax and logic

Note: needs: [tflint, checkov] means this job waits for both scans to complete first.

Local equivalent: Run make validate-full ENV=staging to execute all these checks locally. See the Makefile reference for details.

4. Terraform plan (PR only)

Generates an execution plan for pull request review:

terraform-plan:
  name: Terraform Plan
  runs-on: ubuntu-latest
  needs: terraform-check
  if: github.event_name == 'pull_request_target'

  defaults:
    run:
      working-directory: "environments/staging"

  steps:
    - name: Checkout code
      uses: actions/checkout@v5
      with:
        ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}

    - name: Configure AWS credentials (OIDC)
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.GITHUB_ACTIONS_ROLE_NAME }}
        aws-region: ${{ env.AWS_REGION }}
        role-session-name: GitHubActions-Terraform-Plan-Staging

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    - name: Terraform Init
      run: |
        terraform init \
          -backend-config="bucket=${{ env.TF_STATE_BUCKET }}" \
          -backend-config="key=environments/staging/terraform.tfstate" \
          -backend-config="region=${{ env.AWS_REGION }}" \
          -backend-config="use_lockfile=true"

    - name: Terraform Plan
      run: terraform plan -out=tfplan.binary
      continue-on-error: true

    - name: Save Plan Artifact
      if: always()
      uses: actions/upload-artifact@v5
      with:
        name: terraform-plan-artifact
        path: ${{ env.TF_WORKING_DIR }}/tfplan.binary
        retention-days: 1

Key features:

  • Only runs on pull requests (if: github.event_name == 'pull_request_target')
  • Uses OIDC for secure, keyless AWS authentication
  • Backend configuration passed dynamically from environment variables
  • continue-on-error: true ensures artifact is saved even if plan fails
  • Plan binary saved for 1 day

5. Plan comment (PR only)

Posts the Terraform plan to the pull request as a comment:

plan-comment:
  name: Post Plan Comment
  needs: terraform-plan
  if: github.event_name == 'pull_request_target'
  uses: ./.github/workflows/terraform-plan-pr-comment.yml
  with:
    planfile: tfplan.binary
    working-directory: "environments/staging"
    aws-region: eu-west-1
    environment: staging

The reusable workflow (terraform-plan-pr-comment.yml):

  1. Downloads the plan artifact from the previous job
  2. Initializes Terraform with -backend=false
  3. Uses the towardsthecloud/terraform-plan-pr-commenter@v1 action to:
    • Convert binary plan to readable format
    • Post formatted plan as PR comment
    • Include header: "Terraform Plan for staging in eu-west-1"

Benefits:

  • Reviewers see infrastructure changes before merge
  • No need to run terraform plan locally
  • Plan preserved in PR history

6. Terraform apply (push to main only)

Applies changes to AWS infrastructure after merging to main:

terraform-apply:
  name: Terraform Apply
  runs-on: ubuntu-latest
  needs: terraform-check
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  environment: staging

  defaults:
    run:
      working-directory: "environments/staging"

  steps:
    - name: Checkout code
      uses: actions/checkout@v5

    - name: Configure AWS credentials (OIDC)
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.GITHUB_ACTIONS_ROLE_NAME }}
        aws-region: ${{ env.AWS_REGION }}
        role-session-name: GitHubActions-Terraform-Apply-Staging

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    - name: Terraform Init
      run: |
        terraform init \
          -backend-config="bucket=${{ env.TF_STATE_BUCKET }}" \
          -backend-config="key=environments/staging/terraform.tfstate" \
          -backend-config="region=${{ env.AWS_REGION }}" \
          -backend-config="use_lockfile=true"

    - name: Terraform Apply
      run: terraform apply -auto-approve

    - name: Terraform Output
      if: success()
      run: |
        echo "### Terraform Outputs :rocket:" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY
        echo '```' >> $GITHUB_STEP_SUMMARY
        terraform output >> $GITHUB_STEP_SUMMARY
        echo '```' >> $GITHUB_STEP_SUMMARY

    - name: Deployment Status
      if: always()
      run: |
        if [ $? -eq 0 ]; then
          echo "✅ Deployment to staging environment successful!" >> $GITHUB_STEP_SUMMARY
        else
          echo "❌ Deployment to staging environment failed!" >> $GITHUB_STEP_SUMMARY
        fi

Key features:

  • Only runs on pushes to main branch
  • environment: staging enables GitHub environment protection (can require manual approval)
  • Uses OIDC for authentication with session name GitHubActions-Terraform-Apply-Staging
  • Runs terraform apply -auto-approve (no manual confirmation in CI/CD)
  • Posts Terraform outputs to GitHub Step Summary
  • Shows deployment status (success/failure)

OIDC authentication

The workflows use OpenID Connect (OIDC) for secure, keyless authentication to AWS:

- name: Configure AWS credentials (OIDC)
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.GITHUB_ACTIONS_ROLE_NAME }}
    aws-region: ${{ env.AWS_REGION }}
    role-session-name: GitHubActions-Terraform-Apply-Staging

How it works:

  1. GitHub generates a JWT token for the workflow
  2. Workflow assumes the IAM role using the JWT
  3. AWS validates the token against the OIDC provider
  4. AWS STS returns temporary credentials
  5. Workflow uses credentials to run Terraform commands

Security benefits:

  • No AWS credentials stored in GitHub Secrets
  • Temporary credentials automatically expire
  • Repository-scoped access via trust policy
  • Full audit trail via CloudTrail

Required permissions in the workflow:

permissions:
  id-token: write
  contents: read
  pull-requests: write

Learn more: How to set up GitHub OIDC federation for AWS deployments

Environment protection

The environment: staging field in the terraform-apply job enables GitHub environment protection:

terraform-apply:
  environment: staging  # Enables protection rules

To require manual approval before deployment:

  1. Navigate to SettingsEnvironments in your GitHub repository
  2. Create or select the environment (e.g., production, staging)
  3. Enable Required reviewers
  4. Add team members who must approve deployments

When protection is enabled, the workflow will pause at the terraform-apply job and wait for approval from designated reviewers before proceeding.

Path-based filtering

The workflow only runs when relevant files change:

on:
  push:
    paths:
      - "environments/staging/**"
      - "modules/**"
      - ".github/workflows/terraform-deploy-staging.yml"

Benefits:

  • Workflows don't run for unrelated changes
  • Changing one environment doesn't trigger workflows for other environments
  • Faster CI/CD with fewer unnecessary runs

Artifacts

The terraform-plan job saves the plan as an artifact:

- name: Save Plan Artifact
  uses: actions/upload-artifact@v5
  with:
    name: terraform-plan-artifact
    path: ${{ env.TF_WORKING_DIR }}/tfplan.binary
    retention-days: 1

This artifact is:

  • Downloaded by the plan-comment job to post the plan to the PR
  • Retained for 1 day
  • Can be downloaded manually from the workflow run for inspection

Next steps