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.
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:
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
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_SERVICEaws 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:
{
"HealthChecks": {
"Path": "/health",
"TimeoutSeconds": 5
}
}And wire it up in 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.