Last updated on April 28th, 2026 at 12:09 pm

I set up this deployment pipeline using GitLab CI/CD with Amazon ECS Fargate and CodeDeploy to achieve zero-downtime deployments.

While the overall setup works well, there are a few parts that are not obvious when you first try it – especially around task definition updates and how deployments are triggered.

In this guide, I’ll walk through the setup and highlight the parts that can cause issues.


Why ECS Fargate with GitLab?

  • No server management – Fargate is fully managed.
  • GitLab CI/CD automates your build and deployment pipeline.
  • Blue/Green deployment ensures zero-downtime updates with rollback safety.

Project Overview

We’ll create a simple Flask app, containerize it, push it to Amazon ECR, and use GitLab to deploy it to ECS Fargate with CodeDeploy Blue/Green.

deploy docker ecs blue green from gitlab
deploy docker ecs blue green from gitlab

What you’ll get:

  • Docker app with /health check
  • .gitlab-ci.yml pipeline
  • Blue/Green deployment with ALB test listener
  • Automatic traffic shifting after health validation

Where things get tricky

The overall flow looks simple, but there are a few points where things can break:

  • The ECS service doesn’t automatically pick up a new image unless a new task definition is registered
  • CodeDeploy requires the correct AppSpec format, otherwise deployments fail silently
  • The GitLab pipeline can succeed even if the deployment does not fully complete

These issues are easy to miss when following a basic setup.

Step 1: Project Structure

Your project might look like this:

ecs-fargate-app/
├── Dockerfile
├── app.py
├── taskdef.json
├── .gitlab-ci.yml

Step 2: Dockerize the App

Dockerfile

FROM python:3.9-slim
COPY app.py .
RUN pip install flask
CMD ["python", "app.py"]

app.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def home():
return "Hello from ECS!"

@app.route('/health')
def health():
return "OK", 200

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

Step 3: Task Definition (taskdef.json)

Update the image placeholder later in the GitLab pipeline.

{
"family": "my-task",
"containerDefinitions": [
{
"name": "my-container",
"image": "<IMAGE_URI>",
"portMappings": [
{
"containerPort": 80,
"protocol": "tcp"
}
],
"essential": true
}
]
}

Step 4: GitLab CI/CD Pipeline

Create .gitlab-ci.yml in your root directory:

stages:
- build
- deploy
- validate

image: docker:24.0.5

services:
- docker:24.0.5-dind

variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
AWS_DEFAULT_REGION: us-east-1
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
ECR_URI: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_REPO

before_script:
- apk add --no-cache curl bash python3 py3-pip jq
- pip3 install awscli
- aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URI

build:
stage: build
script:
- docker build -t $ECR_REPO:$IMAGE_TAG .
- docker tag $ECR_REPO:$IMAGE_TAG $ECR_URI:$IMAGE_TAG
- docker push $ECR_URI:$IMAGE_TAG

deploy:
stage: deploy
script:
- |
sed "s|<IMAGE_URI>|$ECR_URI:$IMAGE_TAG|g" taskdef.json > rendered-taskdef.json

- |
TASK_DEF_ARN=$(aws ecs register-task-definition --cli-input-json file://rendered-taskdef.json | jq -r '.taskDefinition.taskDefinitionArn')

- |
cat <<EOF > appspec.json
{
"version": 1,
"Resources": [
{
"TargetService": {
"Type": "AWS::ECS::Service",
"Properties": {
"TaskDefinition": "$TASK_DEF_ARN",
"LoadBalancerInfo": {
"ContainerName": "my-container",
"ContainerPort": 80
}
}
}
}
]
}
EOF

- |
APPSPEC_CONTENT=$(jq -Rs . < appspec.json)
aws deploy create-deployment \
--application-name "$CODEDEPLOY_APP" \
--deployment-group-name "$CODEDEPLOY_GROUP" \
--revision revisionType=AppSpecContent,appSpecContent="{\"content\": $APPSPEC_CONTENT, \"sha256\": \"\"}"

validate:
stage: validate
script:
- |
echo "Validating health endpoint on test listener..."
for i in {1..10}; do
curl -f http://$TEST_LB_DNS/health && break || sleep 5
done
echo "App passed health check"

Step 5: GitLab CI/CD Environment Variables

Go to your GitLab project ➝ Settings ➝ CI/CD ➝ Variables and add:

KeyDescription
AWS_ACCESS_KEY_IDYour IAM access key
AWS_SECRET_ACCESS_KEYYour IAM secret key
AWS_ACCOUNT_IDYour AWS account number
ECR_REPOYour ECR repo name (e.g., ecs-app)
CODEDEPLOY_APPCodeDeploy Application Name
CODEDEPLOY_GROUPDeployment Group Name
TEST_LB_DNSALB test listener DNS name

Step 6: How Blue/Green Works with Fargate

In this setup, GitLab pushes a new Docker image and triggers a deployment through CodeDeploy.

CodeDeploy then creates a new task set (green version) and runs health checks before shifting traffic.

One thing I noticed is that even if the pipeline completes, the traffic shift depends entirely on the health checks passing — so it’s important that the /health endpoint is reliable.

  • GitLab pipeline pushes a new Docker image to ECR
  • Registers a new ECS Task Definition
  • Creates a new deployment via CodeDeploy
  • ECS launches new task behind the test target group
  • ALB health checks the new version via /health
  • If healthy, CodeDeploy shifts traffic to green version
CodeDeploy Blue Green Validation
CodeDeploy Blue Green Validation

No downtime. No stress.


Conclusion

If your application is deployed on Kubernetes but traffic still does not reach it, the issue may not be networking. In many cases, the Service has no endpoints because it is not connected to any Pods.

By combining GitLab CI/CD with ECS Fargate and Blue/Green deployments, you get a modern, scalable, and safe deployment pipeline. Even though Fargate limits lifecycle hooks, its simplicity and scalability make it perfect for most workloads.