Article

Zero-Downtime OS Upgrades: Mastering EC2 Root Volume Replacement

9 min read
Originally published on Medium. Read, clap or comment on Medium ↗

How we engineered a bulletproof OS upgrade process using AWS’s Replace Root Volume feature with both console and CLI workflows

As a Product Reliability Engineer at our tech company, I recently tackled one of the most nerve-wracking challenges in infrastructure management: upgrading the operating system of a critical production EC2 instance without any downtime. Our legacy Ubuntu instance needed to move to a newer, more secure OS, but with strict SLA requirements and zero tolerance for service interruption.

After researching various approaches, I discovered AWS’s relatively new “Replace Root Volume” feature, which proved to be a game-changer. Here’s how we leveraged both the AWS Console and CLI to execute a flawless zero-downtime OS upgrade.

The Challenge

Our production environment consisted of:

Traditional in-place upgrades were too risky, and manual EBS swapping was complex. We needed a strategy that would allow us to prepare everything in advance and execute the swap with minimal complexity.

The Strategy: Replace Root Volume Feature

AWS’s Replace Root Volume feature, introduced in 2021, allows you to replace an EC2 instance’s root volume while preserving:

The process involves:

  1. Creating a new AMI with the target OS and configuration
  2. Using Replace Root Volume to swap the root volume
  3. Testing and validating the upgraded instance

Implementation Walkthrough

Phase 1: Preparing the Configuration Instance

First, I launched a temporary EC2 instance to serve as our configuration baseline:

# Launch configuration instance with Ubuntu 22.04
aws ec2 run-instances \
--image-id ami-0c02fb55956c7d316 \
--instance-type t3.medium \
--key-name production-key \
--security-group-ids sg-12345678 \
--subnet-id subnet-87654321 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=config-instance-ubuntu22}]'

Phase 2: System Configuration and Application Setup

I configured the new instance to match our production requirements exactly:

# Connect to configuration instance
ssh -i production-key.pem ubuntu@config-instance-ip
# Update system packages
sudo apt update && sudo apt upgrade -y# Install required packages
sudo apt install nginx certbot python3-certbot-nginx nodejs npm git -y# Configure system settings
sudo systemctl enable nginx
sudo systemctl enable ssh# Set up log directories with correct permissions
sudo mkdir -p /var/log/app
sudo chown www-data:www-data /var/log/app

Phase 3: Nginx Configuration

I replicated our production Nginx setup exactly:

# /etc/nginx/sites-available/production-app
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;

# Allow Certbot to work
location /.well-known/acme-challenge/ {
root /var/www/html;
}

location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;

# SSL certificate paths (will be populated after Certbot)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;

# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/production-app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx

Phase 4: Application Deployment

I deployed and configured our application stack:

# Create application user and directories
sudo useradd -m -s /bin/bash appuser
sudo mkdir -p /opt/app
sudo chown appuser:appuser /opt/app
# Switch to app user and deploy
sudo su - appuser
cd /opt/app
git clone https://github.com/company/production-app.git .
npm install --production
npm run build# Create systemd service
sudo tee /etc/systemd/system/app-service.service << EOF
[Unit]
Description=Production App
After=network.target[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/app
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000[Install]
WantedBy=multi-user.target
EOF# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable app-service
sudo systemctl start app-service

Phase 5: SSL Certificate Preparation

I prepared the SSL certificate setup (certificates will be obtained after the root volume replacement):

# Create directory structure for certificates
sudo mkdir -p /etc/letsencrypt/live/yourdomain.com
sudo mkdir -p /var/www/html/.well-known/acme-challenge
# Create a script for post-replacement certificate setup
sudo tee /usr/local/bin/setup-ssl.sh << 'EOF'
#!/bin/bash
# Post-replacement SSL setup scriptecho "Setting up SSL certificates..."# Obtain certificates
certbot --nginx -d yourdomain.com -d www.yourdomain.com \
--non-interactive \
--agree-tos \
--email admin@yourdomain.com \
--redirect# Test certificate renewal
certbot renew --dry-runecho "SSL setup complete!"
EOFsudo chmod +x /usr/local/bin/setup-ssl.sh

Phase 6: Final Configuration and Testing

Before creating the AMI, I performed comprehensive testing:

# Create validation script
sudo tee /usr/local/bin/validate-system.sh << 'EOF'
#!/bin/bash
# System validation script
echo "=== System Validation ==="
echo "OS Version:"
lsb_release -aecho -e "\n=== Service Status ==="
systemctl is-active nginx && echo "✓ Nginx running" || echo "✗ Nginx failed"
systemctl is-active app-service && echo "✓ App service running" || echo "✗ App service failed"echo -e "\n=== Network Tests ==="
curl -f http://localhost/health && echo "✓ HTTP health check passed" || echo "✗ HTTP health check failed"echo -e "\n=== Log Files ==="
ls -la /var/log/nginx/
ls -la /var/log/app/echo -e "\n=== Validation Complete ==="
EOFsudo chmod +x /usr/local/bin/validate-system.sh# Run validation
sudo /usr/local/bin/validate-system.sh

Phase 7: Creating the Custom AMI

With everything configured and tested, I created a custom AMI:

Using AWS Console:

  1. Navigate to EC2 Dashboard > Instances
  2. Select the configuration instance
  3. Actions > Image and templates > Create image
  4. Set Name: ubuntu-22-04-production-configured
  5. Set Description: Ubuntu 22.04 with production configuration
  6. Click “Create Image”

Using AWS CLI:

# Create AMI from configuration instance
aws ec2 create-image \
--instance-id i-1234567890abcdef0 \
--name "ubuntu-22-04-production-configured" \
--description "Ubuntu 22.04 with production configuration" \
--no-reboot \
--tag-specifications 'ResourceType=image,Tags=[{Key=Name,Value=production-base-ubuntu22},{Key=Environment,Value=production}]'
# Wait for AMI to be available
aws ec2 wait image-available --image-ids ami-newami123456

Phase 8: The Root Volume Replacement

Now for the main event — replacing the root volume during our maintenance window.

Method 1: Using AWS Console

Navigate to EC2 Dashboard

Initiate Root Volume Replacement

Monitor the Process

Method 2: Using AWS CLI

# First, get the current instance details
aws ec2 describe-instances --instance-ids i-production123 \
--query 'Reservations[0].Instances[0].{State:State.Name,RootDevice:RootDeviceType,BlockDevices:BlockDeviceMappings}'
# Create a root volume replacement task
aws ec2 create-replace-root-volume-task \
--instance-id i-production123 \
--image-id ami-newami123456 \
--tag-specifications 'ResourceType=replace-root-volume-task,Tags=[{Key=Name,Value=production-os-upgrade}]'# Monitor the replacement task
TASK_ID="replacevol-1234567890abcdef0"
aws ec2 describe-replace-root-volume-tasks \
--replace-root-volume-task-ids $TASK_ID# Wait for completion (you can also use this in a loop)
while true; do
STATUS=$(aws ec2 describe-replace-root-volume-tasks \
--replace-root-volume-task-ids $TASK_ID \
--query 'ReplaceRootVolumeTasks[0].TaskState' \
--output text)

echo "Current status: $STATUS"

if [ "$STATUS" == "succeeded" ]; then
echo "Root volume replacement completed successfully!"
break
elif [ "$STATUS" == "failed" ]; then
echo "Root volume replacement failed!"
exit 1
fi

sleep 30
done

Phase 9: Post-Replacement Validation

Once the replacement was complete, I immediately ran our validation suite:

# Connect to the upgraded instance
ssh -i production-key.pem ubuntu@production-instance-ip
# Run system validation
sudo /usr/local/bin/validate-system.sh# Set up SSL certificates (since we have a new root volume)
sudo /usr/local/bin/setup-ssl.sh# Run comprehensive health checks
curl -f https://yourdomain.com/health
curl -f https://yourdomain.com/api/status# Check SSL certificate
echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null | openssl x509 -inform pem -noout -text# Verify all services are running correctly
systemctl status nginx app-service# Check application logs
tail -f /var/log/app/application.log
tail -f /var/log/nginx/access.log

Advanced CLI Automation

For repeated upgrades across multiple instances, I created an automation script:

#!/bin/bash
# automated-root-volume-replacement.sh
INSTANCE_ID="$1"
AMI_ID="$2"
NOTIFICATION_EMAIL="$3"if [ $# -ne 3 ]; then
echo "Usage: $0 <instance-id> <ami-id> <notification-email>"
exit 1
fiecho "Starting root volume replacement for instance: $INSTANCE_ID"# Create replacement task
TASK_ID=$(aws ec2 create-replace-root-volume-task \
--instance-id "$INSTANCE_ID" \
--image-id "$AMI_ID" \
--query 'ReplaceRootVolumeTask.ReplaceRootVolumeTaskId' \
--output text)echo "Created replacement task: $TASK_ID"# Monitor progress
while true; do
TASK_INFO=$(aws ec2 describe-replace-root-volume-tasks \
--replace-root-volume-task-ids "$TASK_ID" \
--query 'ReplaceRootVolumeTasks[0]')

STATUS=$(echo "$TASK_INFO" | jq -r '.TaskState')
START_TIME=$(echo "$TASK_INFO" | jq -r '.StartTime')

echo "$(date): Status = $STATUS (Started: $START_TIME)"

case "$STATUS" in
"succeeded")
echo "✓ Root volume replacement completed successfully!"

# Send success notification
aws sns publish \
--topic-arn "arn:aws:sns:region:account:notifications" \
--message "Root volume replacement succeeded for $INSTANCE_ID" \
--subject "OS Upgrade Success"

break
;;
"failed")
echo "✗ Root volume replacement failed!"

# Get failure reason
FAILURE_REASON=$(echo "$TASK_INFO" | jq -r '.TaskErrorMessage // "Unknown error"')
echo "Failure reason: $FAILURE_REASON"

# Send failure notification
aws sns publish \
--topic-arn "arn:aws:sns:region:account:notifications" \
--message "Root volume replacement failed for $INSTANCE_ID: $FAILURE_REASON" \
--subject "OS Upgrade Failed"

exit 1
;;
"in-progress"|"pending")
# Continue monitoring
;;
*)
echo "Unknown status: $STATUS"
;;
esac

sleep 60
doneecho "Replacement completed. Instance should be available shortly."

The Results

The upgrade was a resounding success:

Performance Metrics:

Process Benefits:

Lessons Learned

What Worked Exceptionally Well

Replace Root Volume is a game-changer: This feature eliminates most of the complexity of traditional EBS swapping while providing the same benefits.

AMI-based approach: Creating a fully configured AMI meant the replacement instance was immediately ready for production.

Console + CLI flexibility: Having both options allowed for interactive monitoring during initial attempts and full automation for subsequent upgrades.

Challenges and Solutions

SSL certificate timing: Certificates needed to be re-obtained after replacement since they reference the specific root volume. Solution: Automated certificate setup script.

Application state persistence: Some applications store state in /tmp or other root volume locations. Solution: Moved state to attached EBS data volumes.

Monitoring blind spots: Brief monitoring gaps during instance restart. Solution: Extended monitoring timeout thresholds during maintenance windows.

Best Practices for Root Volume Replacement

Pre-Replacement Checklist

  1. Create comprehensive AMI: Include all configurations, not just the application
  2. Test AMI thoroughly: Launch test instances from AMI before production use
  3. Document data locations: Ensure all persistent data is on separate EBS volumes
  4. Plan certificate renewal: Automate SSL certificate setup post-replacement
  5. Prepare rollback procedure: Know how to revert using the original root volume

During Replacement

  1. Use both console and CLI: Console for visibility, CLI for automation
  2. Monitor CloudWatch: Watch instance metrics during the process
  3. Coordinate with team: Ensure all stakeholders know about the maintenance
  4. Have communication ready: Prepare status updates for users if needed

Post-Replacement Validation

# Comprehensive post-replacement validation script
#!/bin/bash
echo "=== Post-Replacement Validation ==="# System checks
echo "Checking OS version..."
lsb_release -aecho "Checking kernel version..."
uname -r# Service validation
echo "Validating services..."
services=("nginx" "app-service" "ssh")
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service"; then
echo "✓ $service is running"
else
echo "✗ $service is not running"
fi
done# Network connectivity
echo "Testing network connectivity..."
curl -f -s -o /dev/null https://yourdomain.com/health && echo "✓ HTTPS health check passed" || echo "✗ HTTPS health check failed"# SSL certificate validation
echo "Validating SSL certificate..."
CERT_EXPIRY=$(echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates | grep notAfter | cut -d= -f2)
echo "Certificate expires: $CERT_EXPIRY"# Performance baseline
echo "Running performance baseline..."
ab -n 100 -c 10 https://yourdomain.com/ > /tmp/performance-test.log
grep "Requests per second" /tmp/performance-test.logecho "=== Validation Complete ==="

Conclusion

AWS’s Replace Root Volume feature transformed our OS upgrade process from a complex, error-prone operation into a streamlined, automated workflow. By combining the visual feedback of the AWS Console with the automation power of the CLI, we achieved zero-downtime upgrades with minimal operational complexity.

The approach scales beautifully — what started as a single-instance upgrade has become our standard operating procedure for maintaining our entire EC2 fleet. The combination of thorough preparation, AMI-based configuration management, and AWS’s robust infrastructure automation gives us the confidence to upgrade production systems during business hours.

For teams still struggling with OS upgrades, I highly recommend exploring this approach. The Replace Root Volume feature, combined with proper AMI management and automation, represents a significant leap forward in infrastructure reliability engineering.

The key insight is that modern cloud infrastructure doesn’t just provide compute resources — it provides operational capabilities that can fundamentally change how we approach traditionally risky operations. By leveraging these capabilities thoughtfully, we can deliver both innovation and reliability.


What’s your experience with EC2 root volume replacement? Have you implemented similar zero-downtime strategies? I’d love to hear about your approaches and lessons learned in the comments.

Tags: #AWS #EC2 #RootVolumeReplacement #AMI #DevOps #SRE #ReliabilityEngineering #Ubuntu #ZeroDowntime #CloudInfrastructure #Automation

← All articles Discuss on Medium ↗