Production-inspired AWS networking project with VPC peering, ALB, EC2, ECS on EC2, Flask, Docker, Terraform, and RDS MySQL
This project builds a multi-VPC AWS application architecture where application workloads in separate VPCs connect privately to a centralized RDS MySQL database through VPC peering.
The project includes two compute patterns:
- ECS on EC2 running a Dockerized Flask application.
- Standalone EC2 running a Flask application through user data and systemd.
Both application paths are placed behind Application Load Balancers and connect to a private MySQL database in a separate VPC.
The goal of this project is to demonstrate real AWS networking, private database connectivity, security group design, ALB troubleshooting, ECS on EC2 behavior, user-data bootstrapping, and Infrastructure as Code with Terraform.
Many cloud environments separate applications and databases into different networks for security, isolation, and operational control.
However, this creates real engineering challenges:
- How do applications in one VPC privately reach a database in another VPC?
- How do security groups behave across VPC peering?
- How do ALBs forward traffic to private compute?
- How do ECS tasks differ from EC2 instances when connecting to databases?
- How do you troubleshoot timeouts, failed health checks, and container startup issues?
- How do you build this repeatably with Terraform?
This project answers those questions through a working AWS implementation.
Build a production-inspired AWS lab where:
- public users access applications through ALBs
- application workloads run privately
- database access stays private
- RDS is isolated in a separate VPC
- VPC peering enables private cross-VPC communication
- security groups control application-to-database access
- ECS and EC2 compute patterns are both tested
- Terraform provisions the environment repeatably
Save your architecture image as:
Evidence/01-architecture/architecture-diagram.png
This project has two application paths that both reach the same private RDS MySQL database through VPC peering.
ββββββββββββββββββββββββββββ
β Users β
βββββββββββββββ¬βββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β Application Load Balancer β
β Public Subnet β
βββββββββββββββ¬βββββββββββββ
β
ββββββββββββββββββββ΄βββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββ
β ECS on EC2 Flask App β β Standalone EC2 Flask App β
β Private Subnet β β Private Subnet β
β Dockerized App Runtime β β User Data + systemd Runtime β
ββββββββββββββββ¬βββββββββββββββ ββββββββββββββββ¬βββββββββββββββ
β β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β VPC Peering β
β Private Cross-VPC Route β
βββββββββββββββ¬βββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β Private RDS MySQL Databaseβ
β Database VPC β
ββββββββββββββββββββββββββββ
| Category | Technologies |
|---|---|
| Cloud Provider | AWS |
| Infrastructure as Code | Terraform |
| Networking | Amazon VPC, Public/Private Subnets, Route Tables, Internet Gateway, NAT Gateway, VPC Peering |
| Load Balancing | Application Load Balancer, Target Groups, Health Checks |
| Compute Pattern 1 | Amazon ECS on EC2, Auto Scaling Group, Capacity Provider |
| Compute Pattern 2 | Amazon EC2, User Data, systemd, Gunicorn |
| Container Registry | Amazon ECR |
| Application Runtime | Python, Flask, Gunicorn |
| Containerization | Docker |
| Database | Amazon RDS for MySQL |
| Private Access | AWS Systems Manager Session Manager |
| Security | IAM Roles, Security Groups, Private Subnets, Least-Privilege Network Access |
| Logging & Troubleshooting | CloudWatch Logs, systemd Journals, Docker Logs, ALB Target Health |
ec2-demo1.mp4
ecs-demo.1.mp4
| Designing and deploying multiple AWS VPCs with isolated application and database layers. | Enabling private communication between separate VPCs without exposing internal traffic to the public internet. | Connecting application workloads in one VPC to an RDS MySQL database in another VPC. | Routing public HTTP traffic through ALBs to private compute resources securely and reliably. |
| Running containerized Flask applications on Amazon ECS using EC2-backed capacity. | Using EC2 user data and systemd to automatically configure and launch an application at boot time. | Building a Docker image locally, tagging it correctly, and pushing it to Amazon ECR for deployment. | Keeping the database private while still allowing approved application traffic over internal AWS networking. |
| Using source-based security group rules to tightly control traffic between ALBs, app tiers, and the database. | Handling resource ordering and cross-resource dependencies, especially around peering and security rules. | Diagnosing failed health checks, target group issues, and port mismatches that caused 502 errors. | Troubleshooting real issues across containers, ports, database access, networking, and runtime behavior. |
| Accessing private EC2 instances securely through AWS Systems Manager instead of exposing SSH publicly. |
Verifying the stack through /health, /info, and /dbcheck application routes.
|
Combining networking, security, compute, and database patterns that reflect real-world cloud environments. | Showing not just deployment, but the ability to design, build, test, and fix a complete AWS solution. |
- Separate VPCs for the application and database layers
- Public and private subnets for tiered network segmentation
- VPC peering routes for private app-to-database communication
- Application Load Balancers for routing external traffic to application workloads
- ECS cluster using EC2-backed capacity
- ECS capacity provider backed by an Auto Scaling Group
- ECS task definition and ECS service for the containerized Flask application
- Dockerized Flask application image pushed to Amazon ECR
- Standalone EC2 Flask application deployed through user data
- Private RDS MySQL database
- Security groups for the ALB, app workloads, ECS tasks, EC2 instances, and database access
- SSM access for private instance troubleshooting without public SSH
- Health check and database validation endpoints
| AWS Resource | Purpose |
|---|---|
| Amazon VPC | Creates isolated network boundaries for application and database environments. |
| Public Subnets | Place internet-facing ALBs where users can reach them. |
| Private Subnets | Host ECS, EC2, and RDS resources without direct public exposure. |
| VPC Peering | Enables private cross-VPC communication between application and database networks. |
| Application Load Balancer | Routes public HTTP traffic to healthy private application targets. |
| Amazon EC2 | Runs the standalone Flask app and ECS container instances. |
| Amazon ECS | Orchestrates the Dockerized Flask app on EC2-backed capacity. |
| Amazon ECR | Stores and serves the Docker image used by ECS. |
| Amazon RDS for MySQL | Provides the private managed MySQL database backend. |
| IAM | Grants EC2, ECS, ECR, SSM, and logging permissions. |
| Security Groups | Restrict traffic between ALB, app workloads, and database. |
| Systems Manager | Enables private instance access without SSH exposure. |
| CloudWatch Logs | Captures application, ECS, Docker, and system logs for troubleshooting. |
PROJECT 1/
βββ app/ # Flask application source code
β βββ app.py # Flask API with health and DB validation endpoints
β βββ Dockerfile # Container image definition
β βββ requirements.txt # Python dependencies
β
βββ Evidence/ # Project screenshots and validation proof
β βββ Evidence.md # Deployment notes and testing evidence
β βββ architecture-diagram.png # Architecture diagram
β
βββ modules/ # Reusable Terraform modules
β βββ subnet/
β β βββ main.tf # Subnet resources
β β βββ output.tf # Subnet outputs
β β βββ variable.tf # Subnet variables
β β
β βββ vpc/
β βββ main.tf # VPC resources
β βββ output.tf # VPC outputs
β βββ variable.tf # VPC variables
β
βββ scripts/ # Build and deployment automation
β βββ build_and_push_ecr.sh # Builds Docker image and pushes it to ECR
β βββ build_everything.sh # Runs the full build/deploy workflow
β βββ destroy_everything.sh # Destroys deployed infrastructure
β
βββ templates/ # EC2 and ECS bootstrap templates
β βββ ec2_app_user_data.sh.tftpl # User data for standalone EC2 Flask app
β βββ ecs_user_data.sh.tftpl # User data for ECS container instances
β
βββ vpc-peering.tf # Core networking resources
βββ vpc-1.tf # Primary region infrastructure
βββ vpc-2.tf # Secondary region / additional infrastructure
βββ vpc-3.tf # Tertiary region / additional infrastructure
βββ workflow.tf # Deployment workflow resources
βββ variable.tf # Terraform input variables
βββ output.tf # Terraform outputs
βββ .terraform.lock.hcl # Terraform provider lock file
βββ terraform.tfstate # Local Terraform state file
The application exposes a small set of routes that help validate each layer of the deployment: the web app, the load balancer, the runtime environment, and the database connection.
| Route | Method | What It Tests | Description |
|---|---|---|---|
/ |
GET |
Frontend + app runtime | Loads the visual dashboard page for the ECS or EC2 version of the app. |
/health |
GET |
ALB health check + app availability | Returns a lightweight health response used to confirm the app is reachable. |
/info |
GET |
Runtime metadata | Shows app mode, hostname, database host, database name, and database user. |
/dbcheck |
GET |
App-to-database path | Opens a live MySQL connection and confirms the app can reach the private RDS database. |
/ β Visual dashboard
/health β Application health
/info β Runtime metadata
/dbcheck β RDS connectivity validation
The Flask application exposes several validation routes to confirm that the workload is running correctly and can connect to the private RDS MySQL database.
GET /health
Expected response:
{
"status": "ok",
"app_mode": "EC2 or ECS",
"hostname": "instance-or-container-hostname"
}This confirms that the Flask application is running successfully.
ποΈ Database Connectivity Check
GET /dbcheck
Expected response:
{
"status": "connected",
"result": {
"current_db": "labdb",
"db_time": "timestamp"
}
}This confirms that the application can privately connect to the RDS MySQL database.
A visual dashboard that displays:
- Application Mode β identifies whether the app is running on EC2 or ECS
- Hostname β shows the instance or container hostname
- Database Host β displays the configured RDS database endpoint
- Database Name β shows the active MySQL database name
- Database User β shows the database user configured for the application
- Available Routes β lists the supported Flask endpoints
- Test Buttons β provides quick validation buttons for
/health,/info, and/dbcheck
Before deploying, update: Important variables: The ECS service depends on a Docker image being available in Amazon ECR. The script should: After the image exists in ECR, return to the project root and re-apply Terraform so the ECS service can launch using the uploaded image. |
Before running the automated script, update: Confirm these values are correct: The Use this method when you want the fastest path to deploy the infrastructure, upload the container image, and trigger ECS to run with the latest ECR image. |
This project supports two deployment paths: a manual Terraform workflow and an automated script-based workflow. Both reach the same final architecture, but the automated path reduces repeated commands, lowers the chance of human error, and speeds up the build-test-deploy cycle.
| Deployment Method | What You Do Manually | Estimated Time | Best Use Case |
|---|---|---|---|
| Manual Deployment | Run Terraform commands, build the Docker image, push to ECR, validate the image, then re-apply Terraform to start ECS. |
15β30 minutes Depends on Docker startup, Terraform apply time, image upload speed, and troubleshooting. |
Best for learning, debugging, and understanding every step in the deployment workflow. |
| Automated Deployment | Run one script that handles Terraform initialization, infrastructure deployment, Docker build, ECR push, and Terraform re-apply. |
5β10 minutes Most of the workflow runs without repeated manual commands. |
Best for repeatable deployments, faster testing, and reducing manual mistakes. |
| Workflow Step | Manual Deployment | Automated Deployment | Automation Benefit |
|---|---|---|---|
| Terraform initialization | Manual command | Handled by script | Removes repeated setup steps |
| Base infrastructure deployment | Manual terraform apply |
Handled by script | Creates required AWS resources consistently |
| Docker image build | Manual script execution | Included in automation | Reduces context switching |
| ECR authentication and image push | Manual validation required | Handled by script | Prevents missing image/tag issues |
| Second Terraform apply | Must be remembered manually | Handled automatically | Ensures ECS can start with the uploaded image |
| Total estimated time | 15β30 minutes | 5β10 minutes | Saves roughly 50β70% of deployment time |
| Automation reduces the number of manual commands required to build, push, and deploy the application. | The same script can be run multiple times to recreate the workflow with consistent results. | Automation helps avoid missed steps such as forgetting to push the ECR image or re-apply Terraform. |
| Faster deployment cycles make it easier to test infrastructure, application changes, and networking updates. | The script connects the Docker build, ECR push, and ECS deployment flow into one repeatable process. | Real environments rely on automated deployment pipelines instead of repeated manual console or CLI steps. |
Manual deployment is useful for learning how each piece works. Automated deployment is better for speed, consistency, and repeatability.
Manual Deployment
β Best for learning and troubleshooting
Automated Deployment
β Best for repeatable infrastructure delivery
π ECS Deployment Flow Docker image built locally β Image pushed to ECR β ECS task definition references ECR image β ECS service launches task β ECS task receives its own ENI and security group β ALB forwards traffic to task β Task connects to RDS over VPC peering
ββββββββββββββββββββββββββββββββββββββββββββββββ
β π 1. User Request β
β User sends traffic to the public endpoint β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β βοΈ 2. Application Load Balancer β
β ALB receives traffic and routes requests β
β to healthy private EC2 targets β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β π₯οΈ 3. Private EC2 Instance β
β EC2 launches inside a private subnet β
β with no direct public SSH access β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β βοΈ 4. User Data Bootstrap β
β Installs Python, Flask, PyMySQL, β
β Gunicorn, and required system packages β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β π¦ 5. Flask App Setup β
β Application code is written to disk β
β during instance initialization β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β π 6. systemd + Gunicorn β
β systemd starts and manages the Flask app β
β as a persistent Gunicorn service β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β π 7. Private Database Access β
β EC2 Flask app connects to RDS MySQL β
β privately over VPC peering β
ββββββββββββββββββββββββββββββββββββββββββββββββ
π Public Access
Internet β ALB:80
Only the Application Load Balancers are internet-facing.
β
π§± Private Application Access
ALB SG β App SG
Application workloads run in private subnets and only accept traffic from the ALB security group.
β
ποΈ Private Database Access
App SG / ECS Task SG β DB SG:3306
The database security group allows MySQL traffic only from approved application security groups.
β
π οΈ Administrative Access
IAM Role + SSM β Private EC2 Access
Private EC2 instances are accessed through AWS Systems Manager Session Manager.
No public SSH access is required.
| Security Group | Inbound Source | Port | Purpose |
|---|---|---|---|
| ALB SG | Internet 0.0.0.0/0 |
80 |
Allows public HTTP traffic to reach the Application Load Balancer |
| EC2 App SG | ALB SG | App Port | Allows the ALB to forward traffic to the standalone EC2 Flask app |
| ECS Task SG | ALB SG | App Port | Allows the ALB to forward traffic to ECS Flask tasks |
| DB SG | EC2 App SG | 3306 |
Allows the EC2 Flask app to connect to RDS MySQL |
| DB SG | ECS Task SG | 3306 |
Allows ECS tasks to connect to RDS MySQL |
| ECS Instance SG | No public inbound required | N/A | Supports ECS host outbound access and SSM-based administration |
The database does not allow public access. MySQL traffic is only accepted from approved application security groups.
This project uses VPC peering to enable private communication between the application VPCs and the database VPC.
The peering connection allows application workloads to reach the private RDS MySQL database without sending database traffic over the public internet.
Each side of the peering connection needs a route table entry.
| Route Table | Destination | Target |
|---|---|---|
| Application VPC Route Table | Database VPC CIDR | VPC Peering Connection |
| Database VPC Route Table | Application VPC CIDR | VPC Peering Connection |
Application VPC
β
VPC Peering Connection
β
Database VPC
β
Private RDS MySQL
When security group rules reference security groups across VPC peering, Terraform may try to create those rules before the peering connection and route table entries are fully ready.
To avoid dependency issues, add explicit depends_on relationships where needed.
VPC Peering Connection
β
Route Table Entries
β
Cross-VPC Security Group Rules
This ensures that private routing exists before Terraform applies the database access rules.
This prevents race-condition style failures where the security group rule appears correct but connectivity does not work reliably.
Use the following commands to validate that the ALB, Flask application, and private RDS connectivity are working correctly.
curl http://<alb-dns-name>/healthExpected result:
{
"status": "ok",
"app_mode": "EC2 or ECS",
"hostname": "instance-or-container-hostname"
}curl http://<alb-dns-name>/Expected result:
The root route should return the application dashboard showing runtime details, database configuration values, and available test routes.
curl http://<alb-dns-name>/dbcheckExpected result:
{
"status": "connected"
}This confirms that the application can reach the private RDS MySQL database through the private network path.
Private EC2 instances are accessed through AWS Systems Manager Session Manager, not public SSH.
After connecting to the private EC2 instance through Session Manager, use the following commands to troubleshoot the application.
sudo systemctl status flask-db-app --no-pager -lsudo journalctl -u flask-db-app --no-pager -n 100sudo ss -lntp | grep -E ':80|:8080|:5000'curl -i http://127.0.0.1:8080/healthpython - <<'PY'
import os
import socket
host = os.getenv("DB_HOST")
port = int(os.getenv("DB_PORT", "3306"))
s = socket.create_connection((host, port), timeout=5)
print("TCP connection to database succeeded")
s.close()
PYThis test confirms that the private EC2 instance can establish a TCP connection to the RDS database endpoint.
Use these commands from the ECS container instance to inspect running containers and test database connectivity from inside the application container.
sudo docker pssudo docker exec -it <container-id> shenv | grep DBpython - <<'PY'
import os
import socket
host = os.getenv("DB_HOST")
port = int(os.getenv("DB_PORT", "3306"))
s = socket.create_connection((host, port), timeout=5)
print("TCP connection to database succeeded")
s.close()
PYThis confirms that the ECS task can reach the private RDS MySQL database through VPC peering and the configured security group rules.
| Problem | Root Cause | Fix |
|---|---|---|
| ECS EC2 instance did not appear in the cluster | ECS agent startup ordering issue | Deferred ECS startup until after cloud-init completed |
| ECS task failed to pull image | ECR image tag mismatch | Pushed the correct image tag to ECR and aligned it with the task definition |
| Docker push failed | Missing AWS_REGION variable |
Defined AWS_REGION in the build script |
| RDS connection timed out | Database security group did not allow the actual source security group | Allowed inbound MySQL traffic from the ECS task SG and EC2 app SG |
| Security group-only rule behaved inconsistently | Terraform dependency order issue with VPC peering | Added explicit depends_on for peering and route resources |
ALB returned 502 Bad Gateway |
Target was unhealthy due to incorrect app port or failed health check | Aligned the target group port, application port, health check path, and SG rules |
| MySQL query failed | SQL alias used a reserved-keyword-style name | Changed the alias to db_time |
| Terraform ECS service recreation failed | Old ECS service was still draining | Waited for the service to become inactive or temporarily used a new service name |
| Decision | Reason |
|---|---|
| Use ALB as the public entry point | Keeps compute resources private while still allowing controlled HTTP access |
| Use private subnets for compute | Reduces direct exposure of EC2 and ECS workloads |
| Place RDS in a separate VPC | Demonstrates database isolation and cross-VPC application access |
| Use VPC peering | Enables private IP communication without exposing the database publicly |
| Use security-group-to-security-group database rules | Provides tighter access control than broad CIDR-based rules |
| Use SSM instead of SSH | Avoids public SSH exposure and removes the need to manage SSH keys |
| Use Terraform | Makes the infrastructure repeatable, reviewable, and easier to rebuild |
Use /dbcheck route |
Provides simple application-level validation of database connectivity |
This project is production-inspired but still a lab. The next improvements would make the platform more secure, reliable, automated, and observable.
- Move database credentials to AWS Secrets Manager
- Add HTTPS using AWS Certificate Manager
- Redirect HTTP traffic to HTTPS
- Add AWS WAF in front of the Application Load Balancers
- Replace broad IAM permissions with tighter custom IAM policies
- Enable or verify RDS encryption at rest
- Add stricter outbound rules where practical
- Add least-privilege IAM policies for EC2, ECS, ECR, SSM, and logging
- Enable Multi-AZ RDS
- Add Auto Scaling Groups for the EC2 application tier
- Run multiple ECS container instances across Availability Zones
- Enable ALB access logs
- Add CloudWatch alarms for ALB, ECS, EC2, and RDS
- Improve container-level logging
- Add health check tuning for more graceful deployments
- Add a GitHub Actions or Jenkins pipeline
- Automate Docker image build and ECR push
- Run
terraform fmt,terraform validate, andterraform planin CI - Add container image scanning
- Add Terraform security scanning using Checkov or tfsec
- Add manual approval before production-style
terraform apply - Store Terraform state remotely in an S3 backend with state locking
- Add CloudWatch dashboards
- Enable VPC Flow Logs
- Add RDS performance monitoring
- Add synthetic health checks
- Forward logs to a SIEM or centralized logging platform
- Track application errors, ALB target health, database connections, and ECS task failures
| Limitation | Current Status |
|---|---|
| HTTPS | Not implemented yet |
| Secrets Manager | Database credentials are not fully managed through Secrets Manager yet |
| AWS WAF | Not enabled yet |
| Autoscaling | Limited autoscaling coverage |
| Region Design | Currently designed as a single-region deployment |
| CI/CD | No formal pipeline yet |
| Observability | No full CloudWatch dashboard or centralized logging pipeline yet |
These limitations are acceptable for the current version because the main goal is to prove private cross-VPC application-to-database connectivity first.
This project demonstrates practical cloud engineering beyond a basic βhello worldβ deployment.
It shows how to combine networking, compute, containers, load balancing, security groups, private database access, and Terraform into one working AWS architecture.
The value of this project is not just that the application works. The value is that it proves the ability to troubleshoot real infrastructure problems across multiple layers.
Application
β
Container
β
EC2 Host
β
Application Load Balancer
β
Security Groups
β
Route Tables
β
VPC Peering
β
RDS MySQL
β
Terraform State and Dependencies
| Area | Value Shown |
|---|---|
| AWS Networking | Multi-VPC routing and private connectivity |
| Cloud Security | Security group-based access control and private-by-default design |
| Compute | EC2 and ECS on EC2 deployment patterns |
| Containers | Docker image build, ECR push, and ECS runtime execution |
| Infrastructure as Code | Terraform-managed AWS deployment |
| Troubleshooting | Real debugging of ALB, ECS, RDS, ports, routes, and security groups |
| Application Integration | Flask application successfully reaches RDS through private networking |
| Operational Access | SSM-based private instance debugging without public SSH |
To avoid ongoing AWS charges, destroy the deployed infrastructure when testing is complete.
terraform destroyAfter Terraform finishes, verify that the following resources were removed:
- Application Load Balancers
- NAT Gateways
- RDS instances
- EC2 instances
- ECS services
- ECS container instances
- ECR images
- VPC peering connections
- VPC routes and route tables
- Security groups
- CloudWatch log groups created during testing
NAT Gateways, RDS instances, and running EC2 instances can continue generating charges if they are left active.
If terraform destroy takes too long or appears stuck on a resource such as an ECS service, the service may still be draining or waiting on attached resources.
Example:
aws_ecs_service.app[0]: Still destroying...
If Terraform remains stuck for an extended period, manually delete the blocking resource from the AWS Console or AWS CLI, then re-run:
terraform destroyIf the resource was already deleted manually but Terraform is still stuck tracking it, remove that resource from the Terraform state.
First, find the resource name in state:
terraform state listThen remove the deleted resource from state:
terraform state rm "resource_name_from_state"Example:
terraform state rm "aws_ecs_service.app[0]"After removing the stale resource from state, run destroy again:
terraform destroy
Beware, terraform is not perfect and your destroy will lock up, you need to delete those resources yourself.
This allows Terraform to continue removing the remaining infrastructure cleanly.
