cloudwithshad
GUIDED LAB · ~90 MINUTES

Build a two-tier app with VPC + RDS

A custom VPC, public and private subnets, an EC2 web server, and an RDS MySQL database the web server talks to — all wired together the way real companies do it.

8
STEPS
90 min
DURATION
FREE
TIER OK
VPC + RDS
SERVICES
1
Plan your network on paper first
5 min

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:

VPC · 10.0.0.0/16 · region: af-south-1 PUBLIC SUBNET · 10.0.1.0/24 AZ: af-south-1a EC2 Web Server Amazon Linux Nginx + Node.js SG: web-sg IGW PRIVATE SUBNET A · 10.0.2.0/24 AZ: af-south-1a RDS MySQL (primary) db.t3.micro · 20 GB PRIVATE SUBNET B · 10.0.3.0/24 AZ: af-south-1b RDS standby (Multi-AZ) automatic failover SQL on 3306
The architecture we're going to build, step by step.
ResourceValue
VPC CIDR10.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
RegionWhichever you used in Week 1 (consistency helps)
Why two private subnets?

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.

2
Create the VPC
10 min

Time to actually build it. Sign in to the AWS console and navigate to VPC.

  1. Click "Create VPC" top-right
  2. Select "VPC and more" (this wizard creates everything in one shot)
  3. Fill in the form using the values below
aws VPC → Create VPC
Name tag auto-generation
cloudwithshad-week2
IPv4 CIDR block
10.0.0.0/16
Number of Availability Zones (AZs)
2
Number of public subnets
2
Number of private subnets
2
NAT gateways
None (NAT gateways cost ~$32/month — skip for the bootcamp)
VPC endpoints
None

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.

Exam tip

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.

3
Verify subnets & routing
10 min

Before moving on, take a quick look at what AWS created. In the VPC dashboard:

  1. Click "Subnets" in the left sidebar
  2. Filter by VPC: select your cloudwithshad-week2 VPC
  3. 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:

# Public subnet route table 10.0.0.0/16 local # traffic inside the VPC 0.0.0.0/0 igw-0abc1234... ← Internet Gateway = public

Now click on a private subnet and look at its route table:

# Private subnet route table 10.0.0.0/16 local # traffic inside the VPC # NO route to 0.0.0.0/0 — that's what makes it private
Why this matters

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.

4
Create Security Groups
10 min

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

TypeSourcePortWhy
SSHMy IP22So you can SSH in
HTTP0.0.0.0/080Public web traffic
HTTPS0.0.0.0/0443Public web traffic (TLS)

db-sg — for the RDS database

TypeSourcePortWhy
MySQL/Auroraweb-sg (the SG itself, not an IP!)3306Only the web tier can talk to the DB
The SG → SG trick

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.

Common mistake

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.

5
Launch the EC2 web server
15 min

Head to EC2 → Launch instance. The fields that matter:

FieldValue
Namecloudwithshad-web
AMIAmazon Linux 2023 (free tier eligible)
Instance typet2.micro or t3.micro (free tier)
Key pairUse existing from Week 1, or create new
Network settingsEdit →
VPCcloudwithshad-week2
SubnetThe PUBLIC one (10.0.1.0/24)
Auto-assign public IPEnable
Security groupSelect existing → web-sg

In the "User data" field (Advanced details, near the bottom), paste this:

#!/bin/bash dnf update -y dnf install -y nginx mariadb105 systemctl start nginx systemctl enable nginx cat > /usr/share/nginx/html/index.html <<'EOF' <h1>☁️ cloudwithshad</h1> <p>Week 2 · Lab 3</p> <p>✅ Two-Tier App Running</p> EOF

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".

If the page doesn't load

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?

6
Provision the database — pick your path
15-20 min
🧭 Two paths to the same architecture

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.

⚠️ Cost warning — read before clicking anything

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

FieldValue
Engine typeMySQL
Creation methodFull configuration (gives you all the cost-control switches we need)
Engine versionThe default latest (e.g. MySQL 8.4.x) is fine
Enable RDS Extended SupportLeave OFF ← it's a paid add-on

Templates

FieldValue
TemplateDev/Test ← what we use for the lab. Intended for non-production workloads. Avoid Production (pre-loads expensive HA defaults).
⚠️ Dev/Test pre-loads three "expensive" defaults — you MUST switch each one

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:

  1. Availability: Dev/Test defaults to Multi-AZ DB instance (2x cost). Switch to Single-AZ DB instance deployment (1 instance) in the next section.
  2. Storage autoscaling: Dev/Test enables autoscaling with a high max. Turn it OFF in the Storage section, or set max = 25 GiB.
  3. Enhanced Monitoring: Dev/Test enables it by default (CloudWatch Logs charges). Uncheck "Enable Enhanced Monitoring" in the Monitoring section.

Availability and durability

FieldValue
Deployment optionSingle-AZ DB instance deployment (1 instance) ← critical. The other options double or triple your bill.

Settings

FieldValue
DB instance identifiercloudwithshad-db
Master usernameadmin

Credentials management

FieldValue
Manage credentialsSelf managed ← important. ("Managed in AWS Secrets Manager" is the default and adds Secrets Manager charges.)
Master passwordPick a strong password and save it in your password manager.

Instance configuration

FieldValue
DB instance class filterBurstable classes. Also tick "Include previous generation classes" so db.t3.micro appears.
Instance typedb.t3.micro or db.t4g.micro

Storage

FieldValue
Storage typeGeneral Purpose SSD (gp3) ← critical. Default is "Provisioned IOPS SSD (io2)", ~$700/month.
Allocated storage20 GiB
Storage autoscalingLeave OFF

Connectivity

FieldValue
Compute resourceDon't connect to an EC2 compute resource
VPCcloudwithshad-week2-vpcNOT the Default VPC
DB subnet groupCreate one with only the 2 private subnets (don't use the default group, which often includes public subnets)
Public accessNo ← critical
VPC security groupChoose existing → remove default, add db-sg
RDS ProxyLeave OFF

Monitoring

FieldValue
Database InsightsStandard (not Advanced)
Performance InsightsUncheck "Enable Performance Insights"
Enhanced MonitoringUncheck "Enable Enhanced Monitoring"
Sanity check before you hit Create

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.

If Path A keeps timing out

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.

6b
Path B — Self-hosted MySQL in a private subnet (with NAT)
25 min

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.

⚠️ Cost warning — NAT gateway is NOT free tier

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.

Why a brand new VPC for Path B?

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:

  1. EC2 → Instances → select cloudwithshad-web
  2. 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:

aws VPC → Create VPC
Name tag auto-generation
cloudwithshad-pathb
IPv4 CIDR block
10.1.0.0/16 (different from Path A so they don't conflict)
Number of Availability Zones (AZs)
2
Number of public subnets
2
Number of private subnets
2
NAT gateways
In 1 AZ ← critical change (NOT "None", NOT "1 per AZ")
VPC endpoints
None

"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.

What just happened under the hood

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:

FieldValue
Namecloudwithshad-web
AMIAmazon Linux 2023
Instance typet2.micro or t3.micro
Key pairYour existing key from Week 1
VPCcloudwithshad-pathb-vpc
SubnetA public subnet (e.g. cloudwithshad-pathb-subnet-public1-...)
Auto-assign public IPEnable
Security groupSelect existing → web-sg
User dataSame script as Step 5 (nginx + mariadb105 client + placeholder page)

Step 6b.5 — Launch the database EC2 (in a PRIVATE subnet)

EC2 → Launch instance:

FieldValue
Namecloudwithshad-db
AMIAmazon Linux 2023
Instance typet2.micro or t3.micro
Key pairSame key as the web EC2
VPCcloudwithshad-pathb-vpc
SubnetA PRIVATE subnet (e.g. cloudwithshad-pathb-subnet-private1-...) ← this is the architectural win
Auto-assign public IPDisable (it's in a private subnet — no public IP needed; NAT handles outbound)
Security groupSelect 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.)

#!/bin/bash # Log everything for troubleshooting exec > >(tee /var/log/cloudwithshad-setup.log) exec 2>&1 set -euxo pipefail DB_PASSWORD="cloudwithshad2026" DB_NAME="cloudwithshad" DB_USER="admin" echo "=== Starting database setup ===" # Update system dnf update -y # Install MariaDB server dnf install -y mariadb105-server # Configure MariaDB to accept remote connections cat > /etc/my.cnf.d/cloudwithshad.cnf <<EOF [mysqld] bind-address=0.0.0.0 EOF # Enable and start MariaDB systemctl enable mariadb systemctl restart mariadb # Wait until MariaDB is actually ready echo "Waiting for MariaDB..." for i in {1..60}; do if mysqladmin ping --silent; then echo "MariaDB is ready." break fi echo "Attempt $i/60..." sleep 2 done # Final readiness check mysqladmin ping --silent # Configure database and user mysql <<EOF CREATE DATABASE IF NOT EXISTS ${DB_NAME}; CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}'; GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'%'; FLUSH PRIVILEGES; EOF # Verify bind address echo "=== Listening ports ===" ss -tlnp | grep 3306 || true # Verify user creation echo "=== MariaDB Users ===" mysql -e "SELECT User,Host FROM mysql.user;" # Create marker file echo "cloudwithshad-db ready - $(date)" > /var/log/cloudwithshad-db.log echo "=== Database setup completed successfully ==="

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.

Save this for Step 7

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).

If the user data didn't finish (rare but possible)

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).

7
Connect from EC2 to the database & test
10 min

This step works the same for both paths — the only difference is the host you connect to.

Path A — RDS
Host: the RDS endpoint (e.g. cloudwithshad-db.xxxxx.us-east-1.rds.amazonaws.com)
Username: admin
Password: the master password you set in Step 6
Path B — EC2 MySQL
Host: the DB EC2's private IPv4 address (e.g. 10.1.x.x)
Username: admin
Password: cloudwithshad2026 (from the user data script)

SSH into your web EC2 from your laptop:

ssh -i your-key.pem ec2-user@<WEB-EC2-PUBLIC-IP>

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):

# Path A (RDS): mysql -h cloudwithshad-db.xxxxx.us-east-1.rds.amazonaws.com -u admin -p # Path B (EC2 MySQL) — use the DB EC2's private IP: mysql -h 10.0.x.x -u admin -p

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:

mysql> USE cloudwithshad; Database changed mysql> CREATE TABLE students (id INT, name VARCHAR(50)); mysql> INSERT INTO students VALUES (1, 'Your Name'); mysql> SELECT * FROM students; +----+-----------+ | id | name | +----+-----------+ | 1 | Your Name | +----+-----------+
Take a screenshot

Capture that MySQL output and drop it in the cohort WhatsApp group. That's your Week 2 Lab 3 submission. 🎉

📖 Error decoder — what each common error actually means

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.

Want to prove it's NOT public? (Path A only)

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.

8
Clean up (do not skip!)
10 min

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-web and cloudwithshad-db in 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.
Pro tip for the cert

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