This content originally appeared on DEV Community and was authored by Latchu@DevOps
In this article, Iβll walk you through deploying a Python Flask application on AWS ECS Fargate, fully automated using OpenTofu, Docker, Trivy scanning, and an Application Load Balancer (ALB).
Youβll get:
Full project structure
Complete OpenTofu code
Dockerfile + Flask App
Build + Scan + Push pipeline using Trivy
Automatic ECS deployment behind ALB
Public access via ALB URL
Let’s get started! 
Project Structure
pythonapp/
βββ tofu
βββ Dockerfile
βββ app
β βββ app.py
β βββ requirements.txt
βββ ecs.tf
βββ iam.tf
βββ main.tf
βββ network.tf
βββ scripts
β βββ build_scan_push.sh
βββ terraform.tfstate
βββ terraform.tfstate.backup
βββ variables.tf
Flask Application
app/app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello from Secure Python App!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
app/requirements.txt
Flask==2.2.5
Dockerfile (Python Slim + Flask App)
Dockerfile
FROM python:3.10-slim
WORKDIR /app
COPY app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ .
EXPOSE 5000
CMD ["python", "app.py"]
Build β Scan β Push Script (Trivy Security Scan)
scripts/build_scan_push.sh
#!/bin/bash
IMAGE_NAME=$1
IMAGE_TAG=$2
DOCKER_USER=$3
DOCKER_PASS=$4
REPORT_PATH="/home/ubuntu/trivy-report.txt"
echo "=== Building Docker image ==="
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" .
echo "=== Running Trivy scan ==="
trivy image --exit-code 1 --severity HIGH,CRITICAL "${IMAGE_NAME}:${IMAGE_TAG}" > "$REPORT_PATH" 2>&1
SCAN_STATUS=$?
echo "=== Scan report stored at $REPORT_PATH ==="
if [ $SCAN_STATUS -ne 0 ]; then
echo "❌ Trivy scan failed β image NOT pushed!"
exit 1
fi
echo "=== Logging in to Docker Hub ==="
echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin
echo "=== Tagging image ==="
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "=== Pushing image ==="
docker push "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "✔ Scan passed β image pushed successfully!"
variables.tf
variable "image_name" {
type = string
}
variable "image_tag" {
type = string
}
variable "docker_username" {
type = string
}
variable "docker_password" {
type = string
sensitive = true
}
variable "aws_region" {
type = string
default = "ap-south-1"
}
variable "app_port" {
type = number
default = 5000
}
VPC + Networking Setup
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 Roles for ECS
iam.tf
resource "aws_iam_role" "ecs_task_execution" {
name = "ecsTaskExecutionRole11"
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"
}
ECS Cluster, Task Definition, ALB, and Service
ecs.tf
resource "aws_ecs_cluster" "main" {
name = "secure-cluster"
}
resource "aws_lb" "app_lb" {
name = "app-lb"
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_target_group" "app_tg" {
name = "app-tg"
port = 5000
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"
}
resource "aws_lb_listener" "http_listener" {
load_balancer_arn = aws_lb.app_lb.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app_tg.arn
}
}
resource "aws_ecs_task_definition" "app" {
family = "python-secure-app"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
network_mode = "awsvpc"
execution_role_arn = aws_iam_role.ecs_task_execution.arn
container_definitions = jsonencode([{
name = "web"
image = "${var.docker_username}/${var.image_name}:${var.image_tag}"
portMappings = [{
containerPort = 5000
protocol = "tcp"
}]
}])
}
resource "aws_ecs_service" "app_service" {
name = "secure-python-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.public_1.id, aws_subnet.public_2.id]
security_groups = [aws_security_group.ecs_sg.id]
assign_public_ip = true
}
load_balancer {
target_group_arn = aws_lb_target_group.app_tg.arn
container_name = "web"
container_port = 5000
}
depends_on = [aws_lb_listener.http_listener]
}
Main OpenTofu File (Build β Scan β Push β Deploy)
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.aws_region
}
resource "null_resource" "build_scan_push" {
provisioner "local-exec" {
command = <<EOT
bash ./scripts/build_scan_push.sh \
${var.image_name} \
${var.image_tag} \
${var.docker_username} \
${var.docker_password}
EOT
}
}
resource "null_resource" "build_complete" {
depends_on = [null_resource.build_scan_push]
}
Deploy
Run:
tofu init
tofu fmt
tofu validate
tofu plan
tofu apply
Youβll be asked:
var.image_name
var.image_tag
var.docker_username
var.docker_password
Enter your Docker Hub details and image tag (like v1).
If you can check with ECS Cluster,
If you check with container image,
If you check with Load balancer,
After apply completes, open your ALB URL:
http://<alb-dns-name>
You should see:
**Hello from Secure Python App!**
Final Thoughts
With this setup you now have:
Fully automated Docker build
Trivy vulnerability scanning
Auto-push to Docker Hub
ECS Fargate deployment
ALB with public access
Infrastructure maintained via OpenTofu
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
This content originally appeared on DEV Community and was authored by Latchu@DevOps





