Learn how to build a production-grade legacy source environment in AWS for cloud migration. This comprehensive guide covers VPC setup, EC2 web servers, RDS PostgreSQL databases, and database-connected applications.
In this comprehensive guide, we’ll create a simulated “on-premises” environment within AWS that represents the legacy infrastructure we’ll be migrating FROM. This production-grade setup includes a VPC with a web server and a PostgreSQL database with realistic sample data, perfectly designed for learning cloud migration strategies.
Estimated Time: 45-60 minutes
Region: ap-south-1 (Mumbai)
Difficulty: Intermediate
Before diving into AWS Application Migration Service (MGN) and Database Migration Service (DMS), it’s crucial to understand the source environment. This hands-on approach gives you:
Before we begin, ensure you have:
⚠️ Resources that will incur charges:
Estimated Phase 1 Cost: ~$0.70-1.40 per day while running
Our legacy environment will consist of:
┌─────────────────────────────────────────────────────────┐
│ on-prem-vpc (10.0.0.0/16) │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Public Subnet (10.0.1.0/24) - ap-south-1a │ │
│ │ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ EC2 Web Server │ │ │
│ │ │ - Apache httpd │ │ │
│ │ │ - PostgreSQL client │ │ │
│ │ │ - Public IP: xxx.xxx.xxx.xxx │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Private Subnet (10.0.2.0/24) - ap-south-1a│ │
│ │ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ RDS PostgreSQL (legacy-db) │ │ │
│ │ │ - Single-AZ │ │ │
│ │ │ - db.t3.micro │ │ │
│ │ │ - Port: 5432 │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Private Subnet (10.0.3.0/24) - ap-south-1b│ │
│ │ (Required for DB subnet group) │ │
│ └────────────────────────────────────────────┘ │
│ │
│ Internet Gateway ←→ Public Subnet │
└─────────────────────────────────────────────────────────┘
on-prem-vpc10.0.0.0/16on-prem-vpcon-prem-public-subnet-1a10.0.1.0/24on-prem-vpcon-prem-private-subnet-1a10.0.2.0/24Important: AWS now requires DB subnet groups to have subnets in at least 2 Availability Zones, even for single-AZ databases.
on-prem-vpcon-prem-private-subnet-1b10.0.3.0/24on-prem-igwon-prem-vpcon-prem-public-rton-prem-vpcon-prem-public-rt0.0.0.0/0on-prem-igwon-prem-public-rt, go to Subnet associations tabon-prem-public-subnet-1aon-prem-vpc (it won’t have a name)on-prem-private-rton-prem-private-subnet-1a (10.0.2.0/24)on-prem-private-subnet-1b (10.0.3.0/24)Click Security Groups in the left sidebar
Click Create security group
Configure:
on-prem-web-sgSecurity group for on-premises web serveron-prem-vpcInbound rules - Click Add rule for each:
Rule 1 (SSH):
Rule 2 (HTTP):
Outbound rules: Leave default (All traffic to 0.0.0.0/0)
Click Create security group
📝 Note down the Security Group ID (e.g., sg-0abc123web)
Click Create security group again
Configure:
on-prem-db-sgSecurity group for on-premises databaseon-prem-vpcInbound rules - Click Add rule:
on-prem-web-sg security group IDOutbound rules: Leave default
Click Create security group
📝 Note down the Security Group ID (e.g., sg-0def456db)
on-prem-web-key.pem for Mac/Linux.ppk for Windows (PuTTY)chmod 400 on-prem-web-key.pemStep 1: Name and tags
on-prem-web-serverStep 2: Application and OS Images
Step 3: Instance type
Step 4: Key pair
on-prem-web-keyStep 5: Network settings
on-prem-vpcon-prem-public-subnet-1aon-prem-web-sgStep 6: Configure storage
Step 7: Advanced details
#!/bin/bash
# Update system
yum update -y
# Install Apache web server
yum install -y httpd
# Install PostgreSQL 16 client
yum install -y postgresql16
# Start and enable Apache
systemctl start httpd
systemctl enable httpd
# Create a simple homepage
cat > /var/www/html/index.html <<'EOF'
<!DOCTYPE html>
<html>
<head>
<title>On-Premises Web Server</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 10px;
max-width: 600px;
margin: 0 auto;
}
h1 { margin-top: 0; }
.status {
background: rgba(255, 255, 255, 0.2);
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>🏢 On-Premises Web Server</h1>
<p><strong>Environment:</strong> Legacy/Source</p>
<p><strong>Status:</strong> Running</p>
<div class="status">
<p><strong>Server Info:</strong></p>
<ul>
<li>Web Server: Apache httpd</li>
<li>VPC: on-prem-vpc (10.0.0.0/16)</li>
<li>Subnet: Public (10.0.1.0/24)</li>
</ul>
</div>
<p style="margin-top: 30px; font-size: 0.9em; opacity: 0.8;">
This server will be migrated to AWS using Application Migration Service (MGN)
</p>
</div>
</body>
</html>
EOF
# Set permissions
chmod 644 /var/www/html/index.html
chown apache:apache /var/www/html/index.html
http://YOUR_PUBLIC_IPFor Mac/Linux:
ssh -i on-prem-web-key.pem ec2-user@YOUR_PUBLIC_IP
For Windows (PuTTY):
Once connected via SSH, run:
psql --version
Expected output: psql (PostgreSQL) 16.x
Before creating the RDS instance, we need a DB subnet group.
on-prem-db-subnet-groupSubnet group for on-premises legacy databaseon-prem-vpcNote: AWS has updated the interface and now requires subnets in at least 2 Availability Zones. Follow these steps:
ap-south-1-zg-1 (or any available group)on-prem-private-subnet-1a (10.0.2.0/24) - ap-south-1aon-prem-private-subnet-1b (10.0.3.0/24) - ap-south-1bImportant: AWS now requires at least 2 AZs for DB subnet groups, even for single-AZ databases. This ensures better availability planning.
legacy-dbpostgres (default, leave as is)LegacyDB2024!)Important: Choose the deployment option based on your needs:
Single-AZ DB instance deployment (1 instance) ✅ RECOMMENDED
Multi-AZ DB instance deployment (2 instances)
Multi-AZ DB cluster deployment (3 instances)
Select: Single-AZ DB instance deployment (1 instance)
on-prem-vpcon-prem-db-subnet-groupon-prem-db-sglegacydb (⚠️ Important! This creates the database)legacy-dblegacy-db.abc123xyz.ap-south-1.rds.amazonaws.com)Connect to your EC2 web server using SSH (as done in Step 8.2)
psql --host=YOUR_RDS_ENDPOINT --port=5432 --username=postgres --dbname=legacydb
Replace YOUR_RDS_ENDPOINT with the actual endpoint from Step 10.11.
When prompted, enter the master password you created in Step 10.3.
Expected output:
psql (16.x)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
legacydb=>
Copy and paste the following SQL commands:
-- Create users table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample data
INSERT INTO users (username, email) VALUES
('amodh', 'amodh@onprem.local'),
('john_doe', 'john@onprem.local'),
('jane_smith', 'jane@onprem.local'),
('admin', 'admin@onprem.local');
-- Create a products table for more realistic data
CREATE TABLE products (
id SERIAL PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample products
INSERT INTO products (product_name, price, stock_quantity) VALUES
('Laptop', 45999.99, 15),
('Mouse', 599.99, 50),
('Keyboard', 1299.99, 30),
('Monitor', 12999.99, 20),
('Headphones', 2499.99, 40);
-- Create an orders table
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
product_id INTEGER REFERENCES products(id),
quantity INTEGER NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample orders
INSERT INTO orders (user_id, product_id, quantity) VALUES
(1, 1, 1),
(1, 2, 2),
(2, 3, 1),
(3, 4, 1),
(4, 5, 3);
-- Check users
SELECT * FROM users;
-- Check products
SELECT * FROM products;
-- Check orders with details
SELECT
o.id,
u.username,
p.product_name,
o.quantity,
o.order_date
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;
-- Exit psql
\q
Expected output for users:
id | username | email | created_at
----+------------+----------------------+---------------------
1 | amodh | amodh@onprem.local | 2025-10-17 10:30:00
2 | john_doe | john@onprem.local | 2025-10-17 10:30:00
3 | jane_smith | jane@onprem.local | 2025-10-17 10:30:00
4 | admin | admin@onprem.local | 2025-10-17 10:30:00
(4 rows)
Let’s enhance our web application to connect to the database.
While still SSH’d into the web server:
# Install Python and pip
sudo yum install -y python3 python3-pip
# Install psycopg2 (PostgreSQL adapter for Python)
sudo pip3 install psycopg2-binary
# Create application directory
sudo mkdir -p /var/www/cgi-bin
cd /var/www/cgi-bin
# Create the database application
sudo cat > /var/www/cgi-bin/db_app.py <<'PYEOF'
#!/usr/bin/env python3
import cgi
import cgitb
import psycopg2
import os
cgitb.enable()
# Database configuration
DB_HOST = "YOUR_RDS_ENDPOINT"
DB_NAME = "legacydb"
DB_USER = "postgres"
DB_PASS = "YOUR_DB_PASSWORD"
print("Content-Type: text/html\n")
print("""
<!DOCTYPE html>
<html>
<head>
<title>Legacy Application - Database View</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 30px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 { color: #333; border-bottom: 3px solid #667eea; padding-bottom: 10px; }
h2 { color: #667eea; margin-top: 30px; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th {
background: #667eea;
color: white;
padding: 12px;
text-align: left;
}
td {
padding: 10px;
border-bottom: 1px solid #ddd;
}
tr:hover { background: #f0f0f0; }
.status {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>🏢 Legacy On-Premises Application</h1>
""")
try:
# Connect to database
conn = psycopg2.connect(
host=DB_HOST,
database=DB_NAME,
user=DB_USER,
password=DB_PASS
)
cur = conn.cursor()
print('<div class="status">✅ Database Connection: Successful</div>')
# Fetch users
print("<h2>Users Table</h2>")
cur.execute("SELECT id, username, email, created_at FROM users ORDER BY id")
users = cur.fetchall()
print("<table>")
print("<tr><th>ID</th><th>Username</th><th>Email</th><th>Created At</th></tr>")
for user in users:
print(f"<tr><td>{user[0]}</td><td>{user[1]}</td><td>{user[2]}</td><td>{user[3]}</td></tr>")
print("</table>")
# Fetch products
print("<h2>Products Table</h2>")
cur.execute("SELECT id, product_name, price, stock_quantity FROM products ORDER BY id")
products = cur.fetchall()
print("<table>")
print("<tr><th>ID</th><th>Product</th><th>Price (₹)</th><th>Stock</th></tr>")
for product in products:
print(f"<tr><td>{product[0]}</td><td>{product[1]}</td><td>{product[2]}</td><td>{product[3]}</td></tr>")
print("</table>")
# Fetch orders with JOIN
print("<h2>Recent Orders</h2>")
cur.execute("""
SELECT o.id, u.username, p.product_name, o.quantity, o.order_date
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
ORDER BY o.order_date DESC
""")
orders = cur.fetchall()
print("<table>")
print("<tr><th>Order ID</th><th>Customer</th><th>Product</th><th>Qty</th><th>Date</th></tr>")
for order in orders:
print(f"<tr><td>{order[0]}</td><td>{order[1]}</td><td>{order[2]}</td><td>{order[3]}</td><td>{order[4]}</td></tr>")
print("</table>")
cur.close()
conn.close()
except Exception as e:
print(f'<div class="error">❌ Database Error: {str(e)}</div>')
print("""
<p style="margin-top: 40px; color: #666; text-align: center;">
This legacy application will be migrated using AWS MGN and DMS
</p>
</div>
</body>
</html>
""")
PYEOF
Replace the placeholders in the script:
# Replace with your actual values
sudo sed -i 's|YOUR_RDS_ENDPOINT|YOUR_ACTUAL_ENDPOINT|g' /var/www/cgi-bin/db_app.py
sudo sed -i 's|YOUR_DB_PASSWORD|YOUR_ACTUAL_PASSWORD|g' /var/www/cgi-bin/db_app.py
# Make it executable
sudo chmod +x /var/www/cgi-bin/db_app.py
# Enable CGI module
sudo sed -i 's|#LoadModule cgid_module modules/mod_cgid.so|LoadModule cgid_module modules/mod_cgid.so|g' /etc/httpd/conf.modules.d/01-cgi.conf
# Configure CGI directory
sudo tee -a /etc/httpd/conf/httpd.conf > /dev/null <<'EOF'
# CGI Configuration
<Directory "/var/www/cgi-bin">
AllowOverride None
Options +ExecCGI
AddHandler cgi-script .py
Require all granted
</Directory>
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
EOF
# Restart Apache
sudo systemctl restart httpd
http://YOUR_PUBLIC_IP/cgi-bin/db_app.py✅ If you see this data, your application is successfully connected to the database!
Before proceeding to Phase 2, verify all components:
on-prem-vpc created with CIDR 10.0.0.0/16http://PUBLIC_IP/http://PUBLIC_IP/cgi-bin/db_app.pylegacy-db is available📝 Save these values for Phase 3 and Phase 4:
| Resource | Value | Notes |
|---|---|---|
| VPC ID | vpc-xxxxxxxx | on-prem-vpc |
| Public Subnet ID | subnet-xxxxxxxx | 10.0.1.0/24 |
| Private Subnet 1 ID | subnet-xxxxxxxx | 10.0.2.0/24 |
| Private Subnet 2 ID | subnet-xxxxxxxx | 10.0.3.0/24 |
| Web Server Instance ID | i-xxxxxxxx | Source for MGN |
| Web Server Public IP | x.x.x.x | For testing |
| Web Server Private IP | 10.0.1.x | Internal IP |
| Web SG ID | sg-xxxxxxxx | on-prem-web-sg |
| DB SG ID | sg-xxxxxxxx | on-prem-db-sg |
| RDS Endpoint | legacy-db.xxx.ap-south-1.rds.amazonaws.com | Source for DMS |
| DB Username | postgres | Master username |
| DB Password | ****** | Keep secure! |
| DB Name | legacydb | Initial database |
Solution:
chmod 400 your-key.pemSolution:
sudo systemctl status httpdsudo tail -f /var/log/httpd/error_logSolution:
Solution:
pip3 list | grep psycopg2ls -l /var/www/cgi-bin/db_app.pysudo tail -f /var/log/httpd/error_logSymptoms:
/cgi-bin/db_app.pyAH01241: error spawning CGI child: exec of '/var/www/cgi-bin/db_app.py' failed (Exec format error)Solution:
# Install dos2unix
sudo yum install dos2unix -y
# Fix shebang line
sudo sed -i '1s|^.*|#!/usr/bin/env python3|' /var/www/cgi-bin/db_app.py
# Set correct permissions
sudo chmod 755 /var/www/cgi-bin/db_app.py
# Fix line endings (Windows to Unix)
sudo dos2unix /var/www/cgi-bin/db_app.py
# Restart Apache
sudo systemctl restart httpd
Root cause: This usually happens when the script was created with Windows line endings or has an incorrect shebang line.
💰 To minimize costs while working on this project:
Stop (don’t terminate) the EC2 instance when not in use
Stop the RDS instance when not actively testing
Delete resources as soon as you complete the migration
✅ Phase 1 Complete!
You now have a fully functional “on-premises” environment with:
Proceed to Phase 2: Build the Target Cloud Environment with VPC peering and a production-grade Multi-AZ RDS instance.
# SSH to web server
ssh -i on-prem-web-key.pem ec2-user@YOUR_PUBLIC_IP
# Connect to database
psql --host=YOUR_RDS_ENDPOINT --port=5432 --username=postgres --dbname=legacydb
# Check Apache status
sudo systemctl status httpd
# View Apache logs
sudo tail -f /var/log/httpd/access_log
sudo tail -f /var/log/httpd/error_log
# Restart Apache
sudo systemctl restart httpd
# Test database connectivity
psql --host=YOUR_RDS_ENDPOINT --port=5432 --username=postgres --dbname=legacydb -c "SELECT COUNT(*) FROM users;"
In this comprehensive guide, we’ve successfully built a production-grade legacy source environment that perfectly simulates an on-premises infrastructure. This foundation is essential for understanding cloud migration strategies and provides the perfect testing ground for AWS Application Migration Service (MGN) and Database Migration Service (DMS).
What we’ve accomplished:
Key Learning Outcomes:
This environment is now ready for the next phase of our migration journey, where we’ll build the target AWS environment and begin the actual migration process using AWS MGN and DMS.
This is Part 1 of a comprehensive AWS migration series. Here is the Part 2, where we’ll build the target cloud environment with Multi-AZ RDS and VPC peering.