Implement Zero-Downtime Blue-Green Deployments on AWS ECS Fargate

Provision ECS Fargate with Terraform, configure CodeDeploy blue-green deployments with weighted ALB target groups and automatic rollback on health check failure.

Domains & Technologies

Domains
ECSCI-CDAWS-CLOUD-ENGINEERING
Technologies
AWSTERRAFORM

Blueprint Walkthrough

Architecture Overview

Blue-green deployment is a release strategy that eliminates downtime by running two identical production environments — blue (current version) and green (new version). Traffic switches from blue to green once the new version passes health checks. If anything goes wrong, you switch back to blue in seconds.

This project implements blue-green deployments on AWS ECS Fargate using AWS CodeDeploy to manage the traffic shift, an Application Load Balancer with two target groups, and a GitHub Actions pipeline that triggers automatic deployments on every merge to main.

This is how most Indian mid-stage startups (Razorpay, Cleartax, Meesho) deploy containerised applications before moving to Kubernetes — ECS Fargate with blue-green is production-grade, cost-effective, and significantly simpler to operate than Kubernetes.

◈ DIAGRAM
GitHub Push to main
|
v
GitHub Actions CI
(build, test, push to ECR)
|
v
AWS CodeDeploy
(manages the shift)
|
+------+------+
| |
v v
Blue TG Green TG
(old v1) (new v2)
| |
+------+------+
|
v
Application Load Balancer
(ALB shifts weight from blue to green)
|
v
Internet
Problem Solved

A traditional deployment on ECS works like this — stop the old containers, start the new ones, users see errors during the gap. Even if the gap is 30 seconds, that is 30 seconds of 503 errors for every user. During peak traffic at Swiggy during lunch hour, 30 seconds of downtime means thousands of failed orders.

Blue-green solves this by never stopping the old version until the new version is fully healthy. The ALB shifts traffic from the blue target group to the green target group over a configurable time period. If health checks fail during the shift, CodeDeploy automatically rolls back to blue with zero manual intervention.

Step-by-Step Implementation Guide

Step 1: Provision ECS Infrastructure with Terraform

Bash
mkdir ecs-blue-green && cd ecs-blue-green
mkdir -p {modules/{vpc,ecs,alb,iam},environments/production}

Create environments/production/main.tf:

HCL
terraform {
required_version = ">= 1.7.0"
backend "s3" {
bucket = "ecs-bg-state-YOUR_ACCOUNT_ID"
key = "production/terraform.tfstate"
region = "ap-south-1"
encrypt = true
dynamodb_table = "ecs-bg-lock"
}
}
provider "aws" { region = "ap-south-1" }
## VPC with 2 public subnets for ALB and 2 private subnets for ECS tasks
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "ecs-bg-vpc" }
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
availability_zone = ["ap-south-1a", "ap-south-1b"][count.index]
map_public_ip_on_launch = true
tags = { Name = "ecs-bg-public-${count.index}" }
}
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index + 4)
availability_zone = ["ap-south-1a", "ap-south-1b"][count.index]
tags = { Name = "ecs-bg-private-${count.index}" }
}
## ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "production-cluster"
setting {
name = "containerInsights"
value = "enabled" # CloudWatch container metrics
}
}
## ALB with two target groups — one for blue, one for green
resource "aws_lb" "main" {
name = "ecs-bg-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
}
resource "aws_lb_target_group" "blue" {
name = "ecs-bg-blue"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip" # ECS Fargate uses IP-based target registration
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 5
timeout = 10
interval = 30
}
}
resource "aws_lb_target_group" "green" {
name = "ecs-bg-green"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 5
timeout = 10
interval = 30
}
}
## Listener starts with all traffic on blue
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
}
## ECS Task Definition
resource "aws_ecs_task_definition" "app" {
family = "webapp"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.ecs_execution.arn
container_definitions = jsonencode([{
name = "webapp"
image = "${aws_ecr_repository.app.repository_url}:latest"
portMappings = [{
containerPort = 8080
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/webapp"
"awslogs-region" = "ap-south-1"
"awslogs-stream-prefix" = "ecs"
}
}
}])
}
## ECS Service — CodeDeploy manages the blue-green switch
resource "aws_ecs_service" "app" {
name = "webapp"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.blue.arn
container_name = "webapp"
container_port = 8080
}
deployment_controller {
type = "CODE_DEPLOY" # This tells ECS that CodeDeploy manages deployments
}
lifecycle {
ignore_changes = [task_definition, load_balancer]
# CodeDeploy manages these — Terraform should not fight with it
}
}

Step 2: Configure AWS CodeDeploy for Blue-Green

HCL
## CodeDeploy Application
resource "aws_codedeploy_app" "app" {
compute_platform = "ECS"
name = "webapp-deployment"
}
## Deployment Group — this is where the blue-green magic happens
resource "aws_codedeploy_deployment_group" "app" {
app_name = aws_codedeploy_app.app.name
deployment_group_name = "webapp-production"
service_role_arn = aws_iam_role.codedeploy.arn
deployment_config_name = "CodeDeployDefault.ECSLinear10PercentEvery1Minutes"
# This shifts 10% of traffic every minute
# Full shift completes in 10 minutes
# Other options:
# CodeDeployDefault.ECSAllAtOnce — instant (risky for production)
# CodeDeployDefault.ECSLinear10PercentEvery3Minutes — slower, safer
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT" # Auto-proceed after health checks pass
}
terminate_blue_instances_on_deployment_success {
action = "TERMINATE"
termination_wait_time_in_minutes = 5 # Keep blue running for 5 mins after green is live
# This gives you time to manually roll back if you notice issues
}
}
ecs_service {
cluster_name = aws_ecs_cluster.main.name
service_name = aws_ecs_service.app.name
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [aws_lb_listener.http.arn]
}
target_group {
name = aws_lb_target_group.blue.name
}
target_group {
name = aws_lb_target_group.green.name
}
}
}
}

Step 3: GitHub Actions Pipeline

Create .github/workflows/deploy.yml:

YAML
name: Deploy to ECS Blue-Green
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-deploy-role
aws-region: ap-south-1
- name: Login to ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
id: build
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/webapp:$IMAGE_TAG .
docker push $ECR_REGISTRY/webapp:$IMAGE_TAG
echo "image=$ECR_REGISTRY/webapp:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Update ECS Task Definition with new image
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: webapp
image: ${{ steps.build.outputs.image }}
- name: Create CodeDeploy deployment (triggers blue-green shift)
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: webapp
cluster: production-cluster
codedeploy-appspec: appspec.yml
codedeploy-application: webapp-deployment
codedeploy-deployment-group: webapp-production
wait-for-service-stability: true

Create appspec.yml in your repository root:

YAML
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: webapp
ContainerPort: 8080
PlatformVersion: LATEST
Validation & Testing
Bash
## 1. Verify ECS service is running with desired count
aws ecs describe-services \
--cluster production-cluster \
--services webapp \
--query 'services[0].[runningCount,desiredCount,deployments]'
## Expected: runningCount == desiredCount, one active deployment
## 2. Test the application is responding through the ALB
ALB_DNS=$(terraform output -raw alb_dns_name)
curl -I http://$ALB_DNS
## Expected: HTTP 200
## 3. Trigger a deployment by pushing to main
git commit --allow-empty -m "trigger: blue-green deployment test"
git push origin main
## 4. Watch the CodeDeploy deployment in the AWS console
## CodeDeploy -> Deployments -> watch traffic shift from 0% to 100% on green
## 5. Verify traffic is shifting
## Run this in a loop during the deployment:
for i in {1..60}; do
curl -s http://$ALB_DNS/version && sleep 5
done
## During the shift you should see responses from both old and new version
## 6. Test automatic rollback
## Deploy a version with a broken health endpoint
## CodeDeploy will detect health check failures and roll back automatically
## Check the CodeDeploy console — deployment status should show ROLLED_BACK
## 7. Verify zero downtime
## Run constant traffic during a deployment:
while true; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://$ALB_DNS)
echo "$(date): $STATUS"
sleep 1
done
## Expected: All 200 responses throughout the entire deployment — zero 5xx errors