S3 백업과 복원 리허설, 운영 점검표
백업은 복원해 봐야 백업이다
백업 파일을 쌓아 두기만 하면, 정작 필요할 때 "파일이 깨져 있다", "복원 순서를 모른다"가 된다. 그래서 백업을 만든 날 실제로 되살려 보는 것(복원 리허설) 까지 해야 끝이다.
이 블로그에서 잃으면 안 되는 것은 두 가지다.
| 무엇 | 어디 | 백업 방법 |
|---|---|---|
| 노트·댓글·이력 | PostgreSQL(pgdata 볼륨) |
pg_dump -Fc (압축된 PostgreSQL 전용 형식, pg_restore로 복원) |
| 업로드 이미지 | images 볼륨 |
폴더째 복사 |
둘을 saip-YYYYMMDD.tar.gz 하나로 묶어 서버 밖(S3) 에 둔다. 서버가 통째로 사라져도 백업은 남는다.
flowchart LR
cron([cron 매일 04:00]) --> sh["`**backup.sh**
pg_dump -Fc
이미지 복사
tar czf`"]
sh -- PutObject만 --> s3@{ shape: cyl, label: "S3 daily/ (서울)" }
s3 -- 14일 뒤 --> gone([수명 주기 규칙이 삭제])
s3 -. 콘솔에서 관리자가 받기 .-> pc["`**복원**
pg_restore --clean`"]
class s3 ext
classDef ext stroke-dasharray: 4 3
S3와 수명 주기 규칙
- S3: AWS의 파일 보관소. 수십 KB짜리 백업 14개면 요금은 사실상 0이다.
- 수명 주기 규칙: "접두사
daily/, 객체 생성 14일 후 만료". 오래된 백업을 스크립트로 지우지 않아도 S3가 알아서 지운다. - 리전 분리: 서버는 시드니, 버킷은 서울. 한 리전에 장애가 나도 다른 쪽은 남는다.
- 버킷 만들 때 퍼블릭 액세스 차단은 그대로, 기본 암호화(SSE-S3)도 그대로.
Note
버킷 네임스페이스에서 "계정 리전 네임스페이스(권장)"는 이름 뒤에 계정 ID와 리전이 붙어 남이 같은 이름을 선점할 수 없게 한다. 예측 가능한 이름(회사-logs)을 지웠다가 남이 같은 이름으로 만들어 데이터를 가로채는 일을 막는다. 이번엔 이름 끝에 임의 문자를 붙여 선점 걱정이 없어서 글로벌 네임스페이스로 했다.
IAM 최소 권한: 올리기만 되는 키
서버에 AWS 키를 둬야 S3에 올릴 수 있다. 그런데 서버가 털리면 키도 같이 털린다. 그래서 키 권한을 daily/에 파일 올리기 하나만 준다.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::<버킷>/daily/*"
}]
}
실제로 확인한 결과:
| 이 키로 | 결과 |
|---|---|
올리기 (aws s3 cp → S3) |
✅ 됨 |
목록 보기 (aws s3 ls) |
❌ 거부 |
| 내려받기 | ❌ 거부 |
삭제 (aws s3 rm) |
❌ 거부 |
공격자가 키를 손에 넣어도 지난 백업을 지우거나 읽을 수 없다. 대신 복원할 때는 관리자가 콘솔에서 내려받는다.
키는 서버에서만 입력한다(대화창·저장소에 붙여 넣지 않는다).
sudo snap install aws-cli --classic
aws configure --profile saip-backup
backup.sh
#!/usr/bin/env bash
set -euo pipefail # 한 줄이라도 실패하면 바로 멈추고 0이 아닌 종료 코드
cd ~/saip
BUCKET=$(sed -n 's/^BACKUP_BUCKET=//p' .env)
[ -n "$BUCKET" ] || { echo "backup FAILED: .env에 BACKUP_BUCKET이 없다" >&2; exit 1; }
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT # 성공하든 실패하든 임시 폴더는 지운다
name=saip-$(date +%Y%m%d).tar.gz
docker compose exec -T db pg_dump -U blog -Fc blog > "$work/db.dump"
docker compose cp app:/data/images "$work/images" > /dev/null
tar czf "$work/$name" -C "$work" db.dump images
aws s3 cp "$work/$name" "s3://$BUCKET/daily/$name" --profile saip-backup --only-show-errors
echo "$(date '+%F %T') backup ok $name $(du -h "$work/$name" | cut -f1)"
exec -T: 터미널 없이 실행(cron에는 터미널이 없다).trap ... EXIT: 스크립트가 어떻게 끝나든 정리 코드를 실행한다.- 버킷 이름을 일부러 틀리게 해 보니
NoSuchBucket오류와 종료 코드 1. 실패가 로그에 남는다.
cron
리눅스의 예약 실행. 다섯 칸은 분 시 일 월 요일이다.
# 매일 04:00. 서버 시간대를 Asia/Seoul로 맞춰 두어서 한국 시간
0 4 * * * bash ~/saip/backup.sh >> ~/saip/backup.log 2>&1
>> backup.log 2>&1로 정상 출력과 오류를 모두 로그 파일에 쌓는다. 확인은 tail ~/saip/backup.log.
복원 리허설
- S3 콘솔에서 백업 파일을 내려받는다(서버 키로는 못 받으니 관리자 권한으로).
- 로컬 앱을 멈추고 로컬 개발 DB에 덮어쓴다.
mkdir restore && tar xzf saip-YYYYMMDD.tar.gz -C restore
docker cp restore/db.dump saip-postgres-1:/tmp/db.dump
docker exec saip-postgres-1 pg_restore -U blog -d blog --clean --if-exists --no-owner /tmp/db.dump
cp -r restore/images/. data/images/
--clean --if-exists: 있는 테이블은 지우고 다시 만든다(없으면 조용히 넘어감).--no-owner: 원래 소유자 정보 대신 지금 접속한 사용자로 만든다.
- 앱을 띄워 운영과 비교 → 노트 링크 목록과 본문 HTML이 완전히 같았다.
서버가 통째로 사라졌을 때의 순서(새 인스턴스 → 설치 → .env → DB만 띄워 복원 → 앱·Caddy → 배포 재실행)는 저장소의 deploy/restore.md에 적어 두었다.
운영 점검표
cd ~/saip
docker compose ps # 셋 다 Up, db는 healthy
docker compose logs --since 1h app | grep -iE "error|exception"
docker compose logs -f --since 10m app # 실시간(Ctrl+C로 종료)
tail -3 backup.log # 매일 "backup ok"
free -m # 메모리, available 확인
df -h / # 디스크 사용률
docker system df # 이미지·볼륨이 차지하는 크기
curl -svI https://<도메인> 2>&1 | grep -i "expire date" # 인증서 만료일
처음 돌려 본 결과: 메모리 909MB 중 available 280MB(스왑 222MB 사용), 디스크 17%, 인증서 2027-01-22까지.
겪은 문제
배포할 때마다 이미지가 쌓였다
docker system df를 보니 이미지가 7개, 1.38GB. 커밋 SHA마다 새 이미지가 남고, 태그가 붙어 있어서 docker image prune(태그 없는 것만 지움)으로도 안 지워진다. 40GB 디스크면 몇 달 뒤 꽉 찬다.
배포 끝에 쓰지 않는 이미지를 전부 지우게 했다.
docker compose pull app && docker compose up -d && docker image prune -af > /dev/null
-a는 태그가 있어도 실행 중이 아니면 지운다. 되돌리기에는 지장이 없다. 이전 SHA 이미지는 ghcr에 그대로 있고, docker compose up이 없는 이미지를 받아 온다. 정리 후 이미지는 3개(postgres, caddy, app).
브라우저가 .tar.gz를 .tar로 저장했다
S3 콘솔에서 받은 파일 이름이 .tar였다. 첫 두 바이트가 1f 8b(gzip 표시)라 내용은 그대로 gzip이고, tar xzf로 풀린다.
head -c 2 파일 | od -An -tx1 # 1f 8b 이면 gzip
댓글 0
로그인하고 댓글 달기