10편에서 매일 자동 백업을 세웠다. 그런데 마지막에 정직하게 인정한 한계가 있었다. 그 백업은 전부 같은 서버 안에 있다는 것. 서버가 통째로 죽으면 백업도 함께 사라진다. 이번 편은 그 절반을 마저 채운다. 백업을 rclone으로 Cloudflare R2에 자동으로 올려서, 서버가 죽어도 백업은 다른 곳에 살아 있게 만든다. 이게 진짜 오프사이트 백업이고, 재해복구의 핵심이다.
결론부터. 10편 백업 스크립트에 rclone 업로드 몇 줄을 붙여, 매일 백업을 Cloudflare R2로 자동 전송하게 만들었다. 서울 서버에서 59MB를 약 2초(~30 MiB/s)에 올렸고, 백업 생성부터 원격 전송까지 전체 7.1초. R2 무료 티어는 10GB·전송요금 $0이라, 420MB 백업으로는 비용 0원이다. 이제 서버가 사라져도 복구 재료는 클라우드에 남는다.
왜 오프사이트인가 — 같은 서버 백업의 맹점
로컬 백업은 실수를 되돌리는 데 좋다. 파일을 잘못 지웠거나 DB가 꼬였을 때, 서버 안의 백업으로 금방 복구한다. 하지만 로컬 백업이 무력해지는 순간들이 있다. 디스크 자체가 고장 나거나, 결제 문제로 서버 계정이 정지되거나, 랜섬웨어가 디스크를 통째로 암호화하는 경우다. 이때는 백업이 원본과 같은 운명을 맞는다.
재해복구의 오래된 원칙이 3-2-1이다. 사본 3개, 서로 다른 매체 2종, 그리고 1개는 반드시 물리적으로 떨어진 곳에. 10편이 “사본 여러 개”까지였다면, 이번 편은 그 “떨어진 1개”를 만드는 작업이다.
왜 Cloudflare R2인가
백업을 보낼 원격 저장소로 R2를 골랐다. 이유는 세 가지다.
| 항목 | 내용 |
|---|---|
| 무료 티어 | 매월 10GB 저장 (우리 백업은 420MB) |
| 전송(egress) 요금 | $0 — 복원할 때 내려받아도 요금 없음 |
| 호환성 | S3 호환이라 rclone·표준 도구 그대로 사용 |
특히 전송 요금 $0이 중요하다. 백업은 평소엔 올리기만 하지만, 정작 필요한 재해 순간엔 통째로 내려받아야 한다. 이때 다운로드 요금을 물리는 서비스라면 “복구하는데 돈이 든다”는 이상한 상황이 된다. R2는 내려받기가 공짜라 그 걱정이 없다.
R2 무료 티어는 신용카드(또는 결제수단) 등록을 요구하는 경우가 있다. 다만 월 10GB·요청 한도 안에서는 실제 청구가 0원이다. 우리 사용량(420MB)은 한도의 5%도 안 되니, 카드를 등록해도 과금될 일은 없다. 카드 등록이 꺼려진다면 Backblaze B2 같은 대안도 rclone 설정만 바꾸면 동일하게 동작한다.
연결 — rclone에 R2 붙이기
R2 대시보드에서 버킷(ddlabx-backup)을 만들고, API 토큰을 발급받았다. 토큰 권한은 그 버킷 하나에만, Object Read & Write로 좁게 줬다. 키가 유출돼도 피해가 이 버킷으로 한정되게 하는 최소 권한 원칙이다.
서버에는 rclone을 설치하고 설정 파일을 직접 만들었다.
# rclone 설치 (공식 스크립트, 최신 버전)
curl https://rclone.org/install.sh | sudo bash
# 설정 파일 (키는 본인 값으로)
sudo nano /root/.config/rclone/rclone.conf
[r2]
type = s3
provider = Cloudflare
access_key_id = (발급받은 Access Key ID)
secret_access_key = (발급받은 Secret Access Key)
endpoint = https://(계정ID).r2.cloudflarestorage.com
region = auto
no_check_bucket = true
# 키가 들어있으니 파일 잠금 (root만 읽기)
sudo chmod 600 /root/.config/rclone/rclone.conf
버킷 목록 조회 403은 정상이다. 연결 확인으로 rclone lsd r2:(전체 버킷 목록)를 하면 AccessDenied 403이 난다. 토큰을 버킷 하나에만 묶었기 때문이다. 이건 오류가 아니라 권한을 좁게 준 증거다. 테스트는 버킷을 콕 집어 rclone ls r2:ddlabx-backup/로 하고, no_check_bucket = true(또는 --s3-no-check-bucket)를 넣어 rclone이 목록 조회를 시도하지 않게 한다. 이걸 모르면 멀쩡한 연결을 실패로 착각한다.
실제 업로드·조회·삭제 테스트가 모두 통과하면 연결 완료다.
자동화 — 백업 스크립트에 업로드 붙이기
10편 스크립트의 보관 회전 직전에, 방금 만든 두 파일을 R2로 올리는 구간을 추가했다.
# 백업 생성 직후, 로컬 회전 앞에 추가
RCLONE_CONFIG=/root/.config/rclone/rclone.conf \
rclone copy "$DEST/db_$STAMP.sql.gz" r2:ddlabx-backup/ --s3-no-check-bucket
RCLONE_CONFIG=/root/.config/rclone/rclone.conf \
rclone copy "$DEST/files_$STAMP.tar.gz" r2:ddlabx-backup/ --s3-no-check-bucket
# 원격도 최근 7개만 유지 (로컬과 동일하게 회전)
for PREFIX in db files; do
rclone lsf r2:ddlabx-backup/ --include "${PREFIX}_*" 2>/dev/null \
| sort | head -n -$KEEP \
| while read -r OLD; do rclone delete "r2:ddlabx-backup/$OLD"; done
done
원격도 로컬과 똑같이 7개만 회전 보관한다. 안 그러면 R2에 백업이 무한정 쌓여 언젠가 무료 한도를 넘기기 때문이다. cron은 10편에서 이미 매일 새벽 3시로 걸어놨으니, 스크립트만 바꾸면 다음 새벽부터 원격 백업까지 자동으로 돈다.
실측 — 서울에서 R2까지, 얼마나 걸리나
스크립트를 돌려 백업 생성부터 R2 업로드까지 전체 시간을 쟀고, 순수 전송 속도는 따로 측정했다.
| 항목 | 값 |
|---|---|
| DB 백업 (원격) | 533KB |
| 파일 백업 (원격) | 59MB |
| 전체 (백업 생성 + 업로드) | 7.1초 |
| 순수 업로드 (59MB) | 약 2초 (~30 MiB/s) |
| R2 비용 | $0 (10GB 무료, 420MB 사용) |
서울 서버에서 해외 오브젝트 스토리지로 보내는 것치고 59MB가 약 2초는 충분히 빠르다. 전송 로그에서는 순간 39 MiB/s까지 나왔다. 매일 새벽 이 정도 부담이면, 자동 원격 백업을 망설일 이유가 없다.
업로드 속도를 측정할 때 한 가지 함정이 있다. 같은 파일을 다시 올리면 rclone이 “이미 존재함”으로 판단해 0 B 전송으로 건너뛴다. 순수 전송 속도를 재려면 speedtest/ 같은 다른 경로로 올려 측정하고, 잰 뒤 그 테스트 파일은 지우면 된다. 실제로 이걸 몰라 처음엔 0 B가 찍혀 당황했다.