๐Ÿš€ Noahops enterprise beta is now open โ€” Get early access โ†’
Back to blog

Zero-Downtime Deployments on AWS ECS: How to Ship Without Breaking Production

Zero-Downtime Deployments on AWS ECS: How to Ship Without Breaking Production

Zero-downtime deployments on AWS ECS are more approachable than most guides make them look. The blue-green guides you'll find online involve Terraform, Elastic Beanstalk, and IAM role hierarchies that take a day to set up. That's not what this is.

This is the practical version โ€” how ECS handles zero-downtime deployments natively, what you actually need to configure, and when to use rolling deployments vs. blue-green. No Terraform required to get started.


Why Deployments Break Production (and What ECS Does About It)

A deployment causes downtime when ECS stops your running task before the new one is healthy. For a split second โ€” or longer โ€” no task is serving traffic. Your ALB has nowhere to route requests. Users see 502s.

ECS prevents this with two mechanisms:

  1. Minimum healthy percent โ€” ECS won't stop old tasks until new ones pass health checks
  2. Maximum percent โ€” ECS controls how many tasks run concurrently during deployment (including old + new)

Set these correctly and your deployment is zero-downtime by default. That's it. No additional tooling required for most startup use cases.


ECS Rolling Deployment: The Right Default for Most Startups

Rolling deployment is ECS's default strategy. ECS launches new tasks alongside old ones, waits for health checks to pass, then drains and stops the old tasks.

The two settings that matter:

minimumHealthyPercent: 100
maximumPercent: 200

With these values:

  • ECS never drops below 100% of your desired task count
  • ECS can run up to 200% (old + new tasks simultaneously) during the rollout
  • Old tasks are only stopped after new tasks are confirmed healthy

If you're running 2 tasks, the deployment sequence looks like:

  1. Launch 2 new tasks (now running 4 total)
  2. New tasks pass ALB health checks
  3. Old tasks stop (back to 2 running)

Your users experienced nothing.

Configuring this in your ECS service (AWS Console):

Go to your ECS Service โ†’ Update โ†’ Deployment configuration:

  • Minimum healthy percent: 100
  • Maximum percent: 200
  • Deployment type: Rolling update

That's the full configuration. No CodeDeploy. No Terraform. Works with GitHub Actions, Bitbucket Pipelines, or any CI/CD that runs aws ecs update-service.


Health Checks: The Part Most Teams Get Wrong

Rolling deployments only prevent downtime if your health check actually reflects whether your app is ready to serve traffic.

Common mistake: Using the default ALB health check path (/) on an app that returns 200 on / even when the database connection is broken.

What your health check should verify:

  • The app process is running
  • Critical dependencies (database, cache) are reachable
  • The app is ready to process requests (not just started)

A minimal health check endpoint:

// Express example
app.get('/health', async (req, res) => {
  try {
    await db.query('SELECT 1'); // verify DB connection
    res.json({ status: 'ok' });
  } catch (err) {
    res.status(503).json({ status: 'error', reason: 'db_unavailable' });
  }
});

ALB health check settings that work:

  • Health check path: /health
  • Healthy threshold: 2 (consecutive successes)
  • Unhealthy threshold: 3 (consecutive failures)
  • Interval: 15 seconds
  • Timeout: 5 seconds

With these settings, ECS waits for 2 consecutive healthy responses before considering a task ready. A new deployment won't cut over until your app has proven it's actually working.


Deployment Circuit Breaker: Automatic Rollback Without a Runbook

ECS has a built-in deployment circuit breaker that automatically rolls back to the previous task definition if a deployment fails. Enable it.

In the AWS Console: ECS Service โ†’ Update โ†’ Deployment configuration โ†’ Enable deployment circuit breaker with rollback

What it does: If ECS can't get the new tasks healthy within the configured timeout (or if too many new tasks fail health checks), it automatically reverts to the previous task definition. No manual intervention. No 2am incident bridge.

In your CI/CD pipeline (AWS CLI):

aws ecs update-service \
  --cluster your-cluster \
  --service your-service \
  --task-definition your-task:NEW_VERSION \
  --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true},minimumHealthyPercent=100,maximumPercent=200" \
  --region us-east-1

This single command triggers a zero-downtime rolling deployment with auto-rollback. Every deploy you push from CI goes through this path.


When Rolling Isn't Enough: ECS Native Blue-Green

Rolling deployments handle most scenarios. But there are cases where you need a harder boundary between old and new:

  • Your new version has a database migration that changes table structure
  • You're changing an API contract and need to validate before cutting over
  • You want to run smoke tests against the new version under real traffic (canary)

For these cases, ECS now supports native blue-green deployments without requiring Elastic Beanstalk or Terraform.

The ECS native blue-green model:

  1. ECS creates a replacement task set (green) alongside the existing one (blue)
  2. You test the green task set via the ALB test listener
  3. When satisfied, you shift traffic from blue to green โ€” either all at once or gradually (canary)
  4. ECS deregisters the blue task set

Setting it up:

In your ECS service, select Blue/green deployment (powered by AWS CodeDeploy) as the deployment type.

You'll need:

  • Two ALB target groups (one for blue, one for green)
  • A CodeDeploy deployment group tied to your ECS service
  • An appspec.yaml in your repo

The appspec.yaml is simpler than it looks:

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "your-container"
          ContainerPort: 3000

Your CI/CD creates a new task definition revision, then triggers a CodeDeploy deployment that handles the traffic shift.

When traffic shifts, in seconds. ECS blue-green is a DNS-level switch at the ALB โ€” not a redeployment.


Handling Database Migrations Without Downtime

Migrations are the hardest part of zero-downtime deployments. If your app and your schema go out of sync โ€” even briefly โ€” users see errors.

The rule: Schema changes must be backward-compatible with the running app version during the deployment window.

Safe migration patterns:

Adding a column:

  1. Deploy migration: add column with a default value (nullable or with default)
  2. Deploy app: app writes to new column
  3. (Optional later) Deploy migration: make column NOT NULL once all rows populated

Renaming a column:

  1. Deploy migration: add new column, copy data
  2. Deploy app: app reads from both old and new column; writes to new column only
  3. Deploy migration: drop old column

Removing a column:

  1. Deploy app: app stops reading/writing the column
  2. Deploy migration: drop column

Never rename a column in one step while the app is running. Your currently-running tasks will fail immediately.


A Production-Ready ECS Deploy Script

Here's the full deploy script NoahOps uses โ€” GitHub Actions compatible, zero additional tooling:

#!/bin/bash
set -e

CLUSTER="your-cluster"
SERVICE="your-service"
REGION="us-east-1"
IMAGE="$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/your-app:$GITHUB_SHA"

# Build and push to ECR
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin "$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
docker build -t $IMAGE .
docker push $IMAGE

# Get current task definition and update image
TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition your-task --region $REGION)
NEW_TASK_DEF=$(echo $TASK_DEFINITION | jq --arg IMAGE "$IMAGE" \
  '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .placementConstraints, .compatibilities, .registeredAt, .registeredBy)')

# Register new task definition
NEW_TASK_ARN=$(aws ecs register-task-definition --region $REGION --cli-input-json "$NEW_TASK_DEF" | jq -r '.taskDefinition.taskDefinitionArn')

# Deploy with zero-downtime rolling + circuit breaker
aws ecs update-service \
  --cluster $CLUSTER \
  --service $SERVICE \
  --task-definition $NEW_TASK_ARN \
  --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true},minimumHealthyPercent=100,maximumPercent=200" \
  --region $REGION

# Wait for deployment to complete
aws ecs wait services-stable --cluster $CLUSTER --services $SERVICE --region $REGION

echo "Deployment complete: $NEW_TASK_ARN"

This runs in your CI pipeline. Every push to main builds a new image, registers a new task definition, and triggers a zero-downtime rolling deployment. If anything fails, ECS rolls back automatically.


NoahOps: Zero-Downtime Deployments Without the Script Maintenance

If you'd rather not maintain this infrastructure yourself, NoahOps handles it.

Every service you deploy through NoahOps runs on your own AWS account with ECS/Fargate โ€” VPC-isolated, with zero-downtime rolling deployments configured by default. GitHub and Bitbucket push โ†’ build โ†’ ECR โ†’ ECS deploy. Deployment circuit breaker enabled. Slack notifications on success and failure.

You don't configure any of this manually. It works on day one.

Request a free demo at noahops.com โ€” and see your first zero-downtime deploy in under 15 minutes.


FAQ

What is a zero-downtime deployment on AWS ECS?

A zero-downtime deployment means new versions of your application are deployed without any period where no tasks are serving traffic. ECS achieves this by running old and new task versions simultaneously during the rollout, only stopping old tasks after new ones have passed health checks.

Do I need Terraform for zero-downtime ECS deployments?

No. ECS rolling deployments with minimumHealthyPercent=100 and maximumPercent=200 are configured directly in the AWS Console or via the AWS CLI. Terraform is useful for managing infrastructure as code at scale, but it's not a prerequisite for zero-downtime deployments.

What's the difference between ECS rolling deployment and blue-green?

Rolling deployments gradually replace old tasks with new ones on the same service. Blue-green deployments run a complete parallel environment (green) and switch traffic at the load balancer level when ready. Rolling is simpler and right for most startups. Blue-green gives you a harder boundary โ€” useful for major releases or schema changes.

How does the ECS deployment circuit breaker work?

When enabled, ECS monitors the health of tasks during a deployment. If new tasks fail health checks repeatedly (the default threshold is 10 failed tasks in a single deployment), ECS automatically rolls back to the previous task definition. No manual action required.

How do I handle database migrations with zero-downtime deployments?

Always make schema changes backward-compatible with the currently-running app version. Add columns as nullable before making them required. Rename columns in two steps (add new, migrate data, drop old). Never remove a column that the running app still references. Run migrations before deploying the new app version.

What health check should my ECS tasks use?

Your health check endpoint should verify that the app is ready to serve traffic โ€” not just that the process started. At minimum: confirm the application responds on the health check path AND verify critical dependencies (database, cache) are reachable. A 200 from a broken app is worse than a 503 because ECS will route traffic to it.

Ready to ship to AWS?

Try NoahOps free โ€” VPC, ECS, RDS, CI/CD in your own AWS account in 15 minutes.