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?
- Project Overview
- Step 1: Project Structure
- Step 2: Dockerize the App
- Step 3: Task Definition (taskdef.json)
- Step 4: GitLab CI/CD Pipeline
- Step 5: GitLab CI/CD Environment Variables
- Step 6: How Blue/Green Works with Fargate
- Conclusion
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.

What you’ll get:
- Docker app with
/healthcheck .gitlab-ci.ymlpipeline- 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:
| Key | Description |
|---|---|
AWS_ACCESS_KEY_ID | Your IAM access key |
AWS_SECRET_ACCESS_KEY | Your IAM secret key |
AWS_ACCOUNT_ID | Your AWS account number |
ECR_REPO | Your ECR repo name (e.g., ecs-app) |
CODEDEPLOY_APP | CodeDeploy Application Name |
CODEDEPLOY_GROUP | Deployment Group Name |
TEST_LB_DNS | ALB 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

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.