Terraform in CI CD Pipelines with GitHub Actions

Running Terraform manually from a developer's laptop is acceptable when learning. In production, every infrastructure change should go through an automated, auditable, repeatable pipeline. This topic shows you how to build a complete Terraform CI/CD workflow using GitHub Actions — the most widely used platform for this purpose.

Why Automate Terraform

Manual Terraform runs have several risks:

  • A developer runs apply without a team review of the plan
  • Different machines use different Terraform versions, producing inconsistent results
  • Credentials exist on developer laptops — a security risk
  • There is no audit trail of who changed what and when

A CI/CD pipeline eliminates all of these problems: every change is reviewed, every run uses the same version, credentials live only in the pipeline, and every action is logged.

The Recommended Terraform Pipeline Flow

Diagram: Pull Request and Merge Workflow

Developer pushes branch
        |
        v
Pull Request opened
        |
        v
CI triggers: terraform fmt -check
             terraform validate
             terraform plan
        |
        v
Plan output posted as PR comment
        |
        v
Teammate reviews the plan output in the PR
        |
        v
PR approved and merged to main
        |
        v
CD triggers: terraform apply -auto-approve
             (applies the exact planned changes)
        |
        v
Notification sent (Slack, email, etc.)
Apply output stored in run logs

Setting Up GitHub Actions for Terraform

Repository Structure

.
├── .github/
│   └── workflows/
│       └── terraform.yml
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

GitHub Actions Workflow File

name: Terraform CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write

env:
  TF_VERSION: "1.9.0"

jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest

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

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id:     ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region:            us-east-1

      - name: Terraform Init
        run: terraform init

      - name: Terraform Format Check
        run: terraform fmt -check

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan

      - name: Post Plan to PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const output = `
            #### Terraform Plan 📋
            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`
            `;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });

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

Storing Credentials Securely in GitHub

Never put AWS credentials in the workflow file or in committed code. Store them in GitHub's encrypted secrets:

  1. Go to your repository on GitHub
  2. Click Settings → Secrets and variables → Actions
  3. Click New repository secret
  4. Add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY

GitHub injects these as environment variables during the pipeline run. They never appear in logs.

Using OIDC Instead of Long-Lived Credentials

Long-lived AWS access keys are a security risk. A better approach is OpenID Connect (OIDC) — GitHub Actions proves its identity to AWS directly, and AWS grants temporary credentials for that specific run. No long-lived keys to rotate or leak.

- name: Configure AWS Credentials via OIDC
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
    aws-region:     us-east-1

This requires an IAM role with a trust policy that allows GitHub Actions to assume it via OIDC — a one-time setup that eliminates stored credentials entirely.

Pinning the Terraform Version

Always pin the Terraform version in both your pipeline and your configuration to ensure every run uses the same binary.

# In the workflow:
terraform_version: "1.9.0"

# In terraform.tf:
terraform {
  required_version = "= 1.9.0"
}

Key Points

  • A Terraform CI/CD pipeline runs format checks and validation on pull requests, posts the plan as a PR comment, and applies automatically on merge to main.
  • Store cloud credentials in GitHub Secrets — never in code or workflow files.
  • Use OIDC-based credential federation to eliminate long-lived AWS access keys from your pipelines entirely.
  • Pin Terraform version in both the workflow and the required_version constraint to guarantee consistent behaviour.
  • Save the plan file with -out=tfplan and apply it with terraform apply tfplan to ensure the applied changes match exactly what was reviewed.

Leave a Comment