Building Production CI/CD Pipelines with GitHub Actions and AWS
Building Production CI/CD Pipelines with GitHub Actions and AWS
Continuous Integration and Continuous Deployment (CI/CD) is essential for modern software development. This guide shows you how to build production-ready pipelines.
Why CI/CD Matters
Without CI/CD:
- Deployments take hours instead of minutes
- Manual steps lead to errors and inconsistencies
- No automated testing means bugs reach production
- Rollbacks are difficult when things go wrong
Pipeline Architecture
Our recommended architecture:
┌─────────────┐
│ Git Push │
└──────┬──────┘
│
v
┌─────────────────┐
│ Run Tests │
│ - Unit │
│ - Integration │
│ - Linting │
└──────┬──────────┘
│
v
┌─────────────────┐
│ Build Docker │
│ Image │
└──────┬──────────┘
│
v
┌─────────────────┐
│ Push to ECR │
└──────┬──────────┘
│
v
┌─────────────────┐
│ Deploy to ECS │
│ (Blue/Green) │
└──────┬──────────┘
│
v
┌─────────────────┐
│ Run Smoke Tests │
└─────────────────┘
Step 1: Setup GitHub Actions
Create .github/workflows/deploy.yml:
name: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: myapp
ECS_SERVICE: myapp-service
ECS_CLUSTER: production
CONTAINER_NAME: web
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
build-and-deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
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: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Download task definition
run: |
aws ecs describe-task-definition \
--task-definition myapp-task \
--query taskDefinition > task-definition.json
- name: Fill in new image ID in task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy to Amazon ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
- name: Run smoke tests
run: |
npm run test:smoke
Step 2: Environment Variables and Secrets
Store sensitive data in GitHub Secrets:
- Go to repository Settings > Secrets and variables > Actions
- Add secrets:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYDATABASE_URL(for app configuration)
Use environment-specific secrets:
- name: Deploy to Staging
if: github.ref == 'refs/heads/develop'
env:
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
API_KEY: ${{ secrets.STAGING_API_KEY }}
Step 3: Multi-Environment Setup
Support dev, staging, and production:
name: Multi-Environment Deploy
on:
push:
branches:
- main
- develop
- 'release/**'
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- branch: develop
environment: development
cluster: dev-cluster
- branch: main
environment: production
cluster: prod-cluster
steps:
- uses: actions/checkout@v3
- name: Deploy to ${{ matrix.environment }}
if: github.ref == 'refs/heads/${{ matrix.branch }}'
run: |
echo "Deploying to ${{ matrix.environment }}"
# Deploy logic here
Step 4: Database Migrations
Handle database migrations safely:
- name: Run database migrations
run: |
npm run migrate:up
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Verify migration
run: |
npm run migrate:status
Step 5: Rollback Strategy
Implement automatic rollback on failure:
- name: Deploy new version
id: deploy
run: |
aws ecs update-service \
--cluster production \
--service myapp \
--task-definition myapp:$NEW_VERSION
- name: Wait for deployment
id: wait
run: |
aws ecs wait services-stable \
--cluster production \
--services myapp
continue-on-error: true
- name: Rollback on failure
if: steps.wait.outcome == 'failure'
run: |
echo "Deployment failed, rolling back..."
aws ecs update-service \
--cluster production \
--service myapp \
--task-definition myapp:$PREVIOUS_VERSION
Advanced Patterns
Blue/Green Deployment
- name: Blue/Green Deploy
run: |
# Deploy to green environment
terraform apply -target=module.green_environment
# Run health checks
./scripts/health-check.sh green
# Switch traffic
terraform apply -target=module.load_balancer
# Monitor for 10 minutes
sleep 600
# Destroy blue environment
terraform destroy -target=module.blue_environment
Canary Deployment
Deploy to subset of users first:
- name: Canary Deploy (10%)
run: |
aws ecs update-service \
--cluster production \
--service myapp-canary \
--desired-count 1
sleep 300 # Monitor for 5 minutes
# Check error rates
./scripts/check-metrics.sh
- name: Full Deploy (100%)
run: |
aws ecs update-service \
--cluster production \
--service myapp \
--desired-count 10
Monitoring and Notifications
Send Slack notifications:
- name: Notify Slack on Success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ Deployment succeeded",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Deployment to production succeeded\nCommit: ${{ github.sha }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Notify Slack on Failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "❌ Deployment failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Deployment to production failed\nCommit: ${{ github.sha }}\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Performance Optimization
Caching Dependencies
- name: Cache node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
Parallel Jobs
Run tests in parallel:
test:
strategy:
matrix:
node-version: [16, 18, 20]
test-suite: [unit, integration, e2e]
runs-on: ubuntu-latest
steps:
- name: Run ${{ matrix.test-suite }} tests on Node ${{ matrix.node-version }}
run: npm run test:${{ matrix.test-suite }}
Security Best Practices
- Use OIDC instead of long-lived credentials:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- Scan Docker images for vulnerabilities:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- Sign commits and verify signatures
- Use branch protection rules
- Require code review before merging
Real-World Metrics
After implementing CI/CD for clients, we typically see:
- Deployment time: 2 hours → 10 minutes (92% reduction)
- Deployment frequency: Weekly → Multiple times per day
- Failed deployments: 15% → 2% (87% reduction)
- Mean time to recovery: 4 hours → 15 minutes
Conclusion
A well-designed CI/CD pipeline:
- ✅ Reduces deployment time and risk
- ✅ Increases deployment frequency
- ✅ Improves code quality through automation
- ✅ Enables faster feedback loops
- ✅ Reduces manual errors
Ready to automate your deployments? Download our CI/CD templates or book a consultation.
Related Articles
DevOps Best Practices for Startups: Deploy 10x Faster Without Breaking Things
A practical guide to implementing DevOps from day one. Learn how to set up CI/CD pipelines, automate deployments, and scale your infrastructure without hiring a DevOps team.
14 min readInfrastructure as Code with Terraform: A Production-Ready Guide
Learn how to manage your infrastructure with Terraform, from basic concepts to production best practices including state management and team collaboration.
12 min read