Building a Scalable AWS EC2 Deployment Template for Flask Applications Using Terraform

Building a Scalable AWS EC2 Deployment Template for Flask Applications Using Terraform

In this blog, we'll explore how to create a reusable Terraform template that automates the deployment of Flask applications on AWS EC2 instances. By leveraging this template, you can launch a fully functional Flask application in minutes, without needing to interact with any GUI. This method is perfect for streamlining deployment processes and ensuring consistent infrastructure setups for future projects.

Planning the Infrastructure

Every successful deployment starts with careful planning. For this project, we’ve designed a simple yet robust architecture consisting of a Virtual Private Cloud (VPC) with two subnets, a load balancer to distribute traffic, and EC2 instances across the subnets. This architecture ensures high availability and scalability while keeping the design straightforward and maintainable.

Writing Terraform Modules

To create a modular and maintainable codebase, we break down our infrastructure into individual Terraform modules. This approach not only declutters the code but also makes debugging and future updates much simpler. Our Terraform modules cover three key components:

VPC Module

The VPC module is responsible for setting up the foundational network infrastructure. It includes the creation of the VPC, public subnets, an Internet Gateway for internet access, and route tables with appropriate associations. This ensures that traffic is correctly routed from the subnets to the wider internet.


resource "aws_vpc" "flask_vpc" {
    cidr_block = var.vpc_cidr
    tags = {
        name = "flask_vpc"
    }
}

resource "aws_subnet" "public" {
    count = var.subnet_count
    vpc_id = aws_vpc.flask_vpc.id    
    cidr_block = element(var.subnet_cidr,count.index)
    availability_zone = element(var.subnet_az,count.index)
    map_public_ip_on_launch = true
    tags = {
        name = "flask_public_subnet"
    }
}    

resource "aws_internet_gateway" "ig" {
    vpc_id = aws_vpc.flask_vpc.id    
    tags = {
        name = "flask_internet_gateway"
    }
}
resource "aws_route_table" "rt" {
    vpc_id = aws_vpc.flask_vpc.id
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = aws_internet_gateway.ig.id
        }
    tags = {
        name = "flask_route_table"
    }
}

resource "aws_route_table_association" "rta" {
    count = var.subnet_count
    subnet_id = aws_subnet.public[count.index].id
    route_table_id = aws_route_table.rt.id
}

Load Balancer Module

The load balancer module defines an Application Load Balancer (ALB) along with its associated components. We configure target groups to manage the backend EC2 instances, and security groups to control access. The load balancer listens on port 80 for incoming traffic and distributes it across the EC2 instances in the target groups. Additionally, the ALB provides a generic domain name that can be easily upgraded to handle HTTPS requests, securing client data with encryption.

resource "aws_security_group" "alb_sg"{
    name = "alb_sg"
    vpc_id = var.vpc_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"]
    }
}

resource "aws_lb" "lb" {
    name = var.lb_name 
    internal = false
    load_balancer_type = "application"
    security_groups = [aws_security_group.alb_sg.id]
    subnets = var.public_subnets
}

resource "aws_lb_target_group" "lb_tg" {    
    name = "${aws_lb.lb.name}-tg"
    vpc_id = var.vpc_id
    port = 80
    protocol = "HTTP"
}

resource "aws_lb_listener" "http_listener" {
    load_balancer_arn = aws_lb.lb.arn
    port = 80 
    protocol = "HTTP"

    default_action {
        type = "forward" 
        target_group_arn = aws_lb_target_group.lb_tg.arn
    }
}

resource "aws_lb_target_group_attachment" "tg_attachment" {
    count = length(var.instance_ids)
    target_group_arn  = aws_lb_target_group.lb_tg.arn
    target_id = element(var.instance_ids , count.index)
    port = 80 
}

EC2 Module

The EC2 module automates the deployment of the application servers. This includes defining EC2 instances and setting up security groups with ingress and egress rules. We've also integrated provisioners to automatically configure and launch the Flask server on each instance, eliminating manual setup tasks. Furthermore, the security group allows incoming traffic on port 80 exclusively from the load balancer, enhancing the security of the instances by restricting access.



resource "aws_security_group" "ec2_sg" {

    name = "ec2_sg" 
    vpc_id = var.vpc_id
    ingress{
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    ingress{
        from_port = 80
        to_port = 80 
        protocol = "tcp"
        security_groups = [var.lb_sg_id]    
    }

    egress{
        from_port = 0
        to_port = 0
        protocol = "-1" 
        cidr_blocks = ["0.0.0.0/0"]
    }
}

resource "aws_instance" "ec2"{

    count = var.ec2_count
    ami = "ami-0522ab6e1ddcc7055"
    instance_type = "t2.micro"
    key_name  = "demo"
    subnet_id = var.subnet_id
    vpc_security_group_ids = [aws_security_group.ec2_sg.id]

    provisioner "remote-exec" {
        inline = [
            "sudo apt-get update",
            "sudo apt-get install -y python3-pip",
            "cd home/ubuntu",
            "git clone https://github.com/rgvar25/flaskHousingpricepredictor.git --branch main --single-branch",
            "cd flaskHousingpricepredictor",
            "pip3 install -r requirements.txt",
            "nohup python3 app.py > app.log 2>&1 &"
        ]

        connection {
            type = "ssh" 
            user = "ubuntu"
            private_key = file("/home/ronit/demo.pem")
            host = self.public_ip
            }
     }
}

Integrating it all

Once the individual modules are written and configured, the next step is to integrate them. This involves linking the modules in a way that allows the output from one module to be used as input for another. For instance, the vpc_id output from the VPC module is passed as an input variable to both the Load Balancer and EC2 modules. This modular approach not only simplifies the deployment process but also makes the infrastructure code more manageable and reusable. With the infrastructure components now interconnected, deploying a Flask application becomes a seamless and automated process.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-south-1"
}

module "vpc" {
  source = "/home/ronit/terra/vpc"
  vpc_cidr     = "10.0.0.0/16"
  subnet_count = 2
  subnet_cidr  = ["10.0.1.0/24", "10.0.2.0/24"]
  subnet_az    = ["ap-south-1a", "ap-south-1b"]
  # Variables will be asked during terraform apply if not provided
}

module "lb" {
 source = "./lb"
 vpc_id = module.vpc.vpc_id
 lb_name = "flask-lb"
 public_subnets = module.vpc.public_subnet_id
 instance_ids = module.ec2.instance_ids
}

module "ec2" {
 source = "./ec2"
 vpc_id = module.vpc.vpc_id
 subnet_id = module.vpc.public_subnet_id[0]
 ec2_count = 1
 lb_sg_id = module.lb.sg_id
}

Enhancing Security and Reusability

In addition to the core modules, we've implemented security best practices by controlling access to the EC2 instances and encrypting client-server communication. The template we've constructed is not only secure but also highly reusable. You can integrate it with a CI/CD pipeline, allowing automatic infrastructure provisioning whenever a commit is made to the code repository. This ensures that your infrastructure is always in sync with your application code, promoting continuous deployment and delivery.

Conclusion

By leveraging Terraform and a modular architecture, we’ve created a powerful, reusable template for deploying Flask applications on AWS. This approach automates the entire deployment process, reduces human error, and ensures consistent infrastructure setups across projects. Whether you’re deploying a small-scale application or scaling up to handle production workloads, this template can save you time and effort, making your DevOps processes more efficient.

Check out the full code on GitHub: Flask Deployment with Terraform