SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor

새로운 VPS로 서버 이전하는 방법: 리허설 기반 전환 가이드

기존 서버를 새로운 VPS로 안전하게 이전하는 체계적인 절차를 안내합니다. 단순 복제가 아닌 인벤토리 작성, DNS TTL 조정, 데이터 동기화 및 검증을 포함한 리허설 기반 전환 전략을 통해 서비스 중단 없이 서버를 성공적으로 마이그레이션하는 방법을 상세히 설명합니다.

새로운 VPS로 서버 이전하기: 리허설을 통한 전환

서버를 새로운 VPS로 이전할 때는 단순히 데이터를 복사하는 것이 아니라, 리허설을 거친 전환 과정으로 접근해야 합니다. 새로운 서버를 처음부터 구축하고, 데이터를 두 번 동기화하며, DNS를 변경하기 전에 새로운 서버가 자체 IP 주소에서 정상적으로 작동하는지 검증하십시오. 그 후 레코드를 전환하고, 확실해질 때까지 기존 서버를 계속 실행해 두어야 합니다. 바이트를 복사하는 것은 쉬운 부분입니다. 작업 순서가 이전 과정의 성공 여부와 비용을 결정합니다.

이 가이드는 웹 애플리케이션, 데이터베이스, TLS(transport layer security) 인증서를 실행하는 단일 Linux 서버를 다룹니다. 이는 대부분의 단일 서버 구성에 해당합니다. 두 대의 호스트가 관여하므로, 모든 예제에는 주석으로 해당 명령이 실행되는 호스트를 명시했습니다. 주소는 문서화 범위에서 가져왔습니다. 198.51.100.10은 기존 서버이고, 203.0.113.20는 새로운 서버입니다.

시작하기 전에 전체 런북을 읽어 보십시오. 첫 번째 단계인 DNS TTL 낮추기는 실제로 중요한 단계를 수행하기 며칠 전에 완료되어야 합니다.

구축 전 인벤토리 작성하기

무엇을 구성할지 정의하지 않은 서버는 재구축할 수 없습니다. 기존 서버가 수행하는 작업을 기록하는 데 한 시간을 투자하십시오. 마이그레이션 후 문제가 발생하는 지점은 항상 아무도 기억하지 못했던 부분, 즉 cron 작업, 방화벽 예외 설정, 애플리케이션 디렉터리 외부에 존재하는 환경 변수 파일 등입니다.

기존 서버에서 다음 명령을 실행하고, 새 서버에서 확인할 수 있는 곳에 출력 결과를 보관하십시오.

# old server: packages you asked for, not the dependencies they pulled in
apt-mark showmanual > ~/inv-packages.txt

# old server: what runs now, and what starts at boot
systemctl list-units --type=service --state=running --no-pager > ~/inv-services.txt
systemctl list-unit-files --state=enabled --no-pager >> ~/inv-services.txt
systemctl list-timers --all --no-pager > ~/inv-timers.txt

# old server: what is listening, on which address and port
sudo ss -tulpn > ~/inv-ports.txt

# old server: human accounts, skipping the system ones
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $3, $6, $7}' /etc/passwd

apt-mark showmanual은 의존성으로 설치된 모든 패키지를 제외하므로 유용한 목록입니다. 5년 된 서버에서 dpkg --get-selections를 전체 실행하면 2,000줄이 넘는 결과가 출력되어 실제 의도를 파악하기 어렵습니다.

예약된 작업은 두 곳에 숨어 있으므로 모두 확인해야 합니다. 매달 한 번만 실행되는 작업은 마이그레이션 후 6주가 지나서야 발견하게 될 것입니다.

# old server: per-user crontabs, then the system drop-ins
for u in $(cut -d: -f1 /etc/passwd); do sudo crontab -lu "$u" 2>/dev/null | sed "s/^/$u: /"; done
sudo ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourly

다음은 일반 파일이 아닌 구성 요소들입니다. 방화벽 규칙, 인증서, 데이터베이스, 그리고 실제로 이동해야 할 데이터의 양을 확인하십시오.

# old server
sudo ufw status verbose        # or: sudo nft list ruleset
sudo certbot certificates
sudo -u postgres psql -c '\l'  # or: sudo mysql -e 'SHOW DATABASES;'
sudo du -xh --max-depth=1 / | sort -h

certbot certificates는 각 인증서 이름, 포함된 도메인, 만료일, 디스크상의 파일 경로를 출력합니다. 이 출력 결과가 TLS 체크리스트가 됩니다. du -x은 단일 파일 시스템 내에서만 작동하므로, 마운트된 백업 볼륨까지 탐색하여 실제보다 10배 큰 용량을 보고하는 일을 방지합니다.

서버 외부에 존재하여 매번 잊히는 항목이 두 가지 있습니다. 첫째, 서버의 IP 주소를 허용 목록(allowlist)에 등록해 둔 모든 제3자 서비스입니다. 결제 게이트웨이, 관리형 데이터베이스, SMTP 릴레이, 파트너 API 등이 해당합니다. 새 서버는 새로운 주소를 가지므로, 전환 작업 이후가 아니라 이전에 해당 허용 목록에 새 IP를 추가해야 합니다. 둘째, 직접 생성하지 않은 DNS 레코드입니다. 텍스트 내에 이전 IP를 명시하는 MX 레코드나 SPF 레코드 등이 여기에 해당합니다.

기존 루트 파일시스템을 복제하지 않고 재구축해야 하는 이유

전체 루트 파일시스템을 새 VPS로 복제하는 것이 더 빠르게 보일 수 있으며, 실제로도 그렇습니다. 하지만 문제가 발생하기 전까지만 그렇습니다. 수년간 운영 환경에서 사용된 루트 파일시스템에는 문서화되지 않은 수동 설정, 더 이상 존재하지 않는 저장소의 패키지, 이전 플랫폼의 가상 하드웨어에 맞춰진 부팅 설정이 포함되어 있습니다. 마이그레이션을 결심하게 된 원인을 포함하여 모든 것을 그대로 가져오게 됩니다.

재구축은 첫날에는 더 느리지만, 그 이후로는 매일 비용을 절감해 줍니다. 현재 릴리스를 설치하고, 기본적인 보안 강화(hardening)를 적용한 뒤, 데이터만 복사하십시오. 애플리케이션 디렉터리, 사이트 설정, 데이터베이스 덤프, 인증서, 사용자 업로드 파일이 이에 해당합니다. 설명할 수 없는 것은 무엇이든 가져오지 마십시오. 새 VPS에서의 첫 10분 가이드를 따라 새 서버를 시작하는 것처럼 시작하고, 인벤토리에 있는 서비스를 하나씩 추가하면서 다음 서비스를 추가하기 전에 각각의 정상 작동을 확인하십시오.

이미지나 스냅샷 복구가 적절한 경우

재구축이 아닌 다른 방법을 선택해야 하는 예외적인 상황이 하나 있습니다. 기존 서버가 부팅되지 않거나, 더 이상 소스 코드로부터 애플리케이션을 재구축할 수 없는 경우라면 제공업체의 이미지나 스냅샷을 복구하는 것이 실용적인 해결책입니다. 여기에는 명확한 한계가 있습니다. 복구된 디스크는 해당 플랫폼의 가상 장치와 네트워크 명명 규칙을 그대로 따르기 때문에, 동일한 제공업체 내에서, 종종 같은 플랜 계열 내에서만 작동합니다.

실행 중인 서버의 스냅샷은 라이브 데이터베이스를 파일 수준에서 복사할 때 발생하는 것과 동일한 일관성 문제를 안고 있습니다. 이미지 복구는 마이그레이션 계획이 아닌 복구 경로로 간주해야 하며, 스냅샷을 기반으로 계획을 세우기 전에 스냅샷이 백업과 다른 이유를 먼저 읽어보시기 바랍니다.

파일 이동 방식: SSH를 통한 rsync

기존 서버에서 rsync을 실행하여 새 서버로 데이터를 전송합니다. 기존 서버가 이미 데이터를 보유하고 있고 sudo 권한으로 모든 데이터를 읽을 수 있으므로, 밀어넣기(push) 방식이 일반적으로 더 간단합니다.

# old server: dry run first, and read what it says it will do
sudo rsync -aHAX --dry-run --itemize-changes \
  -e 'ssh -i /root/.ssh/id_ed25519' \
  /srv/app/ deploy@203.0.113.20:/srv/app/

# old server: the real bulk pass
sudo rsync -aHAX --info=progress2 \
  -e 'ssh -i /root/.ssh/id_ed25519' \
  /srv/app/ deploy@203.0.113.20:/srv/app/

플래그 설정이 중요합니다. -a는 권한, 타임스탬프, 심볼릭 링크 및 소유권을 보존합니다. -H은 하드 링크를 별도의 복사본으로 확장하지 않고 하드 링크 상태로 유지합니다. -A은 POSIX ACL(접근 제어 목록)을 복사하며, -X는 확장 속성을 복사합니다. 마지막 두 옵션을 사용하지 않으면 SELinux 레이블과 ACL이 확장 속성에 저장되어 관리되므로, 파일이 동일해 보여도 실제 동작은 다를 수 있습니다.

대부분의 실패는 다음 두 가지 세부 사항에서 발생합니다.

경로 끝의 슬래시(/)는 데이터가 저장될 위치를 결정합니다. /srv/app/은 해당 디렉터리의 내용을 의미합니다. /srv/app는 디렉터리 자체를 의미합니다. 이를 잘못 설정하면 새 서버에 /srv/app/app가 생성되며, 애플리케이션이 설정된 경로보다 한 단계 상위에서 파일을 찾게 되어 파일을 찾을 수 없다는 오류가 발생합니다.

sudo 환경에서 물결표(~)는 root의 홈 디렉터리를 의미합니다. sudo rsync 내부에 -e 'ssh -i ~/.ssh/id_ed25519'을 작성하면 자신의 홈 디렉터리가 아닌 /root/.ssh에서 키를 찾습니다. 키가 해당 위치에 없으면 SSH는 Permission denied (publickey)을 출력하고, rsync는 rsync: connection unexpectedly closed을 출력하며 0이 아닌 종료 코드로 종료됩니다. 키 경로는 전체 경로로 작성하십시오. 경로를 수정한 후에도 인증 메시지가 계속 나타난다면, publickey 실패의 원인 목록을 확인하고 새 서버의 디렉터리 권한을 점검해야 합니다.

소유권과 관련하여 한 가지 결정을 내려야 합니다. root 권한으로 실행할 때 rsync는 기본적으로 이름을 기준으로 소유자와 그룹을 매핑합니다. 따라서 기존 서버의 www-data 소유 파일은 숫자 UID(사용자 ID)가 다르더라도 새 서버에서 www-data 소유가 됩니다. 이는 서버 재구축 시 의도한 결과입니다. --numeric-ids는 대상 서버에 계정이 존재하지 않는 파일 시스템을 복사할 때만 추가하십시오. 이 경우 ls -ln로 결과를 확인해야 합니다. 일치하는 계정이 없는 UID 소유 파일은 숫자 그대로 표시되며, 해당 파일을 읽는 모든 서비스는 접근이 거부됩니다.

기존 서버가 여전히 트래픽을 처리하는 동안 며칠 앞서 대량 복사를 수행하십시오. rsync는 변경된 부분만 전송하므로 두 번째 실행부터는 몇 시간 대신 몇 분 내에 완료됩니다. 전환 작업(cutover) 창 내에서 수행하는 마지막 복사에는 --delete을 추가하여 기존 서버에서 삭제된 파일이 새 서버에서도 삭제되도록 합니다.

# old server: final pass, inside the window, after the app has stopped writing
sudo rsync -aHAX --delete --info=progress2 \
  -e 'ssh -i /root/.ssh/id_ed25519' \
  /srv/app/ deploy@203.0.113.20:/srv/app/

--delete은 소스에서 삭제된 파일을 대상에서도 삭제하므로, 소스 경로를 잘못 지정하고 --delete을 사용하면 대상 디렉터리가 비워질 수 있습니다. 매번 실행할 때마다 반드시 --dry-run를 사용하여 먼저 테스트하십시오. 노트북의 SSH 세션이 끊기면 긴 전송 작업도 중단되므로, 기존 서버에서 tmux 또는 screen 내부에서 실행하십시오. 기존 서버가 여전히 사용자를 서비스 중인 상태에서 복사 작업으로 인해 네트워크 대역폭이 포화된다면 --bwlimit=20M를 추가하십시오.

데이터베이스 이전 방법: 네이티브 덤프

데이터베이스는 파일 디렉터리처럼 보일지라도 단순한 파일의 집합이 아닙니다. 데이터베이스는 파일, 메모리 내 상태, 그리고 쓰기 전용 로그(write-ahead log)의 조합이며, 데이터베이스 자체가 정의하는 특정 시점에만 일관성을 유지합니다. 따라서 반드시 전용 도구를 사용해야 합니다.

PostgreSQL은 역할(role)이 클러스터 전체에 적용되고 pg_dump에는 포함되지 않으므로 두 번의 덤프가 필요합니다.

# old server
sudo -u postgres pg_dumpall --globals-only -f /var/backups/globals.sql
sudo -u postgres pg_dump -Fc -f /var/backups/appdb.dump appdb
# new server: restore in this order
sudo -u postgres psql -f /var/backups/globals.sql
sudo -u postgres createdb -O appuser appdb
sudo -u postgres pg_restore -d appdb --no-owner /var/backups/appdb.dump

globals.sql를 생략하면 모든 테이블은 복원되지만, GRANT 문이 존재하지 않는 사용자를 참조하기 때문에 어떤 애플리케이션 역할로도 데이터를 읽을 수 없게 됩니다. -Fc은 커스텀 아카이브 형식으로 기록하며, 이는 pg_restore로만 읽을 수 있고 나중에 특정 테이블만 선택하여 복원할 수 있게 해줍니다. 동일한 메이저 버전이나 더 높은 버전으로 복원하십시오. 17에서 16으로 되돌리는 것과 같은 하위 버전으로의 복원은 지원되지 않으며, pg_restore은 파일 헤더에서 지원되지 않는 버전 오류를 발생시키며 아무것도 기록하기 전에 작업을 거부합니다.

MySQL과 MariaDB는 기본값이 아닌 4가지 옵션을 포함한 하나의 명령어를 사용합니다.

# old server
sudo mysqldump --single-transaction --routines --triggers --events \
  --databases appdb > /var/backups/appdb.sql
# new server
sudo mysql < /var/backups/appdb.sql

--single-transaction는 쓰기 작업을 차단하지 않고 일관된 스냅샷을 생성하지만, InnoDB 테이블에만 적용됩니다. 동일한 데이터베이스 내의 MyISAM 테이블은 이러한 보장 없이 복사되므로, 덤프를 신뢰하기 전에 스토리지 엔진을 확인하십시오. --routines, --triggers, --events는 기본적으로 비활성화되어 있습니다. 즉, 일반적인 덤프를 사용하면 데이터는 복원되지만 저장 프로시저와 스케줄된 이벤트는 누락됩니다. 데이터베이스 사용자와 권한은 mysql 시스템 데이터베이스에 저장되는데, --databases appdb 덤프는 이 영역을 건드리지 않으므로 새로운 서버에서 CREATE USERGRANT을 사용하여 직접 다시 생성해야 합니다. MariaDB 11은 mariadb-dump과 동일한 도구를 제공하며 mysqldump을 심볼릭 링크로 유지하므로, 2026년 8월 기준으로 두 이름 모두 사용할 수 있습니다.

SQLite는 단일 파일이며, 애플리케이션이 쓰기 작업을 수행하는 중에 복사하면 파일이 손상될 수 있습니다. SQLite에는 다음과 같은 안전한 이전 경로가 있습니다.

# old server
sqlite3 /var/lib/app/app.db ".backup '/var/backups/app.db'"

어떤 엔진을 사용하든 덤프를 신뢰하기 전에 반드시 내용을 확인하십시오. 디스크 용량 부족으로 인해 중간에 중단된 덤프 파일도 복원 과정에서는 오류를 발생시키지 않으며, 파일이 잘린 지점까지만 복원됩니다.

# old server: a complete mysqldump ends with a line reading "-- Dump completed on ..."
tail -n 3 /var/backups/appdb.sql

# new server: after restore, count rows in a table whose size you know
sudo mysql -e 'SELECT COUNT(*) FROM appdb.orders;'

실행 중인 데이터베이스를 rsync로 복사할 수 없는 이유

rsync는 파일을 하나씩 복사합니다. 실행 중인 데이터베이스는 여러 파일에 동시에 쓰기 작업을 수행하므로, rsync가 마지막 파일에 도달할 때쯤이면 첫 번째 파일은 이미 구버전이 됩니다. 이렇게 복사된 데이터는 서로 다른 시점의 페이지들을 포함하게 되며, 이는 데이터베이스가 실제로 존재한 적 없는 상태입니다. 그 결과 서버가 시작을 거부하거나, 더 심각하게는 일주일 동안은 정상적으로 작동하다가 손상된 페이지를 참조하는 쿼리가 실행되는 순간 장애가 발생할 수 있습니다. 그 사이에는 어떠한 경고도 없습니다.

데이터베이스 파일 자체를 옮기는 안전한 방법은 두 가지입니다. 데이터베이스를 중지하고 복사한 뒤 다시 시작하는 방법은 정확하고 간단하지만, 복사하는 시간만큼의 다운타임이 발생합니다. 다른 방법은 실행 중인 서버의 물리적 복사를 위해 설계된 도구를 사용하는 것입니다. PostgreSQL의 경우 pg_basebackup를 사용하며, 이는 서버와 조율하여 일관성 있는 복사본을 생성합니다.

# new server: pull a physical copy from the old one
pg_basebackup -h 198.51.100.10 -U replicator -D /var/lib/postgresql/16/main -X stream -P

이 방법은 REPLICATION 속성을 가진 역할과 이전 서버에 일치하는 pg_hba.conf 항목이 필요하므로, 덤프보다 설정이 복잡합니다. 데이터베이스 용량이 커서 덤프 및 복원 작업이 허용된 점검 시간 내에 완료되지 않을 때 유용합니다. 일반적인 단일 서버 마이그레이션에서는 덤프를 사용하는 것이 더 효율적입니다.

전환 후가 아닌 전환 전에 인증서를 재구축하십시오

TLS 인증서는 IP 주소가 아닌 도메인 이름에 종속되므로, 인증서 파일 자체는 문제없이 이동할 수 있습니다. 깔끔하게 이동되지 않는 것은 갱신 과정입니다. Certbot의 기본 HTTP-01 챌린지는 인증 기관이 인증 대상 도메인의 80번 포트로 파일을 가져오도록 요청합니다. DNS가 새 서버를 가리키기 전까지는 해당 요청이 이전 서버로 전달되므로 새 서버의 갱신은 실패하게 됩니다.

첫 번째 방법은 기존 인증서와 그 갱신 상태를 복사하는 것입니다. 인증서는 어느 서버에 있든 만료일까지 유효합니다.

# old server
sudo rsync -aHAX -e 'ssh -i /root/.ssh/id_ed25519' \
  /etc/letsencrypt/ root@203.0.113.20:/etc/letsencrypt/

/etc/letsencrypt/renewal/ 하위의 각 파일은 해당 인증서를 발급한 인증 플러그인을 명시합니다. 따라서 새 서버에도 동일한 플러그인(예: python3-certbot-nginx)을 설치해야 합니다. 그렇지 않으면 첫 갱신 시 알 수 없는 인증자라는 메시지와 함께 실패합니다. 갱신 기능에 의존하기 전에 먼저 작동 여부를 확인하십시오.

# new server, after DNS has moved
sudo certbot renew --dry-run

두 번째 방법은 DNS-01 챌린지를 사용하여 새 서버에서 새 인증서를 발급받는 것입니다. 이는 TXT 레코드를 통해 제어권을 증명하며 80번 포트를 전혀 사용하지 않습니다. 이 방법은 마이그레이션 전, 즉 도메인 이름이 여전히 이전 서버로 연결되는 상태에서도 작동하므로 DNS 제공업체를 자동화할 수 있다면 더 깔끔한 선택입니다. DNS-01 챌린지로 인증서 발급하기에서 플러그인 및 자격 증명 설정 방법을 다룹니다.

어떤 방법을 선택하든 DNS를 변경하지 않은 상태에서 새 서버가 실제로 무엇을 제공하는지 확인하십시오.

# your laptop
echo | openssl s_client -connect 203.0.113.20:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -dates -issuer

-servername는 SNI(Server Name Indication)를 전송하며, 이는 웹 서버가 올바른 가상 호스트를 선택하게 만드는 요소입니다. 이를 생략하면 해당 IP의 기본 인증서를 받게 되며, 실제 문제는 아니지만 인증서 불일치 오류가 발생한 것처럼 보일 수 있습니다.

전환 며칠 전에 DNS TTL 낮추기

DNS는 신중하게 마이그레이션을 진행해도 문제가 발생할 수 있는 지점입니다. 지연 시간이 내장되어 있어 당일에 단축할 수 없기 때문입니다. A 레코드를 캐싱한 리졸버는 전달받은 TTL(time to live) 시간 동안 해당 레코드를 계속 제공합니다. 10분 전에 이전 값으로 레코드를 캐싱한 리졸버의 경우, 지금 TTL을 낮추더라도 아무런 효과가 없습니다. 해당 리졸버는 이전 TTL이 만료될 때까지 이전 값을 유지하며, 그 이후에야 새로운 짧은 TTL을 학습합니다. 따라서 전환 시점보다 최소한 이전 TTL 기간만큼 앞서서 TTL을 낮춰야 합니다. 하루 전이 가장 안전합니다. 이 과정이 생소하다면 레코드, 리졸버 및 캐싱에 대한 설명을 먼저 확인하십시오.

아래 수치는 TTL 자체를 계산한 결과이며 실제 측정값이 아닙니다.

ChartHow long a stale IP can survive, by record TTL
The data behind this chart
[
  {
    "label": "TTL 3600 s (common default)",
    "ttl_seconds": 3600,
    "worst_case_stale_minutes": 60
  },
  {
    "label": "TTL 900 s",
    "ttl_seconds": 900,
    "worst_case_stale_minutes": 15
  },
  {
    "label": "TTL 300 s (lowered for cutover)",
    "ttl_seconds": 300,
    "worst_case_stale_minutes": 5
  },
  {
    "label": "TTL 60 s (short window)",
    "ttl_seconds": 60,
    "worst_case_stale_minutes": 1
  }
]

TTL을 3600 초로 설정하여 게시된 A 레코드는 변경 후에도 최대 60 분 동안 사용자를 이전 IP로 보낼 수 있습니다. 이를 300 초로 낮추면 최악의 경우에도 5 분으로 단축됩니다. 이 수치는 보장값이 아니라 최소치로 간주해야 합니다. 일부 리졸버는 자체적인 최소 TTL을 적용하여 더 짧은 값을 무시하기도 하며, 일부 애플리케이션 런타임은 프로세스가 실행되는 동안 주소를 캐싱하므로 변경 전에 시작된 클라이언트는 재시작하기 전까지 다시 조회하지 않을 수 있습니다.

낮은 TTL이 적용되었는지 확인할 때는 자신의 캐시가 아닌 권한 있는 네임 서버의 응답을 확인하십시오.

# your laptop: ask the zone's own nameserver, so no cache is involved
dig +short NS example.com
dig +noall +answer @$(dig +short NS example.com | head -n1) example.com A

해당 응답 라인의 두 번째 필드가 초 단위의 TTL입니다. 그런 다음 사람들이 자주 잊는 레코드들을 점검하십시오. 이전 서버에 IPv6가 있었다면 AAAA 레코드, CNAME이 아닌 별도의 A 레코드인 경우의 www 이름, 서버 자체를 가리키는 MX 레코드, 이전 IP가 나열된 SPF 레코드, 그리고 새 주소의 역방향 DNS(PTR) 레코드가 포함됩니다. 서버가 메일을 발송한다면 전환 전에 제공업체의 제어판을 통해 PTR 레코드를 설정하십시오. 수신 메일 서버는 이를 확인하며, 다른 모든 설정이 정상이어도 PTR 레코드가 없으면 몇 시간 뒤 메일이 거부될 수 있습니다.

DNS를 변경하기 전에 IP 주소로 새 서버 확인하기

DNS가 여전히 이전 서버를 가리키고 있는 상태에서 새 서버의 전체 애플리케이션을 테스트할 수 있습니다. 요청 한 번에 대해 이름 조회를 재정의하십시오.

# your laptop: force one name to the new IP, for this request only
curl -sS --resolve example.com:443:203.0.113.20 \
  -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://example.com/

--resolve은 연결 대상만 변경합니다. TLS 인증서는 여전히 실제 도메인 이름으로 검증되므로, 이 방법은 서비스뿐만 아니라 인증서까지 함께 검증합니다. %{ssl_verify_result}은 체인이 검증되었을 때 0을 출력합니다.

브라우저에서 사이트를 클릭하며 확인하려면, 노트북의 /etc/hosts 파일이나 Windows의 C:\Windows\System32\drivers\etc\hosts 파일에 한 줄을 추가하여 전체 시스템의 이름 조회를 재정의하십시오.

203.0.113.20 example.com www.example.com

그런 다음 사용자가 이용하는 방식 그대로 애플리케이션을 확인하십시오. 로그인하고, 데이터베이스에서 데이터를 읽어오는 페이지를 로드하십시오. 데이터를 쓰는 폼을 제출하십시오. 파일을 업로드하여 디스크에 정상적으로 저장되는지 확인하십시오. 이메일을 발송하는 기능을 실행하고 메일이 도착하는지 확인하십시오. 새로운 IP 주소에서 나가는 SMTP는 종종 예상치 못한 문제를 일으키기 때문입니다. 확인이 끝나면 즉시 hosts 파일의 내용을 삭제하십시오. 해당 내용을 그대로 두면 다른 사람들에게는 완벽하게 보이는 사이트를 혼자서 한 시간 동안 디버깅하게 될 것입니다.

단계별 전환 절차

  1. 며칠 전: TTL 값을 낮추고, 대량 rsync를 실행하며, 새 서버를 구축한 뒤 hosts 파일 재정의를 통해 테스트합니다.
  2. 당일, 작업 시간 전: 모든 서드파티 허용 목록(allowlist)에 새 IP를 추가하고, 새 서버의 백업 작업이 구성되어 저장소로 올바르게 지정되었는지 확인합니다.
  3. 작업 시작: 기존 서버의 애플리케이션을 유지보수 모드로 전환하여 쓰기 요청을 차단합니다.
  4. 최종 데이터베이스 덤프를 생성한 뒤, --delete 옵션으로 마지막 rsync를 실행합니다.
  5. 새 서버에 덤프를 복원하고 서비스를 시작합니다.
  6. --resolve 및 hosts 파일 재정의를 통해 실제 쓰기 작업을 포함한 테스트를 다시 수행합니다.
  7. A 및 AAAA 레코드를 새 IP로 변경합니다.
  8. 두 서버를 모두 모니터링합니다. 기존 서버의 접근 로그를 통해 여전히 접속하는 사용자를 확인하며, TTL이 지남에 따라 이 수치는 0에 가까워져야 합니다.
  9. 유지보수 페이지를 제거합니다.
  10. 기존 서버를 최소 일주일 동안은 그대로 유지하고 건드리지 않습니다.

유지보수 모드 단계는 사람들이 흔히 건너뛰지만, 사용자를 보호하는 핵심 과정입니다. 새 데이터베이스가 쓰기 작업을 수락한 이후에 롤백을 수행하려면 해당 데이터를 잃거나, 새 데이터베이스를 덤프하여 다시 기존 서버로 로드해야 합니다. 몇 분간의 읽기 전용 상태를 유지하는 비용은 매우 저렴합니다. 반면 두 데이터베이스 모두에 쓰기가 발생하면 며칠 동안 수동으로 데이터를 대조해야 하는 상황이 벌어집니다.

롤백 계획

롤백은 단 하나의 동작, 즉 DNS 레코드를 198.51.100.10으로 되돌리는 것입니다. 이 작업이 가능한 이유는 앞서 수행한 네 가지 조치 덕분입니다.

  • 기존 서버는 여전히 서비스가 실행 중이며 데이터도 온전합니다. 해당 서버의 쓰기 작업만 중단했을 뿐, 폐기하지 않았기 때문입니다.
  • TTL 값이 여전히 낮게 설정되어 있어, 전환 과정만큼이나 빠르게 원상 복구가 가능합니다.
  • 타사 허용 목록(allowlist)에서 기존 IP를 교체하는 대신 새 IP를 추가했습니다. 기존 주소를 삭제하면 결제 게이트웨이에서 롤백 경로가 차단됩니다.
  • 새 서버에서 발생한 쓰기 작업은 오직 테스트 트랜잭션뿐이므로, 식별할 수 없는 데이터 변경은 없습니다.

작업 창이 열리기 전에 롤백을 수행할 기준을 결정하십시오. 다음 두 가지 기준이면 충분합니다. 정해진 시간 내에 진단할 수 없는 오류가 발생하거나, 데이터 손실이 조금이라도 감지되는 경우입니다. 사전에 기준을 문서화해 두어야 10분짜리 장애를 긴 시간의 장애로 만드는 무의미한 추측의 시간을 방지할 수 있습니다.

마이그레이션 성공 여부 확인

사이트가 로드된다고 해서 마이그레이션이 끝난 것은 아닙니다. 나중에야 문제가 드러나는 항목들을 점검해야 합니다.

# new server
systemctl --failed
journalctl -p err -b --no-pager | tail -n 40
sudo certbot certificates
sudo systemctl list-timers --all --no-pager

systemctl --failed 보고가 0 loaded units listed로 나타나는 것이 정상입니다. certbot certificates은 예상하는 만료 날짜를 표시해야 하며, list-timers은 인벤토리에 있는 모든 예약 작업의 다음 실행 시간을 공백 없이 정확히 보여주어야 합니다.

그다음, 모니터링을 수행하면서 새 서버를 의도적으로 한 번 재부팅하십시오. 누군가 수동으로 시작만 하고 활성화하지 않은 서비스는 새벽 3시에 예기치 않은 재부팅이 발생하기 전까지는 완벽하게 작동하는 것처럼 보입니다.

# new server
sudo reboot

# your laptop, once it comes back
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/

애플리케이션이 컨테이너에서 실행되는 경우, 이 함정은 다른 형태로 나타납니다. compose 스택은 재부팅 후 다시 시작되려면 명시적인 재시작 정책이 필요하기 때문입니다.

마지막 점검은 미루기 가장 쉽지만 가장 중요한 작업인 백업 작업입니다. 백업되지 않은 서버로 끝나는 마이그레이션은 한 가지 위험을 다른 위험으로 교체한 것에 불과합니다. 새 서버에서 수동으로 백업을 실행한 다음, 백업본에서 파일 하나를 임시 디렉터리로 복원해 보십시오. 실제로 복원 테스트를 거친 restic 저장소만이 실제 상황에서 도움이 됩니다. 구형 서버와 새 서버를 일주일 동안 병행 운영한다면, 각 호스트에 접근하고 설정하는 일관된 방식을 유지하여 두 서버의 설정이 서로 어긋나지 않도록 해야 합니다.

컷오버 이후: 기존 서버 및 마지막 작업

기존 서버를 1~2주간 유지하십시오. 해지하려던 플랜의 한 달 치 비용이 발생하지만, 이는 유일한 롤백 수단입니다. 그 후 나머지 항목을 정리하십시오.

  • 새 서버에서 ~/.ssh/config 내의 동일한 호스트 이름을 재사용하면 첫 연결 시 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!가 발생합니다. 해당 이름이 이제 다른 호스트 키로 응답하기 때문입니다. 변경 사유가 확실할 때만 ssh-keygen -R example.com을 사용하여 오래된 항목을 삭제하십시오. 동일한 경고가 중간자 공격(interception attack)의 징후일 수 있으므로 습관적으로 삭제해서는 안 됩니다. 마이그레이션은 어떤 키가 어디에 접근할 수 있는지 검토하기 좋은 시기이며, 이에 관한 내용은 소규모 서버군에서의 SSH 키 관리를 참조하십시오.
  • 기존 서버의 최종 스냅샷이나 백업을 하나 생성하여 기존 제공업체가 아닌 다른 곳에 저장하십시오.
  • 모니터링 체크, SPF 레코드, 타사 허용 목록에서 기존 IP를 순서대로, 그리고 가장 마지막에 제거하십시오.
  • 최종 백업본이 다른 곳에서 정상적으로 읽히는지 확인한 후에만 기존 플랜을 해지하십시오.

FAQ

새 VPS로 서버를 마이그레이션하는 데 얼마나 걸립니까?

사용자가 체감하는 서비스 중단 시간은 보통 최종 데이터베이스 덤프, 최종 rsync 작업, 서비스 시작 단계에 국한되므로 소규모 애플리케이션의 경우 10분에서 30분 정도 소요됩니다. 전체 일정은 더 길어지는데, 전환 전에 DNS TTL을 최소한 기존 TTL 기간만큼 낮춰야 하며 하루 정도 여유를 두는 것이 안전하기 때문입니다. 대용량 데이터 복사 작업도 며칠 전에 미리 계획하십시오. 이 작업은 운영 중인 서버를 대상으로 실행되며, 이후 반복 실행 시 마지막 작업 이후 변경된 데이터만 전송합니다.

실행 중인 MySQL이나 PostgreSQL 데이터베이스를 덤프 대신 rsync로 복사해도 됩니까?

아니요. rsync는 파일을 하나씩 복사하지만 데이터베이스는 여러 파일에 동시에 쓰기 작업을 수행하므로, 복사본에는 서로 다른 시점의 데이터 페이지가 섞여 있어 데이터베이스가 한 번도 도달한 적 없는 상태가 됩니다. 이 경우 데이터베이스가 시작되지 않거나, 시작되더라도 나중에 손상된 페이지에 접근하는 쿼리가 발생하면 실패할 수 있습니다. pg_dumppg_dumpall --globals-only를 사용하거나, mysqldump --single-transaction을 사용하거나, 아니면 데이터베이스를 먼저 중지한 후 파일을 복사하십시오. 대규모 PostgreSQL 클러스터의 경우 pg_basebackup를 사용하면 실행 중인 서버의 일관된 물리적 복사본을 만들 수 있습니다.

DNS를 변경하기 전에 새 VPS를 어떻게 테스트합니까?

본인 컴퓨터의 이름 해석(name lookup) 설정을 재정의하십시오. 단일 요청의 경우 curl --resolve example.com:443:203.0.113.20 https://example.com/를 사용하면 실제 도메인 이름으로 인증서를 확인하면서 연결 대상만 새 IP로 보낼 수 있습니다. 브라우저 테스트를 위해서는 노트북의 /etc/hosts 파일에 203.0.113.20 example.com을 추가한 뒤 로그인, 데이터베이스 읽기, 폼 쓰기, 파일 업로드 과정을 확인하고 해당 줄을 삭제하십시오. 인증서만 검사하려면 openssl s_client -connect 203.0.113.20:443 -servername example.com을 실행하십시오.

TTL은 얼마로 설정해야 하며, 언제 낮춰야 합니까?

A 및 AAAA 레코드를 300초로 낮추고, 전환 시점으로부터 최소한 기존 TTL 기간만큼 앞서서 수행하십시오. 변경 전에 레코드를 캐시한 리졸버는 기존 TTL이 만료될 때까지 이전 값을 유지하므로, 기존 TTL이 86400이었다면 1시간 전에 낮추는 것은 아무런 효과가 없습니다. 마이그레이션 후 며칠이 지나 기존 서버의 접근 로그가 조용해지면 다시 정상 값으로 올리십시오.

TLS 인증서를 복사해야 합니까, 아니면 새 서버에서 새로 발급받아야 합니까?

둘 다 가능합니다. /etc/letsencrypt/를 복사하면 기존 만료일까지 인증서가 유효하게 유지되지만, 새 서버에도 동일한 certbot 인증 플러그인을 설치해야 합니다. 그렇지 않으면 첫 갱신 시 실패하므로 DNS 전환 후 certbot renew --dry-run을 실행하여 확인하십시오. DNS-01 챌린지를 사용할 수 있다면 새로 발급받는 것이 더 깔끔합니다. 이는 TXT 레코드를 통해 제어권을 증명하므로 DNS가 새 서버를 가리키기 전에도 작동하기 때문입니다. HTTP-01 챌린지는 검증 요청이 기존 서버로 전달되므로 DNS를 옮기기 전까지는 새 서버에서 사용할 수 없습니다.