CI/CD with GitHub Actions
New to compliance.tf CI/CD? See the CI/CD overview for prerequisites and authentication concepts.
Store the Token
- Go to your repository on GitHub
- Navigate to Settings > Secrets and variables > Actions
- Click New repository secret
- Name:
CTF_TOKEN - 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:
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.
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.
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:
- 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 acompliance.tfprefix) - Disabled controls (
?disable=) for audit review
- 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.