Table of Contents

Introduction

Automating infrastructure deployments is a critical practice for modern DevOps teams. By integrating Terraform with GitHub Actions, you can create powerful CI/CD pipelines that automatically plan, validate, and apply infrastructure changes whenever code is pushed to your repository. This approach eliminates manual deployment steps, reduces human error, and ensures consistent infrastructure across environments.

In this comprehensive guide, we’ll walk through setting up a production-ready GitHub Actions workflow for Terraform, covering everything from basic configuration to advanced patterns like environment-specific deployments and pull request previews.

Prerequisites

Before getting started, ensure you have the following:

  • A GitHub repository containing your Terraform configuration
  • An AWS account (or another cloud provider) with appropriate permissions
  • Terraform state stored remotely (e.g., in an S3 bucket)
  • Basic familiarity with GitHub Actions and Terraform

Setting Up AWS Credentials

The first step is to securely store your cloud provider credentials as GitHub Secrets. Navigate to your repository’s Settings → Secrets and variables → Actions and add the following secrets:

  • AWS_ACCESS_KEY_ID — Your AWS access key
  • AWS_SECRET_ACCESS_KEY — Your AWS secret key
  • AWS_REGION — Your preferred AWS region (e.g., us-east-1)

For production environments, consider using OpenID Connect (OIDC) instead of long-lived credentials. This approach is more secure and eliminates the need to rotate access keys.

OIDC Configuration for AWS

# oidc.tf — Create an OIDC provider for GitHub Actions
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

resource "aws_iam_role" "github_actions" {
  name = "github-actions-terraform"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:your-org/your-repo:*"
          }
        }
      }
    ]
  })
}

Basic GitHub Actions Workflow

Create a workflow file at .github/workflows/terraform.yml:

name: Terraform CI/CD

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

permissions:
  contents: read
  pull-requests: write
  id-token: write

env:
  TF_VERSION: "1.7.0"
  AWS_REGION: "us-east-1"

jobs:
  terraform:
    name: Terraform Plan & Apply
    runs-on: ubuntu-latest

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

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::role/github-actions-terraform
          aws-region: ${{ env.AWS_REGION }}

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

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

      - name: Terraform Init
        run: terraform init

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        continue-on-error: true

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

Adding Pull Request Comments

One of the most valuable features is posting the Terraform plan output as a comment on pull requests. This allows team members to review infrastructure changes before they’re applied:

      - name: Comment PR with Plan
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const plan = `${{ steps.plan.outputs.stdout }}`;
            const truncated = plan.length > 60000
              ? plan.substring(0, 60000) + '\n\n... (truncated)'
              : plan;

            const body = `## Terraform Plan Output
            \`\`\`hcl
            ${truncated}
            \`\`\`
            *Plan: ${{ steps.plan.outcome }}*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body
            });            

Multi-Environment Deployments

For organizations managing multiple environments (development, staging, production), you can use a matrix strategy or separate workflows:

jobs:
  terraform:
    name: Deploy to ${{ matrix.environment }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging, prod]
      max-parallel: 1

    environment: ${{ matrix.environment }}

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

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

      - name: Terraform Init
        run: |
          terraform init \
            -backend-config="key=${{ matrix.environment }}/terraform.tfstate"          

      - name: Terraform Plan
        run: |
          terraform plan \
            -var-file="environments/${{ matrix.environment }}.tfvars" \
            -out=tfplan          

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

Adding Security Scanning

Integrate security scanning tools to catch misconfigurations before they reach production:

      - name: Run tfsec
        uses: aquasecurity/[email protected]
        with:
          soft_fail: true

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          framework: terraform
          soft_fail: true

State Locking and Backend Configuration

Ensure your Terraform backend is properly configured for CI/CD:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "infrastructure/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Best Practices

1. Never Store Secrets in Code

Always use GitHub Secrets or a secrets manager like AWS Secrets Manager or HashiCorp Vault. Never commit credentials, API keys, or sensitive values to your repository.

2. Use Remote State with Locking

Always use a remote backend with state locking enabled. This prevents concurrent modifications and ensures state consistency when multiple pipeline runs occur.

3. Require Plan Approval for Production

For production environments, use GitHub’s environment protection rules to require manual approval before terraform apply runs:

    environment:
      name: production
      url: https://console.aws.amazon.com

4. Pin Provider and Terraform Versions

Always pin your Terraform and provider versions to avoid unexpected changes:

terraform {
  required_version = "~> 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.30"
    }
  }
}

5. Use Terraform Workspaces Carefully

While workspaces can be useful, prefer separate state files per environment for better isolation and security in CI/CD pipelines.

Troubleshooting Common Issues

Pipeline Timeout

Terraform operations on large infrastructures can take a long time. Increase the job timeout:

jobs:
  terraform:
    timeout-minutes: 60

State Lock Conflicts

If a pipeline fails mid-apply, the state might remain locked. Use terraform force-unlock <LOCK_ID> carefully, or configure automatic lock expiration on your DynamoDB table.

Plan Output Too Large

GitHub has a 65,536 character limit for PR comments. Truncate the plan output or upload it as an artifact instead.

Conclusion

Integrating Terraform with GitHub Actions creates a robust, automated infrastructure deployment pipeline that enforces code review, security scanning, and consistent deployments across environments. By following the patterns and best practices outlined in this guide, you can build a CI/CD workflow that scales with your infrastructure needs while maintaining security and reliability.

Start with the basic workflow and gradually add features like multi-environment support, security scanning, and PR comments as your team’s needs evolve. The investment in automation pays dividends in reduced deployment errors and faster iteration cycles.