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
Terraform reads registry credentials from TF_TOKEN_<hostname> environment variables, with dots in the hostname replaced by underscores. For the SOC 2 endpoint soc2.compliance.tf the variable is TF_TOKEN_soc2_compliance_tf; for hipaa.compliance.tf it would be TF_TOKEN_hipaa_compliance_tf. Use the variable that matches the hostname in your module source URLs:
module "s3_bucket" {
source = "soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws"
version = "5.0.0"
# ...
}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 workflow runs terraform plan on pull requests and terraform apply on merge to main, with the compliance.tf registry authenticated at job level. It assumes your configuration references compliance.tf module sources like the excerpt above — the workflow itself is standard Terraform CI.
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 }}
# Disable interactive input 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 # adjust to your audit retention requirements — see the retention guidance in the CI/CD overview
- 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 tfplanAbout the setup-terraform wrapper
hashicorp/setup-terraform installs a wrapper (on by default) that exposes steps.plan.outputs.stdout — the PR comment step depends on it. The wrapper is also known to interfere with output redirection in some versions. If your plan.json comes out invalid, set terraform_wrapper: false on the setup step and drop the PR comment step (or generate the comment from a saved plan output file instead).
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)
Modules from compliance.tf 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 Save Plan JSON step — it consumes the plan.json that step produces. (The always() && steps.plan.outcome == 'success' condition exists because the plan step uses continue-on-error; it ensures Checkov runs only on successful plans.)
- 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: 90Modules from compliance.tf 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
fiThis step produces a summary in the GitHub Actions UI. To make it a hard gate (block the PR), add exit 1 as the last line inside the if [ -n "$UPSTREAM" ] block.
For teams in active migration, start with warnings and switch to blocking after Phase 3 of your migration plan.
Next steps
- Audit evidence guide — turn the plan artifacts this workflow uploads into auditor-ready evidence
- CI/CD overview — multi-environment setup, retention guidance, and troubleshooting