CalcuOnline
HomeTech Tutorials / ReviewsDeveloper ToolsSetting Up an Automated CI/CD Deployment Pipeline via GitHub Actions to AWS ECS
Setting Up an Automated CI/CD Deployment Pipeline via GitHub Actions to AWS ECS

Setting Up an Automated CI/CD Deployment Pipeline via GitHub Actions to AWS ECS

Developer Tools 5.0 Updated 24 August 2026

Design choices that matter here

This pipeline authenticates to AWS using GitHub's OIDC provider instead of long-lived IAM access keys stored as repo secrets — the single highest-value security improvement you can make to a GitHub Actions → AWS pipeline. It builds once, scans the image, pushes to ECR, then updates the ECS service with a new task definition revision, relying on ECS's built-in rolling deployment and the ALB health check to roll back automatically if the new tasks never become healthy.

1. Technical architecture

┌──────────────┐  push to main   ┌───────────────────────────┐
│ GitHub repo      │ ───────────────▶│ GitHub Actions workflow        │
└──────────────┘                  │  1. Checkout                     │
                                     │  2. Build Docker image             │
                                     │  3. Trivy vulnerability scan       │
                                     │  4. Assume AWS role via OIDC       │
                                     │  5. Push image to ECR               │
                                     │  6. Render new task definition      │
                                     │  7. Update ECS service               │
                                     │  8. Wait for service stability       │
                                     └──────────────┬────────────────┘
                                                       ▼
                                     ┌───────────────────────────┐
                                     │ ECS Fargate service              │
                                     │  - Rolling deployment (min 100%,  │
                                     │    max 200%)                        │
                                     │  - ALB target group health check   │
                                     │  - Auto-rollback if new tasks        │
                                     │    never reach healthy state         │
                                     └───────────────────────────┘

2. Complete code implementation

2.1 Dockerfile (multi-stage, non-root)

# Dockerfile
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --retries=3 CMD node dist/healthcheck.js
CMD ["node", "dist/server.js"]

2.2 ECS task definition template

{
  "family": "my-api-service",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/myApiTaskRole",
  "containerDefinitions": [
    {
      "name": "api",
      "image": "IMAGE_PLACEHOLDER",
      "portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-api-service",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "environment": [{ "name": "NODE_ENV", "value": "production" }],
      "secrets": [
        { "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:prod/db-url" }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "node dist/healthcheck.js || exit 1"],
        "interval": 15,
        "timeout": 3,
        "retries": 3,
        "startPeriod": 30
      }
    }
  ]
}

2.3 GitHub Actions workflow

# .github/workflows/deploy.yml
name: Build and Deploy to ECS

on:
  push:
    branches: [main]

permissions:
  id-token: write   # required for OIDC
  contents: read

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: my-api-service
  ECS_CLUSTER: production-cluster
  ECS_SERVICE: my-api-service
  ECS_TASK_DEFINITION: .aws/task-definition.json
  CONTAINER_NAME: api

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-ecs-deploy
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build image
        id: build
        env:
          REGISTRY: ${{ steps.ecr-login.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          echo "image=$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> "$GITHUB_OUTPUT"

      - name: Scan image for critical vulnerabilities
        uses: aquasecurity/[email protected]
        with:
          image-ref: ${{ steps.build.outputs.image }}
          severity: CRITICAL
          exit-code: "1"      # fail the job if any CRITICAL CVE is found
          ignore-unfixed: true

      - name: Push image to ECR
        run: docker push "${{ steps.build.outputs.image }}"

      - name: Render new task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ env.ECS_TASK_DEFINITION }}
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build.outputs.image }}

      - name: Deploy to ECS with rolling update
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true
          wait-for-minutes: 10

2.4 AWS OIDC trust policy (for the deploy role)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
        "StringLike": { "token.actions.githubusercontent.com:sub": "repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main" }
      }
    }
  ]
}

3. Step-by-step configuration guide

  1. Create the GitHub OIDC identity provider in IAM (one-time, per AWS account): IAM → Identity providers → Add provider → OpenID Connect → URL https://token.actions.githubusercontent.com, audience sts.amazonaws.com.
  2. Create the deploy IAM role with the trust policy from §2.4, scoped to your exact repo:ORG/REPO:ref:refs/heads/main — never use a wildcard * here, which would let any repo in your GitHub org assume the role.
  3. Attach a least-privilege permissions policy to that role: ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload, ecs:DescribeServices, ecs:DescribeTaskDefinition, ecs:RegisterTaskDefinition, ecs:UpdateService, and iam:PassRole scoped to the ECS execution/task role ARNs only.
  4. Store AWS_ACCOUNT_ID as a repository secret (it's an account number, not a credential, but keeping it out of the workflow file avoids hardcoding per-environment values).
  5. Create the ECR repository: aws ecr create-repository --repository-name my-api-service --image-scanning-configuration scanOnPush=true.
  6. Commit .aws/task-definition.json (§2.2) to the repo with a placeholder image value — the workflow overwrites it on every run via amazon-ecs-render-task-definition.
  7. Create the ECS cluster, service, and ALB target group (via Console, CDK, or Terraform) with the deployment configuration: minimumHealthyPercent: 100, maximumPercent: 200, and a target group health check path matching your app's actual health endpoint.
  8. Push to main and watch the Actions run — the final step blocks until ECS reports the new deployment stable (or times out and fails the job).

4. Error handling and edge cases

Related Reviews

Building a Production-Ready RAG Pipeline with LangChain, Pinecone, and OpenAI
Building a Production-Ready RAG Pipeline with LangChain, Pinecone, and OpenAI
5.0
A complete retrieval-augmented generation pipeline covering chunking strategy, Pinecone indexing with metadata filters, hybrid retrieval, and a FastAPI se…
Read Review → Developer Tools
Optimizing AI Model Inference Latency in Production Using NVIDIA Triton Inference Server
Optimizing AI Model Inference Latency in Production Using NVIDIA Triton Inference Server
5.0
Deploy a model behind NVIDIA Triton with dynamic batching, concurrent model instances, and ONNX Runtime acceleration, then measure and tune p99 latency un…
Read Review → Developer Tools
Setting Up Automated LLM Evaluation Pipelines for Hallucination Detection Using Ragas
Setting Up Automated LLM Evaluation Pipelines for Hallucination Detection Using Ragas
5.0
Wire Ragas metrics (faithfulness, answer relevancy, context precision/recall) into a CI-runnable evaluation pipeline that scores a RAG system against a go…
Read Review → Developer Tools