워드프레스 백업과 복원은 웹 파일과 DB를 한 쌍으로 맺어야 한다

🧐 , 🧐 | 2023-12-22

안녕하세요, 넷마블 기술관리실 조병승입니다.

연말이 다가오니 여기저기 한해를 되돌아보는 회고글이 보이기 시작합니다. 넷마블 기술 블로그의 지난 1년에서 개인적으로 가장 뿌듯했던 순간이 떠올라, 그 이야기를 먼저 꺼내볼까 합니다.

1년에 8번 업데이트하는 워드프레스 코어

넷마블에는 워드프레스로 관리하는 콘텐츠 사이트가 2가지 있습니다. 외부로 공개되는 넷마블 기술 블로그와 내부 개발자가 참고하는 개발자 사이트죠. 몇몇 매뉴얼 제공용 사이트가 더 있었습니다만, 틈틈이 개발자 사이트로 통합하고 있습니다.

워드프레스 코어 버전은 매우 자주 업데이트됩니다. 릴리스 히스토리를 보면, 마이너 버전까지 포함했을 때 2023년에만 8번이나 출시됐습니다. 2022년을 포함하면 20번이네요. 마이너 업데이트도 주로 보안 취약점에 대응하고 있기 때문에, 최대한 함께 커버하는 것이 좋다고 생각합니다. 그래서 분기에 최소 1번 이상 코어 버전 업데이트에 대응하고 있습니다. 

업데이트 주기를 알 수 없는 플러그인

워드프레스 코어를 업데이트한 후, 최우선으로 사이트 외적 구조 영향 여부를 확인합니다. 이때 꼭 워드프레스 플러그인을 함께 챙겨야 합니다. 플러그인으로 구성한 외적 요소가 있기 때문에, 혹시라도 코어 업데이트로 인해 API가 바뀌거나 지원이 끊어질 수 있습니다.

플러그인은 직접 개발하거나 유지보수 하지 않더라도 필요한 기능을 구현하는 가장 빠른 수단 중 하나입니다. 워드프레스 생태계에는 오래도록 지속한 세월과 많은 사용자만큼 무수한 플러그인이 있습니다. ‘워드프레스 생태계의 꽃’이라 할 수 있죠. 

더욱이, 플러그인 생태계에 들어와 있는 여러 업체와 개인 개발자들 이외에도 사용자들까지 합세해서 워드프레스 업데이트에 맞춰서 플러그인 동작에 이상이 없는지 확인하고 사용 후기를 남겨놓습니다. 다만, 워드프레스 코어 업데이트에 영향을 받았을 때 바로바로 대응해 주지 못하는 플러그인도 있기 때문에 ‘양날의 검’이 되기도 합니다. 의존도가 너무 큰 경우에는 플러그인이 업데이트될 때까지는 코어를 업데이트할 수 없는 결과로 이어지기도 하니까요. 

현재 넷마블 기술 블로그에서 사용하는 플러그인은 11개, 개발자 사이트에서 사용하는 플러그인은 25개 정도입니다. (차일드 테마에서 수작업으로 활성화한 기능까지 플러그인으로 친다면 개수는 좀 더 늘어날 수 있습니다.) 워드프레스 코어 업데이트에서 플러그인으로 구현한 기능이 나오면, 플러그인을 제거하면서 의존도를 줄이기도 합니다. 그나마 커머스 기능이 붙어있지 않아서 플러그인이 많지 않아 다행이다 싶기도 합니다.

업데이트했더니 문제가 생겼다면!

워드프레스 코어와 플러그인 업데이트를 하려면, 잘 동작하는지 여부를 꼭 확인해야 합니다. 아마도 업데이트가 긴장되는 이유는, 남들은 다 된다는데 나만 안 될 때가 생기기 때문일 겁니다. 만약 문제가 생긴다면, 이전 상태로 롤백해서 대처하는 것이 사용자에게 영향을 가장 적게 주면서 가장 빠른 방법입니다. 그래서 불시 상황에 대비해 항상 롤백 대책을 마련해야 합니다.

라이브와 완전히 똑같은 환경으로 테스트하려면

분명히 테스트 서버에서는 아무런 문제가 없었는데, 라이브 서버에서는 뭔가 꼭 문제가 생기는 경우가 있습니다. 여러 의존성을 탈피하기 위해, 도커나 컨테이너 환경 등을 통해 배포하라는 조언을 주는 분들도 많으시죠. 아래에서 이야기가 나오겠지만, 워드프레스 웹 파일과 DB를 한 쌍으로 맞춰서 배포해야 합니다. 라이브 서버에서 서비스 중단 지점이 생겨야 안정적이고 편한 배포가 가능하단 의미죠. 워드프레스의 기본 구조를 완전히 분해해서 사용하지 않는 이상, 실현하기 쉽지 않은 계획이라 할 수 있습니다.

사이트 운영 초기에는 테스트 서버에 비슷한 구성으로 사이트를 올려놓고 여러 테스트를 했었습니다. 테스트 횟수가 늘수록, 테스트 사이트 구조가 점점 라이브와 차이가 커질 수밖에 없었죠. 라이브 서버와 테스트 서버 사이에 차이가 커질 수록 테스트 결과에 대한 신뢰도는 떨어집니다.

테스트 서버는 라이브 사이트의 워드프레스 DB는 그대로 두고 테스트 서버용 DB를 따로 사용했습니다. 그리고 웹 파일을 테스트 서버로 복사해서 구성했습니다. 안에 있는 글은 몇몇 개만 옮겨서 비슷하게 만들어 쓰고 있었습니다. 나름 필수 요소인 글과 페이지는 복사해서 넣었는데도, 자꾸 복사로는 완전히 옮겨지지 않는 부분이 생겼습니다. 그 차이를 찾아야 했습니다.

그러던 중 워드프레스 DB에서 몇몇 테이블과 옵션값이 눈에 들어왔습니다. 순수한 워드프레스 상태에서는 없었던 테이블과 옵션값이었죠. 플러그인이 단순히 웹 파일로만 설정값이나 로그 데이터를 가지고 있는 것이 아니라, 워드프레스 DB에 그들의 공간을 별도로 할애해서 자리를 잡고 있었습니다. 별도 테이블이 생성된 것 이외에도, 기존 wp_options 테이블 안에 새 값이 저장돼 있기도 했습니다. 

라이브 서버와 테스트 서버의 웹 파일만 옮겨서는, DB 정합성을 맞출 수 없다는 뜻입니다. 즉, 라이브와 완전히 똑같은 환경으로 테스트하려면, 웹 파일과 DB를 한 쌍으로 만들어서 복사를 해야 한다는 의미입니다. 

사이트가 커지고 무거워지면 그 자체도 다시 시간이 많이 들겠지만, 어차피 롤백 대책을 위해서라도 특정 시점의 파일과 DB는 백업을 떠야 합니다. 백업 파일은 피할 수 없는 일인거죠. 

백업과 복원

요즘 많이 쓰는 깃허브 페이지나 블로그 서비스는 SaaS 형태로 사용하므로, 백업 방식은 각 서비스에 따라 다를 것입니다. 헤드리스 CMS나 워드프레스를 웹 호스팅 등으로 가볍게 사용하는 경우에도 호스팅 서비스 제공자에 맞춘 가이드가 따로 있으리라 생각합니다. 만약, K8s 환경에 워드프레스를 올렸거나 따로 CI/CD를 세팅한 경우에도 그 패턴에 맞춰서 웹과 DB를 한 쌍으로 만들 방법을 찾으셔야 합니다. 저는 단순한 VM에 IaaS 형태로 구성돼 있으므로, 이 기준에 따라 해소 방안을 찾으려 했습니다.

인프라 관점에서 스냅샷을 뜨는 형태로 백업하는 방법은 어떨지 인프라운영팀 김태훈님께 문의를 했습니다. 


조: 김태훈님, VM에 있는 웹 파일과 MariaDB를 그냥 인프라 통째로 스냅샷을 뜨면, 테스트 서버로 복사하고 필요할 때 롤백하면서 쓸 수 있을까요?

김: 스냅샷은 효율적인 백업과 롤백 방법은 될 수 있어요. 그런데, 복제한 테스트 서버 속에 있는 파일이나 데이터를 따로 찾아보려면, 스냅샷 시점마다 VM 자체를 늘려야 할 수도 있어요. OS 구조에 대한 제약, 메모리에 있는 데이터의 보장 방안까지 챙기는 견고한 백업 대책이 될 순 없거든요. 파일이나 데이터 보관 목적으로 스냅샷을 쓰기엔 부담이 좀 있어 보이네요. 만약 소스코드 자체가 깃(Git) 같은 곳에서 형상 관리가 되고 있다면, 차라리 그걸로 관리하는 게 좋아요. 스냅샷 시점마다 VM을 늘려가면서 쓸 정도로 크게 일 벌이려는 건 아니죠?

조: 그러게요. 파일이나 데이터를 시점별로 능동적으로 찾을 편의를 생각하면 스냅샷은 오버 엔지니어링이 될 수도 있겠네요. 그러면 따로 파이썬 같은 걸 써서 프로그래밍해야 하는 거면, ChatGPT 도움을 받아도 제 손에서 끝내기는 쉽지 않을 느낌인데요. 제일 간단한 대안은 뭐가 있을까요?

김: 마침 사용하고 계신 VM이 전부다 리눅스니까, 셸 스크립트로 해봐요. 조건별 상황이 많은 것도 아니고, 단순 선형 실행만 하면 될 정도니까 셸 스크립트면 충분할 거예요. 이미 셸 스크립트로 짜면 될 내용을 조병승님이 매뉴얼로 만들어서 손으로 복붙해서 쓰고 계시잖아요. 그거 손으로 넣는 명령어를 다 한 통에 넣어버리면 돼요.

조: 오, 이걸 한 통에… 한번 도전해 볼게요.


워드프레스 플러그인 중에는 백업 관련 플러그인도 여럿 있습니다. 다만, 원하는 기능을 갖춘 플러그인이 맞는지 하나하나 테스트하가 쉽지 않았습니다. 시간도 시간이지만, 애초에 플러그인 자체도 백업에 포함되는 항목인데, 그걸 플러그인으로 쓴다는 자체가 뭔가 이상하단 느낌도 들었고, 제가 별도로 커스텀 세팅해 둔 값까지 완벽히 다 되는지 확인하기가 매우 번거로웠습니다. 또한 백업 파일을 보관하는 스토리지가 대부분 외부 클라우드 서비스 위주였던터라, 사내 인프라에서 동작하는 파일을 외부로 올리는 부담도 컸습니다.

그래서 김태훈님의 조언대로, 셸 스크립트를 사용해 백업과 복원 스크립트 파일을 만들기로 했습니다. 마침, 평소에 손으로 실행하던 명령어를 매뉴얼로 잘 정리해둔 문서도 있었기에 맨바닥에서 시작하는 것도 아니었으니까요.

백업

백업 자체는 간단합니다. 먼저, DB나 웹 업데이트가 발생하지 않는 시간을 정합니다. 그리고 MariaDB는 덤프 파일로, 워드프레스 웹 파일은 압축 파일로 만듭니다. 그리고 어딘가에 생성한 저장용 서버로 두 파일을 복사합니다.

먼저 백업을 위해 디렉토리나 DB 정보를 변수로 설정합니다.

# 백업 디렉토리 설정
backup_dir="/backup"

# 워드프레스 설치 디렉토리 설정
wordpress_dir="/wordpress"

# 백업 파일 이름 생성
wordpress_file="wordpress_$(date +%Y%m%d_%H%M%S).tar.gz"
wordpress_db="wordpress_DB_$(date +%Y%m%d_%H%M%S).sql"

# 데이터베이스 정보 설정
db_host="db_host"
db_name="wordpress DB name"
db_user="DB account"
db_password="DB password"

# 백업 파일을 저장할 서버 정보
backup_server_host="server host"
backup_server_user="user account"
backup_server_dir="/backup"

이제 워드프레스 웹 파일 압축 함수와 DB 백업 함수를 만듭니다.

backup_web() {
sudo tar -zcf "$backup_dir/$wordpress_file" "$wordpress_dir/"
}

backup_db() {
mysqldump --single-transaction -h "$db_host" -u "$db_user" -p"$db_password" "$db_name" > "$backup_dir"/"$wordpress_db"
}

압축한 파일과 백업한 파일을 복사할 함수를 만듭니다. 저는 서버에서 서버로 복사하는 명령어로 scp를 용했습니다만, 저장 공간이 깃(Git)이라거나 다른 스토리지를 사용하신다면 그에 맞는 복사 명령어를 사용하시면 됩니다. 혹시나 scp로 복사할 때 네트워크 대역폭을 혼자 다 점유하지 않도록 -l 옵션으로 제한을 걸었습니다.

backup_web_copy() {
scp -l 200000 "$backup_dir/$wordpress_file" "$backup_server_user"@"$backup_server_host":"$backup_server_dir"
}

backup_db_copy() {
scp -l 200000 "$backup_dir/$wordpress_db" "$backup_server_user"@"$backup_server_host":"$backup_server_dir"
}

이제 여기까지 만든 함수를 순서대로 실행할 함수를 만들고 호출합니다.

run_backup() {
backup_web
backup_db
backup_web_copy
backup_db_copy
}

run_backup

최초 실행해 보고 나니, 단계별까진 아니더라도 전체 실행을 위해 소요된 시간을 추후에 확인할 수 있도록 최소한의 기록을 남기고 싶었습니다. 스크립트를 시작하자마자, 스크립트를 시작한 시각을 남기고 각 함수 실행이 끝난 자리마다 추가했습니다.

# 로그 설정
logfile="/backup_log_$(date +%Y%m%d_%H%M%S).log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$logfile"
}

# 시작
log "백업을 시작합니다."

# backup_web()
log "웹 파일 백업을 완료했습니다."

# backup_db()
log "데이터베이스 백업을 완료했습니다."

# backup_web_copy()
log "웹 파일 복사를 완료했습니다."

# backup_db_copy()
log "데이터베이스 파일 복사를 완료했습니다."

로그 파일을 열어보면, 대략 소요 시간을 볼 수 있습니다.

백업을 하면서 백업 파일 자체를 다른 서버로 옮겼으므로, 계속 이 서버에 백업 파일을 남겨둘 필요는 없습니다. (계속 쌓으면 서버 저장공간이 금방 부족해집니다.) 생성한 파일명 자체에 시간 값을 붙여놨기에, 파일명을 기준으로 60일이 지난 파일은 지우는 스크립트도 같이 붙였습니다.

# 60일이 지난 백업 파일 정리
sudo find "$backup_dir" -name "wordpress_*" -mtime +60 -exec rm {} \;

이렇게 대충 백업 쪽 정리가 끝났습니다. 더 정교하거나 다듬어야 할 부분은 떠오를 때마다 각 단계별로 함수를 나눴으므로, 단계에 맞춰서 수정 또는 추가하면 됩니다.

복원

다음은 복원입니다. 백업과 달리, 복원은 조금 고민이 필요합니다. 크게는 롤백을 위한 복원인지, 테스트 서버 구성을 위한 복원인지로 나눠지겠네요. 당장 이 둘은 서로 사용할 도메인이 다르기 때문에, 도메인 설정이 바뀌는 것까지 맞춰서 스크립트에 넣어야 합니다.

저는 테스트 서버를 구성하는 기준으로 복원 스크립트를 만들었습니다. 우선 저장된 백업 파일 중에 가장 최근 파일을 인식하는 것부터 시작해야 합니다.

# ls 명령으로 최신 파일을 찾아서 변수에 저장
sqlfile=$(ls -t /tmp *.sql | head -n 1)
targzfile=$(ls -t /tmp *.tar.gz | head -n 1)
sqlname=$(basename "$sqlfile" | sed 's/\..*//')
targzname=$(basename "$targzfile" | sed 's/\..*//')

이미 테스트 서버에는 지금 쓰고 있는 파일이 있습니다. 웹 서버 설정이 이 디렉터리를 가리키고 있기도 하니, 복원할 파일이 이 디렉터리에 오도록 기존 디렉터리의 이름을 바꾸기로 했습니다.

# 기존 디렉터리 이름 변경
today=$(date +%Y%m%d)
sudo mv /wordpress /wordpress_$today

이제 백업한 워드프레스 웹 파일을 그대로 해제하면 됩니다.

sudo tar -zxvf /backup$targzfile -C /.

DB 백업 파일은 밀어 넣으려면, 빈 DB가 먼저 생성돼 있어야 합니다. 만약 테스트 서버에 이미 DB가 있다면, 밀어 넣는 DB를 이름이 다른 DB에 넣어야 데이터가 꼬이지 않습니다. 저는 맨 처음 최신 파일을 변수에 저장할 때 그 파일 이름 자체를 DB명으로 쓰기로 했으니, DB 생성할 때부터 그 변수를 호출해서 사용하도록 했습니다.

# DB 생성
sudo mysql -u account -ppassword -e "CREATE DATABASE $sqlname"

# DB 밀어넣기
sudo mysql -u account -ppassword $sqlname < /tmp/$sqlfile

DB를 다 밀어 넣었으니, 테스트 서버로 쓰는 도메인으로 DB안에 있는 값을 바꿔야 합니다. 대략, wp_posts, wp_postmeta, wp_options 테이블에 있는 도메인만 치환하면 99%가 커버됩니다.

# DB안에서 도메인값 변경
sudo mysql -u account -ppassword -e "update $sqlname.wp_posts set post_content = replace(post_content, 'netmarble.engineering', 'test.test') where post_content like '%netmarble.engineering%'; update $sqlname.wp_posts set guid = replace(guid, 'netmarble.engineering', 'test.test') where guid like '%netmarble.engineering%'; update $sqlname.wp_postmeta set meta_value = replace(meta_value, 'netmarble.engineering', 'test.test') where meta_value like '%netmarble.engineering%'; UPDATE $sqlname.wp_options SET option_value='https://test.test' WHERE option_name='siteurl'; UPDATE $sqlname.wp_options SET option_value='https://test.test' WHERE option_name='home';"

이제 워드프레스 설정 파일에서 도메인과 DB 정보를 변경하면 끝납니다. 워드프레스 설정 파일은 각자마다 사용하는 규칙이나 선언자가 다를 수 있으니, 본인 설정에 맞게 바꾸셔야 합니다.

sed -i "s|define('WP_HOME','https://netmarble.engineering');|define('WP_HOME','https://test.test');|g" /wordpress/wp-config.php
sed -i "s|define('WP_SITEURL','https://netmarble.engineering');|define('WP_SITEURL','https://test.test');|g" /wordpress/wp-config.php
sed -i "s|define( 'DB_NAME', 'wordpress_db' );|define( 'DB_NAME', '$sqlname' );|g" /wordpress/wp-config.php

백업 때와 마찬가지로, 복원에 소요되는 시간이 궁금하다면 단계별로 시간 기록을 남기면 됩니다. 복원에는 특별히 시간 기록을 남기지 않았는데, 막상 지금 이 글을 쓰고 보니 복원은 시간이 얼마나 걸리는 지 궁금하긴 하네요.

Cron

백업과 복원 스크립트를 모두 완성했습니다. 이제 스케줄러에 넣어서 돌리면, 저는 테스트 서버에서 주기적으로 라이브와 동일한 상황을 볼 수 있습니다. 사실상 라이브와 똑같은 샌드박스가 생긴 셈이죠.

요즘 스케줄러 쓴다고 하면 Airflow부터 떠올리시는 분이 더 많은 것 같습니다. 당장은 Crontab만으로도 충분해 보여서 매주 토요일 자정마다 한 번씩 돌도록 스케줄링을 걸어놨습니다. 넷마블 기술 블로그는 백업과 복원 과정이 대략 10분 안에 끝나는데, 개발자 사이트는 약 1시간 정도 걸리더군요. 

남은 과제

위 백업과 복원은 사실상, 도메인만 바꾸고 완전히 복제를 뜬 정도라 wp-content 디렉터리에 있는 미디어 파일을 재생성하지 않아도 모든 섬네일까지 완벽히 연결됩니다. 특히 파일 베이스로 움직이기 때문에, 구글 드라이브 등의 스토리지를 활용하는 방식으로도 연계할 여지가 많습니다.

백업과 복원을 찾을 때 도커로 워드프레스 사이트를 구성해서 올렸다 내리는 분들도 보긴 했지만, 워드프레스 사이트를 CI/CD로 완벽히 구성하고 업데이트 반영하면서 쓰는 사례는 거의 못 본 것 같습니다. 

이제 1년에 4번 정도 마주치는 워드프레스 업데이트 테스트 상황에 조금 더 편하게 대응하고  있습니다. 다만, 편하게 만나는 환경에 직면한 작업자는 저를 포함해 극소수밖에 되지 않아, 단순히 시간 효용을 계산하자면 큰 이득을 얻진 못했다고도 볼 수도 있습니다. 그래도 업데이트 테스트를 빨리 마치고, 안정적인 서비스를 더 빨리 사용자에게 제공할 수 있다는 것만으로도 충분한 이득이 아닐까 합니다. 개발자 사이트는 직접 워드프레스에 로그인해서 글을 쓰는 사용자가 꽤 많이 있습니다. 업데이트로 구텐베르크 에디터의 버그가 줄어들수록, 편의 기능이 늘어날수록, 그 이점을 함께 누릴 수 있는 사람이 많아진다는 의미도 되니까요.

여기까지 글을 정리하면서 되돌아보니, 복원 스크립트는 백업 스크립트처럼 함수화하지 않고 바로 직렬 실행하도록 구성했었네요. 스크립트 함수화 또는 Airflow 적용은 남은 과제 중 하나가 됐네요. (뭔가 한가는 2024년에 새 태스크로 마주칠 것 같은 기분이 듭니다.)

2023년을 돌아보다가 급작스레 옆길로 샜습니다. 조만간 2023년 넷마블 기술 블로그 회고로 돌아오겠습니다.