AWS re/Start Lab · Databases & RDS

Migrating to Amazon RDS

Migrate the café web app database from a local MariaDB running on EC2 to a managed Amazon RDS MariaDB instance — built entirely from the AWS CLI, with TLS-encrypted data restore and zero application code changes.

Lab Summary

The café app started with its database on the same EC2 instance as the web tier — a LAMP stack where MariaDB shared the host with Apache and PHP. The goal of this lab was to externalize that database into Amazon RDS without touching the application code. The work is driven from the AWS CLI: build the network plumbing (private subnets across two AZs, a dedicated SG, a DB subnet group), provision the RDS instance, dump and restore the data over a TLS connection, and finally point the app at the new endpoint via SSM Parameter Store.

What moved

cafe_db — 9 products plus orders and accounts — migrated from a local MariaDB on the LAMP EC2 to a managed db.t3.micro RDS instance with 20 GB of allocated storage and automated daily backups.

What stayed the same

The PHP application code. Connection details live in /cafe/dbUrl in SSM Parameter Store, so the cutover is just an edit of that parameter — the app picks up the new host on the next request.

Coverage note. Only two terminal screenshots were captured during the lab — the RDS provisioning poll and the SSL restore plus verification query. The rest of the workflow (security group, subnets, Parameter Store edit, CloudWatch metric) is documented here as prose plus the actual commands that were run. No captures were invented.

Architecture: before vs. after

The starting topology bundled web tier and DB tier in one EC2 instance inside the public subnet. The final topology splits the DB out into RDS, behind a dedicated security group, across two private subnets in different AZs.

Component Before (LAMP on EC2) After (RDS migration)
Web tier Apache + PHP on CafeInstance (public subnet, t3.small) Unchanged
Database MariaDB local on the same EC2, port 3306 listening on localhost Amazon RDS MariaDB 10.11.11 · db.t3.micro · 20 GB · Single-AZ · private
Network placement Public subnet 10.200.0.0/24 only Adds private 10.200.2.0/23 (AZ-a) and 10.200.10.0/23 (AZ-b)
DB security Implicit (loopback on the EC2) CafeDatabaseSG — TCP 3306 only from CafeSecurityGroup as source SG
Connection in transit Loopback (no encryption needed) TLS required — clients must present the RDS CA bundle
Backups None automatic Daily automated, 30-min backup window, 1-day retention (default)
App cutover mechanism /cafe/dbUrl in SSM Parameter Store — value swap, no redeploy

The lab guide ships Starting architecture and Final architecture diagrams, but they live as Vocareum-hosted images that aren't part of the local source — the table above captures the same delta in text form.

Walkthrough

Five tasks, two screenshots. The CLI commands are reproduced verbatim because they are the lab deliverable; the click-paths through the console are summarized.

01

Seed the existing database with order data

Opened the café website at CafeInstanceURL, placed a few orders from the Menu, and noted the order count on the Order History tab. That number is the integrity check at the other end of the migration: same number of orders must show up after the cutover.

No screenshot — trivial console interaction; verified later via the Order History page after migration.
02

Build the network prerequisites for RDS (CLI)

Connected to the CLI Host via EC2 Instance Connect and ran aws configure with the lab's AccessKey, SecretKey, LabRegion and JSON output. All the following commands run from that host.

2.1 — Dedicated security group for the DB

CafeDatabaseSG protects the future RDS instance. The inbound rule allows TCP 3306, but instead of opening a CIDR range it uses another security group as the source — only instances attached to CafeSecurityGroup can reach the DB. Cleaner than CIDRs because it follows the workload, not the IP.

aws ec2 create-security-group \
  --group-name CafeDatabaseSG \
  --description "Security group for Cafe database" \
  --vpc-id <CafeVpcID>

aws ec2 authorize-security-group-ingress \
  --group-id <CafeDatabaseSG Group ID> \
  --protocol tcp --port 3306 \
  --source-group <CafeSecurityGroup Group ID>

aws ec2 describe-security-groups \
  --query "SecurityGroups[*].[GroupName,GroupId,IpPermissions]" \
  --filters "Name=group-name,Values='CafeDatabaseSG'"

2.2 — Two private subnets in different AZs

The VPC is 10.200.0.0/20 and the public subnet already occupies 10.200.0.0/24. RDS requires a DB subnet group with at least two subnets in different Availability Zones — even for a Single-AZ instance — so failover targets exist. Picked 10.200.2.0/23 in the same AZ as the app (for proximity) and 10.200.10.0/23 in a second AZ as the failover slot.

# Private subnet 1 — same AZ as CafeInstance
aws ec2 create-subnet \
  --vpc-id <CafeVpcID> \
  --cidr-block 10.200.2.0/23 \
  --availability-zone <CafeInstanceAZ>

# Private subnet 2 — a different AZ (e.g. us-west-2b)
aws ec2 create-subnet \
  --vpc-id <CafeVpcID> \
  --cidr-block 10.200.10.0/23 \
  --availability-zone <other-AZ>

aws rds create-db-subnet-group \
  --db-subnet-group-name "CafeDB Subnet Group" \
  --db-subnet-group-description "DB subnet group for Cafe" \
  --subnet-ids <PrivateSubnet1ID> <PrivateSubnet2ID> \
  --tags "Key=Name,Value=CafeDatabaseSubnetGroup"
Why two AZs even for Single-AZ RDS? RDS validates the DB subnet group at creation time. Without two AZs, create-db-instance rejects the request — the requirement exists so a Multi-AZ promotion later is a no-op networking-wise.
No screenshot — pure CLI plumbing; the resulting resources are exercised by Task 3.
03

Provision the RDS MariaDB instance with the CLI

One create-db-instance call with everything inlined: engine, version, size, storage, AZ, SG, subnet group, no public access, root credentials. The call returns immediately but the instance takes ~10 minutes to reach available.

aws rds create-db-instance \
  --db-instance-identifier CafeDBInstance \
  --engine mariadb \
  --engine-version 10.11.11 \
  --db-instance-class db.t3.micro \
  --allocated-storage 20 \
  --availability-zone <CafeInstanceAZ> \
  --db-subnet-group-name "CafeDB Subnet Group" \
  --vpc-security-group-ids <CafeDatabaseSG Group ID> \
  --no-publicly-accessible \
  --master-username root \
  --master-user-password 'Re:Start!9'

Polling the lifecycle

Used describe-db-instances with a --query projection so the output is a flat array of the five fields that actually matter while waiting: endpoint, AZ, backup window, retention, and status. The status walks through creating → modifying → backing-up → configuring-enhanced-monitoring → available, and the endpoint is null until very late in that chain.

aws rds describe-db-instances \
  --db-instance-identifier CafeDBInstance \
  --query "DBInstances[*].[Endpoint.Address,AvailabilityZone,PreferredBackupWindow,BackupRetentionPeriod,DBInstanceStatus]"
Default backup posture. 1-day retention and a 30-minute backup window are RDS defaults. Fine for a lab; in production raise retention to at least 7 days and pick a window that doesn't overlap peak load.
04

Migrate data: dump on the source, restore over TLS to RDS

Connected to the CafeInstance (web tier) via EC2 Instance Connect — this is the host that can reach the new SG because CafeSecurityGroup is the authorized source for CafeDatabaseSG.

4.1 — Dump the local database

--add-drop-database makes the dump self-contained: the restore drops cafe_db first if it exists, then recreates it. Safe to re-run.

mysqldump --user=root --password='Re:Start!9' \
  --databases cafe_db --add-drop-database > cafedb-backup.sql

less cafedb-backup.sql   # quick sanity check on schema + INSERTs

4.2 — Pull the RDS CA bundle

Modern RDS requires TLS by default. Without --ssl-ca the connection is refused. The CA bundle is a static URL — pulled once with curl to the local working directory.

curl -o global-bundle.pem \
  https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem

4.3 — Restore the dump and verify

Same mysql binary, now pointed at the RDS endpoint with the CA bundle and the SQL file piped in via <. Then an interactive session against cafe_db and a SELECT * FROM product to confirm row integrity. The product table is the catalog the website displays, so if it's intact, the menu page will render correctly after the cutover.

# Restore (silent — no output on success)
mysql --user=root --password='Re:Start!9' \
  --host=<RDS-Endpoint> \
  --ssl-ca=./global-bundle.pem \
  < cafedb-backup.sql

# Verify
mysql --user=root --password='Re:Start!9' \
  --host=<RDS-Endpoint> \
  --ssl-ca=./global-bundle.pem \
  cafe_db
MariaDB [cafe_db]> select * from product;
SSL gotcha worth remembering. Drop --ssl-ca and the same command fails with a TLS handshake error against RDS. The CA bundle has to live alongside whatever client connects to the database — that's a deployment artifact, not just a lab quirk.
05

Cut the app over via Parameter Store

The PHP café app doesn't hardcode its DB URL — it reads /cafe/dbUrl from AWS Systems Manager Parameter Store on each request. The cutover is therefore a single edit:

  • Systems Manager → Parameter Store/cafe/dbUrlEdit.
  • Replace the value (previously localhost or the EC2 private IP) with the RDS endpoint cafedbinstance.cfm9rpzhbev6.us-west-2.rds.amazonaws.com.
  • Save. No restart, no redeploy.

Verification: reload CafeInstanceURL in the browser, open Order History, and confirm the order count matches what was recorded in Task 1. Same number → same data → migration succeeded.

Pattern worth keeping. Externalizing connection strings (Parameter Store, Secrets Manager, environment variables injected at boot) decouples infra from code. The app didn't even need to know it was being migrated — that's how a cutover should feel.
No screenshot — single-field edit in Parameter Store; verified end-to-end via the Order History tab.
06

Sanity-check monitoring in CloudWatch

RDS publishes a fixed set of instance metrics to CloudWatch every minute, no extra setup required. The Monitoring tab on the DB instance page exposes them as pre-built graphs.

Metric What it tracks
CPUUtilizationPercent CPU used by the DB engine
DatabaseConnectionsNumber of active client connections
FreeStorageSpaceRemaining bytes of the allocated storage
FreeableMemoryRAM available on the instance
WriteIOPS / ReadIOPSAvg disk I/O ops per second

Sanity-checked the live signal: opened an interactive mysql session from the CafeInstance to RDS and waited a minute — the DatabaseConnections graph went from 0 to 1. Ran exit, waited another minute, refreshed — back to 0. Confirms metrics are flowing on the standard 1-minute granularity.

No screenshot — the metric graph isn't worth a capture; the qualitative behavior is the deliverable.

Key Learnings

Concepts that stuck

DB Subnet Group requires 2+ subnets in different AZs — even for Single-AZ. It's a placement contract for future failover.
Security group as source (instead of a CIDR) follows the workload, not the IP — the cleaner default for service-to-service traffic inside a VPC.
RDS enforces TLS by default. Clients need the CA bundle from truststore.pki.rds.amazonaws.com — treat it as a deployment artifact.
mysqldump --add-drop-database produces an idempotent dump — re-running the restore is safe.
Externalize connection strings (Parameter Store, Secrets Manager). The migration cutover became a one-field edit.
Default RDS backup retention is 1 day — fine for a lab, raise it in production.

Conclusion

The actual data move (mysqldumpmysql <) is the trivial part. The valuable AWS-specific work was upstream: carving out private subnets across two AZs, wiring a security group whose source is another SG, and accepting that the encrypted-by-default connection is non-negotiable.

Downstream, the Parameter Store cutover demonstrated why managed configuration matters: a database tier got swapped out and the application didn't notice. That decoupling is what makes RDS adoption realistic for existing workloads — not the provisioning, the cutover.