GitHub ActionsAWS.NET

Zero-Downtime .NET Deployments to AWS ECS with GitHub Actions

Build a production-grade CI/CD pipeline that builds your .NET container, pushes to ECR, and rolls out to ECS with health-check-gated blue/green deployments.

·12 min read

Why ECS over Lambda for .NET?

AWS ECS with Fargate gives you full control over your container runtime — persistent connections, background workers, and predictable memory — without managing EC2 instances. For .NET APIs that need connection pooling to RDS or Redis, it's the right default.

If your .NET service is stateless and handles bursty traffic, consider Lambda with SnapStart instead. This guide targets always-on services.

The Pipeline Architecture

GitHub Push → Build & Test → Docker Build → ECR Push → ECS Rolling Deploy

We'll wire this entirely in GitHub Actions using OIDC auth (no long-lived AWS keys).

Step 1: OIDC Trust Between GitHub and AWS

First, create the IAM role that GitHub Actions will assume:

iam.tf
resource "aws_iam_role" "github_actions" {
  name = "github-actions-deploy"
 
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
      Action    = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringLike = {
          "token.actions.githubusercontent.com:sub" = "repo:YOUR_ORG/YOUR_REPO:*"
        }
      }
    }]
  })
}

Step 2: The GitHub Actions Workflow

.github/workflows/deploy.yml
name: Deploy to AWS ECS
 
on:
  push:
    branches: [main]
 
permissions:
  id-token: write   # Required for OIDC
  contents: read
 
env:
  AWS_REGION:     us-east-1
  ECR_REPOSITORY: my-dotnet-api
  ECS_SERVICE:    my-api-service
  ECS_CLUSTER:    production
 
jobs:
  deploy:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-deploy
          aws-region: ${{ env.AWS_REGION }}
 
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
 
      - name: Build, tag, and push 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: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster $ECS_CLUSTER \
            --service $ECS_SERVICE \
            --force-new-deployment
 
      - name: Wait for stable deployment
        run: |
          aws ecs wait services-stable \
            --cluster $ECS_CLUSTER \
            --services $ECS_SERVICE

aws ecs wait services-stable will block for up to 10 minutes. If your deployment takes longer, increase the GitHub Actions job timeout.

Step 3: Health Checks are Non-Negotiable

ECS rolling deployments use ALB health checks to gate traffic. In your appsettings.json:

appsettings.json
{
  "HealthChecks": {
    "Path": "/health",
    "TimeoutSeconds": 5
  }
}

And wire it up in Program.cs:

Program.cs
builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString, name: "postgres")
    .AddRedis(redisConnection, name: "redis");
 
app.MapHealthChecks("/health");

Results

With this setup you get zero-downtime deploys: ECS starts new tasks, waits for health checks, then drains old tasks. A full deploy from git push to live traffic typically takes under 4 minutes.