New engagements · 24h
Skip to main content
Technical Blog
CI/CD GitHub Actions AWS Docker DevOps

CI/CD Pipeline Architecture with GitHub Actions and AWS ECS

How to design a production CI/CD pipeline with OIDC authentication, parallel quality gates, automated rollback and zero long-lived AWS credentials. Architecture decisions and operational tradeoffs.

Published April 2026 · 4 min read

Problem

Manual deployments fail in predictable ways. The steps drift between environments. A flag missed in a Friday deployment becomes a production incident on Monday. Security scans that require a separate manual step get skipped under deadline pressure. Rollback procedures that were never rehearsed fail when an incident actually requires them.

The problem is not that teams want to cut corners — it is that manual deployment creates the conditions under which corners get cut systematically.

Context

GitHub Actions eliminates the infrastructure cost of running CI/CD. There is no server to provision, patch or maintain. The pipeline runs where the code already lives. This removes the operational overhead that causes teams to defer pipeline work.

The design decisions that make a pipeline production-grade are not about which tool to use. They are about what gates exist, what conditions trigger them, and what happens when a gate fails.

Architecture

A production pipeline has four properties:

1. Fail fast, fail hard — quality gates run before build steps. If tests fail, the Docker image is never built. If the image build fails, no deployment is attempted. Each stage is a hard gate, not a warning.

2. No static credentials — GitHub Actions authenticates to AWS via OIDC federation. The pipeline assumes an IAM role directly. No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY exists in repository secrets. Credentials are tokens valid for the duration of the pipeline run — nothing to rotate, nothing to leak.

3. Deploy only from main — pull requests run build and test jobs. Deployment executes only on merge to the production branch. This separates validation from deployment and ensures that only reviewed, merged code reaches the environment.

4. Verification after deployment — post-deployment health checks confirm the service is responding before the pipeline reports success. A deployment that passes CI but leaves the service unhealthy is a failed deployment.

Implementation

OIDC authentication setup

Three steps in AWS: create an OIDC identity provider pointing to token.actions.githubusercontent.com, create an IAM role that trusts that provider, and scope the trust policy to the specific repository and branch that can assume the role.

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
    aws-region: eu-west-1

The trust policy condition prevents other repositories — including forks — from assuming the role:

{
  "Condition": {
    "StringLike": {
      "token.actions.githubusercontent.com:sub":
        "repo:org/repo:ref:refs/heads/main"
    }
  }
}

Pipeline structure

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest --cov=app tests/ --cov-report=xml --cov-fail-under=80

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Scan dependencies
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          severity: HIGH,CRITICAL
          exit-code: 1

  build:
    needs: [test, security-scan]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: eu-west-1
      - name: Build and push to ECR
        run: |
          aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REPO
          docker build -t $ECR_REPO:${{ github.sha }} .
          docker push $ECR_REPO:${{ github.sha }}

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster production \
            --service api \
            --force-new-deployment
      - name: Wait for deployment
        run: |
          aws ecs wait services-stable \
            --cluster production \
            --services api

Image tagging

Images are tagged with the Git commit SHA — never with latest. This makes every deployment traceable to an exact commit, and makes rollback a matter of redeploying a previous SHA rather than reconstructing what the previous state was.

Quality gate configuration

Coverage enforcement at 80% minimum runs before the build job. If coverage drops below threshold, the pipeline stops. The Docker image is not built and the ECR registry stays clean.

- run: pytest --cov=app tests/ --cov-fail-under=80

SonarCloud integration adds static analysis and security hotspot detection as a separate parallel job, so it does not block the test run but does block the build:

  sonar:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: SonarSource/sonarcloud-github-action@master
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Operational Considerations

Deployment verificationaws ecs wait services-stable blocks the pipeline until ECS confirms all tasks in the new deployment are healthy. If tasks fail their health checks, the wait command times out and the pipeline fails. ECS rolls back to the previous task definition automatically.

Concurrency control — two simultaneous deployments to the same environment create race conditions. GitHub Actions concurrency groups prevent this:

concurrency:
  group: deploy-production
  cancel-in-progress: false

cancel-in-progress: false is intentional — a deployment in progress should complete, not be cancelled mid-flight.

Secret scanning — Gitleaks or GitHub’s built-in secret scanning catches credentials accidentally committed to source control before they reach the registry. This runs as a pre-build gate, not a post-incident remediation step.

IAM role permissions — the deploy role should have minimum viable permissions. ECR push, ECS service update, ECS task execution. No AdministratorAccess, no PowerUserAccess. Use CloudTrail to observe exactly what actions the pipeline executes, then write the policy to match.

Outcome

A pipeline built on these principles produces the same result on every run: identical build steps, identical quality gates, identical deployment procedure. The team stops accumulating deployment-related incidents caused by procedural variance. Security scanning runs on every merge without requiring a separate scheduled process. Rollback is a pipeline re-run, not an emergency runbook.

The infrastructure cost is zero beyond the GitHub Actions minutes consumed.