Migrate an On-Premises App to AWS

Lift-and-shift an Employee Directory app to EC2 and RDS using AWS DMS.

Introduction

30 Second Summary

When a growing company outgrows its office server closet, it needs to move its applications and data to the cloud without losing a single record. This is cloud migration, and it's one of the most in-demand skills in IT today.

In this project, you will migrate a company's Employee Directory application from a simulated on-premises server to a fully managed cloud architecture on AWS.

What You'll Build

A complete lift-and-shift migration of the CloudHR Systems Employee Directory, using AWS DMS to replicate a MySQL database from a self-managed instance to Amazon RDS.

By the end of this project, you'll have:

  • An EC2 instance running the Employee Directory Node.js application with a local MySQL database simulating an on-premises environment.
  • A completed AWS DMS migration task that replicates employee data to a managed Amazon RDS database.
  • A fully cloud-hosted application reconfigured to use RDS, proving end-to-end migration success.
  • Secret Mission: Add CloudWatch alarms to monitor your RDS database health and get notified if CPU or connections spike.

Are there any prerequisites?

You'll need an AWS account (free tier eligible), basic familiarity with the AWS console, and comfort with Linux commands and SSH.

Set Up the On-Premises Environment on EC2

Before you can migrate a database to the cloud, you need a database to migrate. In this step, you will simulate CloudHR Systems' on-premises server by launching an EC2 instance that runs both the Employee Directory application and a local MySQL database.

This single-server setup is a common pattern for small companies. Everything lives on one machine, which becomes a bottleneck as the company grows. By the end of this step, you will have a working application with real employee data ready to be migrated.

In this step, get ready to:
  • Launch an EC2 instance and connect via SSH.
  • Install Node.js, MySQL, and deploy the Employee Directory app.
  • Configure MySQL binary logging and create a DMS replication user.
Create a key pair and launch EC2

A key pair lets you securely connect to your EC2 instance via SSH. You will create the key pair first, then launch an instance that uses it.

  • In the AWS Console, use the search bar at the top to search for EC2 and select it.
  • In the left sidebar, click Key Pairs under Network & Security.
  • Click Create key pair.
  • Enter cloudhr-keypair as the name.
  • Select RSA as the key pair type.
  • Select .pem as the private key file format.
  • Click Create key pair.

Your browser automatically downloads the cloudhr-keypair.pem file. Keep it somewhere you can find it. You will need it to SSH into your instance.

What is a key pair?

A key pair is a set of cryptographic keys that AWS uses instead of passwords. The public key lives on your EC2 instance, and the private key (the .pem file) stays on your local machine. When you SSH in, your computer proves its identity by using the private key.

Now launch the EC2 instance that will act as CloudHR's on-premises server.

  • In the left sidebar, click Instances.
  • Click Launch instances.
  • Under Name, enter CloudHR-OnPrem-Server.
  • Under Application and OS Images, confirm that Amazon Linux 2023 AMI is selected (it is the default).
  • Under Instance type, select t2.micro (free tier eligible).
  • Under Key pair, select cloudhr-keypair from the dropdown.
  • Under Network settings, click Edit.
  • Keep the default security group rule for SSH (port 22) from 0.0.0.0/0.
  • Click Add security group rule.
  • Set Type to Custom TCP, Port range to 3000, and Source to 0.0.0.0/0.
  • Click Launch instance.

Wait for the instance state to show Running and the status checks to pass (about 1-2 minutes).

Why port 3000?

The Employee Directory application listens on port 3000. Opening this port in the security group lets you access the app from your browser.

Now connect to your instance via SSH using PowerShell.

  • On the Instances page, select your instance and click Connect.
  • Select the SSH client tab.
  • Copy the Public DNS name shown on this page (you will need it for the SSH command).
  • Open PowerShell on your local machine.
  • Navigate to the folder where your cloudhr-keypair.pem file was downloaded.
  • Connect to your instance by running this command:
ssh -i cloudhr-keypair.pem ec2-user@[[EC2_PUBLIC_DNS="your EC2 Public IPv4 DNS"]]
  • When prompted to confirm the connection, type yes and press Enter.

You should see an Amazon Linux prompt, confirming you are connected to your EC2 instance.

Permission denied error?

Make sure you are in the same directory as your .pem file. Also confirm you selected the correct key pair when launching the instance.

help me fix my SSH connection to EC2

Install Node.js, MySQL, and deploy the app

With your SSH connection established, you will now install the application stack. This mirrors a real on-premises server where both the app and database live on the same machine.

  • Install Node.js 24 LTS using the NodeSource setup script by running these commands:
# Add the NodeSource repository for Node.js 24
curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -

# Install Node.js (includes npm)
sudo yum install -y nodejs
  • Verify Node.js installed correctly by running:
node --version

You should see a version number starting with v24.

What does this command do?

The curl command downloads a script from NodeSource that configures the Yum repository for Node.js 24. After the repo is added, yum install pulls the actual Node.js package.

  • Install MySQL 8.0 Community Server by running these commands:
# Add the MySQL community repository
sudo dnf install -y https://dev.mysql.com/get/mysql84-community-release-el9-1.noarch.rpm

# Switch to MySQL 8.0 (the repo defaults to 8.4)
sudo dnf config-manager --disable mysql-8.4-lts-community
sudo dnf config-manager --enable mysql80-community

# Install MySQL server
sudo dnf install -y mysql-community-server

Why pin to MySQL 8.0?

The MySQL repository defaults to version 8.4, but AWS DMS supports MySQL 8.0 as both a source and target. Pinning to 8.0 ensures full compatibility with DMS and your target Amazon RDS instance.

  • Start and enable the MySQL service by running:
# Start MySQL and enable it to start on boot
sudo systemctl enable --now mysqld
  • Retrieve the temporary root password that MySQL generated during installation:
sudo grep 'temporary password' /var/log/mysqld.log

Copy the password shown at the end of the output line. You will need it in the next command.

  • Secure the MySQL installation and set a new root password by running:
sudo mysql_secure_installation

What should I enter?

When prompted, enter the temporary password you just copied. Then set a new root password. Use CloudHR2024! as the new password for this project.

Answer y to all remaining prompts (remove anonymous users, disallow remote root login, remove test database, reload privileges).

Now create the database, seed it with employee data, and set up the application user.

  • Log in to MySQL as root by running:
mysql -u root -p
  • Enter the password CloudHR2024! when prompted.
  • Create the database, table, and seed data by pasting this SQL:
CREATE DATABASE IF NOT EXISTS employee_directory;
USE employee_directory;

CREATE TABLE IF NOT EXISTS employees (
  id INT AUTO_INCREMENT PRIMARY KEY,
  first_name VARCHAR(50) NOT NULL,
  last_name VARCHAR(50) NOT NULL,
  email VARCHAR(100) NOT NULL UNIQUE,
  department VARCHAR(50) NOT NULL,
  role VARCHAR(100) NOT NULL,
  hire_date DATE NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO employees (first_name, last_name, email, department, role, hire_date) VALUES
('Sarah', 'Chen', 'sarah.chen@cloudhr.com', 'Engineering', 'Senior Software Engineer', '2021-03-15'),
('Marcus', 'Johnson', 'marcus.johnson@cloudhr.com', 'Engineering', 'DevOps Engineer', '2022-01-10'),
('Priya', 'Patel', 'priya.patel@cloudhr.com', 'Product', 'Product Manager', '2020-07-22'),
('James', 'Wilson', 'james.wilson@cloudhr.com', 'Sales', 'Account Executive', '2023-02-01'),
('Ana', 'Rodriguez', 'ana.rodriguez@cloudhr.com', 'Human Resources', 'HR Manager', '2019-11-05'),
('David', 'Kim', 'david.kim@cloudhr.com', 'Engineering', 'Cloud Architect', '2021-08-14'),
('Emily', 'Thompson', 'emily.thompson@cloudhr.com', 'Marketing', 'Content Strategist', '2022-06-30'),
('Raj', 'Gupta', 'raj.gupta@cloudhr.com', 'Finance', 'Financial Analyst', '2023-04-18');

What does this SQL do?

It creates the employee_directory database with an employees table. Then it inserts 8 sample employee records representing different departments at CloudHR Systems.

  • Create the application user that the Node.js app will use to connect:
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'AppPass123!';
GRANT ALL PRIVILEGES ON employee_directory.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
  • Verify the data was seeded correctly:
SELECT COUNT(*) FROM employees;

You should see 8 as the result.

  • Exit the MySQL prompt:
EXIT;

Now deploy the Employee Directory application.

  • Create the application directory and move into it by running:
mkdir ~/employee-directory
cd ~/employee-directory
  • Create the package.json file by running:
cat << 'EOF' > package.json
{
  "name": "employee-directory",
  "version": "1.0.0",
  "description": "CloudHR Employee Directory Application",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "4.19.2",
    "mysql2": "3.22.5"
  }
}
EOF
  • Create the server.js file by running:
cat << 'EOF' > server.js
const express = require('express');
const mysql = require('mysql2/promise');

const app = express();
const PORT = 3000;

app.use(express.json());

const dbConfig = {
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'appuser',
  password: process.env.DB_PASSWORD || 'AppPass123!',
  database: process.env.DB_NAME || 'employee_directory',
  port: parseInt(process.env.DB_PORT) || 3306
};

app.get('/', async (req, res) => {
  try {
    const connection = await mysql.createConnection(dbConfig);
    const [rows] = await connection.execute('SELECT * FROM employees');
    await connection.end();

    let html = '<h1>CloudHR Employee Directory</h1>';
    html += '<table border="1" cellpadding="8" cellspacing="0">';
    html += '<tr><th>ID</th><th>Name</th><th>Email</th><th>Department</th><th>Role</th></tr>';
    rows.forEach(emp => {
      html += `<tr><td>${emp.id}</td><td>${emp.first_name} ${emp.last_name}</td><td>${emp.email}</td><td>${emp.department}</td><td>${emp.role}</td></tr>`;
    });
    html += '</table>';
    res.send(html);
  } catch (error) {
    res.status(500).send(`<h1>Database Connection Error</h1><p>${error.message}</p>`);
  }
});

app.get('/api/employees', async (req, res) => {
  try {
    const connection = await mysql.createConnection(dbConfig);
    const [rows] = await connection.execute('SELECT * FROM employees');
    await connection.end();
    res.json(rows);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/health', async (req, res) => {
  try {
    const connection = await mysql.createConnection(dbConfig);
    await connection.execute('SELECT 1');
    await connection.end();
    res.json({ status: 'healthy', database: dbConfig.host });
  } catch (error) {
    res.status(503).json({ status: 'unhealthy', error: error.message });
  }
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Employee Directory running on port ${PORT}`);
  console.log(`Database host: ${dbConfig.host}`);
});
EOF

What does this app do?

The Express.js server has three routes. The root route (/) renders an HTML table of employees. The /api/employees route returns the same data as JSON. The /health route checks database connectivity.

It reads the database host from environment variables, falling back to localhost. This makes it easy to reconfigure later when you cut over to RDS.

  • Install the Node.js dependencies by running:
npm install
  • Start the application in the background by running:
node server.js &

You should see output confirming the app is running on port 3000 with the database host set to localhost.

  • Open a new browser tab and navigate to http://your EC2 Public IPv4 address:3000

You should see the CloudHR Employee Directory page with a table of 8 employees.

Page not loading in the browser?

Make sure you are using http:// (not https://) in the URL. Confirm port 3000 is open in your security group. Also check that you are using the Public IPv4 address from the EC2 console, not the private IP.

help me troubleshoot my Employee Directory app not loading

Enable binary logging for DMS

AWS DMS requires binary logging to be enabled on the source MySQL database. Binary logging records every change made to the database, which DMS reads to replicate data to the target.

  • Create a MySQL configuration file to enable binary logging by running:
sudo tee /etc/my.cnf.d/binlog.cnf << 'EOF'
[mysqld]
server-id=1
log-bin=/var/lib/mysql/mysql-bin
binlog_format=ROW
binlog_row_image=FULL
binlog_checksum=NONE
EOF

What do these settings do?

  • server-id uniquely identifies this MySQL server in a replication setup.
  • log-bin enables binary logging and sets the file path.
  • binlog_format=ROW records changes at the row level, which DMS requires for accurate replication.
  • binlog_row_image=FULL ensures complete row data is logged for each change.
  • binlog_checksum=NONE disables checksums that DMS does not support.
  • Restart MySQL to apply the binary logging configuration:
sudo systemctl restart mysqld
  • Verify binary logging is active by running:
mysql -u root -p -e "SHOW VARIABLES LIKE 'log_bin';"

Enter your root password when prompted. You should see log_bin with a value of ON.

Now create a dedicated MySQL user for DMS with the required replication privileges.

  • Log in to MySQL as root:
mysql -u root -p
  • Create the DMS replication user by running this SQL:
CREATE USER 'dms_user'@'%' IDENTIFIED BY 'DmsPass123!';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'dms_user'@'%';
GRANT SELECT, RELOAD, SHOW VIEW, EVENT, TRIGGER ON employee_directory.* TO 'dms_user'@'%';
FLUSH PRIVILEGES;
EXIT;

Why these privileges?

DMS needs REPLICATION CLIENT to read binary log positions and REPLICATION SLAVE to read the log events themselves. The additional grants (SELECT, RELOAD, etc.) allow DMS to read table structures and data during the full-load phase.

  • Restart the Employee Directory app (since MySQL was restarted) by running:
cd ~/employee-directory
node server.js &
  • Refresh your browser at http://your EC2 Public IPv4 address:3000 to confirm the app still works.

You should see the same employee directory table displaying all 8 records.

Binary logging shows OFF?

Double check that your /etc/my.cnf.d/binlog.cnf file has the correct content and that you restarted MySQL with sudo systemctl restart mysqld after creating the file.

help me enable binary logging on MySQL

Your simulated on-premises environment is complete. Next up, you will create the Amazon RDS database that will become CloudHR's new managed production database.

Create the Amazon RDS Target Database

Your EC2 instance is running the Employee Directory app with a local MySQL database. That works for now, but in a real migration, the goal is to move data into a managed database service.

In this step, you will provision an Amazon RDS MySQL instance. This becomes the migration target where AWS DMS will send your employee data in the next step.

In this step, get ready to:
  • Create an RDS MySQL instance using the Free tier template.
  • Configure the RDS security group to allow connections from EC2.
  • Verify connectivity from EC2 to RDS using the MySQL client.
Create an RDS MySQL instance

Amazon RDS is a managed database service that handles backups, patching, and failover for you. You will create a MySQL 8.0 instance that lives in the same VPC as your EC2 instance.

  • Return to the AWS Management Console in your browser.
  • In the search bar at the top, type RDS and select RDS from the results.
  • In the left navigation pane, click Databases.
  • Click Create database.

The Create database wizard opens with several configuration sections.

  • Under Choose a database creation method, select Standard create.
  • Under Engine options, for Engine type, choose MySQL.
  • For Version, select a MySQL 8.0 version (the default 8.0.x option is fine).
  • Under Templates, select Free tier.

Why Free tier?

The Free tier template automatically sets the instance class to db.t3.micro with 20 GB of General Purpose (gp2) storage. It also disables Multi-AZ deployment, which keeps costs at zero for 12 months from account creation.

  • In the Settings section, for DB instance identifier, enter cloudhr-rds.
  • For Master username, enter admin.
  • Uncheck Auto generate a password.
  • For Master password, enter a password you will remember. Type it again in Confirm password.

Remember your RDS password

Write this password down. You will need it later when connecting from EC2 and when configuring the DMS target endpoint.

  • In the Instance configuration section, confirm Burstable classes (includes t classes) is selected and the class is db.t3.micro.
  • In the Storage section, confirm Allocated storage is 20 GB and Storage type is General Purpose SSD (gp2).
  • In the Connectivity section, for Compute resource, choose Don't connect to an EC2 compute resource. You will configure the security group manually.
  • For Virtual private cloud (VPC), select the same VPC that your EC2 instance uses. This is the default VPC unless you created a custom one.
  • For Public access, choose No. The database only needs to be reachable from within the VPC.
  • For VPC security group (firewall), select Create new.
  • Enter a name for the new security group, such as cloudhr-rds-sg.
  • Expand Additional configuration at the bottom.
  • Leave Initial database name blank. DMS will create the database schema during migration.
  • Confirm Enable automated backups is unchecked (or set backup retention to 0 days). This saves storage costs for the project.
  • Leave all other settings as defaults.
  • Click Create database.

The database status shows Creating. It can take 5-10 minutes to become Available.

  • Wait for the status to change to Available. You can refresh the page periodically to check.
  • Click on cloudhr-rds to open its details page.
  • Under the Connectivity & security tab, find the Endpoint value. Copy this endpoint and save it somewhere. You will use it to connect from EC2.

What is the RDS endpoint?

The endpoint is a DNS name that resolves to your RDS instance's private IP address. It looks like cloudhr-rds.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com. Your application uses this instead of localhost to reach the managed database.

Configure the RDS security group for EC2 access

By default, the new RDS security group blocks all inbound traffic. You need to open port 3306 (MySQL) so your EC2 instance can connect.

  • On the cloudhr-rds details page, under Connectivity & security, find VPC security groups.
  • Click the security group link (cloudhr-rds-sg or similar) to open it in the EC2 console.
  • Select the Inbound rules tab.
  • Click Edit inbound rules.
  • Delete any existing rule that references your local IP address (the wizard may have added one automatically).
  • Click Add rule.
  • For Type, choose MYSQL/Aurora. This automatically sets the port to 3306.
  • For Source, choose Custom, then start typing the name of your EC2 security group. Select it from the dropdown.
  • Click Save rules.

Why use a security group as the source?

By referencing the EC2 security group instead of an IP address, you allow any instance attached to that security group to connect. This is more flexible than hardcoding a private IP, which changes if you stop and start the instance.

Your RDS security group now allows MySQL traffic (port 3306) from any resource in your EC2 security group.

Test connectivity from EC2 to RDS

The final check confirms that your EC2 instance can reach the new RDS database over the network.

  • Switch back to your SSH session on the EC2 instance.
  • Test the connection to RDS by running this command:
mysql -h [[RDS_ENDPOINT="your RDS endpoint e.g. cloudhr-rds.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com"]] -P 3306 -u admin -p
  • When prompted, enter the master password you set when creating the RDS instance.

You should see the MySQL prompt appear, confirming the connection works.

  • Verify the database is empty by running:
SHOW DATABASES;

You should see the default system databases (information_schema, mysql, performance_schema, sys) but no employee_directory database yet. DMS will create that during migration.

  • Exit the MySQL prompt by typing:
EXIT;

Can't connect to RDS?

  • Make sure the RDS instance status is Available (not still Creating).
  • Double check that the RDS security group has an inbound rule for MYSQL/Aurora (port 3306) with your EC2 security group as the source.
  • Confirm both EC2 and RDS are in the same VPC. Check the VPC ID on the EC2 instance details and the RDS Connectivity & security tab.
  • If you see ERROR 2003 (HY000): Can't connect to MySQL server, the security group rule is likely misconfigured.

Help me troubleshoot my EC2 to RDS connection. I'm getting a timeout or connection refused error when trying to connect on port 3306.

Your managed database is ready and waiting. Next, you will set up AWS DMS to migrate the employee data from the local MySQL database into this RDS instance.

Set Up AWS DMS for Database Migration

Your source database is running on EC2 and your target RDS instance is ready. Now you need a way to move data between them without writing custom scripts or dumping and restoring manually.

AWS Database Migration Service (DMS) handles this for you. It creates a dedicated replication instance that reads from your source, transforms the data if needed, and writes to your target. In this step, you will set up the three DMS components: a replication instance, a source endpoint, and a target endpoint.

In this step, get ready to:
  • Create a DMS replication instance in the same VPC as your EC2 and RDS.
  • Create source and target endpoints pointing to your local MySQL and RDS.
  • Update security groups so DMS can reach both databases.
Create a DMS replication instance

A replication instance is a managed compute resource that runs the DMS software. It connects to both your source and target databases, reads data from one, and writes to the other.

  • In the AWS Management Console, use the search bar at the top to search for DMS and select AWS Database Migration Service.
  • In the left navigation pane, click Replication instances.
  • Click Create replication instance.
  • For Name, enter cloudhr-dms-instance.
  • For Instance class, select dms.t3.micro.
  • For Engine version, accept the default.
  • For Multi AZ, choose Dev or test workload (Single-AZ).
  • For Allocated storage (GiB), accept the default of 50 GiB.
  • For VPC, select the same VPC that your EC2 and RDS instances are in.
  • Leave Publicly accessible unchecked.
  • Leave the Advanced, Maintenance, and Tags sections at their defaults.
  • Click Create replication instance.

The instance takes a few minutes to provision. Wait until the Status column shows Available before continuing.

What is a DMS replication instance?

DMS launches a dedicated EC2 instance behind the scenes to run the migration software. The dms.t3.micro class provides 2 vCPU and 1 GiB of memory. That is more than enough for migrating a small table with 8 rows.

The replication instance is free-tier eligible for 750 hours per month during your first year. This means it costs nothing as long as you delete it within the free-tier window.

Instance stuck in Creating?

DMS replication instances typically take 3-5 minutes to reach Available status. If it stays in Creating for more than 10 minutes, check that your VPC has at least two subnets in different Availability Zones. DMS requires a replication subnet group with subnets in multiple AZs.

Help me troubleshoot my DMS replication instance that is stuck in Creating status

Create source and target endpoints

DMS endpoints tell the replication instance how to connect to each database. You need a source endpoint pointing at the local MySQL on EC2, and a target endpoint pointing at your RDS instance.

Before creating endpoints, you need your EC2 instance's private IP address. You will find it on the EC2 instance details page in the Private IPv4 address field. Copy it now.

  • Navigate to EC2 in the AWS Console.
  • Click on your running instance to open its details.
  • Copy the value from the Private IPv4 address field. You will use this as the server name for the source endpoint.

Why use the private IP?

The DMS replication instance lives in the same VPC as your EC2. It can reach the MySQL server using the private IP address without going over the public internet. This is faster and more secure.

Now create the source endpoint.

  • Return to the DMS console. In the left navigation pane, click Endpoints.
  • Click Create endpoint.
  • For Endpoint type, select Source endpoint.
  • For Endpoint identifier, enter a name for your source endpoint (for example, cloudhr-source).
  • For Source engine, select MySQL.
  • For Access to endpoint database, choose Provide access information manually.
  • For Server name, paste the EC2 private IP address you copied.
  • For Port, enter 3306.
  • For User name, enter the DMS replication user you created in Step 1 (for example, dms_user).
  • For Password, enter the password for that user.
  • Scroll down to Test endpoint connection (optional).
  • For VPC, select the same VPC as your replication instance.
  • For Replication instance, select cloudhr-dms-instance.
  • Click Run test.

Don't worry if the test fails

The source endpoint test may fail at this point because the EC2 security group does not yet allow inbound traffic from the DMS replication instance on port 3306. You will fix this in the next substep. If it fails, continue creating the endpoint and re-test after updating security groups.

  • If the test succeeds, click Create endpoint. If it fails, click Create endpoint anyway. You will re-test after updating security groups.

Now create the target endpoint.

  • Click Create endpoint again.
  • For Endpoint type, select Target endpoint.
  • Check the Select RDS DB instance box and select cloudhr-rds from the dropdown.
  • The console auto-fills the endpoint identifier, engine, server name, and port from your RDS instance.
  • For Access to endpoint database, choose Provide access information manually.
  • Enter the RDS master User name and Password you set when creating the RDS instance.
  • Scroll down to Test endpoint connection (optional).
  • Select the same VPC and cloudhr-dms-instance as the replication instance.
  • Click Run test.
  • If the test fails, continue with creating the endpoint. You will fix security groups in the next substep and re-test.
  • Click Create endpoint.

Why did I select the RDS DB instance?

When you check the Select RDS DB instance box, the DMS console automatically pulls the endpoint, port, and engine details from your RDS configuration. This reduces the chance of a typo in the server name.

Update security groups for DMS

DMS needs network access to both your EC2 MySQL instance and your RDS database. Right now, neither security group allows inbound traffic from the DMS replication instance.

First, find the security group associated with your DMS replication instance.

  • In the DMS console, click Replication instances in the left navigation pane.
  • Click on cloudhr-dms-instance to open its details.
  • Note the VPC security group listed. You will reference this when adding inbound rules.

Now update the EC2 security group to allow MySQL traffic from DMS.

  • Navigate to EC2 in the AWS Console.
  • In the left navigation, click Security Groups (under Network & Security).
  • Select the security group attached to your EC2 instance.
  • Click the Inbound rules tab, then click Edit inbound rules.
  • Click Add rule.
  • For Type, select MySQL/Aurora (port 3306).
  • For Source, search for and select the DMS replication instance's security group.
  • Click Save rules.

Now update the RDS security group to also allow traffic from DMS.

  • Select the security group attached to your RDS instance (cloudhr-rds).
  • Click the Inbound rules tab, then click Edit inbound rules.
  • Click Add rule.
  • For Type, select MySQL/Aurora (port 3306).
  • For Source, search for and select the DMS replication instance's security group.
  • Click Save rules.

Why security groups matter here

Security groups act as virtual firewalls around each resource. Even though the DMS replication instance is in the same VPC, it cannot connect to your databases unless the security groups explicitly allow inbound traffic on port 3306 from the DMS instance's security group.

This is the most common reason endpoint connection tests fail. Three relationships must exist: EC2 allows DMS inbound, RDS allows EC2 inbound (already done in Step 2), and RDS allows DMS inbound.

Now re-test both endpoints to confirm connectivity.

  • Return to the DMS console and click Endpoints in the left navigation.
  • Select your source endpoint and click Actions, then Test connection.
  • Select the VPC and replication instance, then click Run test.
  • Wait for the status to show successful.
  • Repeat for your target endpoint. Both must show successful before you proceed.

Endpoint test still failing?

Double check that you selected the correct security group as the source for the inbound rule. The source must be the DMS replication instance's security group, not its IP address.

For the source endpoint, also verify that the MySQL user has REPLICATION CLIENT and REPLICATION SLAVE privileges, and that MySQL is listening on 0.0.0.0 (not just localhost).

Help me debug my DMS endpoint connection test failure

Your DMS infrastructure is ready. You have a replication instance that can reach both databases, with confirmed connectivity on both endpoints. Next up, you will create and run the actual migration task to move your employee data from EC2 to RDS.

Run the Migration and Perform Cutover

Your AWS DMS infrastructure is fully configured with a replication instance and tested endpoints. Now it's time for the main event: executing the actual database migration.

In this step, you'll move all 8 employee records from the local MySQL database to Amazon RDS. Then you'll perform a cutover, reconfiguring the application to read from the managed database and decommissioning the old one.

In this step, get ready to:
  • Create and run a DMS migration task to replicate data from EC2 MySQL to RDS.
  • Verify all employee records arrived in RDS correctly.
  • Cutover the Employee Directory application to use RDS and decommission local MySQL.
Create and run the migration task

A DMS migration task ties together your replication instance, source endpoint, and target endpoint into one executable job. It reads data from the source and writes it to the target according to your table mappings.

  • Return to the DMS console in your browser.
  • In the navigation pane, click Database migration tasks.
  • Click Create task.

The Create database migration task page opens with several configuration sections.

  • In the Task configuration section, enter the following:
  • Set Task identifier to cloudhr-migration-task.
  • For Replication instance, select cloudhr-dms-instance.
  • For Source database endpoint, select your source endpoint (the EC2 MySQL endpoint you created earlier).
  • For Target database endpoint, select your target endpoint (the cloudhr-rds endpoint).
  • For Migration type, select Migrate existing data.

Why Migrate existing data instead of ongoing replication?

The "Migrate existing data" option performs a full load of your source tables into the target. This is the simplest approach for a planned cutover with a small database.

Ongoing replication (CDC) is useful when you need zero downtime during migration of a production system. For our 8-row Employee Directory with a planned maintenance window, a full load is faster and simpler.

  • Scroll down to the Table mappings section.
  • Make sure the editing mode is set to Wizard.
  • Expand Selection rules and click Add new selection rule.
  • For Schema, select Enter a schema.
  • In the Schema name field, type employee_directory.
  • For Table name, enter % to include all tables in the schema.
  • For Action, select Include.

What does the % wildcard do?

The % character is a wildcard that matches all table names within the specified schema. This tells DMS to migrate every table in employee_directory, which in our case is the employees table.

  • In the Migration task startup configuration section, select Automatically on create so the task starts immediately.
  • Click Create task.

DMS begins migrating data immediately. You can monitor progress on the task details page.

  • Click on cloudhr-migration-task in the task list to view its details.
  • Click the Table statistics tab to see row counts as they migrate.
  • Wait for the task status to change to Load complete. This should take less than a minute for our small database.

What should I see in Table statistics?

The Table statistics tab shows a row for the employees table. You should see 8 rows in the Full load rows column, confirming all records were transferred.

Task stuck in Starting or showing errors?

Check that the security groups still allow the DMS replication instance to reach both the EC2 MySQL (port 3306) and RDS (port 3306). If the task shows a connection error, go back to Step 3 and verify your security group rules.

Also confirm that the local MySQL service is still running on EC2. If it was stopped, DMS cannot read from it.

Why is my DMS migration task failing to connect?

Verify migrated data in RDS

Before cutting the application over, you need to confirm that every record arrived intact. A row count comparison between source and target is the minimum verification for any migration.

  • Switch to your SSH session connected to the EC2 instance.
  • Check the row count in the local MySQL source by running this command:
mysql -u appuser -p'AppPass123!' employee_directory -e "SELECT COUNT(*) AS source_count FROM employees;"

You should see a count of 8.

  • Now connect to the RDS instance and check the target row count by running this command:
mysql -h [[RDS_ENDPOINT="your RDS endpoint (e.g. cloudhr-rds.abc123.us-east-1.rds.amazonaws.com)"]] -P 3306 -u admin -p employee_directory -e "SELECT COUNT(*) AS target_count FROM employees;"

Enter your RDS master password when prompted. You should see the same count of 8.

  • Run one more query to spot-check the actual data in RDS:
mysql -h [[RDS_ENDPOINT="your RDS endpoint (e.g. cloudhr-rds.abc123.us-east-1.rds.amazonaws.com)"]] -P 3306 -u admin -p employee_directory -e "SELECT first_name, last_name, department FROM employees;"

You should see all 8 employees with their correct names and departments, matching the data you seeded in Step 1.

Why compare both row counts and sample data?

Row counts confirm no records were dropped during transfer. Spot-checking actual values confirms the data wasn't corrupted or truncated. In a production migration, you would run more comprehensive validation queries across multiple columns.

Seeing 0 rows or an error?

If the employees table doesn't exist in RDS, the DMS task may not have completed successfully. Return to the DMS console and check the task status and any error messages in the task logs.

If you see a connection refused error, verify the RDS security group allows inbound traffic on port 3306 from the EC2 instance.

Why can't I see my migrated data in RDS?

Cutover the application to RDS

The data is verified in RDS. Now you'll perform the cutover: stopping the application, redirecting it to the new database, and decommissioning the old one.

  • In your SSH session, find and stop the running Employee Directory application. If it's running in the foreground, press Ctrl+C to stop it.

If the app is running in the background

If you backgrounded the process or disconnected and reconnected via SSH, find the process ID and kill it:

pkill -f "node server.js"
  • Restart the application with environment variables pointing to RDS by running this command:
DB_HOST=[[RDS_ENDPOINT="your RDS endpoint (e.g. cloudhr-rds.abc123.us-east-1.rds.amazonaws.com)"]] DB_USER=admin DB_PASSWORD=your-rds-master-password DB_NAME=employee_directory node server.js &

What does this command do?

Setting DB_HOST, DB_USER, DB_PASSWORD, and DB_NAME as environment variables overrides the defaults in server.js. The app now connects to your RDS instance instead of localhost.

The & at the end runs the process in the background so you can continue using the terminal.

You should see output confirming the app started with the RDS endpoint as the database host.

  • Open your browser and navigate to your EC2 public IP on port 3000:
http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]:3000
  • Confirm that the Employee Directory page displays all 8 employees with their names, emails, departments, and roles.

The app is now serving data from RDS. Time to decommission the old local database.

  • Back in your SSH session, stop the local MySQL service by running this command:
sudo systemctl stop mysqld
  • Refresh the Employee Directory in your browser.
  • Confirm the app still displays all 8 employees. The application continues to function because it now reads from RDS, not from local MySQL.

What just happened?

You just completed a full lift-and-shift database migration. The local MySQL is stopped (simulating a decommissioned on-premises server), and the application runs entirely on cloud infrastructure: EC2 for compute, RDS for the managed database.

This is the same cutover workflow used in real production migrations. The key difference in production is that the maintenance window is planned, communicated to stakeholders, and often uses CDC replication to minimize downtime.

Seeing a Database Connection Error after cutover?

Double-check the DB_HOST value matches your RDS endpoint exactly. You can find it in the RDS console under your cloudhr-rds instance details, in the Connectivity & security tab.

Also verify DB_PASSWORD matches the master password you set when creating the RDS instance in Step 2. Passwords are case-sensitive.

Why is my app showing a database connection error after switching to RDS?

  • Verify the health endpoint confirms RDS is the database host by running:
curl http://localhost:3000/health

You should see a JSON response with "status": "healthy" and the database field showing your RDS endpoint instead of localhost.

Secret mission

Add CloudWatch Monitoring for RDS

Your migration is complete, but how would you know if the database starts struggling? In production, every cloud-hosted database needs monitoring and alerting. In this secret mission, you will configure CloudWatch alarms that notify you when your RDS instance experiences high CPU or too many connections.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. All resources in this project are free-tier eligible, so there are no ongoing costs while your 12-month free tier is active.

Cost warning

If your 12-month free tier has expired, these resources will incur hourly charges. Delete them immediately after completing the project to avoid unexpected costs.

Resources you used:

  • 1 EC2 instance (t2.micro) running the Employee Directory application.
  • 1 RDS instance (cloudhr-rds, db.t3.micro) with the migrated employee data.
  • 1 DMS replication instance (cloudhr-dms-instance, dms.t3.micro).
  • 1 DMS migration task (cloudhr-migration-task) and 2 DMS endpoints (source and target).
  • 2 security groups (EC2 and RDS).
  • 1 EC2 key pair (cloudhr-keypair).
  • 2 CloudWatch alarms (CPUUtilization and DatabaseConnections).
  • 1 SNS topic with email subscriber.

Keep everything running

No action needed. Choose this if you are still actively building or want to explore the architecture further.

  • All three instances (EC2, RDS, DMS) are free-tier eligible at 750 hours per month for 12 months from account creation.
  • Monitor your usage in the AWS Billing Dashboard to make sure you stay within free-tier limits.
  • CloudWatch alarms are free for up to 10 alarms on the free tier.

Pause - I'll come back to this later

Shut down running instances to free up free-tier hours while keeping your data intact.

  • Stop the EC2 instance: navigate to EC2 > Instances, select your instance, click Instance state, then Stop instance. EBS storage still counts toward free-tier limits but is minimal.
  • Stop the RDS instance: navigate to RDS > Databases, select cloudhr-rds, click Actions, then Stop temporarily. RDS auto-restarts after 7 days, so set a reminder.
  • The DMS replication instance has no stop option. It will continue consuming free-tier hours while running.
  • CloudWatch alarms and the SNS topic remain active at no additional cost.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow this order because some resources have dependencies on others.

Delete DMS resources first (task before endpoints, endpoints before the replication instance):

  • Open the DMS console. In the navigation pane, choose Database migration tasks.
  • Select cloudhr-migration-task, choose Actions, then Delete. Confirm the deletion.
  • In the navigation pane, choose Endpoints. Select your source endpoint, choose Actions, then Delete. Repeat for your target endpoint.
  • In the navigation pane, choose Replication instances. Select cloudhr-dms-instance, choose Actions, then Delete. Confirm the deletion.

Delete the RDS instance:

  • Open the RDS console at https://console.aws.amazon.com/rds/.
  • In the navigation pane, choose Databases, then select cloudhr-rds.
  • Choose Actions, then Delete.
  • Uncheck Create final snapshot? (not needed for this learning project).
  • Enter delete me in the confirmation box, then choose Delete.

Terminate the EC2 instance:

  • Navigate to EC2 > Instances.
  • Select your instance, click Instance state, then Terminate instance. Confirm the termination.

Delete the key pair and security groups:

  • Navigate to EC2 > Key Pairs. Select cloudhr-keypair, choose Actions, then Delete.
  • Navigate to EC2 > Security Groups. Select the security groups you created for this project, choose Actions, then Delete security groups. You may need to wait a few minutes after terminating the EC2 instance before security groups can be deleted.

Delete CloudWatch alarms and the SNS topic:

  • Navigate to CloudWatch > Alarms > All alarms. Select both alarms (CPUUtilization and DatabaseConnections), choose Actions, then Delete.
  • Navigate to SNS > Topics. Select your topic, then choose Delete. Confirm the deletion.

Also delete the .pem key file from your local machine since the key pair no longer exists in AWS.

Nice Work!

Nice Work!

Nice work! You just completed a full cloud migration of the CloudHR Employee Directory application using AWS Database Migration Service.

You've learned how to:

  • Simulate an on-premises environment on EC2 with a Node.js application and local MySQL database, then provision a managed Amazon RDS instance as the migration target.
  • Configure AWS DMS with a replication instance, source endpoint, and target endpoint, then execute a full-load database migration to replicate all employee data to RDS.
  • Perform a production-style cutover by reconfiguring the application to use the RDS endpoint and decommissioning the local database.
  • Secret Mission: Configure CloudWatch alarms for CPU utilization and database connection monitoring on RDS with SNS email notifications.

Ready to quiz yourself?