Before clicking anything in the AWS console, take 5 minutes to plan. Real cloud engineers always plan their network on paper (or a whiteboard) first. Here's what we're building:
| Resource | Value |
|---|---|
| VPC CIDR | 10.0.0.0/16 (65,536 IPs) |
| Public subnet (1a) | 10.0.1.0/24 |
| Private subnet A (1a) | 10.0.2.0/24 |
| Private subnet B (1b) | 10.0.3.0/24 |
| Region | Whichever you used in Week 1 (consistency helps) |
RDS Multi-AZ requires at least 2 subnets in 2 different AZs. Even if you don't enable Multi-AZ today, AWS won't let you create the database without the subnet group set up properly.
Time to actually build it. Sign in to the AWS console and navigate to VPC.
- Click "Create VPC" top-right
- Select "VPC and more" (this wizard creates everything in one shot)
- Fill in the form using the values below
Click "Create VPC". AWS spins up the VPC, an Internet Gateway, 2 public subnets, 2 private subnets, route tables, and route table associations — all wired correctly. Takes about 90 seconds.
The "VPC and more" wizard is great for learning. In production, most teams use Terraform or CloudFormation to script this. We'll cover IaC in Week 3.
Before moving on, take a quick look at what AWS created. In the VPC dashboard:
- Click "Subnets" in the left sidebar
- Filter by VPC: select your
cloudwithshad-week2VPC - You should see 4 subnets: 2 public + 2 private
Click on the public subnet, then the "Route table" tab. You should see a route like this:
Now click on a private subnet and look at its route table:
A subnet is "public" or "private" based on whether its route table has a route to 0.0.0.0/0 via an Internet Gateway. That single line decides everything.
We need two SGs: one for the web server (web-sg) and one for the database (db-sg). The clever part is how they reference each other.
web-sg — for the EC2 web server
| Type | Source | Port | Why |
|---|---|---|---|
| SSH | My IP | 22 | So you can SSH in |
| HTTP | 0.0.0.0/0 | 80 | Public web traffic |
| HTTPS | 0.0.0.0/0 | 443 | Public web traffic (TLS) |
db-sg — for the RDS database
| Type | Source | Port | Why |
|---|---|---|---|
| MySQL/Aurora | web-sg (the SG itself, not an IP!) | 3306 | Only the web tier can talk to the DB |
Instead of allowing an IP, you allow another security group. This means: "Allow anything that belongs to web-sg." Now if you scale to 10 web servers, you don't have to update the DB firewall — they all carry the web-sg badge.
Do NOT open the DB port (3306) to 0.0.0.0/0. That exposes your database to the entire internet. People scan for open MySQL ports 24/7.
Head to EC2 → Launch instance. The fields that matter:
| Field | Value |
|---|---|
| Name | cloudwithshad-web |
| AMI | Amazon Linux 2023 (free tier eligible) |
| Instance type | t2.micro or t3.micro (free tier) |
| Key pair | Use existing from Week 1, or create new |
| Network settings | Edit → |
| VPC | cloudwithshad-week2 |
| Subnet | The PUBLIC one (10.0.1.0/24) |
| Auto-assign public IP | Enable |
| Security group | Select existing → web-sg |
In the "User data" field (Advanced details, near the bottom), paste this:
This script runs once when EC2 boots — installs nginx + mysql client, starts the web server, sets up a placeholder page.
Click Launch instance. Wait ~90 seconds for it to come up. Copy the public IP and paste it in your browser. You should see "cloudwithshad · two-tier app running".
Wait another 60 seconds — user data scripts take a moment. If still nothing, check: (1) is the SG allowing port 80? (2) is the instance in the PUBLIC subnet? (3) does the instance have a public IP?
You have two ways to provision the database tier. Both teach the same two-tier pattern; they differ in who manages the database and how it's networked.
- Path A — Amazon RDS (this Step 6): the managed-service way. Free tier eligible, but the console has cost traps to navigate.
- Path B — Self-hosted MySQL on EC2 in a fresh VPC with NAT (Step 6b below): you create a new VPC with NAT gateway, then install MySQL on an EC2 in a private subnet. Architecturally cleanest, but NAT gateway costs ~$1/day until you delete it — so you MUST clean up the same day.
Pick one — you don't need to do both. If you want the safest free-tier path, Path A. If you want to see a textbook "DB in private subnet" architecture and you'll clean up promptly, Path B.
Path A — Provision the RDS database
Navigate to RDS → Databases → Create database.
The RDS console's defaults are not free tier. If you just click through, the estimate at the bottom of the page will show somewhere around $900+/month. The defaults assume a production database: Multi-AZ cluster, db.m7g.large, 100 GB Provisioned IOPS storage, Secrets Manager, Performance Insights, Enhanced Monitoring — all switched on, all paid.
Follow the table below exactly and keep an eye on the "Estimated monthly costs" box at the bottom of the page as you fill it in. With the right settings, that estimate should drop to roughly $0–$15/month.
Fill in the form section-by-section. The console is long — work top to bottom.
Engine options
| Field | Value |
|---|---|
| Engine type | MySQL |
| Creation method | Full configuration (gives you all the cost-control switches we need) |
| Engine version | The default latest (e.g. MySQL 8.4.x) is fine |
| Enable RDS Extended Support | Leave OFF ← it's a paid add-on |
Templates
| Field | Value |
|---|---|
| Template | Dev/Test ← what we use for the lab. Intended for non-production workloads. Avoid Production (pre-loads expensive HA defaults). |
Picking Dev/Test is fine for learning, but it silently flips on three things that will cost you money. As you fill in the rest of the form, watch for and override each of these:
- Availability: Dev/Test defaults to Multi-AZ DB instance (2x cost). Switch to Single-AZ DB instance deployment (1 instance) in the next section.
- Storage autoscaling: Dev/Test enables autoscaling with a high max. Turn it OFF in the Storage section, or set max = 25 GiB.
- Enhanced Monitoring: Dev/Test enables it by default (CloudWatch Logs charges). Uncheck "Enable Enhanced Monitoring" in the Monitoring section.
Availability and durability
| Field | Value |
|---|---|
| Deployment option | Single-AZ DB instance deployment (1 instance) ← critical. The other options double or triple your bill. |
Settings
| Field | Value |
|---|---|
| DB instance identifier | cloudwithshad-db |
| Master username | admin |
Credentials management
| Field | Value |
|---|---|
| Manage credentials | Self managed ← important. ("Managed in AWS Secrets Manager" is the default and adds Secrets Manager charges.) |
| Master password | Pick a strong password and save it in your password manager. |
Instance configuration
| Field | Value |
|---|---|
| DB instance class filter | Burstable classes. Also tick "Include previous generation classes" so db.t3.micro appears. |
| Instance type | db.t3.micro or db.t4g.micro |
Storage
| Field | Value |
|---|---|
| Storage type | General Purpose SSD (gp3) ← critical. Default is "Provisioned IOPS SSD (io2)", ~$700/month. |
| Allocated storage | 20 GiB |
| Storage autoscaling | Leave OFF |
Connectivity
| Field | Value |
|---|---|
| Compute resource | Don't connect to an EC2 compute resource |
| VPC | cloudwithshad-week2-vpc ← NOT the Default VPC |
| DB subnet group | Create one with only the 2 private subnets (don't use the default group, which often includes public subnets) |
| Public access | No ← critical |
| VPC security group | Choose existing → remove default, add db-sg |
| RDS Proxy | Leave OFF |
Monitoring
| Field | Value |
|---|---|
| Database Insights | Standard (not Advanced) |
| Performance Insights | Uncheck "Enable Performance Insights" |
| Enhanced Monitoring | Uncheck "Enable Enhanced Monitoring" |
Scroll to the very bottom. The "Estimated monthly costs" box should show around $13–$15/month. If it still says hundreds, scroll back up — storage may still be io2, deployment still Multi-AZ, or Performance Insights still on.
Hit Create database. RDS takes 5–10 minutes to provision.
RDS networking can be finicky in some accounts. If you hit a connection timeout after the database is "Available" — even with security groups looking correct — just switch to Path B below. The two-tier architecture concept is identical and Path B works around RDS-specific quirks.
This path gives you the textbook two-tier architecture — database in a private subnet, no public IP, NAT gateway for outbound internet. We're going to create a fresh VPC using the same "VPC and more" wizard, this time with NAT gateways enabled. That's the only real difference. Then we'll launch a new web EC2 and a new db EC2 inside it.
A NAT gateway costs ~$0.045/hour (~$1/day, ~$32/month) whether or not anything uses it. As long as you complete this lab and clean up the same day, total cost is around $0.10–$0.30. If you forget to clean up, it's $1/day. Set a calendar reminder right now to delete the VPC after this lab. Deleting the VPC kills the NAT gateway — that's what stops the bill.
Security groups belong to a VPC — they can't be reused across VPCs. Since we want NAT in this architecture, and NAT is easiest to set up via the wizard at VPC creation, we just start fresh. It's a few extra minutes but you'll see the wizard create everything (VPC, subnets, NAT, routes) in one shot.
Step 6b.1 — Terminate the Step 5 web EC2 (save free-tier hours)
Since we're rebuilding in a new VPC, the old web EC2 from Step 5 just costs you free-tier hours. Terminate it:
- EC2 → Instances → select
cloudwithshad-web - Instance state → Terminate
Step 6b.2 — Create the new VPC (with NAT)
VPC → Create VPC → select "VPC and more". Same as Step 2, with ONE crucial difference:
"In 1 AZ" is the cheapest option (~$32/mo). "1 per AZ" doubles that to ~$64/mo. For learning, one NAT in one AZ is plenty — your private subnet in the other AZ will route through it across AZs (technically a small data charge for cross-AZ traffic, negligible for the lab).
Click Create VPC. The wizard takes ~2 minutes this time (NAT gateway provisioning adds time vs no-NAT). When it finishes, the resource map will show: VPC + 4 subnets + IGW + 1 NAT gateway + route tables, all wired correctly.
The wizard created a NAT gateway in one of your public subnets, attached an Elastic IP to it, and updated both private subnets' route tables to add 0.0.0.0/0 → nat-gateway. Your private subnets now have outbound internet access (for things like dnf install) but no inbound internet access. That's exactly what we want for the DB tier.
Step 6b.3 — Create security groups in the new VPC
Security groups are VPC-scoped, so the ones from Step 4 don't exist in this new VPC. Quickly recreate them — same rules as before:
web-sg (EC2 → Security Groups → Create security group):
- Name:
web-sg, VPC:cloudwithshad-pathb-vpc - Inbound: SSH (22) from My IP, HTTP (80) from 0.0.0.0/0, HTTPS (443) from 0.0.0.0/0
- Outbound: leave the default "All traffic → 0.0.0.0/0" ← don't replace this with specific port rules, or your web EC2 won't be able to reach the DB EC2
db-sg:
- Name:
db-sg, VPC:cloudwithshad-pathb-vpc - Inbound rule 1: MYSQL/Aurora (3306), Source =
web-sg(the SG itself) - Inbound rule 2: SSH (22), Source =
web-sg(so you can SSH from web to db for verification) - Outbound: leave the default "All traffic → 0.0.0.0/0"
Step 6b.4 — Launch the new web EC2 (in the new VPC)
EC2 → Launch instance. Same as Step 5, but in the new VPC:
| Field | Value |
|---|---|
| Name | cloudwithshad-web |
| AMI | Amazon Linux 2023 |
| Instance type | t2.micro or t3.micro |
| Key pair | Your existing key from Week 1 |
| VPC | cloudwithshad-pathb-vpc |
| Subnet | A public subnet (e.g. cloudwithshad-pathb-subnet-public1-...) |
| Auto-assign public IP | Enable |
| Security group | Select existing → web-sg |
| User data | Same script as Step 5 (nginx + mariadb105 client + placeholder page) |
Step 6b.5 — Launch the database EC2 (in a PRIVATE subnet)
EC2 → Launch instance:
| Field | Value |
|---|---|
| Name | cloudwithshad-db |
| AMI | Amazon Linux 2023 |
| Instance type | t2.micro or t3.micro |
| Key pair | Same key as the web EC2 |
| VPC | cloudwithshad-pathb-vpc |
| Subnet | A PRIVATE subnet (e.g. cloudwithshad-pathb-subnet-private1-...) ← this is the architectural win |
| Auto-assign public IP | Disable (it's in a private subnet — no public IP needed; NAT handles outbound) |
| Security group | Select existing → db-sg |
In the "User data" field (Advanced details, near the bottom), paste this script. It installs MariaDB server, sets up the admin user, and creates the cloudwithshad database — all automatically. (The NAT gateway is what lets dnf install reach the internet from this private subnet.)
Click Launch instance. Wait ~2 minutes for the boot + script to finish (downloading MariaDB through NAT is a touch slower than via IGW).
Step 6b.6 — Grab the DB EC2's private IP
EC2 → Instances → click cloudwithshad-db → copy the Private IPv4 address (e.g. 10.1.x.x). This is your "endpoint" for Path B.
Path B's "endpoint" is the DB EC2's private IPv4 address. Username/password: admin / cloudwithshad2026. You'll plug these into the mysql command in Step 7 from your new web EC2 (the one you just launched in the new VPC).
SSH from the web EC2 into the db EC2 using its private IP, then check: cat /var/log/cloudwithshad-db.log. If the file doesn't exist, the script is still running — wait a minute and try again. If after 5 minutes it still hasn't run, check the EC2's system log (EC2 → instance → Actions → Monitor and troubleshoot → Get system log).
This step works the same for both paths — the only difference is the host you connect to.
cloudwithshad-db.xxxxx.us-east-1.rds.amazonaws.com)admin10.1.x.x)admincloudwithshad2026 (from the user data script)SSH into your web EC2 from your laptop:
Once you're inside the web EC2, connect to the database. Use this simple form (don't paste the SSL-flag version the RDS "Connect using" panel suggests — those flags use MySQL-8 syntax the MariaDB client doesn't recognise):
If you see Welcome to the MariaDB monitor and a MySQL> prompt, you're in. (Yes, it says "MariaDB monitor" even when connecting to MySQL 8.4 — that's because we installed the MariaDB client. Same wire protocol, fully compatible.)
Now try a query:
Capture that MySQL output and drop it in the cohort WhatsApp group. That's your Week 2 Lab 3 submission. 🎉
Error: ERROR 2003 ... (110) / (115) / connection times out
What it really means: Network can't reach the DB at all. SYN packet dropped before reply.
Fix: (1) web-sg outbound rules must include port 3306 OR "All traffic" to 0.0.0.0/0 (default). (2) db-sg inbound allows MYSQL/3306 from web-sg. (3) Same VPC for both EC2s.
Error: ERROR 1045 ... Access denied for user
What it really means: 🎉 Good news — the network works! You reached the database, just with wrong credentials.
Fix: Re-check username/password. For Path B, default is admin / cloudwithshad2026.
Error: ERROR 2002 ... Connection refused
What it really means: You reached the host, but nothing is listening on port 3306.
Fix: For Path B: is the DB EC2 still booting? Wait 2 min after launch. Check systemctl status mariadb on the DB EC2.
Error: mysql: unknown variable 'ssl-mode=...'
What it really means: You used MySQL-8 SSL flags but have the MariaDB 10.5 client.
Fix: Drop the SSL flags. Use the plain mysql -h <host> -u admin -p command above.
Try connecting to the RDS endpoint from your laptop (not the EC2 instance). It will hang and time out. That's correct behaviour — the database is invisible to the public internet. Only your EC2 can reach it.
The database tier (RDS or the DB EC2) will burn through your free tier hours if left running. If you used Path B, the NAT gateway is also burning ~$1/day until you delete the VPC. Clean up immediately after your screenshot.
⚠ Cleanup checklist — in this order
- Path B users — DO THIS FIRST: VPC → Your VPCs →
cloudwithshad-pathb-vpc→ Actions → Delete VPC. This deletes the NAT gateway, EIP, subnets, route tables, security groups, AND the EC2s' network attachments all in one shot. Stops the NAT bill immediately. - Path B EC2 instances (if not gone with the VPC): EC2 → Instances → terminate both
cloudwithshad-webandcloudwithshad-dbin the pathb VPC - Path A — RDS: Databases → select
cloudwithshad-db→ Actions → Delete → uncheck "Create final snapshot" → type the confirmation phrase → Delete - Path A — Web EC2: Instances → select
cloudwithshad-web→ Instance state → Terminate - Path A — VPC: VPC → Your VPCs →
cloudwithshad-week2-vpc→ Actions → Delete VPC - Stranded resources: EC2 → Elastic IPs → release any unattached EIPs ($0.005/hr if unattached). Also check VPC → NAT gateways to confirm no NAT is still alive.
Try this lab a second time without looking at the instructions. If you can rebuild the whole thing from memory, you've genuinely internalised VPC + RDS. That's worth at least 5 questions on the Cloud Practitioner exam.
Lab 3 complete!
You just built a production-pattern two-tier application on AWS. The same architecture powers fintech apps, SaaS dashboards, and government services around the world.
Next: Lab 4 — Serverless Image Resizer with S3 + Lambda