Skip to content
Back to Blog
DevOps

GitHub Actions for Infrastructure Automation

Build GitHub Actions workflows for infrastructure: Terraform plans on PRs, automated testing, environment deployments, and rollback strategies.

Dec 2025
13 min read

Introduction

GitHub Actions is a CI/CD platform that allows you to automate workflows directly in your GitHub repository. For infrastructure engineers, GitHub Actions can automate Terraform deployments, run Ansible playbooks, build Docker images, validate configurations, and enforce security policies — all triggered by code changes. This guide teaches you to build production-grade infrastructure pipelines with GitHub Actions.

Core Concepts

YAML
# .github/workflows/infra-deploy.yml
name: Infrastructure Deploy

# Triggers: when does this run?
on:
  push:
    branches: [main]
    paths:
      - 'terraform/**'     # Only when terraform files change
  pull_request:
    branches: [main]
    paths:
      - 'terraform/**'
  workflow_dispatch:        # Manual trigger button in UI
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options: [staging, production]

jobs:
  validate:
    name: Validate Terraform
    runs-on: ubuntu-latest  # GitHub-hosted runner

    # Set permissions for this job
    permissions:
      contents: read
      pull-requests: write  # To comment on PRs

    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.6.0"
          cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

      - name: Terraform Format Check
        run: terraform fmt -check -recursive
        working-directory: ./terraform

      - name: Terraform Init
        run: terraform init
        working-directory: ./terraform/environments/staging

      - name: Terraform Validate
        run: terraform validate
        working-directory: ./terraform/environments/staging

      - name: Terraform Plan
        id: plan
        run: terraform plan -out=tfplan -no-color
        working-directory: ./terraform/environments/staging
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `### Terraform Plan Output

${{ steps.plan.outputs.stdout }}

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

Terraform Apply Workflow

YAML
  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: validate       # Runs after validate job
    if: github.ref == 'refs/heads/main'  # Only on main branch

    environment:
      name: production    # Requires manual approval in GitHub
      url: https://app.company.com

    steps:
      - uses: actions/checkout@v4

      - 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: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Terraform Init and Apply
        run: |
          terraform init
          terraform apply -auto-approve -input=false
        working-directory: ./terraform/environments/production
        env:
          TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
          TF_VAR_api_key: ${{ secrets.EXTERNAL_API_KEY }}

Docker Build and Push

YAML
name: Build and Push Docker Image

on:
  push:
    branches: [main]
    tags: ['v*.*.*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=sha-

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha     # GitHub Actions cache
          cache-to: type=gha,mode=max

Security Scanning in CI

YAML
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Scan Terraform for security issues
      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.0
        with:
          soft_fail: false

      # Scan Docker image for CVEs
      - name: Scan Docker image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

      # Check for secrets in code
      - name: Detect secrets
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: main
          head: HEAD

Ansible Deployment Workflow

YAML
name: Deploy with Ansible

on:
  workflow_dispatch:
    inputs:
      target_hosts:
        description: 'Target hosts pattern'
        default: 'web-servers'
      playbook:
        description: 'Playbook to run'
        default: 'deploy.yml'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan -H ${{ secrets.BASTION_HOST }} >> ~/.ssh/known_hosts

      - name: Run Ansible Playbook
        uses: dawidd6/action-ansible-playbook@v2
        with:
          playbook: ${{ inputs.playbook }}
          directory: ./ansible
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          inventory: |
            [web-servers]
            web-01.company.com
            web-02.company.com
          options: |
            --extra-vars "version=${{ github.sha }}"
            --tags deploy

Reusable Workflows

YAML
# .github/workflows/reusable-terraform.yml
name: Reusable Terraform

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      tf_directory:
        required: true
        type: string
    secrets:
      AWS_ACCESS_KEY_ID:
        required: true
      AWS_SECRET_ACCESS_KEY:
        required: true

jobs:
  terraform:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: |
          terraform init
          terraform plan -out=tfplan
          terraform apply tfplan
        working-directory: ${{ inputs.tf_directory }}
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
YAML
# Calling the reusable workflow
name: Deploy All Environments

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-terraform.yml
    with:
      environment: staging
      tf_directory: terraform/environments/staging
    secrets: inherit

  deploy-production:
    uses: ./.github/workflows/reusable-terraform.yml
    needs: deploy-staging
    with:
      environment: production
      tf_directory: terraform/environments/production
    secrets: inherit

GitHub Actions transforms infrastructure management from manual, error-prone processes into auditable, reproducible pipelines. Start with simple validation workflows, add security scanning, then build full deployment pipelines with approval gates for production.