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
- Initial Architecture (v1.0)
- Architecture Evolution
- Target Architecture (v2.0)
- Tech Stack
- AWS Services Used
- VPC Design
- Security Model
- RDS Migration
- Load Balancer
- Monitoring
- Challenges
- Security Improvements
- Limitations
- What This Project Taught Me
- Project Outcomes
- Roadmap
- Source Code
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
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)
Figure 1: Final 3-tier AWS architecture used in this project.
Internet
│
▼
Application Load Balancer
│
▼
EC2 Instance (Docker + Flask)
│
▼
Amazon RDS PostgreSQL (Private Subnet)
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)
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
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
)
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
The /health endpoint verifies actual database connectivity before reporting healthy:
{
"status": "healthy",
"database": "connected"
}
If RDS goes down, the health check fails, and the ALB stops routing traffic to that instance.
Figure 2: ALB Target Group showing healthy EC2 target.
CloudWatch Monitoring
I configured a unified CloudWatch dashboard covering all three tiers.
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!
Health Check
GET /health
Response: { "status": "healthy", "database": "connected" }
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:
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)