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

YASH REGEYASH REGE
6 min read

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

ToolPurpose
TerraformDefine cloud infrastructure
GitHub ActionsCI/CD pipeline execution
CheckovSecurity and best-practice scanning
Semantic ReleaseAutomated versioning and GitHub releases
GitHub Branch ProtectionEnforce 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 key

  • AWS_SECRET_ACCESS_KEY โ€“ AWS secret access key

  • RELEASE_PLEASE_TOKEN โ€“ GitHub Personal Access Token (PAT) with repo 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, and validate

  • 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"

โŒ Failed Checkov scan

โœ… 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"
}

โœ… Successful pipeline run after fixing encryption

๐Ÿ›ก๏ธ 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

๐Ÿ”’ Branch protection rules on main

โŒ Blocked PR merge

๐Ÿ”ง 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, or plan 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

โœ… Auto-generated changelog & tag on GitHub

โœ… 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!

0
Subscribe to my newsletter

Read articles from YASH REGE directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

YASH REGE
YASH REGE