Part 5: Production Deployment and Best Practices

Introduction

Deploying to production is where GitHub Actions truly shines. I've learned this the hard way after a failed deployment at 2 AM that took down our payment service. Since then, I've refined my deployment workflows to include health checks, rollback mechanisms, and comprehensive monitoring.

In this final part, I'll share production-ready deployment strategies, security best practices, and lessons learned from running GitHub Actions in production.

Production Deployment Workflow

Complete Deployment Pipeline

name: Deploy to Production

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to deploy'
        required: true
        type: choice
        options:
          - staging
          - production
      version:
        description: 'Version to deploy (leave empty for latest)'
        required: false
        type: string

env:
  NODE_VERSION: '18'
  AWS_REGION: 'us-east-1'

jobs:
  prepare:
    name: Prepare Deployment
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      environment: ${{ steps.env.outputs.environment }}
      should-deploy: ${{ steps.check.outputs.should-deploy }}
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Determine version
        id: version
        run: |
          if [ -n "${{ inputs.version }}" ]; then
            VERSION="${{ inputs.version }}"
          elif [ "${{ github.event_name }}" == "release" ]; then
            VERSION="${{ github.event.release.tag_name }}"
          else
            VERSION=$(cat package.json | jq -r .version)
          fi
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "🏷️ Deploying version: $VERSION"
      
      - name: Determine environment
        id: env
        run: |
          if [ -n "${{ inputs.environment }}" ]; then
            ENV="${{ inputs.environment }}"
          elif [ "${{ github.event_name }}" == "release" ]; then
            ENV="production"
          else
            ENV="staging"
          fi
          echo "environment=$ENV" >> $GITHUB_OUTPUT
          echo "🌍 Target environment: $ENV"
      
      - name: Check if should deploy
        id: check
        run: |
          # Add your deployment checks here
          # Example: check if version already deployed
          echo "should-deploy=true" >> $GITHUB_OUTPUT
  
  build:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: prepare
    if: needs.prepare.outputs.should-deploy == 'true'
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v1
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ${{ env.AWS_REGION }}
      
      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: ./services/auth
          file: ./services/auth/Dockerfile
          push: true
          tags: |
            ${{ secrets.ECR_REGISTRY }}/auth-service:${{ needs.prepare.outputs.version }}
            ${{ secrets.ECR_REGISTRY }}/auth-service:latest
          cache-from: type=registry,ref=${{ secrets.ECR_REGISTRY }}/auth-service:buildcache
          cache-to: type=registry,ref=${{ secrets.ECR_REGISTRY }}/auth-service:buildcache,mode=max
          build-args: |
            NODE_ENV=production
            VERSION=${{ needs.prepare.outputs.version }}
  
  deploy:
    name: Deploy to ${{ needs.prepare.outputs.environment }}
    runs-on: ubuntu-latest
    needs: [prepare, build]
    environment:
      name: ${{ needs.prepare.outputs.environment }}
      url: ${{ steps.deploy.outputs.url }}
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
      
      - name: Deploy to ECS
        id: deploy
        run: |
          # Update ECS task definition
          NEW_TASK_DEF=$(aws ecs describe-task-definition \
            --task-definition auth-service-${{ needs.prepare.outputs.environment }} \
            --query 'taskDefinition' | \
            jq --arg IMAGE "${{ secrets.ECR_REGISTRY }}/auth-service:${{ needs.prepare.outputs.version }}" \
            '.containerDefinitions[0].image = $IMAGE | 
             del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)')
          
          # Register new task definition
          NEW_TASK_ARN=$(echo $NEW_TASK_DEF | \
            aws ecs register-task-definition --cli-input-json file:///dev/stdin | \
            jq -r '.taskDefinition.taskDefinitionArn')
          
          # Update service
          aws ecs update-service \
            --cluster my-cluster-${{ needs.prepare.outputs.environment }} \
            --service auth-service \
            --task-definition $NEW_TASK_ARN \
            --force-new-deployment
          
          # Wait for deployment to complete
          aws ecs wait services-stable \
            --cluster my-cluster-${{ needs.prepare.outputs.environment }} \
            --services auth-service
          
          # Get service URL
          URL="https://auth.${{ needs.prepare.outputs.environment }}.example.com"
          echo "url=$URL" >> $GITHUB_OUTPUT
          echo "βœ… Deployed to: $URL"
      
      - name: Run database migrations
        run: |
          # Run migrations in ECS
          aws ecs run-task \
            --cluster my-cluster-${{ needs.prepare.outputs.environment }} \
            --task-definition auth-migrations \
            --launch-type FARGATE \
            --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.SUBNET_IDS }}],securityGroups=[${{ secrets.SECURITY_GROUP_ID }}],assignPublicIp=ENABLED}"
          
          echo "βœ… Database migrations completed"
      
      - name: Health check
        run: |
          echo "πŸ₯ Running health check..."
          
          for i in {1..30}; do
            HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${{ steps.deploy.outputs.url }}/health)
            
            if [ $HTTP_CODE -eq 200 ]; then
              echo "βœ… Health check passed!"
              exit 0
            fi
            
            echo "Attempt $i/30: HTTP $HTTP_CODE - Waiting 10s..."
            sleep 10
          done
          
          echo "❌ Health check failed after 5 minutes"
          exit 1
      
      - name: Smoke tests
        run: |
          echo "πŸ§ͺ Running smoke tests..."
          
          # Test critical endpoints
          curl -f ${{ steps.deploy.outputs.url }}/api/v1/auth/health || exit 1
          curl -f ${{ steps.deploy.outputs.url }}/api/v1/auth/version || exit 1
          
          echo "βœ… Smoke tests passed!"
  
  notify:
    name: Notify Deployment Status
    runs-on: ubuntu-latest
    needs: [prepare, deploy]
    if: always()
    
    steps:
      - name: Notify Slack
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ needs.deploy.result }}
          text: |
            Deployment to ${{ needs.prepare.outputs.environment }}
            Version: ${{ needs.prepare.outputs.version }}
            Status: ${{ needs.deploy.result }}
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
      
      - name: Create deployment record
        if: needs.deploy.result == 'success'
        uses: actions/github-script@v6
        with:
          script: |
            await github.rest.repos.createDeployment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: context.sha,
              environment: '${{ needs.prepare.outputs.environment }}',
              description: 'Deployed version ${{ needs.prepare.outputs.version }}',
              auto_merge: false,
              required_contexts: []
            });

Blue-Green Deployment

Canary Deployment

Rollback Strategy

Security Best Practices

Secret Scanning

OIDC Authentication (No Long-Lived Credentials)

Monitoring and Observability

Best Practices Checklist

Workflow Organization

Environment Protection Rules

In GitHub repository settings:

  1. Go to Settings β†’ Environments

  2. Create production environment

  3. Add protection rules:

    • βœ… Required reviewers (at least 1-2)

    • βœ… Wait timer (5-10 minutes)

    • βœ… Deployment branches (only main)

    • βœ… Environment secrets

Comprehensive Testing Strategy

Key Takeaways

  1. Always have a rollback plan - automate it!

  2. Use environment protection rules for production

  3. Implement blue-green or canary deployments for zero-downtime

  4. Monitor deployments with health checks and metrics

  5. Scan for secrets and vulnerabilities before deploying

  6. Use OIDC instead of long-lived credentials when possible

  7. Add deployment markers to your observability tools

  8. Test in production-like environments first

  9. Notify teams of deployment status

  10. Document your deployment process in the workflow itself

Final Thoughts

GitHub Actions has transformed how I build and deploy software. What started as simple CI checks has evolved into a comprehensive deployment pipeline that handles everything from code quality to production rollouts.

The key is to start simple and iterate. Don't try to build the perfect pipeline on day one. Start with basic CI, add tests, then gradually introduce deployment automation as you gain confidence.

Remember:

  • Workflows are code - version control them, review them, test them

  • Security first - never commit secrets, use environment protection

  • Monitor everything - you can't improve what you don't measure

  • Fail fast - catch issues early in the pipeline

  • Keep it maintainable - use reusable workflows and composite actions

Now go build amazing CI/CD pipelines! πŸš€

Last updated