1. Introduction
A normal deployment replaces your app a little at a time. If the new version has a bug, some real users hit it before anyone notices— monitoring catches it eventually, but "eventually" can be minutes of bad requests.
Blue-green deployment fixes this differently. Instead of replacing the running app in place, you start the new version alongside the old one and test it privately before anyone else sees it. On its own, ECS's blue/green feature doesn't actually wait for you to do that checking — the moment the new version passes its health checks, it shifts all production traffic over automatically, whether or not anyone looked at it first.
What turns "check it privately" into an actual gate — a real point where nothing goes live until you say so — is one more piece this guide adds on top: a manual approval step. With it in place, the deployment stops right after the new version is reachable for checking, and waits for you to approve it before a single real user sees it. If it looks wrong, you reject it instead, and the new version never goes live at all.
In this guide, you'll build that setup on AWS: a container running on ECS Fargate, deployed through a pipeline that builds your code, ships it to a new set of tasks, lets you check it privately behind a test rule, pauses there waiting for your approval, and only then shifts production traffic over — plus, once it's live, keeps the old version standing by for a few minutes in case you need to revert instantly.
A note on how this is built: This guide uses Amazon ECS's own native blue/green deployment feature, not AWS CodeDeploy. Up until July 2025, ECS blue/green deployments needed a separate CodeDeploy application, deployment group, and an appspec.yaml file to coordinate the traffic shift. As of July 2025, ECS does all of that itself — you just set a deployment strategy on the service, point it at two target groups, and ECS handles the shift, the bake time, and rollback on its own. If you're following an older tutorial and can't find a "CodeDeploy" option in the ECS console anymore, this is why — it's been folded into ECS directly.
You'll build it two ways:
- By hand, in the AWS Console — so you actually see how each piece connects to the next one, not just what a template produces
- With CloudFormation — the same architecture, deployed as one stack
Both end up at the same result. Go through the console version first if you're newer to this — it's slower, but it's the version that actually teaches you what's happening.
2. Architecture
The part that actually makes this zero-downtime is the two target groups and the two listener rules — both living on the same listener, port 80.
The production rule catches ordinary traffic and always points at
whichever target group is "live." The test rule only fires for requests carrying a specific header, so you can privately hit the new version before it goes live without needing a second port. ECS itself flips which target group each rule points to during a deployment — nothing gets exposed to real users until it's already been checked.
(If you're used to the older CodeDeploy-based pattern, that one really did use a separate port for test traffic. Native ECS blue/green doesn't need that — the ECS "Create service" wizard only ever asks for one listener, then two rules under it, so a header is what tells the two apart instead of a port.)
3. Before you start
You'll need:
- An AWS account with permissions to create VPCs, ECS, IAM roles, ALBs, CodePipeline, and CodeBuild resources. All resources in this guide are created in
us-east-1. - Docker installed locally, if you want to build and push the first image yourself before the pipeline takes over
- A GitHub account and a repo the pipeline can pull from
Important — clone this repo first:
git clone https://github.com/Gbengard/blue-green-ecs-deployment.git The pipeline needs the pipeline-config/ folder — containing
buildspec.yml, Dockerfile, and index.html — sitting in your own GitHub repo before it can run. Push the whole cloned project as-is, keeping pipeline-config/ as a subfolder rather than flattening its contents into the repo root — Step 9 points CodeBuild at pipeline-config/buildspec.yml, and the buildspec itself cds into pipeline-config/ to find the Dockerfile, so the folder needs to stay where it is. Without this step, the pipeline's Source stage has nothing to pull, and everything after it will fail before it even starts.
Every resource in this guide has a fixed name, so it's easy to follow and easy to find again later. Keep this table open as you work through it.
| Resource | Name |
|---|---|
| VPC | bluegreen-demo-vpc |
| Internet Gateway | bluegreen-demo-igw |
| Public subnet A | bluegreen-demo-public-a |
| Public subnet B | bluegreen-demo-public-b |
| Route table | bluegreen-demo-public-rt |
| ALB security group | bluegreen-demo-alb-sg |
| Service security group | bluegreen-demo-svc-sg |
| ECR repository | bluegreen-demo-app |
| Application Load Balancer | bluegreen-demo-alb |
| Blue (primary) target group | bluegreen-demo-tg-blue |
| Green (alternate) target group | bluegreen-demo-tg-green |
| Production listener rule | port 80, priority 2, path /* |
| Test listener rule | same listener (port 80), priority 1, header X-Bg-Test: true |
| ECS cluster | bluegreen-demo-cluster |
| ECS task definition family | bluegreen-demo-task |
| ECS service | bluegreen-demo-service |
| Task execution role | bluegreen-demo-task-exec-role |
| Task role | bluegreen-demo-task-role |
| ECS infrastructure role | bluegreen-demo-ecs-infra-role |
| CodeBuild project | bluegreen-demo-build |
| CodeBuild service role | bluegreen-demo-codebuild-role |
| S3 artifact bucket | bluegreen-demo-artifacts-<your-account-id> |
| CodePipeline | bluegreen-demo-pipeline |
| CodeStar GitHub connection | bluegreen-demo-github-connection |
| CloudWatch log group | /ecs/bluegreen-demo |
Manual Approach (AWS Console)
Every step below tells you exactly which console page to open, what to click, what to type, and which button ends the step. Screenshots are included wherever they help.
Step 1 — Create the VPC
- Open the VPC console
- Click Create VPC
- Choose VPC only
- Name:
bluegreen-demo-vpc - IPv4 CIDR:
10.20.0.0/16 - Leave everything else as default
- Click Create VPC at the bottom of the page
Step 2 — Create two public subnets
- In the left sidebar, click Subnets
- Click Create subnet
- VPC ID: choose
bluegreen-demo-vpc - Under Subnet 1 of 1: name
bluegreen-demo-public-a, Availability Zone: pick the first one in the list, IPv4 CIDR:10.20.1.0/24 - Click Add new subnet
- Under Subnet 2 of 2: name
bluegreen-demo-public-b, Availability Zone: pick a different zone from subnet A, IPv4 CIDR:10.20.2.0/24 -
Click Create subnet at the bottom
Back on the Subnets page, check the box next to one of the new subnets
Click Actions → Edit subnet settings
Turn on Auto-assign public IPv4 address, then click Save
Repeat Steps 8–10 for the second subnet
Two subnets in two different zones matter here — if one zone has a
problem, your app keeps running in the other.
Step 3 — Internet Gateway and routing
- Left sidebar → Internet Gateways → click Create internet gateway
- Name:
bluegreen-demo-igw→ click Create internet gateway - On the gateway's page, click Actions → Attach to VPC
-
Choose
bluegreen-demo-vpc→ click Attach internet gateway Left sidebar → Route Tables → click Create route table
Name:
bluegreen-demo-public-rt, VPC:bluegreen-demo-vpc→ click Create route tableOpen the new route table → Routes tab → click Edit routes
-
Click Add route → Destination:
0.0.0.0/0, Target: choose
Internet Gateway, then selectbluegreen-demo-igw→ click Save changes Still on the route table, click the Subnet associations tab → click Edit subnet associations
Check both
bluegreen-demo-public-aandbluegreen-demo-public-b→ click Save associations
Step 4 — Security groups
- EC2 console → left sidebar Security Groups, or follow this link → click Create security group
- Name:
bluegreen-demo-alb-sg, Description: anything descriptive, VPC:bluegreen-demo-vpc -
Under Inbound rules, click Add rule: - Type: HTTP, Source: Anywhere-IPv4 (
0.0.0.0/0) Click Create security group
Click Create security group again for the second one
Name:
bluegreen-demo-svc-sg, same VPCUnder Inbound rules, click Add rule: Type: Custom TCP, Port:
80, Source: click the source field, choose Custom, then start typingbluegreen-demo-alb-sgand select it from the dropdown (not an IP address)-
Check the Outbound rules section before you create it. The
console pre-fills a default "All traffic" outbound rule — leave it as is, or if it's been narrowed for any reason, add: Type HTTPS, Port 443, Destination Anywhere-IPv4 (0.0.0.0/0). This one is easy to miss and causes a very specific, confusing failure later: the task starts, then dies withResourceInitializationError: unable to pull secrets or registry auth ... dial tcp ... i/o timeout. Click Create security group
This inbound rule from Step 7 is important: it means only traffic coming through your load balancer can reach the containers; nothing else can hit them directly.
Step 5 — Push an image to ECR
- Click ECR console → click Create repository
- Name:
bluegreen-demo-app - Under Image scan settings, turn on Scan on push
- Click Create repository
- Open the repository → click View push commands (top right)
- A dialog shows four commands with your exact account ID and region filled in — run them locally, building from the
Dockerfileandindex.htmlinpipeline-config/in this repo:
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com docker build -t bluegreen-demo-app pipeline-config/ docker tag bluegreen-demo-app:latest <account-id>.dkr.ecr.<region>.amazonaws.com/bluegreen-demo-app:latest docker push <account-id>.dkr.ecr.<region>.amazonaws.com/bluegreen-demo-app:latest If you get the error aws: [ERROR]: Your session has expired. Please reauthenticate using 'aws login'. password is empty,
run aws login — it opens your browser to log in, then rerun the commands above.
If docker build fails with permission denied while trying to connect to the Docker API at unix:///var/run/docker.sock, Docker itself is fine — your user just isn't in the docker group on this machine, so it can't talk to the Docker daemon without root. Fix it once, permanently, rather than prefixing every command with sudo:
sudo usermod -aG docker $USER Then fully log out and back in (or reboot) — a new group membership
doesn't apply to already-open terminal sessions. Confirm it worked with docker run hello-world; if that runs without sudo, you're set.
Once the push finishes, click Close on the dialog, then refresh the repository page — you should see one image listed.
This step can't be skipped, even in the CloudFormation version — the ECS service you create later needs a real image to exist before it can start any tasks.
Step 6 — Load balancer, target groups, and listener rules
- EC2 console → left sidebar Target Groups, or follow this link → click Create target group
- Target type: IP addresses
- Name:
bluegreen-demo-tg-blue, Protocol: HTTP, Port: 80, VPC:bluegreen-demo-vpc - Under Health checks, leave the path as
/ - Click Next, then click Create target group (skip registering targets — ECS will do that automatically once the service exists)
-
Repeat Steps 1–5 for a second one named
bluegreen-demo-tg-green Left sidebar → Load Balancers, or follow this
link → click Create load balancer-
Under Application Load Balancer, click Create
Name:
bluegreen-demo-alb, Scheme: Internet-facingNetwork mapping: VPC
bluegreen-demo-vpc, select both public subnetsSecurity groups: remove the default one, add
bluegreen-demo-alb-sgUnder Listeners and routing, Listener 1 is already HTTP:80 — set its default action to forward to
bluegreen-demo-tg-blue(this is just a fallback; the real routing happens through the rules you add next)Click Create load balancer
Once it's created, open the HTTP:80 listener → click Add rules
Both the production rule and the test rule live on this same listener — the ECS "Create service" wizard only lets you choose one listener, then pick two different rules under it, so a second port isn't part of the picture here. The two rules are told apart by a header instead: normal requests fall through to the catch-all production rule, and requests carrying a specific test header get caught by the test rule first.
- Rule name:
test-rule - Add condition: choose HTTP Header, header name
X-Bg-Test, valuetrue - Action: forward to
bluegreen-demo-tg-green, then click Next - Priority:
1 - Click Save
- Click the Add rule icon again to add the production rule
- Rule name:
prod-rule - Add condition: Path → value
/* - Action: forward to
bluegreen-demo-tg-blue, then click Next - Priority:
2(higher number = checked after the test rule) - Click Save
Step 7 — IAM roles
Go to the IAM console → left sidebar Roles → click Create role,
for each of these four:
- Trusted entity type: AWS service, Use case: search and select Elastic Container Service, then select Elastic Container Service Task from the use case list that appears → click Next → search for and check
AmazonECSTaskExecutionRolePolicy→ click Next → name itbluegreen-demo-task-exec-role, click Create role - Trusted entity type: AWS service, Use case: search and select Elastic Container Service, then select Elastic Container Service Task from the use case list that appears → click Next → don't attach any policy yet (this demo app doesn't call other AWS services) → click Next → name it
bluegreen-demo-task-role, click Create role - Trusted entity type: AWS service, Use case: search for Elastic Container Service, then choose "Elastic Container Service for Load Balancers" → click Next →
AmazonECSInfrastructureRolePolicyForLoadBalancersshould already be attached → click Next → name itbluegreen-demo-ecs-infra-role, click Create role. (This role is new — it's what lets ECS itself flip the listener rule between blue and green during a deployment, replacing what CodeDeploy's role used to do.) - Trusted entity type: AWS service, Use case: search for and select CodeBuild → click Next.
Now attach the actual permissions this role needs before you ever create the CodeBuild project — this is the fix for a very common failure where the pipeline builds fine but docker push fails at the Build stage, because the role CodeBuild auto-generates for you by default only covers logging and basic artifact access, not ECR. Doing it here, once, means you never have to come back and patch it later:
- Click Create inline policy
- Switch to the JSON tab, delete the placeholder content, and paste this:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:GetBucketAcl", "s3:GetBucketLocation" ], "Resource": "*" }, { "Effect": "Allow", "Action": "ecr:GetAuthorizationToken", "Resource": "*" }, { "Effect": "Allow", "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:PutImage", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload" ], "Resource": "arn:aws:ecr:*:*:repository/bluegreen-demo-app" } ] } - Name it
bluegreen-demo-codebuild-role - Name the policy
bluegreen-demo-codebuild-inline - Click Create role
The S3 permissions use Resource: "*" here rather than a specific bucket ARN, because the pipeline's artifact bucket doesn't exist yet — it gets created in Step 10. That's loose for a real production setup (worth tightening to the actual bucket ARN once you know it), but fine for getting this working the first time.
Step 8 — ECS cluster, task definition, and service
- ECS console, or follow this link → left sidebar Clusters → click Create cluster
- Name:
bluegreen-demo-cluster - Infrastructure: check AWS Fargate (serverless)
- Click Create
- Left sidebar → Task definitions → click Create new task definition
- Task definition family:
bluegreen-demo-task - Launch type: AWS Fargate
- CPU:
.25 vCPU, Memory:0.5 GB - Task role:
bluegreen-demo-task-role - Task execution role:
bluegreen-demo-task-exec-role - Under Container 1: name
bluegreen-demo-container, Image URI: paste the ECR image URI from Step 5, Container port:80 - Under Logging, turn on Use log collection, log group name
/ecs/bluegreen-demo, leave everything else as it is -
Click Create
Open the cluster
bluegreen-demo-cluster→ click Create service (or from the task definition page, use the Deploy menu → Create service)Under Task definition family, choose the one we created earlier:
bluegreen-demo-taskTask definition revision: latest
Service name:
bluegreen-demo-serviceCompute options: Launch type,
FARGATEDesired tasks:
2Under Deployment options, choose Deployment strategy: Blue/green — this is the setting that replaces what used to require a separate CodeDeploy application, don't miss it
Under Deployment bake time, type
3. AWS defaults this to 15 minutes, which makes iterating on a test setup painfully slow — see the note right after Step 11 for exactly what this setting does and doesn't control before you assume a short bake time is unsafeUnder Deployment lifecycle hooks, click Add. This is the part that actually gates production cutover — unlike everything above it, which just configures how the shift happens, not whether it's allowed to happen without anyone checking first
Choose hook type Pause
Lifecycle stages: select PRE_PRODUCTION_TRAFFIC_SHIFT — this pauses right after green has already received test traffic, but before it gets anything real
Set a timeout — 60 minutes is reasonable for testing. Leave the timeout action as Roll back (the default): if you forget to approve it, an unattended deployment should fail safe and revert, not quietly go live on its own
Under Networking: VPC
bluegreen-demo-vpc, both public subnets, security groupbluegreen-demo-svc-sg, turn on Public IPUnder Load balancing: choose Application Load Balancer
Load balancer:
bluegreen-demo-albChoose the VPC you created (
bluegreen-demo-vpc)Under the role, choose
bluegreen-demo-ecs-infra-roleContainer to load balance:
bluegreen-demo-container 80:80Under Load Balancer Type, choose Application Load Balancer
Under Application Load Balancer, choose Use an existing load balancer, then choose
bluegreen-demo-albUnder Listener, click Use an existing listener
Choose HTTP:80 — under Production listener rule choose Priority: 2, and under Test listener rule choose Priority: 1
Target group:
bluegreen-demo-tg-blueGreen target group:
bluegreen-demo-tg-green-
Click Create
Creating the service kicks off its first deployment, and since manual approval is on, this first deployment pauses too, just like every one after it will:
- Open the ECS service → deployment timeline to watch it progress through each phase in real time
-
The deployment should stop at Test traffic shift, showing status Awaiting action — this is the pause hook you just set up
Click Take Action → Continue to let it proceed to Production traffic shift
Step 8a — Confirm the blue environment actually works
Worth stopping here before building the pipeline. Everything from Steps 1–8 is the actual infrastructure — the VPC, security groups, load balancer, target groups, listener rules, and the service itself. If something's wrong with any of it, you want to find that out now, not after you've also added CodeBuild and CodePipeline on top and have to guess which layer the problem is in.
-
Open the ECS service → wait for both tasks under Tasks to show status Running and health status Healthy (give it a minute or two after creation)
Go to the EC2 console → Load Balancers →
bluegreen-demo-alband copy the DNS nameOpen that DNS name in a browser, or run:
curl http://<alb-dns-name>/ You should see the placeholder page's content (Version: v1). If you get a timeout, a 503, or nothing at all, stop here and check, in this order: the tasks are actually healthy in the target group (EC2 console → Target Groups → bluegreen-demo-tg-blue → Targets tab — both should show healthy, not unhealthy or draining), the service security group's outbound rule allows HTTPS (Step 4 — needed to even pull the image and start), and the production listener rule is actually pointing at bluegreen-demo-tg-blue (Step 6).
Once this works, you know the infrastructure itself is solid, and any problems from here on are isolated to the pipeline you're about to build.
Step 9 — CodeBuild project
- Go to the CodeBuild console → click Create build project
- Project name:
bluegreen-demo-build - Source provider: GitHub. Under Credential, click Use override credentials for this project only. Under Connection, click Create a new OAuth app token connection, follow the prompt to log in to your GitHub account, and save the secret name as anything you like. Choose the connection you just created, then paste your repo URL into the GitHub Repository field — for example,
https://github.com/Gbengard/blue-green-ecs-deployment— and your branch name, for example,main - Environment image: Managed image
- Operating system: Amazon Linux
- Runtime: Standard
- Image: use the latest available standard image
- Service role: Existing service role → select
bluegreen-demo-codebuild-role(the one you built with full permissions back in Step 7 — no need to create a new one here, and nothing to patch afterward) - Check Enable this flag if you want to build Docker images or want your builds to get elevated privileges — required, since building a Docker image inside CodeBuild needs this
- Expand Additional configuration, scroll to Environment variables, and add these four — the buildspec reads them and the build fails (or silently builds a malformed ECR URL) without them:
- `AWS_REGION` = your region, e.g. `us-east-1`. CodeBuild is supposed to provide this automatically as a built-in variable, but in practice it didn't always resolve reliably inside the buildspec's shell commands — setting it explicitly here removes the ambiguity and is cheap insurance against a hard-to-diagnose malformed-URL error. - `AWS_ACCOUNT_ID` = your 12-digit account ID (run `aws sts get-caller-identity --query Account --output text` if you don't have it memorized) - `ECR_REPO_NAME` = `bluegreen-demo-app` - `CONTAINER_NAME` = `bluegreen-demo-container` - Buildspec: choose Use a buildspec file, path
pipeline-config/buildspec.yml - Click Create build project
That's it.
Step 10 — CodePipeline
- Go to the CodePipeline console → click Create pipeline
- Choose Build custom pipeline → click Next
- Pipeline name:
bluegreen-demo-pipeline - Execution mode: leave as Queued → click Next
- Source provider: GitHub (via OAuth App)
- Click Connect to GitHub with OAuth — this opens the authorization screen; a person has to click through this manually
- Once connected, choose your repository and branch → click Next
- Build provider: AWS CodeBuild, project:
bluegreen-demo-build→ click Next twice - Deploy provider: Amazon ECS (not "Amazon ECS (Blue/Green)" — that older option is the CodeDeploy-powered one; plain Amazon ECS is what you want here, since the service itself already handles the blue/green shift)
- Cluster name:
bluegreen-demo-cluster - Service name:
bluegreen-demo-service - Image definitions file:
imagedefinitions.json→ click Next -
Review everything → click Create pipeline
The pipeline triggers automatically
Go to the ECS service → Deployments, and when it asks you to Take Action, click it, then click Continue
Wait until the deployment and the pipeline both show as successful<...
























