Day 22: Two-Tier Flask Application with Terraform on AWS

When deploying modern web applications, infrastructure as code (IaC) has become the gold standard. In this post, I'll walk you through architecting and deploying a two-tier Flask application with MySQL on AWS using Terraform. This setup ensures scalability, security, and maintainability - all while keeping sensitive data protected.
Refer Complete Notes: Link for Notes
Refer Complete Code: Link for Code
The Architecture: Why Two-Tier?
A two-tier architecture separates concerns between the presentation layer (web server) and data layer (database). Our Flask application runs on EC2 instances while MySQL operates on Amazon RDS. This separation offers several advantages:
Scalability: Web and database tiers can scale independently
Security: Database resides in private subnets, isolated from public traffic
Maintainability: Updates and patches can be applied to each tier separately
High Availability: Multi-AZ deployment ensures database resilience
Securing Secrets with AWS Secrets Manager
One of the biggest challenges in infrastructure deployment is managing secrets securely. Hardcoding database passwords in Terraform files is a security anti-pattern. Here's our solution:
resource "random_password" "db_password" {
length = 16
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = jsonencode({
username = "admin"
password = random_password.db_password.result
engine = "mysql"
host = aws_db_instance.main.endpoint
})
}
This approach generates a strong, random password and stores it securely in AWS Secrets Manager. The application retrieves credentials at runtime, eliminating hardcoded secrets from your infrastructure code.
Network Isolation: The VPC Foundation
Proper network design is crucial for security. We create a VPC with clear separation between public and private subnets:
# Public subnet for web servers
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
}
# Private subnets for RDS (different AZs for HA)
resource "aws_subnet" "private_1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.aws_region}a"
}
resource "aws_subnet" "private_2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.3.0/24"
availability_zone = "${var.aws_region}b"
}
The web servers live in public subnets with internet access, while the database resides in private subnets, inaccessible directly from the internet. This layered security approach significantly reduces the attack surface.
Security Groups: Granular Access Control
Security groups act as virtual firewalls. We create separate security groups for our web and database tiers:
# Web security group - allows HTTP/HTTPS and SSH
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Allow web traffic"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Restrict to your IP in production
}
}
# Database security group - only allows web tier access
resource "aws_security_group" "database" {
name = "${var.project_name}-db-sg"
description = "Allow database access from web tier"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
}
Notice how the database security group only allows traffic from the web security group on port 3306. This principle of least privilege is essential for securing your database.
Managed Database Service: RDS Configuration
Amazon RDS provides a managed MySQL service that handles patching, backups, and failover. Here's our Terraform configuration:
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
allocated_storage = 20
storage_type = "gp2"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
db_name = "flaskapp"
username = var.db_username
password = random_password.db_password.result
skip_final_snapshot = true
vpc_security_group_ids = [aws_security_group.database.id]
db_subnet_group_name = aws_db_subnet_group.main.name
publicly_accessible = false
}
Key security features:
publicly_accessible = falseensures no direct internet accessDatabase uses the private subnet group
Security group restricts access to web tier only
Application Deployment: EC2 with User Data
The final piece is our web server deployment. We use EC2 user data to bootstrap the Flask application:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = templatefile("${path.module}/user_data.sh", {
db_host = aws_db_instance.main.endpoint
db_username = var.db_username
db_password = random_password.db_password.result
db_name = "flaskapp"
})
}
The user_data.sh script installs Python, Flask, MySQL client, and configures the application to connect to our RDS instance.
Deployment Workflow
With all components defined, deployment is straightforward:
terraform init # Initialize Terraform and providers
terraform plan # Review changes
terraform apply # Deploy infrastructure
Within minutes, you have a fully functional, secure two-tier application. The web server's public DNS output provides the URL to access your Flask application.
Best Practices Implemented
Infrastructure as Code: The entire environment is version-controlled and reproducible
Secret Management: No hardcoded credentials; AWS Secrets Manager handles sensitive data
Network Security: Proper VPC design with public/private subnet segregation
Least Privilege: Security groups restrict access to only necessary ports
High Availability: Multi-AZ RDS deployment ensures database resilience
Cost Optimization: Using t3.micro instances for both tiers in development
Video
Conclusion
This Terraform-based approach to deploying a two-tier Flask application demonstrates modern cloud infrastructure practices. By leveraging AWS services and Terraform's declarative syntax, we've created a secure, scalable foundation that can be extended for production use. The complete separation of concerns, secure credential management, and network isolation provide a robust platform for your Flask applications to thrive in the cloud.
Remember to always customize security settings (especially SSH access) for your specific needs and implement proper monitoring and logging before moving to production. Happy deploying!




