Skip to content

CI/CD with GitHub Actions

New to compliance.tf CI/CD? See the CI/CD overview for prerequisites and authentication concepts.

Store the Token

  1. Go to your repository on GitHub
  2. Navigate to Settings > Secrets and variables > Actions
  3. Click New repository secret
  4. Name: CTF_TOKEN
  5. Value: paste your token from the Access Tokens page

For multi-environment setups, use GitHub Environments with separate secrets per environment.

Configure Your Pipeline

Add the token as an environment variable in your workflow steps that run Terraform:

.github/workflows/terraform.yml (excerpt)
steps:
  - uses: hashicorp/setup-terraform@v3

  - name: Terraform Init
    run: terraform init
    env:
      TF_TOKEN_soc2_compliance_tf: ${{ secrets.CTF_TOKEN }}

  - name: Terraform Plan
    run: terraform plan -out=tfplan
    env:
      TF_TOKEN_soc2_compliance_tf: ${{ secrets.CTF_TOKEN }}

Do not echo the token

Never add echo or print statements that could expose the token in workflow logs. GitHub masks secrets in logs automatically, but avoid unnecessary exposure.

Complete Workflow Example

This is a production-grade workflow for running compliance.tf modules in GitHub Actions. It runs terraform plan on pull requests and terraform apply on merge to main.

.github/workflows/terraform.yml
name: Terraform

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  contents: read
  pull-requests: write

jobs:
  terraform:
    name: Terraform Plan & Apply
    runs-on: ubuntu-latest

    env:
      # compliance.tf registry authentication
      TF_TOKEN_soc2_compliance_tf: ${{ secrets.CTF_TOKEN }}
      # Pin Terraform output to avoid interactive prompts
      TF_INPUT: "false"

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.12.0"

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        continue-on-error: true

      - name: Save Plan JSON
        if: steps.plan.outcome == 'success'
        run: terraform show -json tfplan > plan.json

      - name: Upload Plan Artifacts
        if: steps.plan.outcome == 'success'
        uses: actions/upload-artifact@v4
        with:
          name: terraform-plan
          path: |
            tfplan
            plan.json
          retention-days: 90

      - name: Comment Plan on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const output = `#### Terraform Plan: \`${{ steps.plan.outcome }}\`

            <details><summary>Plan Output</summary>

            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`

            </details>

            *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });

      - name: Plan Status Check
        if: steps.plan.outcome == 'failure'
        run: exit 1

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve tfplan

What is new compared to a standard Terraform workflow

If you already have a GitHub Actions workflow for Terraform, the only addition is the TF_TOKEN_soc2_compliance_tf environment variable. The Terraform commands are identical.

The only line you add
env:
  TF_TOKEN_soc2_compliance_tf: ${{ secrets.CTF_TOKEN }}

Everything else — checkout, setup, init, plan, apply — is the same workflow you already run.

Adding Checkov Verification (Optional)

Compliance.tf modules enforce controls at the module level during terraform plan. You can add Checkov as an independent verification layer. These tools are complementary: compliance.tf prevents non-compliant configurations, Checkov provides a second-opinion scan.

Add this step after the plan step:

Checkov verification step (optional)
      - name: Run Checkov
        if: always() && steps.plan.outcome == 'success'
        uses: bridgecrewio/checkov-action@v12
        with:
          file: plan.json
          framework: terraform_plan
          output_format: cli,json
          output_file_path: console,checkov-report.json
          quiet: true

      - name: Upload Checkov Report
        if: always() && steps.plan.outcome == 'success'
        uses: actions/upload-artifact@v4
        with:
          name: checkov-report
          path: checkov-report.json
          retention-days: 90

Compliance.tf modules are designed to pass Checkov checks. If Checkov flags an issue on a compliance.tf module, contact support — it may indicate a gap in control coverage.

Module Source Governance (Optional)

Add this step to enforce that Terraform modules use compliance.tf sources. It flags:

  • Upstream module sources that should be using compliance.tf (e.g., terraform-aws-modules/ without a compliance.tf prefix)
  • Disabled controls (?disable=) for audit review
Module source governance step (optional)
      - name: Check Module Sources
        if: github.event_name == 'pull_request'
        run: |
          echo "## Module Source Governance Report" >> $GITHUB_STEP_SUMMARY

          # Flag terraform-aws-modules sources not using compliance.tf
          UPSTREAM=$(grep -rn 'source.*=.*"terraform-aws-modules/' --include='*.tf' . \
            | grep -v 'compliance\.tf/' || true)
          if [ -n "$UPSTREAM" ]; then
            echo "### ⚠️ Upstream modules without compliance.tf" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "$UPSTREAM" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "::warning::Found terraform-aws-modules sources not using compliance.tf"
          else
            echo "✅ All terraform-aws-modules sources use compliance.tf" >> $GITHUB_STEP_SUMMARY
          fi

          # Flag any disabled controls for audit visibility
          DISABLED=$(grep -rn 'disable=' --include='*.tf' . || true)
          if [ -n "$DISABLED" ]; then
            echo "### ℹ️ Disabled controls (requires audit justification)" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "$DISABLED" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
          fi

This step produces a summary in the GitHub Actions UI. To make it a hard gate (block the PR), add exit 1 after the upstream module warning.

For teams in active migration, start with warnings and switch to blocking after Phase 3 of your migration plan.