Terraform Practical Guide (14)

Deploying a Serverless Python Flask App on AWS ECS Fargate Using Terraform

Introduction

Are you ready to take your Python Flask app to the next level by deploying it serverless on AWS ECS Fargate? In this blog, we’ll walk you through the entire process of deploying a Python Flask app on AWS ECS Fargate using Terraform. This approach allows you to leverage the power of containerized applications and the flexibility of serverless architecture, making your deployments more efficient and scalable.

With this step-by-step guide, you’ll learn how to automate the deployment of your Flask app on AWS Fargate. We’ll cover everything from setting up your AWS environment and writing Terraform scripts to deploying and managing your Flask app in a serverless manner. By the end of this blog, you’ll have a solid understanding of using Terraform to deploy and manage Python Flask applications on AWS ECS Fargate, ensuring a smooth and automated workflow for your cloud infrastructure.

Prerequisites

Before we start, ensure you have the following prerequisites:

  1. AWS Account: An AWS account to deploy the resources.

  2. Terraform Installed: Install Terraform on your local machine. You can download it from Terraform’s official site.

  3. AWS CLI Configured: Configure the AWS CLI with your AWS credentials to allow Terraform to communicate with AWS.

  4. Docker Installed: Install Docker to build and test your application locally.

Diagrammatic Representation

1. Develop the Python Flask App

Create a file named app.py with the following content:

from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/')
def home():
    html_content = """
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Your YouTube Showcase</title>
        <style>
            header {
                background-color: #ff0000; /* Customize with your channel's color */
                color: #ffffff;
                text-align: center;
                padding: 2rem;
            }
            /* Reset some default styles */
            body, h1, p {
                margin: 0;
                padding: 0;
            }

            /* Set a background color or image */
            body {
                background-color: #f5f5f5;
                font-family: Arial, sans-serif;
            }

            /* Center align content */
            .container {
                max-width: 1200px;
                margin: 0 auto;
                padding: 2rem;
                text-align: center;
            }

            .video-container, .playlist-container, .shorts-container {
                display: flex;
                justify-content: center;
                gap: 1rem;
            }

            .video, .playlist, .short {
                flex: 1;
                border: 1px solid #ddd;
                padding: 1rem;
                background-color: #ffffff;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }
            /* Footer section */
            footer {
                text-align: center;
                padding: 1rem;
                background-color: #333;
                color: #ffffff;
            }
        </style>
    </head>
    <body>
        <header>
            <h1>DheerajTechInsight</h1>
            <p class="channel-description">Welcome to my channel! Subscribe for awesome content.</p>
        </header>
        <div class="container">
            <h1>Popular Videos</h1>
            <br>
            <!-- Popular YouTube Videos -->
            <div class="video-container">
                <div class="video">
                    <iframe width="560" height="315" src="https://www.youtube.com/embed/trFW03zP7Uw?si=9cXiME0hC3dwH6mi" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="video">
                    <iframe width="560" height="315" src="https://www.youtube.com/embed/lwyr6E5kaQA?si=UsV5cHJLdiDPPpqy" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="video">
                    <iframe width="560" height="315" src="https://www.youtube.com/embed/hnfeyKNJ4pM?si=qyZNMHxI5-wJYmVa" frameborder="0" allowfullscreen></iframe>
                </div>
            </div>

            <!-- Popular YouTube Playlists -->
            <br>
            <h1>Popular Playlists</h1>
            <br>
            <div class="playlist-container">
                <div class="video">
                    <iframe width="560" height="315" src="https://www.youtube.com/embed/videoseries?si=YLd39R6zYB6a7VuV&amp;list=PLz8JBMMd7yjWMJ0YeOkfnTohirAZT_pBU" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="video">
                    <iframe width="560" height="315" src="https://www.youtube.com/embed/videoseries?si=pmR7Ep3IH_M1UkO7&amp;list=PLz8JBMMd7yjUukUG1M78ypP9GjWaP8rqf" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="video">
                    <iframe width="560" height="315" src="https://www.youtube.com/embed/videoseries?si=m707kduCb5UYnUmV&amp;list=PLz8JBMMd7yjWA5qpXSVAcbi-_u9n6d7uw" frameborder="0" allowfullscreen></iframe>
                </div>
            </div>

            <!-- Popular YouTube Shorts -->
            <br>
            <h1>Popular Shorts</h1>
            <br>
            <div class="shorts-container">
                <div class="short">
                    <iframe width="315" height="560" src="https://youtube.com/embed/73toBt5R4zE?si=4iMebKKwOPyoUcYR" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="short">
                    <iframe width="315" height="560" src="https://youtube.com/embed/L75o4sa7iXQ?si=tJv6_90r3CINw6uW" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="short">
                    <iframe width="315" height="560" src="https://youtube.com/embed/LKysazmlDxk?si=MYycCA2N1tNzNoUH" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="short">
                    <iframe width="315" height="560" src="https://youtube.com/embed/4OfEN3XGcic?si=10IE-7djtqcZuHnn" frameborder="0" allowfullscreen></iframe>
                </div>
                <div class="short">
                    <iframe width="315" height="560" src="https://youtube.com/embed/AgGPGujpJCo?si=TVZ5CmamRWv5NPXa" frameborder="0" allowfullscreen></iframe>
                </div>
            </div>
        </div>
        <footer>
            © 2024 DheerajTechInsight | All rights reserved
        </footer>
    </body>
    </html>
    """
    return render_template_string(html_content)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

This simple Flask application will serve a welcome message on the root endpoint.

2. Develop the Dockerfile for the App

Create a file named Dockerfile in the same directory as app.py:

# Use the official Python image
FROM python:3-alpine3.15

# Set the working directory in the container
WORKDIR /app

# Copy the application files to the container
COPY app.py /app/

# Install dependencies
RUN pip install flask

# Expose port 80 for the Flask app
EXPOSE 80

# Command to run the application
CMD ["python", "app.py"]

This Dockerfile specifies the steps to containerize the Flask app using a lightweight Python image.

3. Build and Test the Application Locally

Build the Docker image:

docker build -t my-python-app .

Run a container from the image:

docker run -p 80:80 my-python-app

Visit http://localhost in your browser to see the Flask app running and it will display like below

4. Deploy AWS Networking Landscape

Use the following Terraform code to set up the networking infrastructure:

# Data source to get the current AWS account ID
data "aws_caller_identity" "current" {}

#---------------------Create Custom VPC----------------------
resource "aws_vpc" "CustomVPC" {
  cidr_block           = "10.0.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "CustomVPC"
  }
}
#---------------------Create IGW And associate with VPC----------------------
resource "aws_internet_gateway" "IGW" {
  vpc_id = aws_vpc.CustomVPC.id

  tags = {
    Name = "IGW"
  }
}
#---------------------Create 2 Public Subnets----------------------
resource "aws_subnet" "PublicSubnet1" {
  vpc_id                  = aws_vpc.CustomVPC.id
  cidr_block              = "10.0.0.0/18"
  availability_zone       = "us-west-2a"
  map_public_ip_on_launch = true

  tags = {
    Name = "PublicSubnet1"
    Type = "Public"
  }
}
resource "aws_subnet" "PublicSubnet2" {
  vpc_id                  = aws_vpc.CustomVPC.id
  cidr_block              = "10.0.64.0/18"
  availability_zone       = "us-west-2b"
  map_public_ip_on_launch = true

  tags = {
    Name = "PublicSubnet2"
    Type = "Public"
  }
}
#---------------------Create Custom Public route table----------------------
resource "aws_route_table" "PublicRouteTable" {
  vpc_id = aws_vpc.CustomVPC.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.IGW.id
  }
  tags = {
    Name = "PublicRouteTable"
  }
}
#---------------------Create route table association with public route table----------------------
resource "aws_route_table_association" "PublicSubnetRouteTableAssociation1" {
  subnet_id      = aws_subnet.PublicSubnet1.id
  route_table_id = aws_route_table.PublicRouteTable.id
}

resource "aws_route_table_association" "PublicSubnetRouteTableAssociation2" {
  subnet_id      = aws_subnet.PublicSubnet2.id
  route_table_id = aws_route_table.PublicRouteTable.id
}

#----------------------Create Target Group--------------------------
resource "aws_lb_target_group" "CustomTG" {
  name        = "CustomTG"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.CustomVPC.id
  target_type = "ip"

  health_check {
    healthy_threshold   = "3"
    interval            = "30"
    protocol            = "HTTP"
    matcher             = "200"
    timeout             = "3"
    path                = "/"
    unhealthy_threshold = "2"
  }
}

#---------------------Create Security Group For Load Balancer----------------------
resource "aws_security_group" "elb_sg" {
  name        = "allow_http_elb"
  description = "Allow http inbound traffic for elb"
  vpc_id      = aws_vpc.CustomVPC.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }
  tags = {
    Name = "terraform-elb-security-group"
  }
}

# Fetch subnet ids & Create a Load Balancer & ELB Listener
data "aws_subnets" "GetSubnet" {
  depends_on = [aws_subnet.PublicSubnet1, aws_subnet.PublicSubnet2]
  filter {
    name   = "vpc-id"
    values = [aws_vpc.CustomVPC.id]
  }
  filter {
    name   = "tag:Type"
    values = ["Public"]
  }
}
resource "aws_alb" "CustomELB" {
  name            = "CustomELB"
  depends_on      = [aws_subnet.PublicSubnet1, aws_subnet.PublicSubnet2]
  internal        = false
  security_groups = [aws_security_group.ecs_task_sg.id]
  subnets         = data.aws_subnets.GetSubnet.ids
  tags = {
    Name = "CustomELB"
  }
}
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_alb.CustomELB.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "forward"
    forward {
      target_group {
        arn = aws_lb_target_group.CustomTG.arn
      }
      stickiness {
        enabled  = true
        duration = 28800
      }
    }
  }
}

This code sets up a custom VPC, two public subnets, an internet gateway, and route tables to route internet traffic.

5. Deploy AWS ECS Fargate to Host the Python App

Here is the Terraform code to set up ECS Fargate and deploy the Flask app:

# Set up CloudWatch group and log stream and retain logs for 30 days
resource "aws_cloudwatch_log_group" "cb_log_group" {
  name              = "/ecs/cb-app"
  retention_in_days = 30

  tags = {
    Name = "cb-log-group"
  }
}

resource "aws_cloudwatch_log_stream" "cb_log_stream" {
  name           = "cb-log-stream"
  log_group_name = aws_cloudwatch_log_group.cb_log_group.name
}

# Create an ECR repository
resource "aws_ecr_repository" "my_app" {
  name                 = "my-app"
  image_tag_mutability = "MUTABLE"
  force_delete         = true
}

# Docker Build and Push to ECR
resource "null_resource" "docker_build_push" {
  depends_on = [aws_ecr_repository.my_app]
  provisioner "local-exec" {
    interpreter = ["bash", "-c"]
    command     = <<-EOT
      aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin ${data.aws_caller_identity.current.account_id}.dkr.ecr.us-west-2.amazonaws.com
      docker build -t my-python-app .
      docker tag my-python-app:latest ${data.aws_caller_identity.current.account_id}.dkr.ecr.us-west-2.amazonaws.com/my-app:latest
      docker push ${data.aws_caller_identity.current.account_id}.dkr.ecr.us-west-2.amazonaws.com/my-app:latest
    EOT
  }
}

# Define an IAM role for ECS task execution
resource "aws_iam_role" "custom-ecs-role" {
  name = "custom-ecs-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Action = "sts:AssumeRole",
      Effect = "Allow",
      Principal = {
        Service = "ecs-tasks.amazonaws.com"
      }
    }]
  })

  inline_policy {
    name = "ECRPermissions"
    policy = jsonencode({
      Version = "2012-10-17",
      Statement = [
        {
          Effect = "Allow",
          Action = [
            "ecr:GetAuthorizationToken",
            "ecr:BatchCheckLayerAvailability",
            "ecr:BatchGetImage",
            "ecr:GetDownloadUrlForLayer",
            "ecr:InitiateLayerUpload",
            "ecr:UploadLayerPart",
            "ecr:CompleteLayerUpload",
            "ecr:PutImage"
          ],
          Resource = "*"
        }
      ]
    })
  }

  inline_policy {
    name = "CloudWatchLogsPermissions"
    policy = jsonencode({
      Version = "2012-10-17",
      Statement = [{
        Effect = "Allow",
        Action = [
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        Resource = "*"
      }]
    })
  }
}

resource "aws_ecs_cluster" "my_cluster" {
  name = "my-ecs-cluster"
}

# ECS Task security group
resource "aws_security_group" "ecs_task_sg" {
  name        = "ecs-task-sg"
  description = "Security group for ECS tasks"
  vpc_id      = aws_vpc.CustomVPC.id
  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }
}

# Create an ECS service
resource "aws_ecs_service" "my_service" {
  name            = "my-service"
  cluster         = aws_ecs_cluster.my_cluster.id
  task_definition = aws_ecs_task_definition.my_task.arn
  launch_type     = "FARGATE"
  desired_count   = 1

  network_configuration {
    subnets          = data.aws_subnets.GetSubnet.ids
    security_groups  = [aws_security_group.ecs_task_sg.id]
    assign_public_ip = true
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.CustomTG.arn
    container_name   = "ECS_Container"
    container_port   = 80
  }

  depends_on = [aws_lb_listener.http]
}

# Define the ECS task definition
resource "aws_ecs_task_definition" "my_task" {
  depends_on = [null_resource.docker_build_push]
  family     = "ECS_Task"
  runtime_platform {
    operating_system_family = "LINUX"
    cpu_architecture        = "X86_64"
  }
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  task_role_arn            = aws_iam_role.custom-ecs-role.arn
  execution_role_arn       = aws_iam_role.custom-ecs-role.arn
  cpu                      = 1024
  memory                   = 2048

  container_definitions = jsonencode([{
    name  = "ECS_Container"
    image = "${data.aws_caller_identity.current.account_id}.dkr.ecr.us-west-2.amazonaws.com/my-app:latest"
    portMappings = [{
      containerPort = 80
      hostPort      = 80
      protocol      = "tcp"
    }]
    "logConfiguration" : {
      "logDriver" : "awslogs",
      "options" : {
        "awslogs-group" : "/ecs/cb-app",
        "awslogs-region" : "us-west-2",
        "awslogs-stream-prefix" : "ecs"
      }
    }
  }])
}


# Create an Application Auto Scaling Target
resource "aws_appautoscaling_target" "ecs_service" {
  max_capacity       = 4
  min_capacity       = 1
  resource_id        = "service/${aws_ecs_cluster.my_cluster.name}/${aws_ecs_service.my_service.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

# Create an Application Auto Scaling Policy for Scaling Out
resource "aws_appautoscaling_policy" "scale_out_policy" {
  name               = "scale-out"
  service_namespace  = "ecs"
  resource_id        = aws_appautoscaling_target.ecs_service.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs_service.scalable_dimension
  policy_type        = "StepScaling"

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 60
    metric_aggregation_type = "Average"

    step_adjustment {
      metric_interval_lower_bound = 0
      scaling_adjustment          = 1
    }
  }
}

# Create an Application Auto Scaling Policy for Scaling In
resource "aws_appautoscaling_policy" "scale_in_policy" {
  name               = "scale-in"
  service_namespace  = "ecs"
  resource_id        = aws_appautoscaling_target.ecs_service.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs_service.scalable_dimension
  policy_type        = "StepScaling"

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 60
    metric_aggregation_type = "Average"

    step_adjustment {
      metric_interval_lower_bound = 0
      scaling_adjustment          = -1
    }
  }
}

# CloudWatch Alarm for Scaling Out
resource "aws_cloudwatch_metric_alarm" "scale_out_alarm" {
  alarm_name          = "ecs-scale-out-alarm"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "60"
  statistic           = "Average"
  threshold           = "75"
  alarm_description   = "Alarm for scaling out ECS service"
  dimensions = {
    ClusterName = aws_ecs_cluster.my_cluster.name
    ServiceName = aws_ecs_service.my_service.name
  }

  alarm_actions = [aws_appautoscaling_policy.scale_out_policy.arn]
}

# CloudWatch Alarm for Scaling In
resource "aws_cloudwatch_metric_alarm" "scale_in_alarm" {
  alarm_name          = "ecs-scale-in-alarm"
  comparison_operator = "LessThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "60"
  statistic           = "Average"
  threshold           = "25"
  alarm_description   = "Alarm for scaling in ECS service"
  dimensions = {
    ClusterName = aws_ecs_cluster.my_cluster.name
    ServiceName = aws_ecs_service.my_service.name
  }

  alarm_actions = [aws_appautoscaling_policy.scale_in_policy.arn]
}

This Terraform code sets up an ECR repository, ECS cluster, task definition, and ECS service to deploy the Flask application.

👁‍🗨👁‍🗨 YouTube Tutorial 📽

❗️❗️Important Documentation To Be Viewed❗️❗️

⛔️ Hashicorp Terraform
⛔️ AWS CLI
⛔️ Hashicorp Terraform Extension Guide
⛔️ Terraform Autocomplete Extension Guide
⛔️ AWS Subnet
⛔️ AWS Route Table
⛔️ AWS Route Table Association
⛔️ NAT Gateway
⛔️ Elastic IP

 🥁🥁 Conclusion 🥁🥁

In conclusion, deploying a serverless Python Flask app on AWS ECS Fargate using Terraform offers a powerful and scalable solution for modern web applications. By following this guide, you’ve learned how to set up Flask app on AWS ECS with Terraform, automate the deployment process, and leverage best practices for serverless Flask app AWS ECS. Whether you’re building a simple Python web app or a complex containerized Flask application, using Terraform infrastructure as code ensures a consistent and efficient deployment. Remember, deploying scalable Flask apps on AWS Fargate not only simplifies your workflow but also enhances your app’s performance and reliability. Happy deploying!

By following these steps, you can easily deploy and scale your applications on AWS ECS Fargate using Terraform.

Happy coding!

📢 Stay tuned for my next blog…..

So, did you find my content helpful? If you did or like my other content, feel free to buy me a coffee. Thanks.

Dheeraj_Pic1 (2)

Author - Dheeraj Choudhary

I am an IT Professional with 11+ years of experience specializing in DevOps & Build and Release Engineering, Software configuration management in automating, build, deploy and release. I blog about AWS and DevOps on my YouTube channel, which focuses on content such as, AWS, DevOps, open source, AI-ML and AWS community activities.

RELATED ARTICLES

AWS Glue Presentation (1)

Automate S3 Data ETL Pipelines With AWS Glue Using Terraform

Discover how to automate your S3 data ETL pipelines using AWS Glue and Terraform in this step-by-step tutorial. Learn to efficiently manage and process your data, leveraging the power of AWS Glue for seamless data transformation. Follow along as we demonstrate how to set up Terraform scripts, configure AWS Glue, and automate data workflows.

Comments are closed.