๐ CI/CD for Terraform Using GitHub Actions: Validate Before You Deploy


Infrastructure as Code (IaC) allows teams to manage cloud infrastructure in a repeatable and automated way. Terraform is one of the most widely used IaC tools โ but writing code is just the first step.
Without proper validation, scanning, and versioning, we risk deploying misconfigured, insecure, or untracked infrastructure changes.
In this article, Iโll walk through a complete GitHub Actions CI/CD setup for Terraform with three key stages:
โ
Validate and scan Terraform changes on every pull request
๐ Deploy automatically when code is merged to main
๐ท๏ธ Automatically version and tag releases using semantic-release
๐งฉ Why This Pipeline?
Manual Terraform deployments can lead to:
โ Missed validation or formatting issues
โ ๏ธ Security risks like unencrypted resources
๐ซ Merges without review or checks
๐ Inconsistencies between environments
๐ No proper versioning or changelogs
This GitHub Actions pipeline solves that by:
โ
Automating validation and security scans
โ
Blocking merges on failed checks
โ
Deploying only validated, reviewed code
โ
Automatically tagging versions and generating changelogs
๐ง Tools Used
Tool | Purpose |
Terraform | Define cloud infrastructure |
GitHub Actions | CI/CD pipeline execution |
Checkov | Security and best-practice scanning |
Semantic Release | Automated versioning and GitHub releases |
GitHub Branch Protection | Enforce merge safety rules |
๐ Storing Secrets Securely
For this pipeline, the following secrets are stored in GitHub repository secrets:
AWS_ACCESS_KEY_ID
โ AWS programmatic access keyAWS_SECRET_ACCESS_KEY
โ AWS secret access keyRELEASE_PLEASE_TOKEN
โ GitHub Personal Access Token (PAT) withrepo
permissions for semantic-release
These secrets are accessed inside the GitHub Actions workflows using ${{ secrets.<SECRET_NAME> }}
.
Add them by navigating to:GitHub Repo โ Settings โ Secrets and variables โ Actions โ New repository secret
.
๐ Pipeline Structure Overview
We use two GitHub Actions workflows:
1๏ธโฃ PR Validation Pipeline
Triggered when a pull request is opened from a feature/*
branch into main
.
๐ What it does:
Runs
terraform fmt
,init
, andvalidate
Runs Checkov for security scans
Runs
terraform plan
Posts the plan as a PR comment
Blocks merge if validation or scans fail using branch protection
๐ Workflow File: .github/workflows/onpr-test.yml
name: Run security scans and plan
on:
pull_request:
types: [opened, synchronize]
branches: [main]
permissions:
pull-requests: write
env:
# verbosity setting for Terraform logs
TF_LOG: INFO
# Credentials for deployment to AWS
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
jobs:
run-scans:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.12.2"
- name: Install GitHub CLI
run: |
sudo apt-get update
sudo apt-get install -y gh
- name: Terraform fmt
id: fmt
run: terraform fmt
continue-on-error: true
- name: Terraform init
id: init
run: terraform init
- name: Terraform validate
id: validate
run: terraform validate
- name: Run Checkov action
id: checkov
uses: bridgecrewio/checkov-action@master
with:
soft_fail: false
framework: terraform
output_format: sarif
output_file_path: reports/results.sarif
- name: Terraform plan
id: plan
run: terraform plan > plan.txt
- name: Post Plan Output to PR
run: |
gh pr comment ${{ github.event.pull_request.number }} --body-file plan.txt
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
2๏ธโฃ Post-Merge Deployment & Semantic Release Pipeline
Triggered when code is merged into main
.
๐ What it does:
Runs
terraform init
Runs
terraform apply -auto-approve
Runs
semantic-release
to:
โ Analyze commit messages (feat, fix, etc.)
โ Bump version based on commit type
โ Generate and publish GitHub release
โ Create/update changelog file
๐ Workflow File: .github/workflows/onmerge-deploy.yml
name: Deploy on Merge
on:
push:
branches:
- main # Trigger release on pushes to main branch
permissions:
contents: write
pull-requests: write
env:
# verbosity setting for Terraform logs
TF_LOG: INFO
# Credentials for deployment to AWS
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false # Avoid using
- name: Install terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.12.2"
- name: Terraform init
id: init
run: terraform init
- name: Terraform apply
id: apply
run: terraform apply -auto-approve
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install semantic-release
run: npm install semantic-release @semantic-release/github @semantic-release/commit-analyzer @semantic-release/release-notes-generator
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
run: npx semantic-release
๐ .releaserc
โ Semantic Release Config
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}
๐ฆ What Are We Deploying?
For this demo, we deploy a simple AWS SNS topic using Terraform.
๐ Unencrypted SNS Topic Example
resource "aws_sns_topic" "demo_topic" {
name = "demo-sns-topic"
}
This creates an SNS topic without encryption โ a potential security risk.
โ Checkov Flags a Security Violation
When the PR is raised, Checkov detects:
CKV_AWS_26: "Ensure all data stored in the SNS topic is encrypted"
โ Fixing the Violation
We update the resource to enable encryption with a managed AWS KMS key and push the changes:
resource "aws_sns_topic" "test_topic" {
name = var.sns_topic_name
kms_master_key_id = "alias/aws/sns"
}
๐ก๏ธ Enforcing Safe Merges with Branch Protection
To ensure only secure, validated code gets merged into main
, weโve enabled GitHub branch protection rules.
โ This requires:
All checks (like the PR pipeline) must pass
Pull requests must be reviewed
Direct pushes to main are blocked
๐ง How to enable branch protection:
1๏ธโฃ Navigate to GitHub Repo โ Settings โ Branches
2๏ธโฃ Click Add rule and set the branch name pattern to main
3๏ธโฃ Enable the following options:
Require status checks to pass before merging โ Select the job from the PR validation pipeline (e.g.,
run-scans
)Require pull request reviews before merging
Restrict who can push to matching branches โ Blocks direct commits to
main
4๏ธโฃ Save the rule
๐ Connecting CI checks to branch protection:
The status checks enforced here are the same ones from the PR validation workflow.
If
terraform fmt
,validate
, orplan
fails โ merge is blocked.If Checkov finds a security issue โ merge is blocked.
With this setup, the PR validation workflow acts as a gatekeeper, and branch protection enforces that no insecure or unvalidated Terraform code reaches production.
๐ Semantic Release Flow
We follow Conventional Commits (feat
, fix
, chore
) to drive versioning.
feat:
โ Minor bump (e.g., 1.0.0 โ 1.1.0)fix:
โ Patch bump (e.g., 1.1.0 โ 1.1.1)
When a merge occurs:
โ
Semantic Release analyzes commits
โ
Creates a GitHub release with version tag
โ Final Thoughts
With these workflows, we now have a production-ready Terraform CI/CD pipeline that:
โ
Automates validation & scanning
โ
Blocks insecure infrastructure changes
โ
Deploys only approved, peer-reviewed code
โ
Automatically versions and tags releases
๐ Repository
View the full source code on GitHub
๐ฌ Have questions or feedback? Feel free to drop them in the comments below!
Subscribe to my newsletter
Read articles from YASH REGE directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
