Containerized MERN Application Deployment on AWS ECS

Designed and deployed a production-ready MERN application (React frontend + Express backend + MongoDB) as Docker containers on AWS ECS, with images hosted in ECR. Infrastructure is provisioned via Terraform with CI/CD implemented using GitHub Actions with a private self-hosted runner on AWS (OIDC-based), integrated with SonarQube for code quality checks.

Terraform AWS ECS ECR ALB GitHub Actions OIDC SonarQube Docker MERN

Project Overview

This project demonstrates an end-to-end containerized deployment of a MERN stack application on AWS. The frontend (React) and backend (Express + MongoDB) are packaged as separate Docker images and pushed to Amazon ECR. Each image runs as an ECS task with separate task definitions for client and server components. An Application Load Balancer (ALB) routes traffic and exposes the services to the internet.

The infrastructure is completely managed using Terraform modules for reproducibility and scalability. CI/CD is implemented using GitHub Actions with a private self-hosted runner on EC2 that authenticates to AWS using OpenID Connect (OIDC) for secure, short-lived credentials. SonarQube is integrated in the pipeline for comprehensive static code analysis and quality gates.

Technical Architecture

Technology Stack

  • Frontend: React (Create React App / Vite)
  • Backend: Node.js + Express.js
  • Database: MongoDB (containerized)
  • Container Runtime: Docker
  • Container Registry: Amazon ECR
  • Orchestration: AWS ECS (Fargate)
  • Load Balancer: Application Load Balancer
  • DNS: Route53
  • Infrastructure: Terraform
  • CI/CD: GitHub Actions
  • Code Quality: SonarQube

Project Specifications

  • Duration: 4 weeks
  • Role: DevOps + Full-stack Engineer
  • Services: 2 ECS Services (Client & Server)
  • Repositories: 2 ECR Repositories
  • Runner: Self-hosted on EC2
  • Security: OIDC-based authentication
  • Monitoring: CloudWatch Logs & Metrics
  • Deployment: Blue-Green with ALB

Containerization

Multi-stage Docker builds for optimized images

AWS ECS

Serverless container orchestration with Fargate

CI/CD Pipeline

Automated builds, tests, and deployments

Security

OIDC authentication and IAM best practices

Project Metrics & Impact

2
ECS Services
2
ECR Repositories
1
Self-hosted Runner
100%
Infrastructure as Code
5min
Average Deploy Time
99.9%
Service Availability

Challenges & Solutions

Challenge: Secure CI/CD Credentials and Least-Privilege Access

Solution: Implemented GitHub OIDC to mint short-lived AWS credentials for the self-hosted runner, applying narrowly scoped IAM roles for both the runner and ECS tasks to follow the principle of least privilege.

Challenge: Blue/Green Deployments with ECS

Solution: Leveraged ECS service deployment configurations and ALB listener rules to perform rolling deployments with gradual traffic shifting between task sets, ensuring zero-downtime deployments.

Challenge: Integrating SonarQube in CI Pipeline

Solution: Added SonarQube scan steps in GitHub Actions that send analysis results to the SonarQube server running in the AWS environment, with fail-fast mechanisms on critical code quality issues.

Challenge: ALB Routing for Multiple Services

Solution: Configured ALB listeners with path-based routing rules and separate target groups to efficiently route traffic between client and server containers based on request paths.

Development Timeline

Week 1

Infrastructure Setup & Terraform Modules

Created Terraform modules for VPC, ECS cluster, ECR repositories, and ALB. Set up basic infrastructure components and networking.

Week 2

Application Containerization & ECR Integration

Containerized React frontend and Express backend, optimized Docker images with multi-stage builds, and integrated with ECR for image storage.

Week 3

ECS Services & ALB Configuration

Configured ECS task definitions for both services, set up ALB listeners and target groups, implemented health checks and auto-scaling policies.

Week 4

CI/CD Pipeline & SonarQube Integration

Built GitHub Actions pipeline with self-hosted runner, integrated OIDC authentication, added SonarQube quality gates, and performed end-to-end testing.

Code & Pipeline Highlights

Terraform ECS Task Definition

{
    "compatibilities": [
        "EC2",
        "FARGATE"
    ],
    "containerDefinitions": [
        {
            "cpu": 0,
            "environment": [],
            "environmentFiles": [],
            "essential": true,
            "image": "471112857795.dkr.ecr.us-east-1.amazonaws.com/saeedafzal2030/thinkify_client:latest",
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/thinkify_client",
                    "awslogs-create-group": "true",
                    "awslogs-region": "us-east-1",
                    "awslogs-stream-prefix": "ecs"
                },
                "secretOptions": []
            },
            "mountPoints": [],
            "name": "thikify_client",
            "portMappings": [
                {
                    "appProtocol": "http",
                    "containerPort": 80,
                    "hostPort": 80,
                    "name": "http",
                    "protocol": "tcp"
                }
            ],
            "systemControls": [],
            "ulimits": [],
            "volumesFrom": []
        }
    ],
    "cpu": "1024",
    "enableFaultInjection": false,
    "executionRoleArn": "arn:aws:iam::471112857795:role/ecsTaskExecutionRole",
    "family": "thinkify_client",
    "memory": "2048",
    "networkMode": "awsvpc",
    "placementConstraints": [],
    "registeredAt": "2025-09-07T16:05:00.332Z",
    "registeredBy": "arn:aws:iam::471112857795:user/TF",
    "requiresAttributes": [
        {
            "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
        },
        {
            "name": "ecs.capability.execution-role-awslogs"
        },
        {
            "name": "com.amazonaws.ecs.capability.ecr-auth"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
        },
        {
            "name": "com.amazonaws.ecs.capability.task-iam-role"
        },
        {
            "name": "ecs.capability.execution-role-ecr-pull"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
        },
        {
            "name": "ecs.capability.task-eni"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "revision": 1,
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "status": "ACTIVE",
    "taskDefinitionArn": "arn:aws:ecs:us-east-1:471112857795:task-definition/thinkify_client:5",
    "taskRoleArn": "arn:aws:iam::471112857795:role/ecsTaskExecutionRole",
    "volumes": [],
    "tags": []
}

GitHub Actions CI/CD Pipeline

name: Intigration workflow

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

jobs:
  frontend:
    runs-on: self-hosted
    environment: Dev
    permissions:
      contents: read
      id-token: write  # For OIDC authentication if needed in future

    
    steps:
      # 🔹 Clean workspace before checkout
      - name: Clean workspace
        run: |
          echo "Cleaning workspace..."
          echo $GITHUB_WORKSPACE
          sudo rm -rf "$GITHUB_WORKSPACE"/* || true
          sudo rm -rf "$GITHUB_WORKSPACE"/.[!.]* || true
  
      # 🔹 Checkout repo with clean option
      - name: Checkout code
        uses: actions/checkout@v5
        with:
          clean: true
  
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
  
      - name: Verify environment & list client folder
        run: |
          node --version
          cd client
          ls -ltr
  
      - name: Install dependencies and audit
        working-directory: ./client
        run: |
          rm -rf node_modules package-lock.json
          npm install || true
          npm audit fix || true
  
      - name: Run tests
        working-directory: ./client
        run: npm test
  
      - uses: SonarSource/sonarqube-scan-action@v5.3.1
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
        with:
          projectBaseDir: './client'
          args: >
            -Dsonar.projectKey=thinkify_cicd
            -Dsonar.verbose=true
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4.3.1
        with:
          aws-region: us-east-1
          role-to-assume: arn:aws:iam::471112857795:role/GithubAccessToECS
      
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
      
      
      - name: Build and tag docker images
        working-directory: ./client
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: saeedafzal2030/thinkify_client
          IMAGE_TAG: ${{ github.sha }}

        run: |
          docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
          docker tag $REGISTRY/$REPOSITORY:$IMAGE_TAG $REGISTRY/$REPOSITORY:latest

          docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
          docker push $REGISTRY/$REPOSITORY:latest
      
      - name: Run Trivy vulnerability scanner on docker image
        uses: aquasecurity/trivy-action@0.28.0
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: saeedafzal2030/thinkify_client
          IMAGE_TAG: ${{ github.sha }}
        with:
          scan-type: 'image'
          image-ref: "${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}"
          format: 'table'
          output: 'thinkify-report-client-${{ github.sha }}'
          exit-code: '0'
          severity: 'MEDIUM,HIGH,CRITICAL'
      
      - name: Upload Trivy Scan Reports
        uses: actions/upload-artifact@v4
        with:
          name: 'thinkify-report-client-${{ github.sha }}'
          path: 'thinkify-report-client-${{ github.sha }}'
          if-no-files-found: 'warn'
          retention-days: 7
      
      - name: Docker cleanup
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: saeedafzal2030/thinkify_client
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker rmi $REGISTRY/$REPOSITORY:$IMAGE_TAG
          docker rmi $REGISTRY/$REPOSITORY:latest

    outputs:
      registry: ${{ steps.login-ecr.outputs.registry }}


  backend:
    runs-on: self-hosted
    environment: Dev

    permissions:
      contents: read
      id-token: write  # For OIDC authentication if needed in future

    steps:
      # 🔹 Clean workspace before checkout
      - name: Clean workspace
        run: |
          echo "Cleaning workspace..."
          echo $GITHUB_WORKSPACE
          sudo rm -rf "$GITHUB_WORKSPACE"/* || true
          sudo rm -rf "$GITHUB_WORKSPACE"/.[!.]* || true
        
      - name: Checkout code
        uses: actions/checkout@v5
        with:
            clean: 'true'

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: Install dependencies and audit
        working-directory: ./server
        run: |  
          node --version
          npm install
          npm audit fix || true
      
      - name: Run tests
        working-directory: ./server
        run: npm test
      
      # - uses: SonarSource/sonarqube-scan-action@v5.3.1
      #   env:
      #     SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
      #     SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
      #   with:
      #     projectBaseDir: './server'


      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4.3.1
        with:
          aws-region: us-east-1
          role-to-assume: arn:aws:iam::471112857795:role/GithubAccessToECS

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
     
      - name: Build, tag, and push docker image to Amazon ECR
        working-directory: ./server
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: saeedafzal2030/thinkify_server
          IMAGE_TAG: ${{ github.sha }}

        run: |
          docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG -t $REGISTRY/$REPOSITORY:latest .
          docker tag $REGISTRY/$REPOSITORY:$IMAGE_TAG $REGISTRY/$REPOSITORY:latest

          docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
          docker push $REGISTRY/$REPOSITORY:latest

      - name: Run Trivy vulnerability scanner on docker image
        uses: aquasecurity/trivy-action@0.28.0
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: saeedafzal2030/thinkify_server
          IMAGE_TAG: ${{ github.sha }}
        with:
          scan-type: 'image'
          image-ref: "${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}"
          format: 'table'
          output: 'thinkify-report-server-${{ github.sha }}'
          exit-code: '0'
          severity: 'MEDIUM,HIGH,CRITICAL'
      
      - name: Upload Trivy Scan Reports
        uses: actions/upload-artifact@v4
        with:
          name: 'thinkify-report-server-${{ github.sha }}'
          path: 'thinkify-report-server-${{ github.sha }}'
          if-no-files-found: 'warn'
          retention-days: 7
      
      - name: Docker cleanup
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: saeedafzal2030/thinkify_server
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker rmi $REGISTRY/$REPOSITORY:$IMAGE_TAG
          docker rmi $REGISTRY/$REPOSITORY:latest


    outputs:
      registry: ${{ steps.login-ecr.outputs.registry }}