Skip to main content
Terraform intermediate Lesson 11 of 11

Terraform CI/CD with GitHub Actions (plan/apply)

Learn a safe CI/CD workflow for Terraform using GitHub Actions: formatting, validation, plan, and gated apply.

CI/CD for Terraform is about two things:

  1. making Terraform runs repeatable
  2. reducing the risk of applying unintended changes

A common safe pattern is:

  • CI: fmtinitvalidateplan (for every PR)
  • Apply: run apply only after review/approval (often on main)

Learning outcomes

After this tutorial you can:

  • understand a Terraform GitHub Actions workflow
  • generate and persist an execution plan artifact
  • gate apply on branch/approval

1) Terraform steps in CI

Typical CI steps:

  • terraform fmt -check
  • terraform init
  • terraform validate
  • terraform plan

Example commands:

terraform fmt -check -recursive
terraform init
terraform validate
terraform plan -out=tfplan

Create .github/workflows/terraform.yml

Adjust backend/provider configuration to your environment.

name: Terraform

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

permissions:
  contents: read

jobs:
  terraform-plan:
    name: Terraform Plan (PR)
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest

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

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0

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

      - name: Terraform Init
        run: terraform init

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        run: terraform plan -out=tfplan

      - name: Upload plan artifact
        uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan

  terraform-apply:
    name: Terraform Apply (main)
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    needs: []

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

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0

      - name: Terraform Init
        run: terraform init

      - name: Download plan artifact (optional)
        uses: actions/download-artifact@v4
        with:
          name: tfplan
          path: .

      - name: Terraform Apply
        run: terraform apply -auto-approve tfplan

Notes on the above workflow

  • The plan job runs on PRs.
  • The apply job runs on main.
  • For the strongest safety, you should ensure the tfplan used for apply matches the code that triggered the apply.

Many teams implement apply using:

  • environments with required reviewers in GitHub
  • or by generating plan and applying the plan within the same run

3) Secrets and credentials

You must authenticate Terraform to your cloud provider. Common patterns:

  • AWS: use OIDC or long-lived IAM credentials in GitHub Secrets
  • Azure: service principal credentials
  • GCP: service account key (less preferred) or workload identity federation

At a minimum, store provider credentials in GitHub Secrets and reference them as environment variables.

Example (AWS - conceptual):

env:
  AWS_REGION: us-east-1
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

4) Plan output for human review (optional)

Instead of only saving tfplan, you can also print a human-readable plan:

terraform plan -no-color

Some teams publish it as a PR comment.

5) Cleanup / destroy

Avoid running destroy in CI unless explicitly required. When needed, run manually with careful plan review:

terraform destroy

Frequently Asked Questions

Why run `plan` in CI instead of `apply`?
`plan` is a safer change preview. It lets you review intended changes and prevents accidental production changes without approval.