1. Introduction
Nowadays, applications are moving towards microservices because of their scalability, rollback deployment, and faster development.
But with the growing number of services, deployment pipelines have 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.).
I combined AWS Elastic Container Service (ECS) Fargate and Amazon ECR, combined with GitLab CI/CD, which made the pipeline deploy only the service which was changed.
In this guide, I have created 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 monitoring
By the end, you’ll have:
- Independent deployments per microservice
- Folder-based Automated CI/CD
- Production grade ECS services
- Scalable, secure, monitored architecture
- No manual SSH or PM2
The Real Problem
When I was previously working with ECS, every change in code triggered a full redeployment of all services, although the change was in one service folder. It caused unnecessary downtime and more deployment times, and rollback was risky. I also faced Docker image drift in various environments.
I found a solution
- Only the service that changes is rebuilt and deployed
- Each service remains fully isolated
- Deployments are repeatable and predictable
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
- Suits for microservices
3. Microservices Structure
Repo Structure:
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"]
I used 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
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 \
745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS \
--password-stdin 745812456855.dkr.ecr.us-east-1.amazonaws.com
docker push \
745812456855.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::745812456855:role/ecsTaskExecutionRole"
container_definitions = jsonencode([
{
name = "billing"
image = "745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest"
portMappings = [{
containerPort = 3001
protocol = "tcp"
}]
essential = true
}
])
}
Similar type for all microservices.
7. Application Load Balancer Routing
Each microservice has 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/* |
Security Considerations
Security was a first-class concern in this design.
- Each ECS task runs with VPC networking. Each ECS task has ENI-level network isolation.
- Containers cannot be accessed via SSH; therefore, all deployments take place through CI/CD.
- IAM task execution roles are created following the principle of least privilege.
- All container images are stored in private ECR repositories on Amazon.
- The ALB serves as the only public access point to ECS, and all services remain in a private subnet inside the VPC.
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, I configured the GitLab yml file 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 745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest
- aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 745812456855.dkr.ecr.us-east-1.amazonaws.com
- docker push 745812456855.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:
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:745812456855:NotifyTeam
Effects and Outcomes of Implementing New Architecture
By deploying this architecture I was able to:
- Reduce deployment time, since only those services that had their code changed must be rebuilt.
- Allow teams to deploy their microservices independently of one another.
- No need to deploy via SSH using PM2.
- Create a more predictable and safer rollback process.
- Provide for reuse of the same Terraform modules in all target environments.
With the increasing number of services, it became clear that this architecture can be scaled successfully.
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 so much.
✔ Choose ECR instead of using public registries
It works well 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.
✔ With this CloudWatch setu you get 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 with Fargate for high availability
- Complete observability via CloudWatch
- Infrastructure is defined as code
If you are deploying microservices on AWS and experiencing slow or high risk deployments, this might be a good working solution validated across real-world environments with the increasing number of services.
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)