Modern applications often consist of multiple microservices running independently. In this guide, we will deploy two microservices (Node.js + Python Flask) to AWS ECS Fargate, automatically:
- Building Docker images
- Scanning with Trivy
- Pushing to Docker Hub
- Deploying to ECS Fargate with Application Load Balancer
- Routing paths /users and /orders
All using OpenTofu (Terraform alternative).
π Project Structure
microservices-ecs-tofu/
βββ services/
β βββ user-service/
β β βββ Dockerfile
β β βββ app.js
β β βββ package.json
β β βββ node_modules/...
β βββ order-service/
β βββ Dockerfile
β βββ app.py
β βββ requirements.txt
βββ tofu/
βββ ecs.tf
βββ iam.tf
βββ network.tf
βββ main.tf
βββ terraform.tfvars
βββ variables.tf
βββ scripts/
βββ build_scan_push.sh
π§© Microservices Source Code
1οΈβ£ User Service (Node.js + Express)
services/user-service/app.js
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("User Service Running!");
});
app.get("/users", (req, res) => {
res.json([
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" }
]);
});
app.listen(3000, () => console.log("User Service on port 3000"));
services/user-service/package.json
{
"name": "user-service",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^5.1.0"
}
}
services/user-service/Dockerfile
FROM node:20-bookworm-slim
WORKDIR /app
COPY package.json .
RUN npm install --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
2οΈβ£ Order Service (Python + Flask)
services/order-service/app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return "Order Service Running!"
@app.route("/orders")
def orders():
return jsonify([
{ "id": 101, "item": "Laptop" },
{ "id": 102, "item": "Phone" }
])
if __name__ == "__main__":
app.run(host="0.0.0.0", port=4000)
services/order-service/requirements.txt
Flask==2.2.5
services/order-service/Dockerfile
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 4000
CMD ["python", "app.py"]
ποΈ OpenTofu Infrastructure Code
Below is the full infrastructure:
/microservices-ecs-tofu/tofu/network.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
}
resource "aws_subnet" "public_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-south-1a"
map_public_ip_on_launch = true
}
resource "aws_subnet" "public_2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-south-1b"
map_public_ip_on_launch = true
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
}
resource "aws_route_table_association" "public_1" {
subnet_id = aws_subnet.public_1.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_2" {
subnet_id = aws_subnet.public_2.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "ecs_sg" {
name = "ecs-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 5000
to_port = 5000
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
π IAM Role for ECS Tasks
/microservices-ecs-tofu/tofu/iam.tf
resource "aws_iam_role" "ecs_task_execution" {
name = "ecsTaskExecutionRole111"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ecs_task_execution_policy" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
π Build β Scan β Tag β Push (Docker + Trivy)
/microservices-ecs-tofu/tofu/main.tf
resource "null_resource" "build_scan_push" {
for_each = var.services
provisioner "local-exec" {
command = <<EOT
bash ./scripts/build_scan_push.sh \
${each.key} \
${each.value.image_name} \
${each.value.image_tag} \
${var.docker_username} \
${var.docker_password}
EOT
}
}
resource "null_resource" "build_complete" {
depends_on = [
null_resource.build_scan_push
]
}
π§ Trivy + Docker Push Script
/microservices-ecs-tofu/tofu/scripts/build_scan_push.sh
#!/bin/bash
SERVICE_NAME=$1
IMAGE_NAME=$2
IMAGE_TAG=$3
DOCKER_USER=$4
DOCKER_PASS=$5
if [ $# -ne 5 ]; then
echo "Usage: ./build_scan_push.sh <service> <image_name> <image_tag> <docker_user> <docker_pass>"
exit 1
fi
cd "$(dirname "$0")/../../services/$SERVICE_NAME" || exit 1
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" .
trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--exit-code 0 \
"${IMAGE_NAME}:${IMAGE_TAG}"
echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_TAG}"
docker push "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_TAG}"
π’ ECS Cluster + ALB + Services
/microservices-ecs-tofu/tofu/ecs.tf
resource "aws_ecs_cluster" "main" {
name = "microservices-cluster"
}
resource "aws_lb" "main" {
name = "microservice-alb"
load_balancer_type = "application"
security_groups = [aws_security_group.ecs_sg.id]
subnets = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Microservices Running!"
status_code = "200"
}
}
}
locals {
service_list = var.services
}
Target groups + Listener rules + ECS tasks + ECS services
π Variables + tfvars
/microservices-ecs-tofu/tofu/variables.tf
variable "docker_username" { type = string }
variable "docker_password" { type = string, sensitive = true }
variable "aws_region" {
type = string
default = "ap-south-1"
}
variable "services" {
type = map(object({
image_name = string
image_tag = string
port = number
path = string
}))
}
/microservices-ecs-tofu/tofu/terraform.tfvars
docker_username = "latchudevops"
docker_password = "********"
services = {
user-service = {
image_name = "user-service"
image_tag = "v1"
port = 3000
path = "/users"
}
order-service = {
image_name = "order-service"
image_tag = "v1"
port = 4000
path = "/orders"
}
}
π Deploy the Entire Architecture
cd /microservices-ecs-tofu/tofu
tofu init
tofu fmt
tofu validate
tofu plan
tofu apply
If you can check with ECS Cluster in AWS Console,
The Load balancer has been created
The path based routing in ALB
Target group too Healthy
If you try to access the ALB
Destroy
Just destroy the tofu once its completed
tofu destroy
π Thanks for reading! If this post added value, a like β€οΈ, follow, or share would encourage me to keep creating more content.
β Latchu | Senior DevOps & Cloud Engineer
βοΈ AWS | GCP | βΈοΈ Kubernetes | π Security | β‘ Automation
π Sharing hands-on guides, best practices & real-world cloud solutions





Top comments (0)