Posted on
Artificial Intelligence

Automating Server Migrations with Artificial Intelligence

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Automating Server Migrations with Artificial Intelligence

Server migrations are where uptime promises meet reality. Version mismatches, dependency tangles, and last‑minute surprises can turn a routine move into a late‑night fire drill. The good news: AI can help you plan and automate server migrations more accurately and with less guesswork—without abandoning your tried‑and‑true Bash, SSH, and rsync workflows.

This article shows how to bring AI into a deterministic Linux migration pipeline. You’ll collect inventory via Bash, ask an AI model to propose a migration plan, dry‑run critical steps, and execute changes with tools like rsync and Ansible. You’ll also get copy‑paste commands for apt, dnf, and zypper.

Why add AI to your migration toolbox?

  • AI is great at summarization and pattern matching across messy configs/logs. Give it inventories and constraints; get back a structured, human‑readable plan.

  • AI reduces blind spots. It can surface OS/package incompatibilities, systemd unit changes, library ABI breaks, and deprecations you might overlook.

  • You keep the controls. Use AI for planning and code generation, then execute with deterministic tools (SSH, rsync, Ansible). If a step fails, you still know exactly why.

AI won’t replace your SRE muscles—it just helps you move faster and safer.


Prerequisites: Install the tools

We’ll use curl, jq, rsync, SSH/sshpass, Ansible (for execution), and optionally restic (for backups). Choose your package manager.

Apt (Debian/Ubuntu):

sudo apt update
sudo apt install -y curl jq rsync openssh-client sshpass ansible python3 python3-pip restic

Dnf (Fedora/RHEL/CentOS/Alma/Rocky):

# Optional but often needed on RHEL-like systems for extra packages:
# sudo dnf install -y epel-release

sudo dnf install -y curl jq rsync openssh-clients sshpass ansible python3 python3-pip restic

Zypper (openSUSE/SLES):

sudo zypper refresh
sudo zypper install -y curl jq rsync openssh sshpass ansible python3 python3-pip restic

Notes:

  • On some RHEL/CentOS versions, you may need epel-release for ansible/restic: sudo dnf install -y epel-release.

  • If ansible is unavailable, try ansible-core instead.

You’ll also need an OpenAI‑compatible API endpoint to get AI suggestions. Set your environment like this:

export OPENAI_API_KEY="sk-your-key"
export AI_BASE_URL="https://api.openai.com/v1"
export AI_MODEL="gpt-4o-mini"   # or any OpenAI-compatible model, e.g., 'gpt-4o'

If you use a different provider with an OpenAI‑compatible API (e.g., OpenRouter, local gateway), adjust AI_BASE_URL and AI_MODEL accordingly.


Step 1: Discover your source environment (Bash inventory)

Start with a reliable, scriptable inventory that doesn’t require agents. This script connects over SSH, gathers OS/package info, services, disks, and ports, then writes JSON. Feed that to AI in the next step.

Create discover.sh:

#!/usr/bin/env bash
set -euo pipefail

HOSTS_FILE="${1:-hosts.txt}"
OUT_DIR="${2:-inventory}"
mkdir -p "$OUT_DIR"

get_facts() {
  local host="$1"
  ssh -o BatchMode=yes -o StrictHostKeyChecking=no "$host" '
    set -e
    os=$( ( [ -f /etc/os-release ] && cat /etc/os-release ) || echo "ID=unknown" )
    kernel=$(uname -r)
    arch=$(uname -m)
    hostname=$(hostname -f 2>/dev/null || hostname)
    pkgs=$(
      if command -v dpkg >/dev/null 2>&1; then dpkg -l | awk "NR>5{print \$2\"=\"\$3}";
      elif command -v rpm >/dev/null 2>&1; then rpm -qa --qf "%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n";
      elif command -v pacman >/dev/null 2>&1; then pacman -Q;
      else echo "unknown";
      fi
    )
    services=$(
      if command -v systemctl >/dev/null 2>&1; then systemctl list-unit-files --type=service --no-legend | awk "{print \$1\":\"\$2}";
      else echo "unknown";
      fi
    )
    listening=$(
      if command -v ss >/dev/null 2>&1; then ss -ltnp 2>/dev/null | tail -n +2;
      elif command -v netstat >/dev/null 2>&1; then netstat -ltnp 2>/dev/null | tail -n +3;
      else echo "unknown";
      fi
    )
    disks=$(lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT -J 2>/dev/null || echo "{}")
    echo "{"
    echo "  \"host\": \"'$host'\","
    echo "  \"hostname\": \"${hostname}\","
    echo "  \"kernel\": \"${kernel}\","
    echo "  \"arch\": \"${arch}\","
    echo "  \"os_release\": $(printf %s "$os" | jq -Rs .),"
    echo "  \"packages\": $(printf %s "$pkgs" | jq -Rs .),"
    echo "  \"services\": $(printf %s "$services" | jq -Rs .),"
    echo "  \"listening\": $(printf %s "$listening" | jq -Rs .),"
    echo "  \"disks\": ${disks}"
    echo "}"
  ' > "$OUT_DIR/$host.json" || echo "{\"host\":\"$host\",\"error\":\"ssh_failed\"}" > "$OUT_DIR/$host.json"
}

export -f get_facts
export OUT_DIR

# Parallelize with xargs if you like; here we go sequentially for clarity
while read -r h; do
  [ -z "$h" ] && continue
  echo "Collecting from $h..."
  get_facts "$h"
done < "$HOSTS_FILE"

echo "Aggregating..."
jq -s '.' "$OUT_DIR"/*.json > "$OUT_DIR/all.json"
echo "Wrote $OUT_DIR/all.json"

Create hosts.txt with one host per line, and ensure your SSH keys work. Then run:

bash discover.sh hosts.txt

You’ll get inventory/all.json containing your fleet’s facts.

Security tip: Redact secrets before sending inventory to any external API. For example:

jq 'walk(if type=="string" then gsub("(?i)password=.*";"password=REDACTED") else . end)' inventory/all.json > inventory/redacted.json

Step 2: Ask AI for a migration plan (with curl + jq)

Use your JSON inventory and AI to produce a migration blueprint: OS target versions, package mapping, service diffs, data migration steps, and a cutover plan.

Create ai_plan.sh:

#!/usr/bin/env bash
set -euo pipefail

INVENTORY_JSON="${1:-inventory/all.json}"
: "${OPENAI_API_KEY:?Set OPENAI_API_KEY}"
: "${AI_BASE_URL:?Set AI_BASE_URL}"
: "${AI_MODEL:?Set AI_MODEL}"

PROMPT=$(cat <<'EOF'
You are an SRE producing a server migration plan.
Inputs:

- An inventory of Linux servers with OS, packages, services, listening ports, disks.
Task:
1) Group hosts by role (web, db, cache, etc) based on packages/services/ports.
2) Recommend target OS versions and note incompatibilities (systemd unit changes, deprecated packages).
3) Map critical packages to target equivalents; flag manual steps.
4) Propose data migration approach (rsync/restic/snapshots), downtime expectations, dry-runs, and verification checks.
5) Output a concise, actionable plan with shell commands and an Ansible outline. Use fenced code blocks for commands.
EOF
)

curl -sS "$AI_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d @<(jq -n \
    --arg model "$AI_MODEL" \
    --arg content "$PROMPT" \
    --argjson inventory "$(jq '.' "$INVENTORY_JSON")" \
    '{
      model: $model,
      messages: [
        {role:"system", content:"You are a careful, senior Linux SRE. Be specific, safe, and pragmatic."},
        {role:"user", content: ($content + "\n\nInventory JSON:\n" + ($inventory|tojson))}
      ],
      temperature: 0.2
    }'
  ) | jq -r '.choices[0].message.content' | tee migration_plan.md

Run it:

bash ai_plan.sh inventory/all.json

You’ll get migration_plan.md with a proposed plan, including commands and Ansible outlines you can adapt.

Tip: Keep a human in the loop. Review and edit the plan before executing. Save deltas in version control.


Step 3: Dry‑run your data movement (rsync and backups)

Before the real thing, do an rsync dry‑run to estimate time, bandwidth, and conflicts. Optionally snapshot or back up with restic.

Example dry‑run from old web server to new:

# From new target host:
rsync -avz --dry-run --numeric-ids --delete --itemize-changes \
  -e "ssh -o StrictHostKeyChecking=no" \
  user@old-web:/var/www/ /var/www/

If using restic for point‑in‑time backups:

export RESTIC_REPOSITORY="s3:https://s3.example.com/my-migration"
export RESTIC_PASSWORD="strong-passphrase"

# Initialize (one-time)
restic init

# Backup critical data before rsync/cutover
restic backup /etc /var/www /var/lib/postgresql

# Verify
restic snapshots
restic check

Confirm ownership and ACLs match:

rsync -avXH --numeric-ids --dry-run user@old:/etc/ /etc/

Step 4: Execute idempotently (Ansible)

AI can generate a good starting playbook; you finalize it and run with Ansible so you get idempotency and logs.

Minimal example site.yml:

---

- hosts: web_new
  become: true
  tasks:
    - name: Ensure packages present
      package:
        name:
          - nginx
          - certbot
        state: present

    - name: Deploy app files (assumes staged under /staging/var/www)
      synchronize:
        src: /staging/var/www/
        dest: /var/www/
        archive: yes
        delete: yes
      delegate_to: localhost

    - name: Deploy nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        mode: '0644'
      notify: Reload nginx

  handlers:
    - name: Reload nginx
      service:
        name: nginx
        state: reloaded

Inventory example hosts.ini:

[web_new]
new-web-01 ansible_host=10.0.0.51 ansible_user=ubuntu

Run:

ansible-playbook -i hosts.ini site.yml

Pro tip: Use tags to separate “prepare”, “sync”, and “cutover” phases, so you can re‑run safely.


Step 5: Validate and cut over

Create a simple validation script to sanity‑check services and ports. You can also ask AI to propose additional service‑specific checks from your inventory.

validate.sh:

#!/usr/bin/env bash
set -euo pipefail

TARGET="${1:?usage: validate.sh host}"
ssh -o StrictHostKeyChecking=no "$TARGET" '
  set -e
  echo "[System]"
  uname -a
  lsb_release -a 2>/dev/null || cat /etc/os-release

  echo "[Nginx]"
  if command -v nginx >/dev/null 2>&1; then
    nginx -t
    systemctl is-active nginx || true
    ss -ltnp | grep ":80\|:443" || true
  fi

  echo "[PostgreSQL]"
  if command -v psql >/dev/null 2>&1; then
    systemctl is-active postgresql || true
    psql -U postgres -c "select version();" || true
  fi
'

When validation passes:

  • Lower TTL/DNS changes ahead of time, then flip records.

  • Keep the old server in read‑only or maintenance mode briefly to catch regressions.

  • Monitor logs, error rates, and latency. Roll back only with a tested plan.


Real‑world example: Small web app migration

  • Source: Ubuntu 18.04 VM running Nginx + Python app + PostgreSQL 10.

  • Target: Ubuntu 22.04 VM with Nginx + Python 3.10 venv + PostgreSQL 14.

Workflow: 1) Run discover.sh to capture OS, packages, services, and ports. 2) Use ai_plan.sh to generate a plan. It should flag: - systemd unit diffs - Nginx version changes (http2, ciphers) - PostgreSQL 10→14 dump/restore path (pg_dumpall or logical dump) - Python dependencies needing rebuild in a new venv 3) Stage data: - restic backup on old VM - Dry‑run rsync for /var/www, /etc/nginx/sites-available, and /var/lib/postgresql 4) Execute: - Create new venv, install wheels - Restore DB to PG14 using pg_dump/pg_restore - Deploy configs with Ansible, reload Nginx 5) Validate: - Run validate.sh new-web-01 - Check app health endpoint and DB connections 6) Cut over DNS, monitor, and keep old instance for 48 hours as a safety net.


Best practices and guardrails

  • Keep humans in the loop. Treat AI outputs as drafts, not truth.

  • Redact secrets before sending to external APIs. Consider hosting an in‑house AI gateway.

  • Make every step idempotent. Prefer Ansible or declarative scripts over ad‑hoc commands.

  • Log everything. Save AI prompts/responses and execution logs for auditability.

  • Test on a subset first. Bake time estimates from your dry‑runs into maintenance windows.


Conclusion and next steps

AI won’t click the buttons for you—but it will help you see around corners and standardize migrations that used to be bespoke. Start small:

1) Install the tooling and run the discovery script against a lab host. 2) Generate an AI‑assisted plan and adjust it to your standards. 3) Dry‑run rsync and write a minimal Ansible playbook. 4) Execute on a non‑critical service, validate, and iterate.

Want a head start? Drop your inventory JSON into the provided ai_plan.sh and adapt the generated migration_plan.md to your environment. From there, make your migrations boring—on purpose.