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