5일차/ 운영 서버에서 백업과 SSL 인증서 갱신 자동화 구성

5일차/ 운영 서버에서 백업과 SSL 인증서 갱신 자동화 구성

서버 운영 시 중요한 루틴 중 하나로 불리는 정기적인 백업. 그런 밈도 있지 않은가. 디자인과 사람들이 같이 작업하다가 누군가 비명 지르면 모두가 차분하게 ctrl(⌘) + s를 누른다고.. 여하튼 나도 블로그 웹사이트를 만들며 아래와 같은 생각을 했었다. 도커 상태에 따라 웹사이트가 아예 죽기도 했던 경험을 해서 더욱 그러하다.

  1. 이거 백업 안 하고 운영하다가 언젠가 다 날릴 수도 있지 않을까? 그럼 어떻게 복구하지. 고생해서 만들었는데..
  2. SSL 인증서 만료되면 곧바로 서비스가 중단될 수 있을 텐데..

누구나 한 번쯤은 해보는 생각이지만 '지금 당장 안 해도 되잖아'라는 이유로 미뤄두기 쉬운 일이다. 그래서인지 강의에도 운영 관리에 대한 파트가 포함되어 있었고, 그에 따라 자동화를 직접 수행해보았다. crontab, 스크립트, 로그 관리까지 정리해보았다.

  • Ghost 콘텐츠가 저장된 Docker 볼륨 → 매주 자동 백업
  • 백업 .tar.gz 파일 → 날짜별 최신 5개만 남기고 자동 정리
  • certbot Docker 컨테이너 → 인증서 주기적 갱신
  • 각 작업 → crontab + 로그 파일로 상태 확인 가능

백업 자동화가 필요한 이유

백업은 단순히 데이터를 지키는 것 이상의 의미가 있다

  • 책임감: 운영 환경에서는 내가 작성한 코드가 실제 사용자에게 영향을 미친다
  • 자동화 사고: 반복적인 작업을 자동화하는 개발자적 사고방식 기르기
  • 운영 마인드: 개발뿐만 아니라 서비스 운영에 대한 이해도 높이기

먼저 도커 볼륨 백업 파일로 저장

docker run --rm -v ghost_content:/volume -v $(pwd):/backup alpine sh -c
"cd /volume && tar czf /backup/ghost_content_backup_$(date +%Y%m%d).tar.gz
."

ls 로 잘 백업되었는지 확인해보니 잘 되어 있음. 백업본으로 복원하고 싶다면 아래 코드를 입력.

docker run --rm -v ghost_content:/volume -v $(pwd):/backup alpine sh -c
"cd /volume && tar xzf /backup/ghost_content_backup_YYYYMMDD.tar.gz"

1. Ghost Docker 볼륨 백업 자동화

Ghost는 모든 콘텐츠를 Docker 볼륨(ghost_content)에 저장한다. 따라서 이 볼륨을 crontab을 활용해 주기적으로 .tar.gz 파일로 백업해두는 게 좋다.

우선 리눅스에서 크론탭 편집 모드로 들어간다.

crontab -e

처음 실행 시 사용할 편집기를 선택하라는 메시지가 뜨는데 초보자들도 쓰기 쉽다는 nano를 선택했다. vi 에디터도 써보고 싶은데 이건 나중에 좀 더 공부해보고 시도해봐야 할 것 같다.

1.의 nano 선택 후크론탭 편집 모드에 진입하면 아래처럼 기본 설명이 표시된다.

crontab에 백업 명령 작성

0 3 * * 0 /usr/bin/docker run --rm -v ghost_content:/volume -v /home/ubuntu:/backup alpine sh -c "cd /volume && tar czf /backup/ghost_content_backup_$(date +\%Y\%m\%d).tar.gz ."
  • 매주 일요일 오전 3시 실행
  • ghost_content: Docker 볼륨 이름
  • /home/ubuntu: 백업 파일이 저장될 디렉토리
  • tar.gz: 날짜 기반 압축 파일로 저장
  • /home/ubuntu/ghost_content_backup_YYYYMMDD.tar.gz 형식으로 저장
  • %는 crontab에서 특수문자라서 \%로 이스케이프하지 않으면 저장이 안 된다.
    나는 처음 저장할 때 crontab이 깨지는 경험을 했다.
nano 에디터에서 크론탭 저장하는 법
- 저장: Ctrl + OEnter
- 종료: Ctrl + X

크론 표현식 참고

크론탭 문법이 처음이라면 이 표를 참고해보자.

# m h dom mon dow command
필드 의미
m 분 (0–59)
h 시 (0–23)
dom 일 (1–31)
mon 월 (1–12)
dow 요일 (0–7, 일요일은 0 또는 7)
command 실행할 명령어

예를 들어 매일 새벽 3시에 명령어를 실행하고 싶다면 다음처럼 쓴다. 온라인 cron 표현식 생성기를 활용하면 쉽게 작성 가능하다.

0 3 * * * /home/ubuntu/backup.sh

수동 실행 테스트와 결과 메시지

기존에 입력한 크론 명령어는 매주 일요일 새벽 3시에 백업을 수행하도록 하는 것이었다. 그때까지 기다릴 수 없으니 정상 작동 여부를 수동 명령어를 통해 확인해야 한다.

docker run --rm -v ghost_content:/volume -v /home/ubuntu:/backup alpine sh -c "cd /volume && tar czf /backup/ghost_content_backup_$(date +%Y%m%d).tar.gz ."

그럼 ls 입력 시 ghost_content_backup_20250716.tar.gz 파일이 생성되어 있는 게 보인다.


2. 백업 파일 정리 자동화 (최신 5개만 유지)

백업 파일 .tar.gz 가 백업 파일이 무한히 쌓이면 디스크 용량 문제가 생길 수 있다. 용량도 용량이지만 쌓이기만 하고 관리되지 않는 자동화는 결국 또다른 부채가 된다. 최신 5개만 유지하고 나머지를 자동 삭제하는 셸 스크립트를 작성했다.

스크립트 : cleanup_old_backups.sh
#!/bin/sh
# 백업 디렉토리 경로
BACKUP_DIR="/home/ubuntu"

# 백업 파일 목록에서 최신 5개를 제외한 나머지를 삭제
cd "$BACKUP_DIR"
ls -1t ghost_content_backup_*.tar.gz | tail -n +6 | while read file; do
  rm -f "$file"
done
  • 가장 최근 5개를 제외한 .tar.gz 파일을 삭제
  • xargs 대신 while read를 쓴 이유: 파일명에 공백이 있어도 안전

처음에는 이렇게 썼지만 ls -lt ghost_content_backup_*.tar.gz | tail -n +6 | xargs rm -f 결과는 rm: invalid option – 'w' 였다. ls 결과의 첫 컬럼이 파일명이 아니라 권한(-rw-r--r--) 이다 보니 xargs가 제대로 동작하지 않았다.

ls -lt는 파일 목록을 다음과 같이 rm -f -rw-r--r– 1 ubuntu ubuntu 20480 Jul 20 10:31 ghost_content_backup_20240720.tar.gz 로 전체를 출력한다. 이때 xargs rm -f가 같이 쓰이면 전체 라인을 하나의 인자로 받아서 rm-rw-r--r--를 옵션으로 잘못 인식하고 오류를 낸다. 긴 줄 전체가 xargs로 넘어가면서 rm이 엉뚱한 걸 인자로 받게 된 상황이다.

ls -lt 에서 -l-t의 결합은 아래와 같다.
-l → 긴 형식 출력 (파일 메타 정보까지 포함)
-t → 파일을 수정 시간 기준으로 내림차순 정렬

이는 -l-1대체하고 while read 조합으로 하면 해결된다.
-1(숫자 1) → 한 줄에 파일 하나씩 출력하라는 옵션
while read file은 각 줄을 하나씩 읽고 공백이 있는 파일명도 안전하게 처리

정상 작동: ls -1t + while read
"파일 목록을 최근 수정 시간 순으로 정렬, 각 파일 이름을 한 줄에 하나씩 출력"
xargs에 대해서도 더 알아봤다.
표준 입력(stdin)으로 받은 값을 명령어의 인자(argument)로 넘겨주는 도구
어떤 명령어의 인자(파일 이름, 문자열 등)가 파일에서 나오거나 파이프로 전달되는 경우, xargs는 그 값을 받아서 명령어 뒤에 자동으로 붙여주는 역할. xargs는 특히 파이프(|)와 함께 쓰일 때 진가를 발휘한다.

예시)
cat list.txt | xargs echo

list.txt
에 [apple, banana, carrot]가 담겨 있다면

내부적으로 아래처럼 변환돼 실행
echo apple banana carrot

crontab에 추가 입력

앞서 1. 단계에서는 백업을 주기적으로 하는 크론 명령을 넣었다. 이 단계에서는 저장된 백업 파일들을 자동으로 정리하는 명령을 추가로 작성해서 넣는다. 위에서 짜놓은 스크립트가 정해진 시간에 실행되도록 하는 것이다.

0 0 1 * * /home/ubuntu/cleanup_old_backups.sh > /home/ubuntu/cleanup.log 2>&1
  • 매월 1일 자정 실행
  • 최근 실행 로그 덮어쓰기
crontab: installing new crontab

입력해 둔 크론탭을 확인하고 싶을 때에는 crontab -l 을 입력하면 된다.


3. SSL 인증서 갱신 자동화 (Let's Encrypt + certbot)

이제 마지막 단계다. 인증서는 Let's Encrypt를 쓰고 있었고 기존에 certbot을 Docker 컨테이너로 실행하고 있었다. 인증서 자동 갱신 스크립트를 짜서 주 1회 자동 실행, 인증서는 만료 30일 이내일 때만 실제 갱신이 진행된다.

스크립트: renew_cert.sh
#!/bin/sh
docker run --rm \
  -v "/home/ubuntu/103_ONEDEV_STEP3/certbot-etc:/etc/letsencrypt" \
  -v "/home/ubuntu/103_ONEDEV_STEP3/nginx1:/usr/share/nginx/html" \
  certbot/certbot renew --webroot --webroot-path=/usr/share/nginx/html
  • 실제 갱신은 인증서 만료 30일 전부터만 시도됨

crontab 등록

0 0 * * 0 /home/ubuntu/103_ONEDEV_STEP3/renew_cert.sh >> /home/ubuntu/103_ONEDEV_STEP3/renew_cert.log 2>&1
  • 매주 일요일 자정 실행
  • 로그는 renew_cert.log에 누적 저장되도록 처리
    • /home/ubuntu/103_ONEDEV_STEP3/renew_cert.log 2>&1
    • 처음에 위 부분을 빼먹어서 로그 파일이 생성되지 않았다. 직접 실행할 때에도 >> logfile 2>&1 을 붙이면 로그 파일 저장 가능

renew_cert.sh 스크립트를 실행해보면 아래와 같은 로그가 나온다. 나의 인증서 만료 기한은 10-13일이므로 만료 30일 이내가 되지 않았기에 갱신을 시도하지 않는다는 메시지이다.

The following certificates are not due for renewal yet:
  /etc/letsencrypt/live/give-it-a-shot.site/fullchain.pem expires on 2025-10-13 (skipped)
No renewals were attempted.
실행하려는데 Permission denied가 뜬다면 실행 권한을 먼저 부여
chmod +x /home/ubuntu/103_ONEDEV_STEP3/renew_cert.sh

4. 최종 crontab 요약

  1. ghost docker 볼륨 백업 자동화
# 1. 매주 일요일 03:00 백업 생성
0 3 * * 0 /usr/bin/docker run --rm -v ghost_content:/volume -v /home/ubuntu:/backup alpine sh -c "cd /volume && tar czf /backup/ghost_content_backup_$(date +\%Y\%m\%d).tar.gz ."
  1. 백업 파일 정리 자동화
# 2. 매월 1일 00:00 백업 정리 + 최근 실행 로그 덮어쓰기
0 0 1 * * /home/ubuntu/cleanup_old_backups.sh > /home/ubuntu/cleanup.log 2>&1
  1. SSL 인증서 갱신 자동화
# 3. 매주 일요일 자정 00:00에 renew_cert.sh 스크립트 실행하고 실행 결과를 renew_cert.log파일에 기록
0 0 * * 0 /home/ubuntu/103_ONEDEV_STEP3/renew_cert.sh >>/home/ubuntu/103_ONEDEV_STEP3/renew_cert.log 2>&1

이렇게 3가지를 구성해두면 운영 중인 Ghost 블로그는 다음과 같은 장점을 가진다.

  • 백업을 수동으로 할 필요도, 백업 시기를 놓치는 일도 없다.
  • 백업 파일이 알아서 관리 되니 무한정 쌓이지 않는다.
  • SSL 인증서 만료로 인한 장애를 걱정하지 않아도 된다.

Docker 기반의 워크플로우에 맞춰 자동화된 백업 + 인증서 시스템을 구성하고 싶다면 이 구성이 좋은 출발점이 될 수 있다.


마무리하며

자동화는 실행보다 정리까지 설계해야 끝난다는 걸 이번에 실감했다.
백업만 만들고 정리를 안 하면 결국 또 다른 일거리로 되돌아올 것이다.

cron은 예외처리 없는 자동화다. 작은 문법 하나, 예를 들어 % 이스케이프를 깜빡하는 것만으로 전체 스케줄이 깨진다. 한 줄 명령이 '그대로 실행될 거라 믿고 저장'하는 구조이기 때문에 더더욱 조심스럽게 다뤄야 하고 그렇기에 수동 테스트도 해봤다.

특히 xargsls 조합에서 오류를 만났을 때 출력 형식에 따라 명령어 조합이 얼마나 쉽게 어긋날 수 있는지 알게 됐다. 출력 형식 하나 바뀐 것만으로 rm이 엉뚱한 인자를 받고 실패하는 경험을 하며 -l, -1, while read 같은 옵션의 의미를 단순히 외우는 게 아니라 맥락 속에서 체득할 수 있었다. 또한 -l, -1처럼 비슷하게 생긴 문자를 입력할 때 주의가 필요할 것 같다. 왜 오류가 났는지 디버깅하는 과정에서 xargs, while read 같은 옵션들의 의미가 또렷하게 와닿았다.

로그 처리에서도 마찬가지다. >는 덮어쓰기, >>는 누적 저장. 겉보기엔 비슷하지만 결과는 완전히 다르다. 자동화의 작은 디테일 하나가 운영 효율과 안정성에 큰 차이를 만든다는 걸 체감했다.

이전에 GCP 스케줄러 처리를 앞두고 "정해진 시점에 실행되게 하는 자동화 프로세스" 정도로만 생각했었는데. 이번 작업을 통해 "어느 시점의 백업을 남겨야 하는지", "인증서 갱신은 얼마나 자주 할지" 등 운영자의 시선에서 설계해야 할 요소가 훨씬 많다는 걸 알게 됐다.