← Course Index

Build & Deploy Pipeline

~30 min · CI/CD

Ref
Primary Source
DevOps Directive — GitHub Actions Full Course (YouTube)

Free, comprehensive video course from beginner to advanced CI/CD pipelines. Best free Actions resource available. Read →

Pipeline: Test → Build → Push → Deploy

A complete production pipeline runs automatically on every push to main.

Deploy to S3 + CloudFront (Static Sites)

# .github/workflows/deploy-static.yml
name: Deploy Static Site
on:
  push:
    branches: [main]

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 && npm test

  deploy:
    needs: test           # Only if tests pass
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm run build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
          aws-region: us-east-1

      - name: Sync to S3
        run: |
          aws s3 sync ./dist s3://BUCKET_NAME             --delete             --cache-control "public,max-age=31536000,immutable"
          aws s3 cp dist/index.html s3://BUCKET_NAME/index.html             --cache-control "no-cache"

      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation             --distribution-id DISTRIBUTION_ID             --paths "/*"

Deploy Docker Container to EC2

# .github/workflows/deploy-docker.yml
name: Build and Deploy Docker
on:
  push:
    branches: [main]

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

    steps:
      - uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: GH_ACTOR_VAR
          password: GH_TOKEN_VAR

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/OWNER/REPO:GIT_SHA_VAR
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: EC2_HOST_VAR
          username: ubuntu
          key: EC2_KEY_VAR
          script: |
            docker pull ghcr.io/OWNER/REPO:GIT_SHA_VAR
            docker compose up -d --no-deps api
            docker system prune -f

OIDC — No Long-Lived AWS Keys Required

Instead of storing AWS access keys as secrets, use OIDC federation. GitHub Actions gets temporary AWS credentials automatically:

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
      aws-region: us-east-1
      # No access keys needed — GitHub JWT exchanged for 1-hour AWS credentials
💡 Best practice

OIDC issues short-lived credentials (1 hour). Nothing to rotate, nothing to leak. Configure once, forget about key management forever. Use OIDC whenever possible.

Check Your Understanding

1. Your deploy job should only run if the test job passes. Which keyword enforces this?
2. Why is OIDC better than storing AWS access keys as GitHub secrets?
3. How do you cache Docker build layers between GitHub Actions runs?