DEV Community

Cover image for How I Deployed a Flask App on AWS with RDS, ALB, and VPC as a Student
Tanay Jain
Tanay Jain

Posted on

How I Deployed a Flask App on AWS with RDS, ALB, and VPC as a Student

Most Flask deployment tutorials end after launching an EC2 instance.

Mine did too.

The application worked, but the architecture had serious problems:

  • Database exposed on the same machine as the application
  • No load balancing
  • No monitoring
  • No network isolation
  • Application accessible directly through EC2 public IP

So I rebuilt the entire stack using AWS VPC, RDS, ALB, Security Groups, IAM, and CloudWatch — to understand how production cloud systems are actually designed.

This article documents that journey.


Table of Contents


Why I Built This

I wanted to move beyond tutorial deployments and understand:

  • How databases are isolated in production
  • Why load balancers exist and what they actually do
  • How AWS networking works at a real level
  • How monitoring is implemented across multiple services
  • How security is enforced between application layers

Instead of deploying another demo app, I focused on building a production-style architecture around a simple application — a Flask visit counter backed by PostgreSQL.


Initial Architecture (v1.0)

Flask App
    │
PostgreSQL Container
    │
Docker Compose
    │
Single EC2 Instance
Enter fullscreen mode Exit fullscreen mode

Everything ran together on one machine. It worked for learning but had real limitations:

  • PostgreSQL inside a container — no managed backups, no isolation
  • No health checks — broken app still received traffic
  • No monitoring — no visibility into what was happening
  • Direct EC2 access — no traffic management layer

Architecture Evolution

Before (v1.0) After (v2.0)
Flask + PostgreSQL on one machine Flask on EC2 + RDS PostgreSQL
Direct EC2 public IP access ALB as single entry point
No monitoring CloudWatch Dashboard
No network isolation Public + Private Subnets
PostgreSQL in a container Managed Amazon RDS
Single point of failure Production-style 3-tier architecture

Target Architecture (v2.0)

Architecture Diagram

Figure 1: Final 3-tier AWS architecture used in this project.

Internet
    │
    ▼
Application Load Balancer
    │
    ▼
EC2 Instance (Docker + Flask)
    │
    ▼
Amazon RDS PostgreSQL (Private Subnet)
Enter fullscreen mode Exit fullscreen mode

This follows a standard 3-tier architecture pattern:

Tier Components
Presentation Application Load Balancer
Application EC2 + Docker + Flask
Data Amazon RDS PostgreSQL

Tech Stack

Application

  • Python, Flask
  • PostgreSQL

Containerization

  • Docker
  • Docker Compose

AWS Services

  • VPC, EC2, RDS PostgreSQL
  • Application Load Balancer
  • IAM, CloudWatch

Networking and Security

  • Security Groups
  • Public and Private Subnets
  • Internet Gateway, Route Tables

AWS Services Used

Service Purpose
Amazon VPC Network isolation and segmentation
Amazon EC2 Application hosting
Docker Application containerization
Amazon RDS PostgreSQL Managed database
Application Load Balancer Traffic distribution and health checks
IAM Access control and least-privilege
Amazon CloudWatch Monitoring and metrics
Security Groups Traffic filtering between layers
Internet Gateway Internet access for public subnets
Route Tables Subnet traffic routing

Building a Custom VPC

The first step was replacing the default VPC with a custom one designed around isolation.

VPC: 10.0.0.0/16
│
├── Public Subnets
│   ├── 10.0.1.0/24   (AZ: ap-south-1b)
│   └── 10.0.4.0/24   (AZ: ap-south-1a)
│
└── Private Subnets
    ├── 10.0.2.0/24   (AZ: ap-south-1b)
    └── 10.0.3.0/24   (AZ: ap-south-1a)
Enter fullscreen mode Exit fullscreen mode

Public subnets host the EC2 instance and ALB — internet-facing resources.

Private subnets host RDS — no route to the internet gateway, no direct access from anywhere outside the VPC.

The database is unreachable from the internet not just because of a firewall rule, but because it has no path there at all.


Security Group Layering

Each layer only accepts traffic from the layer directly above it:

Internet
   │
   ▼  HTTP 80, HTTPS 443
ALB Security Group
   │
   ▼  HTTP 80 — from ALB Security Group only
EC2 Security Group
   │
   ▼  PostgreSQL 5432 — from EC2 Security Group only
RDS Security Group
Enter fullscreen mode Exit fullscreen mode

Nobody can reach EC2 directly from the internet.
Nobody can reach RDS — not from the internet, not even from a developer's machine.
Every request must pass through the ALB first.


IAM and Access Control

Before deploying anything, I set up proper access control:

  • IAM Admin User with MFA — root account never used for daily work
  • IAM Roles for AWS service-to-service access
  • Least-privilege policy — no account has more permissions than it needs

Root account credentials are the most dangerous thing in an AWS account. Building this habit early matters.


Migrating Database to Amazon RDS

Moving PostgreSQL from a Docker container to Amazon RDS was one of the biggest architectural improvements.

Containerized PostgreSQL Amazon RDS PostgreSQL
Manual backups Automated backups
Manual patching Automatic patching
Container restart = potential data loss Managed persistent storage
No monitoring built in CloudWatch integration

The Flask application connects to RDS using SSL:

psycopg2.connect(
    host=DB_HOST,
    sslmode="require",
    connect_timeout=5
)
Enter fullscreen mode Exit fullscreen mode

RDS is accessible only from the EC2 Security Group. Nothing else can reach it.


Application Load Balancer

The ALB became the single entry point for all traffic.

Health Check Configuration:

Path:               /health
Interval:           30 seconds
Healthy Threshold:  2
Success Code:       200
Enter fullscreen mode Exit fullscreen mode

The /health endpoint verifies actual database connectivity before reporting healthy:

{
  "status": "healthy",
  "database": "connected"
}
Enter fullscreen mode Exit fullscreen mode

If RDS goes down, the health check fails, and the ALB stops routing traffic to that instance.

ALB Target Health

Figure 2: ALB Target Group showing healthy EC2 target.


CloudWatch Monitoring

I configured a unified CloudWatch dashboard covering all three tiers.

CloudWatch Dashboard

Figure 3: CloudWatch dashboard monitoring EC2, RDS, and ALB metrics.

Load Balancer

  • Request Count
  • Target Response Time
  • Healthy Host Count

EC2

  • CPU Utilization
  • Network In / Out

RDS

  • CPU Utilization
  • Database Connections
  • Free Storage Space

Having visibility across all layers meant that when something broke during testing, I could immediately see which tier was affected.


Challenges I Encountered

RDS Connectivity Issues

When I first deployed, the Flask application could not connect to PostgreSQL at all.

The issue was Security Group configuration. The RDS Security Group was only allowing inbound traffic from a specific IP — which changed every time the EC2 instance restarted.

Fixing this by referencing the EC2 Security Group ID instead of an IP address made me understand AWS network security in a way no tutorial had managed before.

Health Check Design

My first health check endpoint simply returned 200 OK with no logic.

The problem was that the application could lose database connectivity and still appear healthy to the ALB. Traffic kept routing to a broken instance.

I redesigned the endpoint to verify actual database access before returning healthy status. If the database query fails, the endpoint returns 503. The ALB catches this and stops routing traffic until connectivity is restored.


Security Improvements

The migration introduced multiple security improvements over the original setup:

  • Database moved into private subnets — no public IP, no internet route
  • Security Group based access control between every layer
  • IAM Admin User with MFA — root account locked away
  • Least-privilege access model throughout
  • ALB as the only public entry point — EC2 not directly reachable
  • SSL connection between Flask and RDS PostgreSQL

These controls significantly reduced the attack surface compared to the original single-machine deployment.


Approximate Cost

This project was built within AWS Free Tier limits where possible:

Service Cost
EC2 t2.micro Free Tier (750 hrs/month)
RDS db.t3.micro Free Tier (750 hrs/month)
Application Load Balancer Minimal cost during testing
CloudWatch Free Tier metrics

The goal was to experience production-style architecture without significant cost — and to understand which services carry real costs at scale.


Application Endpoints

Home

GET /
Response: Page visited X times!
Enter fullscreen mode Exit fullscreen mode

Health Check

GET /health
Response: { "status": "healthy", "database": "connected" }
Enter fullscreen mode Exit fullscreen mode

Current Limitations

While this architecture follows production patterns, it still has limitations:

  • Single EC2 instance — no redundancy at the application layer
  • No Auto Scaling Group — cannot handle traffic spikes
  • No CI/CD pipeline — deployments are manual
  • No Infrastructure as Code — resources provisioned via console
  • No HTTPS — SSL termination via ACM not yet configured

These are planned improvements for the next iteration.


What This Project Taught Me

Before this project, I knew how to deploy applications.

After this project:

  • I understood why VPC design matters before writing a single line of application code
  • I understood why databases belong in private networks, not just from a tutorial but from seeing what happens when they are not
  • I understood how health checks directly affect availability — a bad health check is worse than no health check
  • I understood how AWS services interact in a real architecture — not in isolation

The shift from deployment to architecture thinking was the biggest takeaway.


Project Outcomes

By the end of this project:

  • Designed a custom AWS VPC with public and private subnets across two Availability Zones
  • Deployed a Dockerized Flask application on Amazon EC2
  • Migrated PostgreSQL from a container to Amazon RDS in private subnets
  • Configured an Application Load Balancer with real health check logic
  • Implemented Security Group based network isolation across all three tiers
  • Built CloudWatch dashboards monitoring EC2, RDS, and ALB metrics
  • Documented the full architecture and deployment workflow

Roadmap

The current architecture uses manually provisioned AWS resources. Next steps:

  • Infrastructure as Code using Terraform — provision the entire stack from code
  • Automated CI/CD using GitHub Actions — code push triggers build, test, and deploy
  • HTTPS with AWS Certificate Manager
  • Container orchestration with Kubernetes on Amazon EKS
  • Auto Scaling Groups for high availability
  • Centralized logging with CloudWatch Logs

Source Code

Full source code, architecture diagrams, Dockerfile, Docker Compose, and documentation:

👉 GitHub Repository


Conclusion

This project taught me that cloud architecture is not about deploying applications — it is about designing systems that are secure, observable, and resilient.

Building the VPC, isolating the database, configuring Security Groups, implementing real health checks, and monitoring the full stack with CloudWatch gave me a much deeper understanding of AWS than any tutorial had.

The application itself is simple.

The infrastructure behind it is where the real learning happened.


Thanks for reading.

Feedback, suggestions, and architecture improvements are always welcome.

Top comments (0)