1. Introduction
Nowadays, applications become more and more based on microservices, because the promise is to scale them easily, deploy each of them independently, and so have a faster development cycle. The architecture is built on containerization as a primitive, enabling teams to isolate their workloads and deploy them uniformly across environments.
But with the growing number of services, deployment pipelines tend to become:
- Slow (like updating all services on every commit)
- Hard to manage (multiple pipelines for a single service)
- This was causing problems (manual deployments, Docker images being inconsistent, etc.).
This is where AWS Elastic Container Service (ECS) Fargate and Amazon ECR, combined with GitLab CI/CD, form an amazing partnership.
In this guide, we will create an actual production-ready microservices deployment system with:
- ECS Fargate for serverless containers
- Five microservices: billing, client_app_api, driver_api, odmu_auth, odmu_contractual
- Amazon ECR as an image registry for containers
- ALB (Application Load Balancer) with multiple target groups
- GitLab CI/CD with folder-based triggers
- Terraform for infrastructure as code
- CloudWatch for observability
By the end, you’ll have:
✔ Independent deployments per microservice
✔ Folder-based Automated CI/CD
✔ Production-grade ECS services
✔ Scalable, secure, monitored architecture
✔ Clean GitLab pipeline without any manual SSH or PM2
2. ARCHITECTURE:
| Component | Purpose |
|---|---|
| ECS Fargate | Serverless containers (no EC2 management) |
| ECR | Private container registry |
| ALB | Routes incoming traffic to the right microservice |
| CloudWatch | Logs and metrics |
| Terraform | Infrastructure provisioning |
| GitLab CI/CD | Automated container build and deployment |
Why ECS Fargate?
- No EC2 provisioning or patching
- Auto-scaling built-in
- Secure (tasks get their own ENI inside VPC)
- Ideal for microservices
3. Microservices Structure
Your repo is structured like this:
services/
├── billing/
├── client_app_api/
├── driver_api/
├── odmu_auth/
└── odmu_contractual/
.gitlab-ci.yml
terraform/
Each microservice has:
- Its own Dockerfile
- Its own business logic
- Its own task definition
- Its own ECS service
4. Dockerizing the Microservices
Example Dockerfile for client_app_api:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Use different ports for each microservice:
Service Port
billing 3001
client_app_api 3000
driver_api 3002
odmu_auth 3003
odmu_contractual 3004
5. Creating ECR Repositories
Using a sample AWS account:
Account ID: 745812456855
Region: us-east-1
aws ecr create-repository --repository-name billing
aws ecr create-repository --repository-name client_app_api
aws ecr create-repository --repository-name driver_api
aws ecr create-repository --repository-name odmu_auth
aws ecr create-repository --repository-name odmu_contractual
Build and Push (example: billing)
docker build -t billing ./services/billing
docker tag billing:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/billing:latest
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS \
--password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
docker push \
123456789012.dkr.ecr.us-east-1.amazonaws.com/billing:latest
6. ECS Fargate: Task Definition (Terraform)
Example for the billing microservice:
resource "aws_ecs_task_definition" "billing" {
family = "billing-task"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "512"
memory = "1024"
execution_role_arn = "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
container_definitions = jsonencode([
{
name = "billing"
image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/billing:latest"
portMappings = [{
containerPort = 3001
protocol = "tcp"
}]
essential = true
}
])
}
Repeat for all microservices.
7. Application Load Balancer Routing
Each microservice gets a separate Target Group:
| Service | Target Group Name | Path Pattern |
|---|---|---|
| billing | tg-billing | /billing/* |
| client_app_api | tg-client | /client/* |
| driver_api | tg-driver | /driver/* |
| odmu_auth | tg-auth | /auth/* |
| odmu_contractual | tg-contract | /contract/* |
Terraform example:
resource "aws_lb_target_group" "billing" {
name = "tg-billing"
port = 3001
protocol = "HTTP"
target_type = "ip"
vpc_id = var.vpc_id
}
- GitLab CI/CD: Folder-Based Deployments This is the most important section. Instead of running one giant pipeline or manually using SSH/PM2, you configure GitLab to only deploy the service that changed.
stages:
- build
- deploy
# ----------------------------
# Billing Microservice
# ----------------------------
build_billing:
stage: build
only:
changes:
- services/billing/**
script:
- docker build -t billing ./services/billing
- docker tag billing:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/billing:latest
- aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
- docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/billing:latest
deploy_billing:
stage: deploy
only:
changes:
- services/billing/**
script:
- aws ecs update-service \
--cluster production-cluster \
--service billing-service \
--force-new-deployment
9. Monitoring & Observability (CloudWatch)
Logs appear under:
/ecs/billing
/ecs/client_app_api
/ecs/driver_api
/ecs/odmu_auth
/ecs/odmu_contractual
Create log groups:
aws logs create-log-group --log-group-name /ecs/billing
aws logs put-retention-policy --log-group-name /ecs/billing --retention-in-days 7
CPU Alarm Example:
aws cloudwatch put-metric-alarm \
--alarm-name BillingCPUHigh \
--metric-name CPUUtilization \
--namespace AWS/ECS \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--dimensions Name=ServiceName,Value=billing-service \
--evaluation-periods 2 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:NotifyTeam
10. Lessons Learned:
Here are some of the lessons I learned from building this system:
✔ Keep microservices fully isolated
The Docker images, task definitions and ALB routes need to be decoupled.
✔ Use folder-based CI/CD
It reduces deployment times dramatically.
✔ Choose ECR instead of using public registries
It integrates seamlessly with ECS.
✔ Allocate the right CPU & Memory
Under-provisioning leads to throttling.
Over-provisioning wastes money.
✔ Terraform modules save weeks
When multiple environment setups are in question, reusability is the key.
✔ You’ll get by with just CloudWatch for 80% of your monitoring
Later, add Prometheus or Grafana Cloud.
11. Conclusion
Containers are the foundation of modern infrastructure, and AWS ECS Fargate enables you to run microservices without managing servers. Mixed with GitLab CI/CD and Terraform, you have a scalable, automated, production-grade workflow.
This architecture ensures:
- Fast, independent deployments
- Secure and isolated workloads
- ALB + Fargate for high availability
- Complete observability via CloudWatch
- Infrastructure is defined as code
Here’s a system that grows with you—from 5 microservices all the way to 50.
If you apply to AWS Community Builders, this post shows (with clear evidence):
✔ Practical hands-on AWS experience
✔ Experience with containers, CI/CD and IaC
✔ Design of production architects
✔ Real, actionable implementations
If you enjoyed this post, please share it on your favourite social network and get in touch if I can help with anything. Feel free to reach out if you’d like to chat about AWS, devops, or container architectures.

Top comments (0)