TerraformPilot

Terraform

Terraform CI/CD with GitHub Actions - Complete Pipeline

Automate Terraform with GitHub Actions. Plan on PR, apply on merge, OIDC authentication, environment protection, and drift detection workflows.

LLuca Berton1 min read

Quick Answer

#
name: Terraform
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan
      - run: terraform apply -auto-approve
        if: github.ref == 'refs/heads/main'

Production Pipeline

#
name: Terraform
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
permissions:
  id-token: write   # OIDC
  contents: read
  pull-requests: write  # PR comments
 
env:
  TF_LOG: ""
  AWS_REGION: us-east-1
 
jobs:
  plan:
    name: Terraform Plan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Configure AWS Credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-terraform
          aws-region: ${{ env.AWS_REGION }}
 
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.5
 
      - name: Terraform Init
        run: terraform init
 
      - name: Terraform Format Check
        run: terraform fmt -check
 
      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        continue-on-error: true
 
      - name: Comment PR
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            const output = `#### Terraform Plan 📖
            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`
            *Pushed by: @${{ github.actor }}*`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
 
      - name: Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1
 
      - name: Upload Plan
        uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan
 
  apply:
    name: Terraform Apply
    needs: plan
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-terraform
          aws-region: ${{ env.AWS_REGION }}
 
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.5
 
      - name: Download Plan
        uses: actions/download-artifact@v4
        with:
          name: tfplan
 
      - name: Terraform Init
        run: terraform init
 
      - name: Terraform Apply
        run: terraform apply -auto-approve tfplan

OIDC Authentication (No Access Keys)

#
# In your AWS account — create the OIDC provider and role
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["ffffffffffffffffffffffffffffffffffffffff"]
}
 
resource "aws_iam_role" "github_terraform" {
  name = "github-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 = {
        StringLike = {
          "token.actions.githubusercontent.com:sub" = "repo:myorg/myrepo:*"
        }
      }
    }]
  })
}

Drift Detection (Scheduled)

#
name: Drift Detection
on:
  schedule:
    - cron: '0 8 * * 1'  # Monday 8 AM
  workflow_dispatch:
 
jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - name: Check Drift
        run: |
          terraform plan -detailed-exitcode -no-color 2>&1 | tee plan.txt
          EXIT_CODE=${PIPESTATUS[0]}
          if [ $EXIT_CODE -eq 2 ]; then
            echo "⚠️ DRIFT DETECTED"
            # Send notification (Slack, email, etc.)
          fi
#

Conclusion

#

Use OIDC for AWS authentication (no access keys to rotate), plan on PRs with comments, apply only on main branch merges, and add GitHub Environment protection rules for production. Schedule weekly drift detection to catch manual changes.

#Terraform#GitHub Actions#CI/CD#DevOps#Infrastructure as Code

Share this article