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






Top comments (0)