DEV Community

Cover image for Deploying Microservices (Python+Nodejs) to AWS ECS Fargate Using OpenTofu + Docker Hub + ALB (Complete Step-by-Step Guide)
Latchu@DevOps
Latchu@DevOps

Posted on

Deploying Microservices (Python+Nodejs) to AWS ECS Fargate Using OpenTofu + Docker Hub + ALB (Complete Step-by-Step Guide)

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
Enter fullscreen mode Exit fullscreen mode

🧩 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"));
Enter fullscreen mode Exit fullscreen mode

services/user-service/package.json

{
  "name": "user-service",
  "version": "1.0.0",
  "main": "app.js",
  "dependencies": {
    "express": "^5.1.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

services/order-service/requirements.txt

Flask==2.2.5
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ 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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ” 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"
}
Enter fullscreen mode Exit fullscreen mode

πŸš€ 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
  ]
}
Enter fullscreen mode Exit fullscreen mode

πŸ”§ 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}"
Enter fullscreen mode Exit fullscreen mode

🏒 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
}
Enter fullscreen mode Exit fullscreen mode

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
  }))
}
Enter fullscreen mode Exit fullscreen mode

/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"
  }
}
Enter fullscreen mode Exit fullscreen mode

🏁 Deploy the Entire Architecture

cd /microservices-ecs-tofu/tofu
tofu init
tofu fmt
tofu validate
tofu plan
tofu apply
Enter fullscreen mode Exit fullscreen mode

If you can check with ECS Cluster in AWS Console,

1

The Load balancer has been created

2

The path based routing in ALB

3

Target group too Healthy

4

If you try to access the ALB

5


Destroy

Just destroy the tofu once its completed

tofu destroy
Enter fullscreen mode Exit fullscreen mode

🌟 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)