Lightsail 스냅샷으로 하위 인스턴스를 생성할 수 없다

Lightsail의 WordPress 인스턴스를 AL2023으로 전환 할 때, All-in-One Migration utility로 import를 시도하는데 이전에 본 적이 없던 internal error가 발생했다. 분명 업로드 가능한 파일 크기도 import하는 압축 파일 보다 크게 설정해 주었고 다른 설정들도 모두 마쳤음에도 계속해서 실패하는 것이다.

YYYY/MM/DD HH:MM:SS [error] 32777#32777: *4 FastCGI sent in stderr: "PHP message: PHP Fatal error:  Uncaught ValueError: Path cannot be empty in /var/www/html/wp-content/plugins/all-in-one-wp-migration/functions.php:1881 
Stack trace: 
#0 /var/www/html/wp-content/plugins/all-in-one-wp-migration/functions.php(1881): fopen() 
#1 /var/www/html/wp-content/plugins/all-in-one-wp-migration/lib/model/import/class-ai1wm-import-upload.php(86): ai1wm_is_filedata_supported() 
#2 /var/www/html/wp-content/plugins/all-in-one-wp-migration/lib/controller/class-ai1wm-import-controller.php(77): Ai1wm_Import_Upload::execute() 
#3 /var/www/html/wp-includes/class-wp-hook.php(341): Ai1wm_Import_Controller::import() 
#4 /var/www/html/wp-includes/class-wp-hook.php(365): WP_Hook->apply_filters() 
#5 /var/www/html/wp-includes/plugin.php(522): WP_Hook->do_action() 
#6 /var/www/html/wp-admin/admin-ajax.php(192): do_action() 
#7 {main} 
  thrown in /var/www/html/wp-content/plugins/all-in-one-wp-migration/functions.php on line 1881" while reading response header from upstream, client: [CLIENT_IP], server: _, request: "POST /wp-admin/admin-ajax.php?action=ai1wm_import&ai1wm_import=1 HTTP/1.1", upstream: "fastcgi://unix:/run/php-fpm/www.sock:", host: "[SERVER_IP]", referrer: "http://[SERVER_IP]/wp-admin/admin.php?page=ai1wm_import"

메모리 문젠가?

Nginx의 로그를 보니 Path cannot be empty ... 라는 PHP Fatal error가 찍혀 있었다. 이 에러메세지 자체로는 문제점이 잘 드러나지 않지만, 검색해보니 업로드가 완료된 후 압축 해제와 import를 처리 하는 과정중에 메모리가 부족해서 이런 문제가 발생할 수 있다는 내용이 있었는데, 업로드가 100%까지 진행되고 나서 그 후에 문제가 발생하는 것으로 볼 때 이 말이 사실일 가능성이 있어 보였다.

혹시나 해서 지금 사용하고 있는 것보다 높은 사양의 인스턴스를 하나 새로 파서 테스트해 보니 오류없이 잘 동작했다. 여기까지 확인해서 메모리 부족 때문에 발생하는 문제라는 확증이 들자 한가지 꼼수가 떠올랐다.

  1. 높은 사양의 인스턴스로 All-in-One으로 마이그레이션을 하고
  2. 스냅샷을 하나 만든 다음
  3. 스냅샷으로 부터 원래의 낮은 사양으로 인스턴스를 생성한다.

“그래, 이런 기발한 방법이!” 라며 먼저 $12짜리 인스턴스에 마이그레이션을 완료하고 스냅샷을 만들었다. 그리고 나서 스냅샷으로 부터 인스턴스를 생성하려고 하는데 이런화면이 나왔다.

스냅샷을 생성할 때의 인스턴스보다 낮은 티어는 생성할 수가 없다는 것이다.

결론

Bitnami 워드프레스를 Amazon Linux 2023으로 마이그레이션

처음 Lightsail로 이사왔을 때 인스턴스 선택에는 큰 고민을 들이지는 않았다. 제공되는 WordPress instance에는 필요한 것들이 이미 다 설치 되어 있어서 인스턴스를 하나 생성하고 기존 블로그를 import하는 것으로 모든 과정들이 비교적 순조롭게 진행된 편이었다. 문제는 이 WordPress 인스턴스가 Bitnami로 되어 있어서 세부적인 관리 작업에는 제약이 많다는 점이다. 그래서 AWS에 최적화 되어 있다는 Amazon Linux2023(AL2023)으로 LEMP를 직접 설치하고 기존의 블로그를 수동으로 마이그레이션 해 보기로 했다.

이번 포스팅에서는 AL2023 인스턴스에 LEMP(Linux, Nginx, MariaDB, PHP)를 설치하고 Bitnami에서 사용하던 데이터베이스와 자료들을 수동으로 옮겨오는 과정을 설명한다. All-in-One 같은 편리한 마이그레이션 유틸리티를 쓰지 않고 굳이 수작업으로 일일이 옮겨야 했던 이유는 All-in-One에서 띄운 오류 때문이었는데 아무런 문제가 없고, 기술적 호기심 또한 없다면 굳이 이 내용을 따라 수작업으로 옮길 필요는 없을 것이다.

AL2023 환경 구축

Lightsail에서 AL2023 인스턴스를 생성한 뒤 시스템을 업데이트하고 LEMP 패키지를 설치한다. Amazon Linux 2023은 Fedora기반으로 패키지 관리에 dnf 명령어를 사용한다. PHP8.3과 필요한 패키지들을 설치해 주고 나서 Nginx, MariaDB, PHP-FPM 서비스들을 실행해 준다.

sudo dnf update -y
sudo dnf install -y nginx mariadb105-server php8.3 php8.3-fpm php8.3-mysqlnd php8.3-gd php8.3-xml php8.3-mbstring php8.3-bcmath php8.3-opcache php8.3-intl php8.3-zip

# 서비스 활성화 및 자동 실행 설정
sudo systemctl start nginx mariadb php-fpm
sudo systemctl enable nginx mariadb php-fpm

데이터베이스 초기화

MariaDB에 WordPress를 위한 database를 생성하고 비밀번호를 설정한다. mysql_secure_installation은 여러 보안 관련한 처리를 해주는데, 사용하지 않는 기능들을 끄거나 비밀번호를 설정하는 과정을 진행해 준다.

설정이 끝나면 root user로 data base를 실행시킨다.

sudo mysql_secure_installation
sudo mysql -u root -p

wordpress database를 생성하고, wpuser사용자를 생성한 다음 권한을 설정하는 과정이다. 나는 비밀번호를 새롭게 만들지 않고 기존에 Bitnami가 생성해 주었던 database 비밀번호(wp-config.php에 있는)를 그대로 사용했다.

CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY '[사용자_비밀번호]';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

워드프레스 설치 및 파일 권한 설정

최신 버전의 워드프레스를 다운로드 받아서 /var/www/html/ 위치에 풀어준다.

# 최신 워드프레스 다운로드
wget https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
sudo cp -r wordpress/* /var/www/html/

AL2023 환경에서 Nginx가 파일을 정상적으로 제어할 수 있도록 소유권과 권한을 수정한다.

# /var/www/html안에 있는 파일들의 권한 설정
sudo chown -R nginx:nginx /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;

# PHP-FPM 실행 그룹을 apache에서 nginx로 변경
sudo sed -i 's/user = apache/user = nginx/g' /etc/php-fpm.d/www.conf
sudo sed -i 's/group = apache/group = nginx/g' /etc/php-fpm.d/www.conf

# 서버 재실행
sudo systemctl restart php-fpm

Nginx 설정

/etc/nginx/conf.d/wordpress.conf 파일을 생성하고 아래의 내용을 추가한다. 이 단계에서 나는 server_name 항목에 “[도메인이름]” 대신에 “_”를 넣어주었고 마이그레이션이 끝나고 나서 기존에 사용하던 public IP를 새로운 인스턴스에 연결시킨 다음에 SSL을 업데이트 하는 과정에서 도메인 이름으로 변경해 주었다.

server {
    listen 80;
    server_name [도메인이름];
    root /var/www/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

설정 후 Nginx를 재실행한다.

sudo systemctl restart nginx

여기까지 되었으면 instance의 IP로 접속했을 때 WordPress의 설치 화면이 보이게 된다.

마이그레이션 수작업

먼저 기존 Bitnami 서버 터미널에서 데이터베이스와 콘텐츠를 가져온다.

# 데이터베이스 덤프
mysqldump -u root -p bitnami_wordpress > backup.sql

# 데이터 압축 (wp-content와 환경설정파일)
cd /opt/bitnami/wordpress
tar -czvf wp_data.tar.gz wp-content wp-config.php

추출한 backup.sql과 wp_data.tar.gz 파일을 새 서버로 전송하고, wp_data.tar.gz 파일은 /var/www/html/ 아래에 풀어준다.

# wp-content 복원
tar -xzvf wp_data.tar.gz
sudo cp -r wp-content/* /var/www/html/wp-content/

DB Import 및 연결 정보 수정

Database backup file을 import 한다.

# Database back up file import
sudo mysql -u root -p wordpress < backup.sql

Database import가 완료되면 /var/www/html/wp-config.php 파일을 열어 새로운 정보로 DB_NAMEDB_USERDB_PASSWORD를 맞춰준다.

여기까지 해서 모든 이전이 잘 수행 되었다면 기존 인스턴스에서 사용하던 Public IP 주소를 떼어다가(detach) 새로운 인스턴스에 붙여(attach)해 준다. 이 과정은 Lightsail의 “네트워크” 항목에서 진행하면 된다.

많은 가이드 문서에서 마이그레이션 이후 IP정보를 업데이트 하는 과정을 거치는데, Lightsail의 경우는 public IP를 그대로 새로운 instance로 옮겨올 수 있어서 이 부분의 귀찮은 과정을 생략할 수 있다.

새로운 HTTPS 인증서 적용

Public IP가 옮겨지고 domain name으로 접속이 잘 이루어진다면, 앞서 말했던 것 처럼 /etc/nginx/conf.d/wordpress.conf 파일의 server_name 부분에 도메인이름을 적어주고 아래의 과정을 따라서 인증서 정보를 업데이트 해준다.

인증서 업데이트를 위한 Certbot 설치

# Certbot 설치
sudo dnf install -y certbot python3-certbot-nginx

# Certbot으로 인증서 업데이트 (Litcoder.com)
sudo certbot --nginx -d Litcoder.com -d www.litcoder.com

certbot certificates 명령어로 생성된 cert와 유효기간을 확인할 수 있다.

Auto renew service 활성화

Expiration date이 되면 자동으로 인증서를 업데이트 하도록 다음과 같이 certbot-renew.timer 서비스를 활성화 해 준다.

sudo systemctl enable certbot-renew.timer
sudo systemctl start certbot-renew.timer

주의! 443은 막혀있다

인증서 업데이트 이후 HTTPS로 접속이 안된다면 443번 포트가 열려 있는지 확인해 보자. Lightsail은 2026년 현재 새롭게 생성한 인스턴스의 22번과 80번 포트만 기본으로 열려 있다. HTTPS를 사용하려면 명시적으로 사용자가 443번을 열어야 한다.

결론

다소 번거로운 과정이기는 했지만, 장기적 관점으로 봤을 때는 Bitnami의 제한에서 벗어나 서버에 대한 세부적인 제어 권한을 가져올 수 있게 되었다는 점에서 시간을 들여 투자해 볼만 한 삽질 이었던 것 같다.

pip 설치 중 컴파일 문제 회피를 위한 잡 기술

Python에서 어떤 패키지를 설치하려면 pip를 사용한다.

pip install <패키지>

이상적으로는 이렇게만 하면 편리하게도 pip가 PiPy에서 해당 패키지를 다운로드 해서 개발환경에 설치 해준다. 하지만 실제 pip 설치는 생각보다는 조금 복잡하다. 특히 macOS나 Linux 환경에서는 pip 설치가 기대치 않았던 C/C++ 컴파일 에러라는 새드 엔딩으로 치닫는 경우가 적지 않다.

이 포스팅에서는 pip에서 소스 코드 빌드를 회피하는 방법을 중심으로, pytubefix와 그 의존성인 nodejs-wheel-binaries 사례를 통해 설치를 실행하기 전에 실패를 예측하는 방법과 이를 최대한 회피하는 방법을 정리해 본다. 다만, 설명의 편의를 위해 local wheel 파일을 설치하는 경우는 제외하고 PiPy에서 다운로드 받는 패키지를 가정하고 설명 하였다.

pip 설치에는 두 가지 경우가 있다

pip가 패키지를 설치하는 방식은 크게 두 가지다.

pip install <패키지>
 ├─ wheel(.whl) 파일이 PiPy에 있음 -> 다운로드 후 설치
 └─ wheel 없음 -> 소스 코드 빌드

PiPy에 원하는 버전의 wheel 파일이 존재하면 이것을 다운로드해서 설치하는 것으로 설치 과정이 끝난다. 이것이 바로 앞에서 말한 “이상적인” 경우이다. 만약 wheel이 없으면 pip는 자동으로 소스 빌드를 시도하게 되는데 이렇게 되면 pip 설치 명령어의 성공 여부는 더이상 pip 자체의 문제가 아니라 다양한 변수들의 의존성에 달려있게 된다.

  • OS 버전
  • 컴파일러(clang, gcc)
  • 각종 SDK
  • 외부 라이브러리(OpenSSL 등)

만약 이 모든 요소가 잘 못 얽히게 되면, 복잡 다단한 원인에 의한 컴파일 문제로 결국은 pip 설치 명령어가 실패 할 수도 있다.

설치 전에 컴파일 가능성 판별하기

pip install 명령어의 –dry-run 옵션은 실제로는 설치를 진행하지 않고 모의 실행(Dry run) 해주는 명령어 인데, 이것을 이용하면 컴파일을 필요로 하는 상황인지의 여부를 미리 확인할 수 있다. macOS에서 Pytubefix 패키지를 설치하려는 경우를 예를들어 살펴보자.

pip install pytubefix --dry-run

전술한 대로 이 명령은 실제로 설치를 하지는 않지만, pip가 무엇을 설치하려고 하는지는 그대로 보여 준다. 출력 중에서 주의해서 봐야 할 부분은 다음이다.

이 메세지는 wheel 파일이 없어서 소스 코드 빌드가 시도 됨을 의미한다.

컴파일 회피 하기

컴파일이 항상 실패하는 것은 아니고, 문제가 발생한 경우에도 간단한 의존성 문제를 해결하는 것으로 해결하는 것도 가능하겠지만 때로는 모든 종속성을 해결해 줄 수 없어 차라리 오래된 버전이라도 미리 컴파일 되어 있는 wheel을 사용하고자 할 때도 있다. –only-binary option을 사용하면 설치가 가능한지 여부를 확인할 수 있는데, 다음과 같이 입력하면 직접 컴파일 하지 않고 whl을 다운로드 받아서 설치가 가능한지 여부를 확인할 수 있다.

pip install pytubefix --dry-run --only-binary=:all:

이 옵션은 컴파일 된 wheel이 있는 패키지들로 dry run을 실행해 보라는 의미이다. 성공하면 모든 의존성이 wheel로 제공된다. –only-binary option을 주지 않았을 때에 비해 낮은 버전이 제시된 것을 눈여겨보자. 소스코드 설치는 v24.13.0 이지만 바이너리 설치는 v22.20.0 이다.

만약 이 명령어에서 실패한다면 안타깝게도 의존성이 소스 빌드 없이는 설치가 불가능 함을 의미한다.

실제사례 – Pytubefix

Pytubefix는 YouTube clip를 다운로드 받을 수 있도록 해주던 PyTube가 더 이상 유지 관리가 되지 않으면서 이를 이어받아 진행되고 있는 오픈소스 프로젝트이다. Pytubefix가 Node.js 환경을 독립적으로 관리할 수 있게 해주는 도구인 nodejs-wheel-binaries에 의존 하는데, 문제는 이 모듈이 macOS 13 이상의 버전에 대해서만 wheel을 제공하고 있다는 점이다.

그래서 macOS Monterey(12) 환경에서 pytubefix 설치를 시도하면 지원되는 가장 최신의 nodejs-wheel-binaries 버전인 v24.13.0 소스코드 빌드를 시도하게 되고 이것이 OpenSSL의 deprecated API 때문에 컴파일에 실패하게 된다.

wheel 없음 -> 소스 빌드 -> nodejs-wheel-binaries 컴파일 -> OpenSSL deprecated API -> 컴파일 실패

nodejs-wheel-binaries란?

nodejs-wheel-binaries는 Node.js 런타임을 Python wheel 형태로 패키징한 라이브러리로 Node.js 실행 파일을 Python 가상환경 내부에 포함시켜 pip install만으로 Node.js를 사용할 수 있게 만들어 준다. pytubefix는 YouTube 대응 로직 일부를 JavaScript 기반 코드로 처리하기 때문에 내부적으로 Node.js 실행을 필요로 하기 때문이 이 패키지에 의존한다. 주의할 점은 시스템에 설치한 Node.js의 버전은 이 동작과는 무관하다는 점이다. 즉, 시스템에 Node.js가 설치되어 있다 하더라도, 의존성이 있는 경우에는 이 패키지의 설치가 필요하다.

결론: 소스 빌드를 회피하는 현실적인 해결책

Pytubefix를 위해서는 반드시 최신버전의 nodejs-wheel-binaries 패키지를 사용하지는 않아도 된다. 위에서 pip 명령어로 wheel 파일이 지원되는 것으로 확인한 v22.20.0을 다음의 명령어로 먼저 설치하고 그 위에 pytubefix의 설치를 실행할 수 있다.

pip install "nodejs-wheel-binaries==22.20.0" pytubefix