Apr 1, 2025
How to Set Up AWS ECS CI/CD Deployment for a Node.js App
A comprehensive guide to building a production-style deployment pipeline using Docker, GitHub Actions, ECR, ECS, ALB, and CloudWatch.
Automating your application deployment pipeline ensures that every commit to production is tested, packaged, and deployed safely without manual intervention. In this guide, we will step through how to set up an end-to-end CI/CD pipeline for a containerized Node.js application deploying to Amazon ECS (Elastic Container Service) behind an Application Load Balancer.
The Goal: Moving from Manual to Automated Deployments
Manual deployments (e.g., SSH-ing into a server, pulling code, and restarting a PM2 process) are error-prone and lead to:
- Lack of deployment traceability.
- Risk of security issues by storing long-lived credentials on local machines or GitHub secrets.
- Downtime during updates and difficulty rolling back changes if a bug slips through.
We will automate this flow so that a simple git push triggers a secure, rolling-replacement deployment with zero downtime.
Pipeline Architecture
Here is the flow of our automated CI/CD pipeline:
GitHub Push
│
▼
GitHub Actions CI
├── Build Docker image
├── Tag with commit SHA
└── Push to Amazon ECR
│
▼
ECS Service Update (rolling deploy)
│
▼
Application Load Balancer
│
▼
ECS Task (Node.js container)
│
▼
CloudWatch Logs
Tech Stack Overview
- Runtime: Node.js (Express API)
- Containerization: Docker
- CI/CD: GitHub Actions
- Registry: Amazon ECR (Elastic Container Registry)
- Compute: Amazon ECS (Fargate launch type)
- Load Balancing: Application Load Balancer (ALB)
- Observability: CloudWatch Logs & Metrics
- Secrets: AWS Secrets Manager
- Authentication: OIDC (OpenID Connect) for secure GitHub-to-AWS connection
Setup Steps
1. Dockerize the Node.js App
First, we write a multi-stage Dockerfile to compile our Node.js app. Using multi-stage builds allows us to package only production dependencies, keeping our final runner image lean and secure:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Runner stage
FROM node:18-alpine AS runner
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
2. Configure AWS ECS & ALB
- ECR Repository: Establish a private repository to host our Docker images.
- ALB: Place an ALB in public subnets with a listener targeting our ECS service port (
3000). - ECS Service: Run tasks on Fargate. Configure the service with rolling updates (e.g., minimum running tasks
100%, maximum200%during updates).
3. Establish OIDC Federation
Instead of saving static AWS Access Keys in GitHub, configure an IAM Role with a trust policy allowing GitHub Actions to assume it via OpenID Connect (OIDC). This eliminates long-lived static keys.
4. Create the GitHub Actions Workflow
Create a .github/workflows/deploy.yml file to handle the build and deployment process on push to the main branch:
name: Deploy to Amazon ECS
on:
push:
branches: [ main ]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecs-deploy
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build & Tag Image
run: |
docker build -t ${{ steps.login-ecr.outputs.registry }}/node-api:${{ github.sha }} .
docker tag ${{ steps.login-ecr.outputs.registry }}/node-api:${{ github.sha }} ${{ steps.login-ecr.outputs.registry }}/node-api:latest
- name: Push to ECR
run: |
docker push ${{ steps.login-ecr.outputs.registry }}/node-api:${{ github.sha }}
docker push ${{ steps.login-ecr.outputs.registry }}/node-api:latest
- name: Update ECS Service
run: |
aws ecs update-service --cluster api-cluster --service node-api-service --force-new-deployment
Monitoring and Logging
To verify performance and troubleshoot, the container logs are piped directly to CloudWatch Logs using the awslogs driver. A metric filter checks for ERROR strings, triggering notifications if log issues spike.
Rollback Handling
ECS Fargate handles rolling updates natively:
- New container tasks start up.
- The ALB sends health check requests to the new tasks.
- If they pass, traffic shifts from old tasks to new ones.
- Old tasks drain and shutdown.
- If the new tasks fail their health checks, ECS halts the deployment, preventing downtime, and retains the older version.
To manually roll back, re-trigger the GitHub Action against a known-good commit SHA to redeploy that image.
Key Takeaways
- No Static Keys: Using OIDC is an industry standard for CI/CD security.
- Health Checks: ALB health checks are the heart of safe rolling deployments; configure target group thresholds carefully to catch issues before older containers shutdown.
- Immutable Images: Tagging Docker images with the Git commit SHA provides clear correlation between code changes and production container builds.