[AWS Lightsail] Bitnami 지원 종료와 PHP 보안 취약점, 결국 WordPress Multisite 서버를 통째로 이전했다 (PHP 7.4 → 8.2)
2026년 10월 25일. 오랜만에 AWS Lightsail 콘솔에 접속했다가 꽤 섬뜩한 경고를 마주했다.
PHP update recommended.
그 아래에는 심각한 PHP 보안 취약점인 CVE-2026-109618, CVE-2026-109619가 표시되어 있었다.
사실 예고된 일이었다.
올해 5월, AWS Lightsail에서 Bitnami가 패키징한 블루프린트의 지원이 종료된다는 공지를 확인했다. 새로운 업데이트가 제공되지 않는다는 사실도 알고 있었다. 그런데 이미 블로그는 잘 돌아가고 있었고, 서버를 새로 만들고 데이터를 옮기는 일이 영 귀찮았다.
“지금 잘 되는데 굳이 건드릴 필요가 있을까?”
그렇게 미루고 미루다 결국 이번 PHP 보안 사태를 맞이했다.
확인해 보니 내 블로그에서 사용하던 PHP 버전은 무려 7.4.28. 이미 지원이 종료된 지 오래된 버전이었다.
단순히 PHP만 업데이트하는 방법도 있었지만, 이번 기회에 묵혀뒀던 기술 부채를 청산하기로 했다. 기존 Bitnami 환경에서 벗어나 AWS Lightsail이 직접 제공하는 WordPress 블루프린트로 서버를 통째로 이관하기로 한 것이다.
생각보다 만만한 작업은 아니었다.
데이터베이스 백업부터 파일 권한 오류, SSH 인증 실패, PHP 버전 호환성 문제, SSL 인증서 설치까지. 하나를 해결하면 또 다른 문제가 나타났다.
그래도 결국 기존 글과 이미지, 관리자 계정과 설정을 유지하면서 PHP 8.2.35 기반의 새 서버로 이전하는 데 성공했다.
이번 글은 그 과정에서 실제로 실행한 명령어와 마주쳤던 오류, 그리고 해결 방법을 기록으로 남긴 것이다.
나처럼 Bitnami WordPress를 운영하면서 이관을 미루고 있는 누군가에게 도움이 되었으면 좋겠다.
1. 이관을 결심한 이유
1.1 Bitnami 블루프린트 지원 종료
AWS Lightsail에서는 WordPress를 손쉽게 설치할 수 있도록 Bitnami가 패키징한 블루프린트를 제공해 왔다.
나도 2022년 4월에 이 방식으로 블로그를 구축했다.
그런데 AWS는 2026년 5월 19일 이후 Bitnami 패키징 블루프린트의 새로운 버전 업데이트를 중단했고, 2026년 11월 19일부터는 관련 블루프린트로 신규 인스턴스를 생성하는 기능도 종료할 예정이라고 발표했다.
물론 기존 인스턴스가 갑자기 중단되는 것은 아니다. 실제로 내 블로그도 아무 문제 없이 돌아가고 있었다.
문제는 돌아간다는 것과 안전하다는 것은 전혀 다른 이야기라는 점이다.
서버는 멀쩡해 보여도 PHP와 Apache, 운영체제와 각종 라이브러리는 계속 오래되어 간다.
1.2 결정타는 PHP 보안 취약점
2026년 10월 21일, AWS에서 PHP 웹 런타임의 심각한 보안 취약점에 관한 권고를 발표했다.
해당 취약점은 특정 환경에서 인증되지 않은 외부 사용자가 조작된 요청을 통해 서버 코드를 실행하거나 데이터에 접근할 수 있는 문제였다.
AWS가 안내한 PHP 수정 버전은 다음과 같다.
| PHP 계열 | 최초 수정 버전 |
|---|---|
| PHP 8.2 | 8.2.35 |
| PHP 8.3 | 8.3.36 |
| PHP 8.4 | 8.4.27 |
| PHP 8.5 | 8.5.12 |
그렇다면 내가 쓰던 PHP 7.4는?
이미 지원 종료된 계열로, 이번 문제에 대한 공식 상위 버전 패치가 제공되지 않는다. 결국 지원 중인 PHP 버전으로 이전해야 했다.
참고로 기존 Bitnami는 /opt/bitnami/php에 자체 PHP를 설치하는 구조라서 일반적인 apt upgrade만으로 WordPress PHP를 업데이트할 수 있는 것도 아니었다.
귀찮다고 미뤄왔던 서버 이전이 이제는 선택이 아니라 필수가 된 셈이다.
2. 기존 서버와 새 서버 비교
이번 이전은 다음 환경에서 진행했다.
| 구분 | 기존 서버 | 새 서버 |
|---|---|---|
| 인스턴스 | LetsGoMinBlog | LetsGoMinBlog-v2 |
| 블루프린트 | Bitnami WordPress Multisite | Lightsail WordPress Multisite |
| PHP | 7.4.28 | 8.2.35 |
| WordPress | 7.0.7 | 7.1.3 |
| RAM | 512MB | 1GB |
| CPU | 2 vCPU | 2 vCPU |
| SSD | 20GB | 40GB |
| 운영체제 | Bitnami 기반 환경 | Debian 12 |
| 웹서버 | Bitnami Apache | Debian Apache |
| 메인 도메인 | letsgomin.com | letsgomin.com |
참고로 내 블로그는 일반적인 WordPress가 아니라 WordPress Multisite 구조다.
메인 블로그 외에도 예전에 만들어 두었던 economin, programmin이라는 하위 사이트가 있었다. 현재는 사용하지 않지만, 데이터베이스 안에는 그대로 남아 있었다.
따라서 일반적인 WordPress 게시글 내보내기만으로 이전하기보다는 DB 전체와 wp-content를 함께 복사하는 방식을 선택했다.
이렇게 해야 글, 댓글, 사용자, 플러그인 설정, 미디어 파일과 멀티사이트 구조를 최대한 그대로 유지할 수 있다.
3. 첫 번째 단계: 기존 서버 백업
서버 이전에서 가장 중요한 것은 새로운 서버를 만드는 일이 아니다.
문제가 생겼을 때 원래 상태로 되돌아갈 수 있도록 준비하는 것이다.
3.1 Lightsail 스냅샷 생성
먼저 기존 인스턴스의 관리 화면에서 스냅샷을 생성했다.
Lightsail → 기존 인스턴스 → 스냅샷 → 수동 스냅샷 생성
스냅샷은 서버의 디스크 상태를 보관하는 백업이다. 이번 마이그레이션에 실패하더라도 기존 상태를 복원하기 위한 보험인 셈이다.
단, 스냅샷만 믿고 진행하지 않았다. 데이터베이스와 파일도 별도로 추출했다.
3.2 WordPress 콘텐츠의 실제 위치 확인
기존 서버에 SSH로 접속해 WordPress 경로부터 확인했다.
ls -ld /opt/bitnami/wordpress
sudo du -sh /opt/bitnami/wordpress/wp-content
그런데 이상하게도 wp-content의 크기가 0으로 표시됐다.
수년간 쓴 블로그인데 콘텐츠가 0이라니?
알고 보니 Bitnami는 wp-content를 심볼릭 링크로 연결해 두고 있었다.
/opt/bitnami/wordpress/wp-content
-> /bitnami/wordpress/wp-content
그래서 다음 명령어로 실제 저장 위치와 크기를 확인했다.
readlink -f /opt/bitnami/wordpress/wp-content
sudo du -shL /opt/bitnami/wordpress/wp-content
결과는 977M.
데이터가 사라진 게 아니라, 처음 실행한 du가 심볼릭 링크 자체만 측정했던 것이다.
여기서 -L 옵션은 심볼릭 링크가 가리키는 실제 위치까지 따라가겠다는 의미다.
Bitnami를 사용한다면 파일을 백업할 때 실제 저장 경로를 반드시 확인하자.
3.3 데이터베이스 크기 확인
WP-CLI를 이용해 데이터베이스 크기를 확인했다.
sudo wp --path=/opt/bitnami/wordpress db size
결과는 약 58MB였다.
디스크 사용량도 확인했다.
df -h /
총 20GB 중 4.6GB를 사용하고 있었고, 약 14GB의 여유 공간이 남아 있었다.
백업 파일을 만들기에는 충분했다.
3.4 멀티사이트 목록 확인
sudo wp --path=/opt/bitnami/wordpress site list
기존 멀티사이트의 등록 정보를 확인할 수 있었다.
blog_id domain
1 letsgomin.com
4 economin.letsgomin.com
6 programmin.letsgomin.com
참고로 Bitnami WP-CLI는 일반 사용자로 실행하면 관리자 권한을 요구하는 경우가 있다.
실제로 sudo 없이 실행했다가 오류가 발생했다. 기존 Bitnami 환경에서는 sudo wp로 실행하니 해결됐다.
4. 데이터베이스와 파일 백업
4.1 백업 디렉터리 만들기
sudo mkdir -p /home/bitnami/migration
sudo chmod 700 /home/bitnami/migration
이 폴더에 SQL, WordPress 콘텐츠, 설정 파일을 보관했다.
여기서 첫 번째 삽질이 시작됐다.
4.2 mysqldump Permission denied 오류
처음에는 아래와 같이 DB 백업을 시도했다.
sudo wp --path=/opt/bitnami/wordpress db export \
/home/bitnami/migration/wordpress-all.sql \
--single-transaction
그런데 다음 오류가 발생했다.
mysqldump: Can't create/write to file
(Errcode: 13 "Permission denied")
분명 sudo로 실행했는데 권한이 없다니?
원인은 백업 디렉터리의 소유자와 권한이었다.
앞서 sudo mkdir로 만든 폴더의 소유자는 root, 권한은 700이었다. 하지만 Bitnami의 WP-CLI는 내부 명령을 daemon 사용자로 실행하기도 한다.
따라서 실제 SQL 파일을 생성하려는 프로세스는 해당 디렉터리에 쓸 수 없었던 것이다.
해결 방법은 간단했다. daemon 사용자가 접근할 수 있는 임시 디렉터리에서 먼저 백업했다.
sudo install -d -m 700 -o daemon -g daemon \
/var/tmp/wp-db-backup
그리고 다시 DB를 추출했다.
sudo wp --path=/opt/bitnami/wordpress db export \
/var/tmp/wp-db-backup/wordpress-all.sql \
--single-transaction
Success: Exported to ...
드디어 성공!
백업된 파일을 원래 디렉터리로 이동했다.
sudo mv /var/tmp/wp-db-backup/wordpress-all.sql \
/home/bitnami/migration/
sudo chmod 600 \
/home/bitnami/migration/wordpress-all.sql
최종 SQL 파일 크기는 약 29MB였다.
실제 데이터베이스 사용량 58MB와 SQL 파일 크기가 다른 것은 이상한 현상이 아니다. DB 내부 저장 용량과 논리적인 SQL 덤프 크기는 다를 수 있기 때문이다.
4.3 WordPress 콘텐츠 백업
sudo tar -czf \
/home/bitnami/migration/wp-content.tar.gz \
-C /bitnami/wordpress wp-content
이미지, 테마, 플러그인 등 기존 콘텐츠를 압축했다.
압축 후 크기는 약 643MB였다.
4.4 WordPress 설정 파일 백업
sudo cp -L /opt/bitnami/wordpress/wp-config.php \
/home/bitnami/migration/wp-config.php
-L은 원본이 심볼릭 링크일 경우 링크 대상 파일을 복사하는 옵션이다.
단, wp-config.php에는 DB 사용자와 비밀번호 등 민감한 설정이 포함되므로 절대 공개하면 안 된다.
그리고 중요한 점이 하나 더 있다.
이 파일은 복원할 설정을 참고하기 위한 것이지, 새 서버에 그대로 덮어쓰기 위한 파일은 아니다.
새 서버는 DB 이름과 설치 경로 등이 다르기 때문이다.
4.5 chmod 와일드카드 오류
백업 파일의 권한을 변경하려고 다음 명령을 실행했다.
sudo chmod 600 /home/bitnami/migration/*
그런데 또 오류가 발생했다.
chmod: cannot access ... No such file or directory
원인은 셸의 와일드카드 확장이다.
*는 sudo 명령이 실행되기 전에 현재 로그인한 사용자의 셸에서 처리된다. 그런데 백업 디렉터리는 root만 접근할 수 있었기 때문에 파일 목록을 펼칠 수 없었던 것이다.
결국 파일명을 직접 지정해 해결했다.
sudo chmod 600 \
/home/bitnami/migration/wordpress-all.sql \
/home/bitnami/migration/wp-content.tar.gz \
/home/bitnami/migration/wp-config.php
이어서 압축 파일의 무결성을 검사했다.
sudo tar -tzf \
/home/bitnami/migration/wp-content.tar.gz \
> /dev/null && echo "Archive OK"
Archive OK
최종 백업은 다음과 같다.
| 파일 | 크기 | 내용 |
|---|---|---|
| wordpress-all.sql | 29MB | 게시글, 댓글, 사용자, 설정 |
| wp-content.tar.gz | 643MB | 이미지, 테마, 플러그인 |
| wp-config.php | 4.2KB | WordPress 설정 |
총 672MB.
이제 기존 블로그를 새 서버로 옮길 준비가 끝났다.
5. 새로운 Lightsail 인스턴스 생성
AWS Lightsail에서 새 인스턴스를 생성했다.
Lightsail → 인스턴스 생성
설정은 다음과 같다.
- 플랫폼: Linux / Unix
- 블루프린트: WordPress Multisite
- 제공자: Lightsail
- 리전: 기존 서버와 동일
- RAM: 1GB
- SSD: 40GB
- 인스턴스 이름:
LetsGoMinBlog-v2
여기서 가장 중요한 것이 블루프린트 제공자다.
Bitnami가 아니라 Lightsail에서 제공하는 WordPress Multisite를 선택해야 한다.
일반 WordPress가 아닌 Multisite를 선택한 이유는 기존 네트워크 구성을 유지하기 위해서였다.
새 인스턴스 생성 후 SSH로 접속해 확인했다.
php -v
cat /etc/os-release
ls -ld /var/www/html
결과는 다음과 같았다.
PHP 8.2.35
Debian GNU/Linux 12 (bookworm)
/var/www/html
이번 PHP 취약점에 대한 수정 버전이 설치된 상태였다.
기존 Bitnami와 가장 큰 차이는 디렉터리 구조였다.
| 항목 | 기존 Bitnami | 새로운 Lightsail |
|---|---|---|
| WordPress | /opt/bitnami/wordpress | /var/www/html |
| 콘텐츠 | /bitnami/wordpress/wp-content | /var/www/html/wp-content |
| 주요 설정 | /opt/bitnami/wordpress/wp-config.php | /var/www/wp-config.php |
| SSH 사용자 | bitnami | admin |
따라서 기존 서버의 경로를 그대로 사용하면 안 된다.
이 차이 때문에 wp-config.php도 새 환경에 맞게 유지하면서 필요한 설정만 변경했다.
6. 서버 간 파일 전송 (SSH / SCP)
백업을 맥북에 다운로드했다가 다시 업로드하는 방법도 있었지만, 이번에는 두 Lightsail 인스턴스를 직접 연결했다.
같은 AWS 리전에 있었으므로 프라이빗 IP를 이용해 서버 간 파일을 전송할 수 있었다.
기존 서버와 새 서버에 별도로 SSH 세션을 열어두면 작업하기 편하다.
6.1 새 서버에서 SSH 키 생성
새 서버에서 실행한다.
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t ed25519 \
-f ~/.ssh/ls-migration \
-N "" \
-C "lightsail-migration"
생성된 파일 중 .pub는 공개키이고, 확장자가 없는 파일은 개인키다.
cat ~/.ssh/ls-migration.pub
여기서 출력되는 공개키를 기존 서버의 ~/.ssh/authorized_keys에 추가했다.
nano ~/.ssh/authorized_keys
기존 공개키는 삭제하지 않고 새로운 키를 한 줄로 추가한다.
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
주의할 점은 개인키를 기존 서버나 외부에 공유해서는 안 된다는 것이다.
6.2 SSH 연결 테스트
새 서버에서 기존 서버로 SSH 연결을 시험했다.
다음 IP는 당시 내 서버의 프라이빗 IP이므로 각자의 환경에 맞게 바꿔야 한다.
ssh -i ~/.ssh/ls-migration \
-o IdentitiesOnly=yes \
bitnami@172.26.6.115 \
"echo SSH_OK"
첫 연결 시 호스트 키 확인 메시지가 나왔다.
Are you sure you want to continue connecting?
호스트 키 지문을 기존 서버의 실제 지문과 비교한 후 수락했지만, 이번에는 다음 오류가 발생했다.
Permission denied (publickey)
네트워크 연결 문제가 아니라 SSH 인증 문제였다.
새 서버의 공개키가 기존 서버에 올바르게 등록되어 있는지, 그리고 authorized_keys의 소유권과 권한이 적절한지 확인했다.
기존 서버에서 다음과 같이 설정을 정리했다.
sudo chown bitnami:bitnami /home/bitnami/.ssh
sudo chown bitnami:bitnami /home/bitnami/.ssh/authorized_keys
sudo chmod 700 /home/bitnami/.ssh
sudo chmod 600 /home/bitnami/.ssh/authorized_keys
그리고 다시 연결했다.
SSH_OK
드디어 서버 간 인증 성공.
6.3 SCP로 백업 파일 복사
기존 백업 디렉터리를 bitnami 사용자가 읽을 수 있도록 권한을 맞춘 후, 새 서버에서 파일을 가져왔다.
scp -i ~/.ssh/ls-migration \
-o IdentitiesOnly=yes \
bitnami@172.26.6.115:/home/bitnami/migration/wordpress-all.sql \
bitnami@172.26.6.115:/home/bitnami/migration/wp-content.tar.gz \
bitnami@172.26.6.115:/home/bitnami/migration/wp-config.php \
/home/admin/migration/
전송 결과는 모두 100%.
약 672MB를 새 서버로 옮겼다.
6.4 SHA-256으로 파일 무결성 검증
복사가 끝났다고 무조건 성공한 것은 아니다.
파일 전송 중 내용이 손상됐을 가능성도 확인해야 한다.
그래서 SHA-256 해시를 비교했다.
기존 서버에서:
cd /home/bitnami/migration
sha256sum wordpress-all.sql wp-content.tar.gz wp-config.php
새 서버에서:
cd /home/admin/migration
sha256sum wordpress-all.sql wp-content.tar.gz wp-config.php
세 파일 모두 해시가 완전히 일치했다.
파일 크기가 같다는 것보다 훨씬 확실한 검증이다.
여기에 압축 파일 검사까지 다시 실행했다.
tar -tzf wp-content.tar.gz > /dev/null && echo "Archive OK"
Archive OK가 출력되었다.
이제 데이터를 복원할 차례다.
7. 새 서버에 WordPress Multisite 복원
7.1 새 서버의 초기 상태 백업
새 서버에도 기본 WordPress와 DB가 이미 설치되어 있었다.
복원 과정에서 문제가 생길 수 있으므로 먼저 새 서버의 초기 데이터를 백업했다.
cd /var/www/html
wp db export \
/home/admin/migration/new-server-before-restore.sql \
--single-transaction
설정 파일도 따로 보관했다.
sudo cp -a /var/www/wp-config.php \
/home/admin/migration/new-server-wp-config.php.bak
백업 없이 기존 SQL을 곧바로 덮어썼다면 복구가 더 어려워졌을 것이다.
7.2 기존 DB 가져오기
기존 서버의 DB 이름은 bitnami_wordpress였지만, 새 서버는 wordpress-db였다.
다행히 DB 이름이 다르더라도 WP-CLI의 wp db import는 현재 서버의 wp-config.php에 정의된 DB 접속 정보를 사용한다.
다만 두 서버 모두 테이블 접두사가 wp_라는 점은 확인했다.
실제 SQL을 가져오기 전에 DB 생성·전환 구문이 포함되어 있는지도 검사했다.
grep -Einm 5 \
'^[[:space:]]*(CREATE DATABASE|USE[[:space:]]|DROP DATABASE)' \
/home/admin/migration/wordpress-all.sql \
|| echo "No database-switch statements found"
이어서 Apache를 중지하고 DB를 복원했다.
sudo systemctl stop apache2
cd /var/www/html
wp db import /home/admin/migration/wordpress-all.sql
Success: Imported from ...
이후 데이터베이스를 검사했다.
wp db check
Success: Database checked.
멀티사이트 목록도 정상적으로 복원됐는지 확인했다.
wp db query \
"SELECT blog_id, domain, path FROM wp_blogs ORDER BY blog_id;"
메인 블로그와 기존 하위 사이트 두 개가 모두 조회됐다.
이 과정에서 중요한 점은 SQL 가져오기 전 새 서버의 초기 DB를 백업하고, 실제 운영 중인 기존 서버의 DB를 대상으로 명령을 실행하지 않는 것이다.
7.3 wp-config.php 도메인 설정 수정
DB는 복원됐지만 새 서버의 WordPress 설정은 아직 임시 도메인을 사용하고 있었다.
define('DOMAIN_CURRENT_SITE', '43.200.2.64.nip.io');
이를 실제 도메인으로 변경했다.
sudo nano /var/www/wp-config.php
define('DOMAIN_CURRENT_SITE', 'letsgomin.com');
다른 DB 접속 정보는 변경하지 않았다.
특히 새 Lightsail 블루프린트에는 HTTP_HOST에 따라 WP_HOME, WP_SITEURL을 동적으로 설정하는 코드도 있었다.
이 때문에 WP-CLI 실행 중 다음 경고가 종종 출력됐다.
PHP Warning: Undefined array key "HTTP_HOST"
CLI에서는 브라우저 요청처럼 HTTP_HOST가 항상 제공되지 않기 때문에 발생한 경고였다.
경고가 나더라도 DB 명령이 정상 완료되는 경우가 있었지만, 실제 오류가 함께 발생하는지 구분해야 한다.
7.4 wp-content 복원
이제 압축 파일을 해제했다.
sudo mkdir -p /home/admin/migration/extracted
sudo tar -xzf \
/home/admin/migration/wp-content.tar.gz \
-C /home/admin/migration/extracted
원본과 동일한 약 977MB가 나왔다.
기본 WordPress 콘텐츠를 별도로 보관하고, 기존 파일을 복사했다.
sudo mv /var/www/html/wp-content \
/home/admin/migration/wp-content-initial
sudo cp -a \
/home/admin/migration/extracted/wp-content \
/var/www/html/
이어 소유권과 권한을 조정했다.
sudo chown -R admin:www-data /var/www/html/wp-content
sudo find /var/www/html/wp-content -type d -exec chmod 2755 {} +
sudo find /var/www/html/wp-content -type f -exec chmod 0644 {} +
업로드 폴더는 이미지 업로드를 위해 그룹 쓰기 권한을 부여했다.
sudo find /var/www/html/wp-content/uploads \
-type d -exec chmod 2775 {} +
sudo find /var/www/html/wp-content/uploads \
-type f -exec chmod 0664 {} +
운영 환경에 따라 캐시·백업 플러그인이 추가 쓰기 권한을 요구할 수 있으므로 해당 경로는 별도로 점검해야 한다.
파일까지 복원했으니 이제 서버를 실행해 보면 될 것 같았다.
그런데 여기서 가장 큰 문제가 터졌다.
8. PHP 8.2로 이전했더니 HTTP 500 오류 발생
Apache 설정 문법도 정상이고 PHP 버전도 맞았다.
sudo apache2ctl configtest
sudo systemctl start apache2
그런데 접속해 보니 블로그가 열리지 않았다.
세 멀티사이트 모두 HTTP 500을 반환했다.
서버는 살아 있는데 WordPress가 죽은 것이다.
8.1 Apache 오류 로그 확인
우선 로그를 확인했다.
sudo tail -n 80 /var/log/apache2/error.log
그리고 마침내 원인을 찾았다.
PHP Fatal error:
Call to undefined function create_function()
/var/www/html/wp-content/plugins/zilla-likes/zilla-likes.php
on line 26
범인은 Zilla Likes 플러그인이었다.
오래전에 설치한 좋아요 기능 플러그인이었는데, PHP 8.2와 호환되지 않았다.
create_function()은 PHP 7.2에서 사용 중단 예정으로 지정되었고, PHP 8.0에서는 완전히 제거됐다.
기존 PHP 7.4에서는 동작하던 코드가 PHP 8.2에서는 치명적인 오류를 일으킨 것이다.
더구나 Zilla Likes는 네트워크 전체에 활성화되어 있었다. 하나의 플러그인이 모든 사이트를 500 오류로 만들고 있었던 셈이다.
8.2 플러그인만 비활성화하기
기존 플러그인을 삭제할 필요는 없었다.
WP-CLI로 네트워크 비활성화했다.
cd /var/www/html
wp plugin deactivate zilla-likes \
--network \
--url=https://letsgomin.com \
--skip-plugins \
--skip-themes
Success: Network deactivated 1 of 1 plugins.
플러그인 상태도 확인했다.
wp plugin list \
--url=https://letsgomin.com \
--skip-plugins \
--skip-themes \
--fields=name,status | grep zilla
zilla-likes inactive
다시 홈페이지를 테스트했다.
HTTP 301
Redirect: https://letsgomin.com/
드디어 메인 블로그에서 500 오류가 사라졌다.
여기서 얻은 교훈은 명확하다.
PHP 메이저 버전을 변경할 때는 WordPress 자체보다 오래된 플러그인이 더 큰 문제가 될 수 있다.
특히 네트워크 전체에 활성화된 플러그인은 모든 사이트에 영향을 미칠 수 있으니 우선 확인해야 한다.
참고: PHP 공식 문서 – create_function()
9. 관리자 로그인 404 오류 해결
메인 블로그가 다시 살아났으니 관리자 페이지도 확인했다.
curl -k -sS \
--resolve letsgomin.com:443:127.0.0.1 \
-o /dev/null \
-w "HTTP %{http_code}\n" \
https://letsgomin.com/wp-login.php
그런데 결과가 404였다.
혹시 로그인 파일이 복원되지 않았나 싶었지만, 이번에는 오류가 아니라 기존 보안 설정 때문이었다.
내 블로그는 All-In-One WP Security(AIOS)를 사용하고 있었고, 로그인 주소 변경 기능을 활성화해 둔 상태였다.
다음 명령어로 해당 옵션을 확인했다.
wp option pluck aio_wp_security_configs \
aiowps_enable_rename_login_page \
--url=https://letsgomin.com \
--skip-plugins --skip-themes
결과는 1.
로그인 주소 변경 기능이 활성화되어 있었다.
변경된 로그인 경로로 테스트하니 HTTP 200을 반환했다.
즉, 기본 로그인 경로를 404로 응답시키는 것은 의도된 보안 설정이었다.
이때 보안 플러그인을 무작정 비활성화하거나 로그인 경로를 공개하지 않는 것이 중요하다.
10. DNS를 바꾸지 않고 새 서버 미리 테스트하기
이번 마이그레이션에서 특히 유용했던 명령어가 있다.
바로 curl --resolve다.
기존 도메인이 아직 이전 서버를 가리키고 있더라도, 특정 IP로 직접 요청을 보내 새 서버를 테스트할 수 있다.
curl -k -sS \
--resolve letsgomin.com:443:127.0.0.1 \
-o /dev/null \
-w "HTTPS %{http_code}\n" \
https://letsgomin.com/
이 명령은 새 서버 SSH 내부에서 실행한 것이다.
결과:
HTTPS 200
이미지도 테스트했다.
IMG=$(find /var/www/html/wp-content/uploads \
-type f \( -iname '*.jpg' -o -iname '*.png' -o -iname '*.webp' \) \
-print -quit)
curl -k -sS \
--resolve letsgomin.com:443:127.0.0.1 \
-o /dev/null \
-w "HTTP %{http_code} / Type: %{content_type}\n" \
"https://letsgomin.com${IMG#/var/www/html}"
HTTP 200 / Type: image/jpeg
HTML도 UTF-8로 제공되고 있었다.
이렇게 DNS를 변경하기 전에 새 서버의 홈페이지와 이미지를 검증할 수 있었다.
여기서 사용한 -k는 새 서버의 정식 SSL 인증서 발급 전, 인증서 검증을 일시적으로 생략하기 위한 옵션이다.
운영 전환 후에는 반드시 -k 없이 SSL을 검증해야 한다.
11. 운영 전환 전 최신 데이터 다시 동기화하기
여기서 끝내면 안 된다.
첫 번째 백업은 서버 이전 작업을 시작할 때 만든 파일이다.
그동안 기존 서버는 계속 운영 중이었으므로, 새로운 글이나 댓글, 설정 변경이 발생했을 수 있다.
따라서 기존 데이터를 다시 추출해 새 서버에 반영하는 사전 동기화 작업을 한 번 더 진행했다.
이번에는 SQL 백업에 --add-drop-table을 추가했다.
sudo wp --path=/opt/bitnami/wordpress db export \
/var/tmp/wp-db-backup/wordpress-precutover.sql \
--single-transaction \
--add-drop-table
이는 기존 테이블을 교체하는 데 필요한 DROP TABLE IF EXISTS 구문을 포함하는 옵션이다.
최신 콘텐츠도 다시 압축했다.
sudo tar -czf \
/home/bitnami/migration/wp-content-precutover.tar.gz \
-C /bitnami/wordpress wp-content
SCP로 새 서버에 전송하고 SHA-256을 검증했다.
새 서버의 초기 DB도 다시 백업한 뒤 최신 SQL을 가져왔다.
cd /var/www/html
wp db import \
/home/admin/migration/wordpress-precutover.sql
wp db check
DB 복원 이후 Zilla Likes가 다시 활성화될 수 있으므로 한 번 더 비활성화했다.
콘텐츠도 최신 파일로 교체하고 Apache를 시작했다.
sudo systemctl start apache2
최종적으로 다시 HTTPS 200을 확인했다.
실제 서비스 전환 시 주의할 점
위 작업은 서비스가 운영 중일 때 수행한 사전 동기화다.
실제 고정 IP 이전 직전에는 다음을 반드시 추가로 확인해야 한다.
- 기존 서버의 게시글·댓글 등 쓰기 작업을 중단한다.
- Apache와 예약 작업뿐 아니라 외부 연동에서 발생하는 쓰기도 차단한다.
- 마지막 DB와 업로드 변경분을 추출한다.
- 새 서버에 반영하고 다시 검증한다.
- 그다음 고정 IP를 이전한다.
참고로 단순 WordPress 유지보수 모드만으로 모든 백그라운드 쓰기가 차단되는 것은 아니다.
내 경우에도 운영 중인 DB 백업과 최종 전환 시점의 데이터가 다를 수 있다는 점이 가장 신경 쓰였다.
서버 이전에서 중요한 것은 데이터를 옮기는 것뿐 아니라 어느 시점의 데이터를 최종본으로 삼을지 명확히 하는 것이다.
12. 고정 IP 이전: DNS는 그대로 유지
기존 블로그는 Lightsail 고정 IP를 사용하고 있었다.
도메인의 DNS를 확인해 보니 다음과 같았다.
dig +short letsgomin.com A
dig +short letsgomin.com AAAA
기존 A 레코드는 고정 IP 3.38.64.82를 가리키고 있었고, AAAA 레코드는 없었다.
고정 IP를 사용하고 있다면 새로운 서버로 옮기는 방법은 생각보다 간단하다.
Lightsail 콘솔에서 기존 인스턴스에 연결된 고정 IP를 분리하고, 그 IP를 새 인스턴스에 다시 연결하면 된다.
Lightsail → 네트워킹 → 고정 IP → 인스턴스 연결 변경
이때 기존 고정 IP를 삭제하거나 새 고정 IP를 만들 필요가 없다.
도메인의 DNS A 레코드도 수정할 필요가 없다.
고정 IP 자체는 동일하고, IP가 연결된 서버만 달라지기 때문이다.
물론 서버의 프라이빗 IP와 파일 경로가 같아지는 것은 아니다.
DNS가 가리키는 주소를 유지하면서 실제 서비스 인스턴스만 교체한 것이다.
13. SSL 인증서 설치 — 또 한 번의 삽질
여기까지 왔으면 거의 다 끝났다고 생각했다.
그런데 SSL 인증서가 남아 있었다.
기존 Bitnami 환경에서는 Let’s Encrypt 인증서를 관리할 때 bncert-tool이나 Lego를 사용했다.
예전에 bncert-tool 오류를 직접 해결했던 글까지 작성했을 정도다.
그런데 새 서버는 Bitnami가 아니라 Debian Apache 환경이었다.
이번에는 Certbot을 사용하기로 했다.
13.1 Certbot 설치
처음에는 Certbot이 설치되어 있지 않았다.
certbot --version
certbot: command not found
Debian 패키지 매니저를 이용해 설치했다.
sudo apt-get update
sudo apt-get install -y \
certbot python3-certbot-apache
13.2 Let’s Encrypt 인증서 발급
sudo certbot --apache -d letsgomin.com
이메일 주소와 약관 동의를 진행했다.
다행히 인증서 발급 자체는 성공했다.
Successfully received certificate.
그런데 인증서를 Apache에 배포하려는 순간 문제가 발생했다.
We were unable to find a vhost
with a ServerName or Address of letsgomin.com.
Certbot이 letsgomin.com에 대응하는 Apache 가상 호스트를 찾지 못한 것이다.
선택 가능한 항목은 두 개였다.
1: 000-default.conf *.nip.io
2: default-ssl.conf HTTPS
여기서 아무 항목이나 선택하기보다 우선 설치 과정을 취소하고 설정을 점검했다.
이미 발급된 인증서는 남아 있었으므로 재발급할 필요는 없었다.
13.3 Apache VirtualHost 설정 수정
먼저 현재 가상 호스트 설정을 확인했다.
sudo apache2ctl -S
그리고 인증서 설정 파일도 확인했다.
sudo grep -nE \
'VirtualHost|ServerName|ServerAlias|DocumentRoot|SSLCertificateFile' \
/etc/apache2/sites-enabled/default-ssl.conf
새 서버의 Apache는 다음 파일을 사용하고 있었다.
/etc/apache2/sites-available/000-default.conf
/etc/apache2/sites-available/default-ssl.conf
먼저 두 파일을 백업했다.
sudo cp -a \
/etc/apache2/sites-available/000-default.conf \
/etc/apache2/sites-available/000-default.conf.bak
sudo cp -a \
/etc/apache2/sites-available/default-ssl.conf \
/etc/apache2/sites-available/default-ssl.conf.bak
HTTP 가상 호스트에 실제 도메인을 추가했다.
sudo nano /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
ServerName letsgomin.com
# 기존 설정 유지
</VirtualHost>
HTTPS 가상 호스트에도 같은 방식으로 등록했다.
sudo nano /etc/apache2/sites-available/default-ssl.conf
<VirtualHost *:443>
ServerName letsgomin.com
# 기존 설정 유지
</VirtualHost>
위 코드는 추가할 위치를 보여주는 예시다. 기존 파일의 나머지 설정은 삭제하지 않고 ServerName만 추가했다.
설정 문법을 확인했다.
sudo apache2ctl configtest
Syntax OK
이어서 Apache 설정을 다시 읽어들였다.
sudo systemctl reload apache2
이제 apache2ctl -S에서 HTTP와 HTTPS 모두 letsgomin.com을 가리켰다.
13.4 발급된 인증서를 Apache에 설치
이미 발급된 인증서를 설치했다.
sudo certbot install \
--cert-name letsgomin.com \
--apache
결과는 다음과 같았다.
Successfully deployed certificate
for letsgomin.com
Failed redirect for letsgomin.com
인증서 배포에는 성공했지만 HTTP → HTTPS 자동 리디렉션 설정에는 실패했다.
잠깐 당황했지만, 이번에는 리디렉션을 처리하는 설정이 이미 존재했다.
바로 WordPress의 Really Simple Security 플러그인이었다.
실제로 다음 명령어로 확인했다.
curl -sSI https://letsgomin.com/
HTTP/1.1 301 Moved Permanently
X-Redirect-By: WordPress - Really Simple Security
Location: https://letsgomin.com/
이미 HTTP 요청을 HTTPS로 이동시키고 있었던 것이다.
따라서 Certbot의 자동 리디렉션 설정 실패는 별도로 수정할 필요가 없었다.
단, 다른 환경에서는 이 플러그인이 없을 수 있으니 반드시 HTTP와 HTTPS 응답을 각각 확인해야 한다.
13.5 인증서와 자동 갱신 검증
이번에는 -k 없이 HTTPS에 접속했다.
curl -sSI https://letsgomin.com/
HTTP/1.1 200 OK
인증서 오류 없이 정상적으로 응답했다.
다음으로 자동 갱신을 검사했다.
sudo certbot renew --dry-run
결과는 다음과 같았다.
Congratulations, all simulated renewals succeeded
타이머도 확인했다.
systemctl list-timers --all | grep certbot
certbot.timer가 등록되어 있었다.
이것으로 기존에 나를 괴롭혔던 SSL 인증서 문제도 해결됐다.
참고로 이번 인증서는 letsgomin.com만 대상으로 발급했다. www.letsgomin.com이나 서브도메인까지 자동으로 보장되는 것은 아니므로 다른 주소를 운영한다면 인증서 대상을 추가해야 한다.
14. 마지막 검증
마지막으로 실제 브라우저에서 블로그에 접속했다.
확인한 사항은 다음과 같다.
- 메인 페이지의 레이아웃과 한글이 정상적으로 출력되는가?
- 기존 게시글이 그대로 유지되는가?
- 이미지가 정상적으로 로딩되는가?
- 메뉴와 페이지 이동이 정상 동작하는가?
- 기존 관리자 계정으로 로그인할 수 있는가?
- 관리자 화면에서 게시글과 미디어 파일을 조회할 수 있는가?
- HTTPS 인증서가 정상적으로 표시되는가?
- HTTP 주소로 접속해도 HTTPS로 이동하는가?
모두 정상적으로 작동했다.
이전 후 새 서버의 스냅샷도 생성했다.
기존 서버는 즉시 삭제하기보다 먼저 운영 데이터의 최신 상태와 백업을 확인하고, 잠시 보관한 후 정리하기로 했다.
참고로 Lightsail 인스턴스는 중지해도 요금이 계속 발생할 수 있으므로, 필요 없는 인스턴스는 최종 검증 후 삭제해야 한다. 수동 스냅샷의 보관 비용도 별도 확인이 필요하다.
15. 이번 마이그레이션에서 배운 것
이번 작업을 하면서 몇 가지를 확실히 배웠다.
15.1 서비스가 동작한다는 사실이 안전을 보장하지는 않는다
내 블로그는 PHP 7.4에서도 잘 돌아가고 있었다.
하지만 사용자가 정상적으로 접근할 수 있다는 사실은 보안 패치나 소프트웨어 유지보수가 제대로 되고 있다는 뜻이 아니다.
문제가 눈에 보이지 않는다고 문제가 없는 것은 아니었다.
15.2 서버 이전은 파일 복사가 전부가 아니다
처음에는 SQL과 이미지만 옮기면 끝날 것이라고 생각했다.
그러나 실제로는 서버 실행 환경, 디렉터리 구조, 소유권, 인증서, 플러그인 호환성까지 함께 고려해야 했다.
특히 PHP 8.2로 변경하면서 발생한 create_function() 오류는 단순한 파일 복사로 해결할 수 없는 문제였다.
기존 데이터는 같아도 실행 환경이 달라지면 결과가 달라질 수 있다는 것이다.
15.3 로그는 생각보다 많은 것을 알려준다
HTTP 500이 발생했을 때 무작정 플러그인을 삭제하거나 WordPress를 다시 설치했다면 시간이 훨씬 오래 걸렸을 것이다.
하지만 Apache 로그에는 정확히 어떤 파일의 몇 번째 줄에서 문제가 발생했는지 기록되어 있었다.
zilla-likes.php on line 26
이 한 줄 덕분에 원인을 파악할 수 있었다.
막연한 추측보다 로그 한 줄이 훨씬 정확했다.
15.4 백업은 복구할 수 있을 때 비로소 백업이다
이번에는 기존 서버 스냅샷을 생성하고, DB와 콘텐츠를 별도 파일로 보관한 뒤 SHA-256으로 무결성까지 검증했다.
새 서버의 초기 DB와 설정도 별도로 백업했다.
덕분에 문제가 발생해도 이전 단계로 돌아갈 수 있다는 확신을 가지고 작업할 수 있었다.
백업 파일이 존재한다는 사실보다 중요한 것은, 그 파일을 실제로 읽고 복원할 수 있는가 하는 점이다.
마치며
생각해 보면 이번 서버 이전은 꽤 오래전부터 해야 할 일이었다.
2022년부터 사용하던 블로그 환경은 그동안 여러 차례 손을 봤지만, 정작 기반이 되는 서버 환경은 거의 그대로였다.
Bitnami 지원 종료 소식을 알았을 때도 “나중에 해야지”라고 생각했다.
그런데 이번 PHP 보안 경고가 결국 그 나중을 오늘로 만들어 버렸다.
이관 과정에서 오류도 많았다.
Permission denied로 백업이 막히고, SSH 인증이 실패하고, PHP 버전 호환성 때문에 홈페이지가 통째로 500 오류를 반환했다. 인증서를 발급받고도 Apache에 설치하지 못해 다시 설정 파일을 뒤져야 했다.
그래도 문제를 하나씩 확인하면서 해결했다.
이번 과정에서 새삼 느낀 것은, 시스템을 운영한다는 것은 단순히 서비스를 실행하는 것이 아니라 계속해서 안전하게 유지하는 일이라는 사실이다.
앞으로는 지원 종료 공지가 뜨면 귀찮다는 이유로 미루지 말아야겠다.
아니, 사실 또 미룰지도 모르겠다.
다만 다음번에는 적어도 어떤 문제가 생길 수 있는지, 그리고 어떻게 안전하게 옮겨야 하는지는 알게 됐다.
결국 오늘 내가 한 일은 서버를 하나 교체한 것이지만, 그 과정에서 얻은 것은 서버를 운영하는 방법에 대한 조금 더 깊은 이해였다.
이 글이 오래된 Bitnami WordPress를 운영하면서 이전을 고민하는 누군가에게 도움이 되었으면 좋겠다.
P.S. 서버는 돌아가는 순간보다, 문제가 발생했을 때 어떻게 복구할 수 있는지가 더 중요한 것 같다.