Deploy a Traditional Web App on AWS
Build a Flask app on EC2 with VPC, RDS, and a load balancer.
Introduction
30 Second Summary
When you visit a website, something has to receive your request, run the code, and send back a response. Behind most apps, that something is a server sitting on a network, connected to a database that someone set up by hand.
In this project, you will deploy a task manager web app on AWS using traditional server infrastructure. You will build every layer yourself, from the network to the server to the database to the load balancer.
What You'll Build
When you're done, you'll paste your load balancer's URL into a browser and see a working Flask task manager where you can add, view, and delete tasks.
By the end of this project, you'll have:
- A task manager app you can open in your browser through an Application Load Balancer URL, running on an EC2 instance you launched and configured yourself.
- A custom VPC network you built from scratch, with security groups enforcing a strict traffic chain: internet to load balancer to server to database. Your database is completely unreachable from the public internet.
- Tasks that persist across server restarts, stored in an RDS MySQL database hidden inside a private subnet, with a Route 53 DNS name so your app finds the database without hardcoding its address.
- Secret Mission: Add an Auto Scaling Group so your app automatically replaces crashed instances and scales under load.
Have you completed Project 1?
This project is Part 2 of a two-project series. Project 1 covered serverless architecture with Lambda, API Gateway, and DynamoDB.
Completing Project 1 first gives you the serverless baseline to compare against. It is not strictly required to start this project.
Before We Start
Before you dive in, take a moment to commit to what you're building and why it matters.
In this project, you'll deploy a traditional web app on AWS by building the network, server, and database infrastructure yourself. In Project 1, serverless services like Lambda and DynamoDB handled all of that behind the scenes. This time, you own every layer.
Install the AWS CLI and Set Up Your Tools
To build a traditional web app on AWS, you need a way to talk to AWS services directly from your terminal. The AWS CLI (Command Line Interface) is the tool that makes this possible.
In this step, you'll install the AWS CLI, connect it to your AWS account with your credentials, and verify everything is working before you create any resources.
In this step, get ready to:
- Install AWS CLI v2 on your machine.
- Configure your AWS credentials and default region.
- Verify your account access from the terminal.
Install AWS CLI v2
The AWS CLI lets you create, configure, and manage AWS resources from your terminal instead of clicking through the console. You need version 2 for this project.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check if you already have the AWS CLI installed by running this command:
aws --version
✔️ I see aws-cli/2.x
Your output looks something like aws-cli/2.27.41 Python/3.11.6 .... As long as the version starts with 2., you're good to go.
ⓧ I see aws-cli/1.x
You have version 1 installed, but this project requires version 2. Version 2 uses a different installer and has features that version 1 does not support.
Follow the install instructions in the next tab to install version 2 alongside your existing version. After installation, close and reopen your terminal, then run aws --version again to confirm it now shows aws-cli/2. at the start.
ⓧ Command not found
No worries. You just need to install the AWS CLI. Follow the instructions for your operating system below.
macOS
- Download and install the AWS CLI by running these two commands in your terminal:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
- Your Mac will ask for your password. Type it and press Enter. You won't see characters appear as you type. That's normal.
- After the installer finishes, close and reopen your terminal.
- Confirm the installation by running:
aws --version
You should see a version string starting with aws-cli/2. printed in your terminal.
Windows
- Download the MSI installer by opening this link in your browser:
https://awscli.amazonaws.com/AWSCLIV2.msi
- Run the downloaded AWSCLIV2.msi file and follow the on-screen prompts. Accept the defaults on each screen.
- After the installer finishes, close and reopen your Command Prompt.
- Confirm the installation by running:
aws --version
You should see a version string starting with aws-cli/2. printed in your terminal.
Still seeing command not found after installing?
Make sure you closed and reopened your terminal after the install completed. The new aws command only becomes available in a fresh terminal session.
On Windows, try searching for cmd in the Start menu and opening a brand new Command Prompt window.
Why can't my terminal find the aws command?
Why use the CLI instead of the AWS Console?
In Project 1, you could do almost everything by clicking through the AWS Console. Traditional architecture demands the CLI because you're managing servers, SSH keys, and network configs that are easier to automate from the command line.
Configure your AWS credentials
The CLI needs to know which AWS account to use and how to authenticate. You'll connect it to your account by providing your access keys and setting a default region.
- Start the configuration wizard by running this command:
aws configure
The CLI will ask you four questions, one at a time. Enter the following values:
- AWS Access Key ID: Paste your Access Key ID and press Enter.
- AWS Secret Access Key: Paste your Secret Access Key and press Enter.
- Default region name: Type us-east-1 and press Enter.
- Default output format: Type json and press Enter.
Where do I find my access keys?
If you already created access keys in Project 1, use those same keys here. If you need new ones, go to the IAM console in AWS.
Click Users in the left sidebar, select your user, open the Security credentials tab, then click Create access key. Copy both the Access Key ID and Secret Access Key before closing the dialog. You can only view the Secret Access Key once.
What do these configuration values do?
Your Access Key ID and Secret Access Key are like a username and password for the CLI. They prove to AWS that commands are coming from your account.
The default region tells AWS where to create your resources. us-east-1 is the US East (N. Virginia) region. The output format controls how the CLI displays results. JSON is structured and easy to read.
Verify your account access
Now that the CLI is configured, you need to confirm it can actually reach your AWS account. This is the moment of truth before you start creating resources.
Before you run the next command, what do you think it will return if your credentials are correct?
- Verify your account access by running this command:
aws sts get-caller-identity
You should see a JSON response containing your AWS account ID, your user ARN, and your user ID. This confirms the CLI is authenticated and ready to manage your AWS resources.
What does this command do?
aws sts get-caller-identity asks AWS "who am I?" using the credentials you just configured. STS stands for Security Token Service. It returns three pieces of information:
- Account: Your 12-digit AWS account number.
- Arn: The Amazon Resource Name that uniquely identifies your user.
- UserId: A unique identifier for your IAM user.
Seeing an error instead of your account details?
If you see a message about invalid credentials, run aws configure again and double-check that you pasted the correct Access Key ID and Secret Access Key. Make sure there are no extra spaces before or after the values.
If your access key was recently created, wait a few seconds and try again. New keys can take a moment to propagate.
Why is aws sts get-caller-identity failing with my credentials?
Your CLI is installed, configured, and verified. Next up, you'll build the VPC network that all of your AWS resources will live inside.
Build Your VPC Network from Scratch
In Step 1, you set up the AWS CLI and verified your account access. Now it's time to build the network that all your resources will live in.
Every AWS resource needs a network. In Project 1, Lambda and DynamoDB lived in AWS-managed networks you never saw. Here, you build the network yourself using a VPC (Virtual Private Cloud). This is the foundation that makes everything else possible.
In this step, get ready to:
- Create a custom VPC with public and private subnets across two Availability Zones.
- Set up three security groups that control who can talk to what.
- Verify the network isolation chain from internet to database.
Create the VPC with subnets
Your VPC is a private, isolated section of the AWS cloud where you control the IP addresses, subnets, and routing. You will create one with four subnets: two public (for resources that face the internet) and two private (for resources that should stay hidden).
- Open your browser and sign into the AWS Management Console at https://console.aws.amazon.com.
- In the top search bar, type VPC and select VPC from the results.
- On the VPC dashboard, click Create VPC.
- For Resources to create, choose VPC and more.
Why VPC and more?
This option creates your VPC, subnets, route tables, and internet gateway all in one step. You could create each one individually, but the wizard saves time while still letting you customize every setting.
- Under Name tag auto-generation, enter task-app. AWS will automatically name your VPC task-app-vpc and label all related resources with the task-app prefix.
- For IPv4 CIDR block, enter 10.0.0.0/16. This gives your VPC over 65,000 IP addresses to work with.
- For Number of Availability Zones, choose 2.
- For Number of public subnets, choose 2.
- For Number of private subnets, choose 2.
- Expand Customize subnet CIDR blocks. Set the four subnet CIDRs to match these exact values:
- Public subnet 1: 10.0.1.0/24.
- Public subnet 2: 10.0.2.0/24.
- Private subnet 1: 10.0.3.0/24.
- Private subnet 2: 10.0.4.0/24.
- For NAT gateways, choose None. NAT gateways cost $0.045/hour and are not needed for this project.
- For VPC endpoints, choose None.
- Leave DNS options with both checkboxes selected.
- Click Create VPC.
AWS creates your VPC, subnets, internet gateway, and route tables all at once. You should see a green success banner at the top of the page confirming all resources were created.
What did VPC and more just create?
The wizard created six things for you in one go. Your VPC is the private network. The four subnets are IP address ranges within that network, split across two Availability Zones for redundancy.
The internet gateway connects your VPC to the public internet. The route tables tell traffic where to go. Public subnets have a route to the internet gateway. Private subnets do not, so resources in them cannot be reached from the internet.
- Click View VPC to see your new task-app-vpc.
- Confirm the IPv4 CIDR shows 10.0.0.0/16 and the State shows Available.
What is a VPC compared to Project 1?
In Project 1, you never created a single network resource. Lambda functions ran in AWS-managed VPCs you never saw. DynamoDB had no network configuration at all. Here, you are building the entire network layer that serverless abstracts away.
Create security groups
Security groups act as virtual firewalls that control which traffic is allowed in and out of your resources. You need three of them, each protecting a different layer of your application.
The three groups form a chain: internet traffic enters through the ALB security group, flows to the EC2 security group, and the EC2 group talks to the RDS security group. Each link in the chain only accepts traffic from the link before it.
- In the VPC console's left navigation pane, click Security groups.
- Click Create security group.
You will create the ALB security group first.
- For Security group name, enter task-alb-sg.
- For Description, enter Allow HTTP from internet.
- For VPC, remove the default VPC and select task-app-vpc.
- Under Inbound rules, click Add rule.
- Set Type to HTTP. This automatically sets the port to 80.
- Set Source to Anywhere-IPv4 (0.0.0.0/0). This allows anyone on the internet to reach the load balancer.
- Click Create security group.
You should see a success banner confirming task-alb-sg was created.
What is a security group?
A security group is a stateful virtual firewall. "Stateful" means that if you allow traffic in, the response is automatically allowed back out. You only define allow rules. There are no deny rules.
By default, a new security group blocks all inbound traffic and allows all outbound traffic. You add inbound rules to open specific ports from specific sources.
Now create the EC2 security group. This one only accepts traffic from the ALB, not from the open internet.
- Click Security groups in the left navigation to return to the list.
- Click Create security group.
- For Security group name, enter task-ec2-sg.
- For Description, enter Allow HTTP from ALB and SSH from my IP.
- For VPC, remove the default VPC and select task-app-vpc.
- Under Inbound rules, click Add rule.
- Set Type to HTTP.
- Set Source to Custom, then search for and select task-alb-sg. This means only traffic coming from the ALB is allowed in on port 80.
- Click Add rule again to add a second rule.
- Set Type to SSH. This automatically sets the port to 22.
- Set Source to My IP. AWS fills in your current public IP address so only you can SSH in.
- Click Create security group.
You should see a success banner confirming task-ec2-sg was created.
Finally, create the RDS security group. This one only accepts database traffic from the EC2 instance.
- Click Security groups in the left navigation to return to the list.
- Click Create security group.
- For Security group name, enter task-rds-sg.
- For Description, enter Allow MySQL from EC2 only.
- For VPC, remove the default VPC and select task-app-vpc.
- Under Inbound rules, click Add rule.
- Set Type to MYSQL/Aurora. This automatically sets the port to 3306.
- Set Source to Custom, then search for and select task-ec2-sg. Only your EC2 instance can reach the database.
- Click Create security group.
You should see a success banner confirming task-rds-sg was created.
Don't see task-app-vpc in the VPC dropdown?
Make sure you are in the us-east-1 region. Check the region selector in the top-right corner of the console. Your VPC only exists in the region where you created it.
help me find my VPC when creating a security group
Verify the network isolation
You now have three security groups forming a chain. Before moving on, take a moment to review the rules and confirm the isolation is correct.
- In the Security groups list, click on task-alb-sg.
- Click the Inbound rules tab. You should see one rule: HTTP (port 80) from 0.0.0.0/0 (the entire internet).
- Go back and click on task-ec2-sg.
- Check its Inbound rules tab. You should see two rules: HTTP from task-alb-sg, and SSH from your IP address.
- Go back and click on task-rds-sg.
- Check its Inbound rules tab. You should see one rule: MySQL/Aurora (port 3306) from task-ec2-sg.
Why does this chain matter?
Notice the pattern: internet to ALB to EC2 to RDS. The database is completely unreachable from the internet. Someone would need to break through two security groups to reach it. This is the network isolation that VPC provides.
In Project 1, DynamoDB handled its own access control through IAM policies. Here, you are building that protection into the network layer itself.
Before you run the verification command, what do you think it will return? Think about what filter you are applying and what value you expect to see.
- Verify your VPC from the CLI by running this command in your terminal:
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=task-app-vpc" --query "Vpcs[0].VpcId" --output text
You should see your VPC ID printed back, something like vpc-0abc123def456789. This confirms your VPC exists and is tagged correctly.
Seeing None or an empty result?
Double-check that your AWS CLI is configured for the us-east-1 region. You can verify by running aws configure get region. If it returns a different region, run aws configure again and set the default region to us-east-1.
help me troubleshoot my VPC CLI verification
Your network is ready. You have a VPC with public and private subnets, and three security groups that lock down traffic at every layer. Next up, you will launch an EC2 instance inside this network and deploy your Flask app.
Launch Your EC2 Instance and Deploy the Flask App
Your network is built. You have a VPC with public and private subnets, an internet gateway, and security groups controlling who can talk to whom.
Now you need a server to run your app. In Project 1, Lambda was the compute layer. Here, you provision the machine yourself using EC2. You will launch a virtual server, SSH into it, install Flask, and deploy a task manager that stores tasks in memory.
In this step, get ready to:
- Launch a t4g.small EC2 instance inside your public subnet.
- SSH into the instance and deploy a Flask task manager app.
- Test the app with curl to confirm it responds to requests.
Launch the EC2 instance
EC2 gives you a virtual server in the cloud that you fully control. You choose the operating system, the size, and which network it lives in.
- In the AWS Management Console, make sure your region is set to us-east-1 (N. Virginia) in the top-right corner.
- Search for EC2 in the top search bar and select EC2.
- On the EC2 Dashboard, click Launch instance.
- Under Name and tags, enter task-app-server for the Name field.
- Under Application and OS Images (Amazon Machine Image), click Quick Start and choose Amazon Linux.
- From the Amazon Machine Image (AMI) dropdown, select Amazon Linux 2023 AMI with the 64-bit (Arm) architecture.
- Under Instance type, search for and select t4g.small.
Why t4g.small?
The t4g.small instance type runs on AWS Graviton2 (ARM) processors. It comes with 2 vCPUs and 2 GiB of memory, which is plenty for a Flask app.
AWS offers a T4g free trial that gives you 750 free hours per month of t4g.small usage until December 31, 2026. This applies to all AWS accounts, even those with an expired free tier.
- Under Key pair (login), click Create new key pair.
- Enter task-app-key as the Key pair name.
- Keep the key pair type as RSA and the file format as .pem.
- Click Create key pair. Your browser downloads task-app-key.pem automatically. Keep this file safe.
What is a key pair?
A key pair is how you prove your identity when connecting to an EC2 instance over SSH. AWS keeps the public key on the server. You keep the private key (the .pem file) on your local machine. Anyone with this file can access your instance, so treat it like a password.
- Under Network settings, click Edit.
- In the VPC dropdown, select task-app-vpc (the VPC you created in Step 2).
- In the Subnet dropdown, select one of your public subnets (10.0.1.0/24 or 10.0.2.0/24).
- Set Auto-assign public IP to Enable. This gives your instance a public IP address so you can SSH into it.
- Under Firewall (security groups), select Select existing security group.
- From the Common security groups dropdown, choose task-ec2-sg (the security group you created in Step 2).
- Leave the Configure storage section at its default (8 GiB gp3).
- Review your settings in the Summary panel on the right, then click Launch instance.
You should see a success message. Click View all instances to return to the Instances page.
Wait for the Instance state column to change from Pending to Running. This takes about 30 seconds.
What is the serverless contrast?
In Project 1, you uploaded code to Lambda and it ran instantly with zero server configuration. Here, you picked an operating system, a machine size, a network, a security group, and a key pair. You are the sysadmin now.
Instance went to Terminated instead of Running?
Double-check that you selected the 64-bit (Arm) architecture for the AMI. The t4g instance family only works with ARM-based AMIs.
Also confirm you placed the instance in a public subnet of task-app-vpc, not a private subnet.
Help me troubleshoot why my EC2 instance went to Terminated instead of Running. I used a t4g.small with Amazon Linux 2023 ARM in task-app-vpc.
SSH into the instance and deploy the Flask app
Your server is running, but it is an empty machine. You need to connect to it, install Flask, and put your app code on it.
- On the Instances page, select your task-app-server instance.
- In the Details tab at the bottom, find and copy the Public IPv4 address. You will use this to connect.
Before you can SSH into the instance, you need to set the right permissions on your key file. The SSH client refuses to use a key that other users on your computer can read.
macOS
- Open Terminal.
- Navigate to the folder where your .pem file was downloaded (usually ~/Downloads) by running:
cd ~/Downloads
- Lock down the key file permissions by running:
chmod 400 task-app-key.pem
- Connect to your instance by running:
ssh -i task-app-key.pem ec2-user@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]
Windows
- Press the Windows key, type PowerShell, and press Enter to open it.
- Navigate to the folder where your .pem file was downloaded (usually Downloads) by running:
cd ~\Downloads
- Lock down the key file permissions by running these three commands:
icacls.exe task-app-key.pem /reset
icacls.exe task-app-key.pem /grant:r "$($env:username):(r)"
icacls.exe task-app-key.pem /inheritance:r
- Connect to your instance by running:
ssh -i task-app-key.pem ec2-user@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]
You may see a prompt asking if you want to continue connecting. Type yes and press Enter. You are now on your EC2 instance.
Seeing Permission denied or bad permissions?
This means your key file permissions are still too open. On macOS, make sure you ran chmod 400 on the correct file. On Windows, re-run the three icacls commands above.
Also confirm you are using ec2-user as the username (not ubuntu or root). Amazon Linux 2023 uses ec2-user by default.
Help me fix SSH permission denied when connecting to my EC2 instance with a .pem key file.
- Install pip (the Python package manager) by running:
sudo dnf install python3-pip -y
- Install Flask by running:
pip3 install flask
You should see Flask and its dependencies install successfully.
Now create the task manager app. You will use nano (a simple text editor that comes pre-installed on Amazon Linux) to create a file called app_inmemory.py.
- Open a new file in nano by running:
nano app_inmemory.py
- Paste the following code into the editor. This is the first half of the app, covering imports, setup, and the routes for viewing and creating tasks:
from flask import Flask, request, jsonify
# Create the Flask app and in-memory storage
app = Flask(__name__)
tasks = []
next_id = 1
# Home route returns a welcome message
@app.route("/")
def home():
return jsonify({"message": "Task Manager API - Traditional Architecture (in-memory)"})
# Return all tasks as JSON
@app.route("/tasks", methods=["GET"])
def get_tasks():
return jsonify(tasks)
What does this code do?
- tasks = [] creates an empty list in memory to hold tasks. When Flask restarts, this list resets to empty.
- next_id = 1 is a counter that gives each new task a unique ID.
- The / route returns a welcome message so you can quickly check if the app is alive.
- GET /tasks returns the entire tasks list as JSON.
- Leave no blank lines after the last line above, and paste the second half of the app directly below it. This adds the create, delete, health check, and startup logic:
# Add a new task to the list
@app.route("/tasks", methods=["POST"])
def create_task():
global next_id
data = request.get_json()
if not data or "title" not in data:
return jsonify({"error": "title is required"}), 400
task = {"id": next_id, "title": data["title"]}
tasks.append(task)
next_id += 1
return jsonify({"message": "Task created", "task": task}), 201
# Delete a task by its ID
@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
global tasks
original_len = len(tasks)
tasks = [t for t in tasks if t["id"] != task_id]
if len(tasks) == original_len:
return jsonify({"error": "Task not found"}), 404
return jsonify({"message": "Task deleted"})
# Health check endpoint
@app.route("/health")
def health():
return jsonify({"status": "healthy"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this second section do?
- POST /tasks accepts a JSON body with a title field, creates a new task object, and appends it to the list.
- DELETE /tasks/<id> removes a task by filtering it out of the list.
- app.run(host="0.0.0.0", port=5000) starts the server on all network interfaces (not just localhost). This is important because the load balancer you add later needs to reach it from outside.
- Save the file by pressing Ctrl+X, then Y, then Enter.
Awesome, I've got everything!
Great. Double-check you saved app_inmemory.py by confirming you are back at the command prompt (you see [ec2-user@... ~]$).
I'd like to double check the full code
Scroll up and confirm your file contains both chunks in order:
- Chunk 1: the imports, app, tasks, next_id, the home() route, and the get_tasks() route.
- Chunk 2: the create_task() route, the delete_task() route, the health() route, and the if __name__ == "__main__" block.
If anything is missing, open the file again with nano app_inmemory.py and correct it.
- Start the Flask app by running:
python3 app_inmemory.py
You should see output including a line that says the server is running on http://0.0.0.0:5000. Flask is now listening for requests.
Why is the terminal stuck?
Flask is running as a foreground process, which means it takes over the terminal. To run commands while Flask is running, you need a second terminal session. Open a new terminal window and SSH into your instance again using the same ssh command from earlier.
Test the app locally on the instance
Your Flask app is running, but you have not proven it actually works yet. Time to send it some requests from a second SSH session.
- Open a new terminal window on your local machine and SSH into the instance again using the same command from earlier.
Before you run the next command, what do you think the response will look like? The app just started and no tasks have been added yet.
- Check the tasks list by running:
curl http://localhost:5000/tasks
You see [] returned. That is an empty JSON array, which means Flask is running and the tasks list is empty. Exactly right.
- Add a task by running:
curl -X POST http://localhost:5000/tasks -H "Content-Type: application/json" -d '{"title":"My first task"}'
You should see a response confirming the task was created, including the task's ID and title.
- Verify the task appears in the list by running:
curl http://localhost:5000/tasks
You see your task in the JSON response. Flask is serving requests and storing tasks in memory.
Why in-memory storage first?
This version stores tasks in a Python list. It works, but everything disappears the moment Flask restarts. In the next step, you will connect a real database (RDS MySQL) so tasks survive restarts. Using in-memory storage first gives you a working app to test before adding database complexity.
Seeing Connection refused?
Make sure Flask is still running in your first SSH session. If you accidentally closed that terminal, SSH in again and run python3 app_inmemory.py to restart it.
Also check that you are running curl from the EC2 instance (in your second SSH session), not from your local machine. The app is only reachable via localhost on the instance itself right now.
Help me troubleshoot why curl to localhost:5000 is returning Connection refused on my EC2 instance.
Your server is up and your app is running. Next up, you will connect a real MySQL database so your tasks survive restarts.
Connect to RDS MySQL and Wire Up the Database
Your Flask app is running and you can add tasks through curl. There is one critical problem. Try restarting Flask right now and every task you created vanishes.
That is because every task lives in a Python list inside app_inmemory.py. The moment the process stops, that memory is wiped clean. In this step, you will create an RDS MySQL database in your private subnets and rewire the Flask app to persist tasks there.
In this step, get ready to:
- Create an RDS MySQL instance in your private subnets.
- Update the Flask app to read and write tasks from MySQL.
- Verify that tasks survive a Flask restart.
Prove the problem: data disappears on restart
Before building the fix, experience the problem firsthand. Your Flask app is still running from Step 3.
- Switch back to your SSH session on the EC2 instance.
- Add a task by running this command:
curl -X POST http://localhost:5000/tasks -H "Content-Type: application/json" -d '{"title":"Important task"}'
- Confirm the task exists by running:
curl http://localhost:5000/tasks
You should see your task in the JSON array.
- Now stop Flask by pressing Ctrl+C.
- Restart it by running:
python3 app_inmemory.py
Before you check, what do you think happened to your task? Is it still there?
- Open a second SSH session (or use another terminal) and run:
curl http://localhost:5000/tasks
You get back an empty array []. Your task is gone. This is exactly the problem that a real database solves.
- Stop Flask again by pressing Ctrl+C in the first SSH session.
Create the RDS MySQL instance
Your database needs to live in a private subnet so it is not directly reachable from the internet. Only your EC2 instance, which sits in a public subnet and is allowed by the task-rds-sg security group, can talk to it.
First, create a DB subnet group that tells RDS which private subnets to use.
- Open the Amazon RDS console at https://console.aws.amazon.com/rds/.
- In the navigation pane, click Subnet groups.
- Click Create DB Subnet Group.
- For Name, enter task-db-subnet-group.
- For Description, enter Private subnets for task-db.
- For VPC, select task-app-vpc.
- In the Add subnets section, select both Availability Zones.
- Under Subnets, select only the two private subnets (10.0.3.0/24 and 10.0.4.0/24). Do not select the public subnets.
- Click Create.
What is a DB subnet group?
A DB subnet group tells RDS which subnets it can place your database instance in. By selecting only your private subnets, you ensure the database has no public IP and no direct internet access.
RDS requires subnets in at least two Availability Zones so it can fail over to another AZ if needed.
Now create the MySQL database instance.
- In the RDS navigation pane, click Databases.
- Click Create database.
- Select Standard Create. Do not use Easy Create because it does not let you configure networking.
- Under Engine options, select MySQL.
- Under Templates, select Free tier (labeled Dev/Test or Sandbox for paid accounts). If you do not see Free tier, select Dev/Test.
- For DB instance identifier, enter task-db.
- For Master username, enter admin.
- For Master password, enter a password you will remember. Write it down because you will need it shortly.
- Under Instance configuration, select db.t4g.micro.
How much does this cost?
The db.t4g.micro instance costs $0.016 per hour in us-east-1. RDS bills per second with a 10-minute minimum. If you complete this project and delete the instance within two hours, the RDS cost is under $0.04.
- Under Storage, set Allocated storage to 20 GB and storage type to gp2.
- Uncheck Enable storage autoscaling.
- Under Connectivity, for Virtual private cloud (VPC), select task-app-vpc.
- For DB subnet group, select task-db-subnet-group.
- For Public access, select No.
- Under VPC security group, choose Choose existing and select task-rds-sg. Remove the default security group if it is selected.
- Expand Additional configuration at the bottom.
- Uncheck Enable automated backups to reduce costs for this learning project.
- Uncheck Enable encryption if checked (not available on free-tier instance classes).
- Click Create database.
RDS will now provision your MySQL instance. This takes about 5-10 minutes. You will see the status change from Creating to Available.
- While you wait, click on task-db in the database list to open its details page.
- On the Connectivity & security tab, find the Endpoint value. It looks like task-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com.
- Copy this endpoint. You will use it to connect your Flask app.
Why is the endpoint only visible once the instance is available?
RDS needs to allocate a network interface and assign a private IP inside your subnet before it can generate a DNS name. The endpoint becomes visible once the instance finishes provisioning.
Serverless contrast: DynamoDB vs. RDS
In Project 1, DynamoDB was instantly available with no subnet groups, no security group wiring, and no waiting for provisioning. RDS takes 5-10 minutes to create and requires you to manage network access.
The trade-off: RDS gives you SQL, joins, and ACID transactions that DynamoDB does not natively support.
Update the Flask app to use MySQL
Your database is ready. Now you will install the MySQL driver, create a new app.py that connects to RDS, and verify the connection works.
- In your SSH session, install the PyMySQL library by running:
pip3 install pymysql
Why PyMySQL?
PyMySQL is a pure Python MySQL client. It has no C extensions, which means it installs cleanly on ARM-based instances like your t4g without needing a compiler.
- Create the new app.py file by running:
nano app.py
Paste the following code into the editor. This is the first section covering imports, configuration, and the database connection helper.
import os
import json
import pymysql
from flask import Flask, request, jsonify
app = Flask(__name__)
# Database configuration from environment variables
DB_HOST = os.environ.get("DB_HOST", "db.task.internal")
DB_USER = os.environ.get("DB_USER", "admin")
DB_PASSWORD = os.environ.get("DB_PASSWORD", "your-password-here")
DB_NAME = os.environ.get("DB_NAME", "taskdb")
# Helper to open a connection to the MySQL database
def get_db_connection():
return pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASSWORD,
database=DB_NAME,
cursorclass=pymysql.cursors.DictCursor,
)
What does this code do?
The four DB_ variables read from environment variables first, with fallback defaults. You will pass the real RDS endpoint via the DB_HOST environment variable when you start Flask.
The get_db_connection() function returns a fresh MySQL connection each time. Using DictCursor means query results come back as dictionaries instead of tuples, which makes them easy to convert to JSON.
Directly below, paste the database initialization function and the home route.
# Create the database and tasks table if they don't exist
def init_db():
conn = pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASSWORD,
cursorclass=pymysql.cursors.DictCursor,
)
try:
with conn.cursor() as cursor:
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_NAME}")
cursor.execute(f"USE {DB_NAME}")
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.commit()
finally:
conn.close()
@app.route("/")
def home():
return jsonify({"message": "Task Manager API - Traditional Architecture"})
What does init_db do?
This function connects without specifying a database name, creates the taskdb database if it does not exist, then creates the tasks table. It runs once when Flask starts, so you never need to manually set up the schema.
Next, paste the GET and POST routes for tasks.
# Fetch all tasks from the database
@app.route("/tasks", methods=["GET"])
def get_tasks():
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT id, title, created_at FROM tasks ORDER BY created_at DESC")
tasks = cursor.fetchall()
for task in tasks:
task["created_at"] = task["created_at"].isoformat()
return jsonify(tasks)
finally:
conn.close()
# Create a new task in the database
@app.route("/tasks", methods=["POST"])
def create_task():
data = request.get_json()
if not data or "title" not in data:
return jsonify({"error": "title is required"}), 400
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("INSERT INTO tasks (title) VALUES (%s)", (data["title"],))
conn.commit()
return jsonify({"message": "Task created", "title": data["title"]}), 201
finally:
conn.close()
What changed from the in-memory version?
Instead of appending to a Python list, create_task runs an INSERT SQL statement. Instead of returning the list directly, get_tasks runs a SELECT query. Each route opens its own connection and closes it in a finally block to prevent connection leaks.
Finally, paste the delete route, health check, and startup block.
# Delete a task by ID
@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM tasks WHERE id = %s", (task_id,))
conn.commit()
if conn.affected_rows() == 0:
return jsonify({"error": "Task not found"}), 404
return jsonify({"message": "Task deleted"})
finally:
conn.close()
@app.route("/health")
def health():
return jsonify({"status": "healthy"})
if __name__ == "__main__":
init_db()
app.run(host="0.0.0.0", port=5000)
What does the main block do differently?
The if __name__ == "__main__": block now calls init_db() before starting Flask. This creates the database and table on first launch. The app still listens on 0.0.0.0 port 5000 so the ALB can reach it in the next step.
- Save and exit nano by pressing Ctrl+X, then Y, then Enter.
✔️ Awesome, I've got everything!
Great. Make sure you saved app.py before continuing.
ⓧ I'd like to double check the full code
Your app.py was built across four paste operations. Scroll up and confirm each section is present in this order:
- Imports, app setup, DB config variables, and get_db_connection().
- The init_db() function and the home() route.
- The get_tasks() and create_task() routes.
- The delete_task() route, health() route, and the if __name__ startup block with init_db().
Now start Flask with the RDS endpoint and your database password passed as environment variables.
- Start Flask by running this command, replacing the RDS endpoint and password with your own values:
DB_HOST=[[RDS_ENDPOINT="your RDS endpoint e.g. task-db.xxxx.us-east-1.rds.amazonaws.com"]] DB_PASSWORD=your-rds-password python3 app.py
Before you look at the output, do you think Flask will connect to the database successfully? Remember, RDS is in a private subnet and your EC2 is in a public subnet.
Flask starts and you see no connection errors. The init_db() function successfully created the taskdb database and tasks table on RDS.
Why did the connection work?
This worked because of the security group chain you built in Step 2. The task-rds-sg security group allows inbound MySQL traffic on port 3306, but only from instances that belong to task-ec2-sg. Your EC2 instance is in that group, so the connection is allowed.
If you had assigned the wrong security group to either the EC2 or RDS instance, the connection would time out. This is VPC network isolation in action.
- Verify the app is running by opening a second SSH session and running:
curl http://localhost:5000/tasks
You should see an empty JSON array []. The tasks table is empty, which is correct.
Seeing a connection error?
If Flask prints an error about connecting to MySQL, check these common causes:
- Verify you copied the RDS endpoint exactly from the Connectivity & security tab. It should end with .rds.amazonaws.com. Do not include the port number.
- Confirm the task-rds-sg security group allows inbound MySQL (port 3306) from task-ec2-sg.
- Make sure the RDS instance status is Available, not still Creating.
- Double-check that your DB_PASSWORD value matches what you set in the RDS console.
Help me debug my RDS connection error from EC2.
Test data persistence across restarts
The real test: does your data survive when Flask stops? This is the exact scenario that broke the in-memory version.
- Add a task by running:
curl -X POST http://localhost:5000/tasks -H "Content-Type: application/json" -d '{"title":"My first persistent task"}'
- Confirm it was created by running:
curl http://localhost:5000/tasks
You should see your task in the response with an id, title, and created_at timestamp.
- In the first SSH session, stop Flask by pressing Ctrl+C.
- Restart Flask with the same environment variables:
DB_HOST=[[RDS_ENDPOINT="your RDS endpoint e.g. task-db.xxxx.us-east-1.rds.amazonaws.com"]] DB_PASSWORD=your-rds-password python3 app.py
What do you think will happen when you check for tasks now? Will they be there, or will they vanish like before?
- In the second SSH session, run:
curl http://localhost:5000/tasks
Your task is still there. The data survived the restart because it lives in RDS MySQL, not in Python's memory. That is the entire point of using a managed database.
Task disappeared after restart?
- Make sure you started app.py (the database version), not app_inmemory.py.
- Verify you passed the same DB_HOST and DB_PASSWORD values both times. Different values would connect to a different (or non-existent) database.
Help me figure out why my tasks disappeared after restarting Flask.
Your app now has a real database. Tasks persist across restarts because they live in RDS MySQL instead of Python memory. Next up, you will add an Application Load Balancer so your app is accessible on port 80 and give your database a friendly DNS name with Route 53.
Add the Application Load Balancer and Route 53
In the previous step, you connected your Flask app to an RDS MySQL database. Your data persists across restarts, and the app works from inside the EC2 instance.
The problem is access. Right now, the only way to reach your app is through the EC2 instance's public IP on port 5000. That is not production-ready. You need a load balancer to accept HTTP traffic on port 80 and route it to your instance. You also need a friendly DNS name so your app does not hardcode the raw RDS endpoint.
In this step, get ready to:
- Create an Application Load Balancer to route HTTP traffic on port 80 to your EC2 instance.
- Set up a Route 53 private hosted zone to give your database a friendly DNS name.
- Test the full traditional architecture end to end through the ALB URL.
Create the target group and Application Load Balancer
Before creating the ALB itself, you need a target group. A target group tells the ALB where to send incoming traffic.
- Switch back to the AWS Management Console.
- Search for EC2 in the top search bar and open the EC2 console.
- In the left sidebar, scroll down to Load Balancing and click Target Groups.
- Click Create target group.
- For Choose a target type, select Instances.
- Enter task-targets as the Target group name.
- Set Protocol to HTTP and change Port to 5000.
- For VPC, select task-app-vpc.
Why port 5000?
Your Flask app listens on port 5000. The ALB receives traffic from the internet on port 80, then forwards each request to port 5000 on your EC2 instance. This port translation is what makes the app accessible on a standard HTTP port without changing your Flask code.
- Leave the health check settings as default (the default path / matches your Flask / route) and click Next.
- In the Register targets section, select the checkbox next to your EC2 instance.
- Click Include as pending below.
- Click Create target group.
You should see task-targets appear in the target groups list.
Now create the ALB that uses this target group.
- In the left sidebar under Load Balancing, click Load Balancers.
- Click Create load balancer.
- Under Application Load Balancer, click Create.
- Enter task-alb as the Load balancer name.
- Keep Scheme as Internet-facing.
- Under Network mapping, select task-app-vpc as the VPC.
- Select both public subnets (the ones with CIDR 10.0.1.0/24 and 10.0.2.0/24) in two different availability zones.
- Under Security groups, remove the default security group and select task-alb-sg.
Why two subnets in different availability zones?
An Application Load Balancer requires subnets in at least two availability zones. This gives the ALB the ability to handle traffic even if one zone goes down. Your VPC was designed with this requirement in mind.
- Under Listeners and routing, keep the default HTTP listener on port 80.
- For Default action, select the task-targets target group.
- Click Create load balancer.
You should see task-alb in the load balancers list with a status of Provisioning. It takes a few minutes to become Active. You will set up Route 53 while it provisions.
Don't see your VPC or subnets?
Make sure you are in the us-east-1 region (check the top-right corner of the console). The VPC and subnets only appear if you are in the same region where you created them.
If task-alb-sg does not appear, confirm the security group belongs to task-app-vpc and not the default VPC.
Help me troubleshoot my ALB creation. I selected task-app-vpc but I cannot see the correct subnets or security group.
Set up Route 53 private hosted zone
Right now your Flask app connects to the database using the raw RDS endpoint. A private hosted zone lets you create a friendly DNS name like db.task.internal that resolves to that endpoint inside your VPC.
- In the AWS Management Console, search for Route 53 and open the Route 53 console.
- In the left navigation pane, click Hosted zones.
- Click Create hosted zone.
- Enter task.internal as the Domain name.
- For Type, select Private hosted zone.
- Under VPCs to associate with the hosted zone, select your region (us-east-1) and choose task-app-vpc.
- Click Create hosted zone.
What is a private hosted zone?
A private hosted zone is a DNS container that only works inside the VPCs you associate with it. Queries from outside those VPCs cannot see these records.
This is perfect for giving internal resources like databases friendly names without exposing them to the public internet.
Now add a CNAME record that maps db.task.internal to your RDS endpoint.
- Inside the task.internal hosted zone, click Create record.
- In the Record name field, enter db (this creates db.task.internal).
- For Record type, select CNAME.
- In the Value field, paste your RDS endpoint (find it in the RDS console under Connectivity & security for your task-db instance).
- Click Create records.
You should see the CNAME record for db.task.internal listed alongside the default NS and SOA records.
Now restart Flask so it uses the db.task.internal DNS name instead of the raw RDS endpoint.
- Switch back to your SSH session on the EC2 instance.
- Stop the running Flask process by pressing Ctrl+C.
- Restart Flask without the DB_HOST override by running this command:
unset DB_HOST
python3 app.py
What does this do?
The unset DB_HOST command removes any environment variable override you set in the previous step. Without it, Flask uses the default value db.task.internal from your app.py code. Route 53 resolves that name to your RDS endpoint inside the VPC.
You should see Flask start up without errors and the init_db function connect successfully.
Flask won't start or shows a connection error?
Double check that your CNAME record value matches the exact RDS endpoint (including the region and .rds.amazonaws.com suffix). A missing or mistyped character will prevent DNS resolution.
Confirm the hosted zone is associated with task-app-vpc. If it is associated with a different VPC, the EC2 instance cannot resolve db.task.internal.
Help me debug my Flask connection error after setting up Route 53. Flask cannot resolve db.task.internal.
Test the full architecture end to end
Your full traditional architecture is now wired up: ALB to EC2 to RDS, with Route 53 providing internal DNS. Time to test it from the outside world.
- Return to the EC2 console in the AWS Management Console.
- In the left sidebar under Load Balancing, click Load Balancers.
- Click on task-alb and check its State. Wait until it shows Active (this may take 2-3 minutes).
- Click Target Groups in the sidebar, then click task-targets.
- Select the Targets tab and confirm your EC2 instance shows a Health status of healthy.
Target status stuck on initial or unhealthy?
Check that Flask is running on 0.0.0.0:5000 (not 127.0.0.1). Flask must accept connections from the ALB, not just localhost.
Verify that task-ec2-sg allows inbound HTTP on port 80 from task-alb-sg. Without this rule, the ALB health check cannot reach your instance.
Help me figure out why my ALB target is not becoming healthy.
Before you open the ALB URL, what do you expect to see? Think about which route the ALB will hit by default.
- Go back to Load Balancers in the sidebar and click on task-alb.
- Copy the DNS name value from the ALB's details (it looks like task-alb-123456789.us-east-1.elb.amazonaws.com).
- Paste the DNS name into your browser's address bar and press Enter.
You should see your Flask app respond with {"message": "Task Manager API - Traditional Architecture"}. The ALB accepted your browser request on port 80 and routed it to Flask on port 5000.
Now verify that tasks work end to end through the ALB. Switch back to your SSH session and run these commands:
- Add a task through the ALB by running this command:
curl -X POST http://[[ALB_DNS="your ALB DNS name"]]/tasks -H "Content-Type: application/json" -d '{"title":"ALB task"}'
- View all tasks through the ALB by running:
curl http://[[ALB_DNS="your ALB DNS name"]]/tasks
You should see the task you just created in the JSON response. The data is stored in RDS and survives Flask restarts.
What is the full request chain?
Your browser (or curl) sends a request to the ALB's public DNS name on port 80. The ALB forwards it to your EC2 instance on port 5000. Flask processes the request and connects to the RDS database using the db.task.internal DNS name. Route 53 resolves that name to the actual RDS endpoint inside the VPC.
Internet traffic flows: ALB (port 80) to EC2 (port 5000) to RDS (port 3306). The database sits in a private subnet with no public access. Only your EC2 instance can reach it.
How does this compare to serverless?
In Project 1, API Gateway was your "load balancer" and it was free for low traffic. Here, the ALB costs $0.0225/hour even when idle.
Route 53 has no direct serverless equivalent because serverless apps do not need internal DNS. These are the hidden costs of traditional architecture.
Browser shows an error or timeout?
Confirm the ALB status is Active (not Provisioning). A provisioning ALB cannot serve traffic yet.
Check that task-alb-sg allows inbound HTTP on port 80 from 0.0.0.0/0. Without this rule, your browser request is blocked before reaching the ALB.
Help me troubleshoot why I cannot reach my Flask app through the ALB DNS name.
Your full traditional architecture is live. You built the network, the server, the database, the load balancer, and the internal DNS. Up next is the Secret Mission where you will add auto scaling so your app can handle traffic spikes automatically.
Secret mission
Add Auto Scaling for Automatic Scaling
Your app runs on a single EC2 instance. If it crashes or traffic spikes, everything goes down. In this secret mission, you'll add an Auto Scaling Group that automatically replaces failed instances and adds capacity under load. This directly contrasts with Lambda's built-in scaling from Project 1, which required zero configuration.
Clean Up Your Resources
Clean Up Your Resources
This project created several AWS resources, some of which cost money every hour they run. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Your RDS instance costs $0.016/hour and your ALB costs $0.0225/hour, even when idle. Each public IPv4 address adds another $0.005/hour. Together, these add up to approximately $1-2 per day.
If you are not continuing immediately, pause or delete your resources to avoid unexpected charges.
Resources you used:
- Auto Scaling Group and Launch Template (Secret Mission). Free.
- Application Load Balancer (task-alb) and target group (task-targets). Costs $0.0225/hour plus minimal LCU charges.
- EC2 instance(s) and key pair. Free via the T4g trial until December 31, 2026.
- RDS MySQL instance (task-db). Costs $0.016/hour, billed per second with a 10-minute minimum.
- Route 53 private hosted zone (task.internal). Costs $0.50/month, but free if deleted within 12 hours of creation.
- VPC (task-app-vpc) with subnets, security groups, route tables, and internet gateway. Free.
- Public IPv4 addresses (EC2 and ALB). Cost $0.005/hour per address.
- Local .pem key pair file on your machine.
- AWS CLI v2 installed on your machine.
Keep everything running
No action needed. Choose this if you are still actively building on this project or plan to continue within the next few hours.
- Your resources will continue running and costing approximately $1-2 per day, primarily from the RDS instance, ALB, and public IPv4 addresses.
- The EC2 instance is free under the T4g trial (750 hours/month until December 31, 2026).
- The VPC, subnets, security groups, route tables, and internet gateway are free.
- AWS CLI v2 remains installed on your local machine.
Pause - I'll come back to this later
Shut down the expensive resources while keeping the free infrastructure in place. This lets you resume later without rebuilding everything from scratch.
- If you completed the Secret Mission, delete the Auto Scaling Group first. In the EC2 console, select Auto Scaling Groups in the left sidebar, select your group, then click Delete. The ASG will keep replacing stopped instances if you leave it running.
- Stop any running EC2 instances. In the EC2 console, go to Instances, select the instance, click Instance state, then Stop instance. Stopped instances do not incur compute charges, but EBS storage charges continue (minimal).
- Stop the RDS instance. In the RDS console, go to Databases, select task-db, click Actions, then Stop temporarily. RDS automatically restarts the instance after 7 days, so check back if you pause longer than that.
- Delete the ALB to stop its hourly charges. In the EC2 console, go to Load Balancers, select task-alb, click Actions, then Delete load balancer.
- Delete the Route 53 hosted zone to avoid the $0.50/month charge. In the Route 53 console, go to Hosted zones, select task.internal, delete the db.task.internal CNAME record first, then delete the hosted zone.
- Keep the VPC, security groups, target group, key pair, and Launch Template. These are free and will save you time when you come back.
Delete - I don't want to use this again
Remove all project resources completely. Delete in this order to avoid dependency errors.
Step 1: Delete the Auto Scaling Group and Launch Template.
- If you completed the Secret Mission, go to the EC2 console and select Auto Scaling Groups in the left sidebar.
- Select your Auto Scaling Group, then click Delete. Confirm the deletion and wait for it to finish.
- Then go to Launch Templates in the EC2 sidebar, select your template, click Actions, then Delete template.
Step 2: Delete the ALB and target group.
- In the EC2 console, go to Load Balancers in the left sidebar.
- Select task-alb, click Actions, then Delete load balancer. Type confirm when prompted.
- Then go to Target Groups in the EC2 sidebar, select task-targets, click Actions, then Delete.
Step 3: Terminate EC2 instances and delete the key pair.
- In the EC2 console, go to Instances. Select any running instances from this project.
- Click Instance state, then Terminate instance. Confirm the termination.
- Then go to Key Pairs in the EC2 sidebar, select the key pair you created, click Actions, then Delete.
Step 4: Delete the RDS instance.
- In the RDS console, go to Databases and select task-db.
- Click Actions, then Delete.
- Uncheck Create final snapshot to avoid ongoing storage charges. Check the acknowledgement box, then click Delete.
Step 5: Delete the Route 53 hosted zone.
- In the Route 53 console, go to Hosted zones and select task.internal.
- Select the db.task.internal CNAME record and click Delete record. You must delete custom records before you can delete the zone.
- Then select the task.internal hosted zone and click Delete hosted zone. If you do this within 12 hours of creation, no charge applies.
Step 6: Delete the VPC.
- In the VPC console, go to Your VPCs and select task-app-vpc.
- Click Actions, then Delete VPC. The console automatically deletes the associated subnets, route tables, internet gateway, and security groups.
- Type delete to confirm, then click Delete.
Step 7: Clean up local files.
- Delete the .pem key pair file from wherever you saved it (usually your Downloads folder).
- To remove your stored AWS credentials, delete the .aws folder in your user profile directory. On Windows, open File Explorer and navigate to %USERPROFILE%\.aws. On macOS or Linux, run:
- Only delete the .aws folder if you do not use the AWS CLI for other projects. It stores all your AWS profiles and credentials.
rm -rf ~/.aws
Step 8: Uninstall AWS CLI v2 (optional).
- On Windows, open Programs and Features from the Control Panel (or type appwiz.cpl in the Start menu search bar).
- Find AWS Command Line Interface in the list, select it, and click Uninstall.
That's a Wrap!
That's a Wrap!
Nice work! You just deployed a traditional web app on AWS, building every layer from the network up.
Across this project and Project 1, you've now worked with 11 core AWS services and can explain when to choose serverless vs traditional architecture.
You've learned how to:
- Build a custom VPC network from scratch with public and private subnets, security groups, and an internet gateway to implement network isolation for your entire application stack.
- Deploy a Python Flask app on an EC2 instance and connect it to an RDS MySQL database in a private subnet, managing the server and database yourself instead of relying on Lambda and DynamoDB.
- Front your app with an Application Load Balancer for production-ready HTTP routing and use Route 53 for internal DNS, completing a full traditional architecture you can contrast with API Gateway's serverless request routing from Project 1.
- Secret Mission: Add an Auto Scaling Group for automatic instance replacement and scaling under load, directly contrasting with Lambda's built-in automatic scaling.
Ready to quiz yourself?