Back to articles
DevOps10 min read

Building CI/CD Pipelines with GitHub Actions

A complete guide to building production-grade deployment pipelines with automated testing, security scanning, and multi-stage deployments.

2024-09-10

Building CI/CD Pipelines with GitHub Actions

GitHub Actions has become my go-to CI/CD platform for most projects. It's deeply integrated with GitHub, has an extensive marketplace of actions, and the YAML-based workflow syntax is intuitive once you understand the core concepts.

A Production-Grade Workflow

Here's a workflow structure I use for deploying containerized applications to AKS:

name: Deploy to Production

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

env:
  REGISTRY: myregistry.azurecr.io
  IMAGE_NAME: web-api

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

  build-and-push:
    needs: [test, security-scan]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - run: az acr login --name myregistry
      - name: Build and push
        run: |
          docker build -t $REGISTRY/$IMAGE_NAME:${{ github.sha }} .
          docker push $REGISTRY/$IMAGE_NAME:${{ github.sha }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - uses: azure/aks-set-context@v4
        with:
          resource-group: rg-prod
          cluster-name: aks-prod
      - run: |
          kubectl set image deployment/web-api \
            api=$REGISTRY/$IMAGE_NAME:${{ github.sha }}
          kubectl rollout status deployment/web-api --timeout=300s

Key Design Decisions

Parallel jobs for speed: The test and security-scan jobs run in parallel since they don't depend on each other. The build-and-push job waits for both to pass before proceeding.

Environment protection rules: The deploy job targets the production environment, which can be configured in GitHub with required reviewers, wait timers, and branch restrictions.

Image tagging with SHA: Using the Git SHA as the image tag creates an immutable, traceable link between the deployed container and the exact code commit.

Secrets Management

Never hardcode credentials. Use GitHub's encrypted secrets and, for Azure, prefer OIDC-based authentication over service principal secrets:

permissions:
  id-token: write
  contents: read

steps:
  - uses: azure/login@v2
    with:
      client-id: ${{ secrets.AZURE_CLIENT_ID }}
      tenant-id: ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

OIDC eliminates the need to rotate secrets and reduces the blast radius of a compromised workflow.

Reusable Workflows

For organizations with multiple repositories, extract common patterns into reusable workflows:

# .github/workflows/reusable-deploy.yml
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      AZURE_CREDENTIALS:
        required: true

This keeps your CI/CD DRY and ensures consistent deployment practices across all projects.

Monitoring Deployments

After deploy, I always add a notification step — whether it's a Slack message, a Teams webhook, or an annotation in Grafana. Knowing exactly when a deployment happened is invaluable for correlating metrics changes with code changes.

A well-designed CI/CD pipeline is the backbone of reliable software delivery. Invest time in getting it right, and it will pay dividends every single day.

Share this article

Need help with your infrastructure?

Let's discuss your project and find the best solution together.

Get in touch