- backup-db.sh: pg_dump | gzip | S3 업로드 + 로컬 7일 보관, 덤프 크기 검증 - restore-db.sh: 목록 조회·복원(확인 프롬프트·복원 전 자동 스냅샷) - systemd timer로 매일 03:00 KST 실행 (AL2023에 cron 없음, 이력이 journalctl에 남음) - S3 버킷: 퍼블릭 차단 + AES256 암호화 + 30일 수명주기 - IAM 역할(mirim-backup-role): 해당 버킷 postgres/ 에 PutObject만 — 최소 권한, 액세스 키 없음 - BACKUP.md: 구성·설계 이유·복원 리허설 절차 검증: 백업 성공(8.5MB→102KB), S3 업로드 확인, 임시 DB 복원 후 운영본과 건수 완전 일치 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
3.4 KiB
Bash
65 lines
3.4 KiB
Bash
#!/bin/bash
|
|
# 이 파일이 하는 일: 학습 플랫폼 DB(PostgreSQL)를 하루 한 번 통째로 백업해 S3에 올린다.
|
|
# cron이 매일 새벽 3시(KST)에 실행한다.
|
|
#
|
|
# 학습 포인트 ① — 백업의 3-2-1 원칙 ("컴퓨터 문제 해결" 코스 참고)
|
|
# 3벌: 원본(도커 볼륨) + 서버 로컬 사본 + S3 사본
|
|
# 2종: 서로 다른 매체(EC2 디스크 / S3)
|
|
# 1벌: 다른 장소(S3는 EC2와 별개 서비스 — EC2가 통째로 날아가도 S3는 남는다)
|
|
#
|
|
# 학습 포인트 ② — set -euo pipefail
|
|
# 기본 셸은 명령이 실패해도 다음 줄을 그냥 실행한다. 백업 스크립트에서 이건 최악이다:
|
|
# pg_dump가 실패했는데 빈 파일을 S3에 올리고 "성공"이라고 끝날 수 있다.
|
|
# -e: 명령 하나라도 실패하면 즉시 중단
|
|
# -u: 정의 안 된 변수를 쓰면 중단 (오타로 빈 경로에 rm 하는 사고 방지)
|
|
# -o pipefail: 파이프(a | b)에서 앞 명령이 실패해도 잡아낸다
|
|
set -euo pipefail
|
|
|
|
# ── 설정 ──
|
|
BUCKET="awesomedev-mirim-backup-apne2"
|
|
CONTAINER="mirim-postgres"
|
|
DB_NAME="mirim"
|
|
DB_USER="mirim"
|
|
BACKUP_DIR="/home/ec2-user/backups"
|
|
LOCAL_KEEP_DAYS=7 # 서버 로컬 사본 보관 기간 (S3는 30일 — 수명주기 규칙이 자동 삭제)
|
|
|
|
# 파일 이름에 날짜를 넣는다: mirim-2026-07-17_030000.sql.gz
|
|
# 학습 포인트: 날짜를 이름에 넣으면 정렬만 해도 시간순이 되고, 언제 것인지 열어보지 않아도 안다.
|
|
TIMESTAMP="$(date +%Y-%m-%d_%H%M%S)"
|
|
FILENAME="mirim-${TIMESTAMP}.sql.gz"
|
|
LOCAL_PATH="${BACKUP_DIR}/${FILENAME}"
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
echo "[$(date '+%F %T')] 백업 시작"
|
|
|
|
# ── 1) 덤프 + 압축 ──
|
|
# 학습 포인트: pg_dump는 DB 전체를 "다시 만들 수 있는 SQL 문장들"로 뽑아낸다.
|
|
# 파일 복사가 아니라 SQL이라 버전이 달라도 복원되고, 텍스트라 gzip이 아주 잘 먹는다(보통 1/5~1/10).
|
|
# docker exec로 컨테이너 안에서 실행하는 이유: pg_dump가 그 안에 있고, DB 포트를 밖에 열지 않았기 때문.
|
|
docker exec "$CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" | gzip > "$LOCAL_PATH"
|
|
|
|
# 덤프가 정말 만들어졌는지 확인 — 크기가 0이면 실패다.
|
|
# 학습 포인트: "명령이 에러 없이 끝남"과 "결과물이 쓸모 있음"은 다르다. 결과를 검증한다.
|
|
SIZE=$(stat -c%s "$LOCAL_PATH")
|
|
if [ "$SIZE" -lt 1000 ]; then
|
|
echo "[오류] 덤프 파일이 너무 작습니다(${SIZE} bytes). 백업 실패로 간주하고 중단합니다."
|
|
rm -f "$LOCAL_PATH"
|
|
exit 1
|
|
fi
|
|
echo " 덤프 완료: ${FILENAME} ($(numfmt --to=iec "$SIZE"))"
|
|
|
|
# ── 2) S3 업로드 ──
|
|
# 학습 포인트: 액세스 키를 이 파일 어디에도 적지 않았다!
|
|
# EC2에 붙인 IAM 역할(mirim-backup-role)이 임시 자격증명을 자동으로 넣어 준다.
|
|
# 키를 파일에 적으면 그 파일이 유출되는 순간 끝이지만, 역할은 이 서버에서만, 이 버킷에만 쓸 수 있다.
|
|
aws s3 cp "$LOCAL_PATH" "s3://${BUCKET}/postgres/${FILENAME}" --only-show-errors
|
|
echo " S3 업로드 완료: s3://${BUCKET}/postgres/${FILENAME}"
|
|
|
|
# ── 3) 오래된 로컬 사본 정리 ──
|
|
# S3는 수명주기 규칙이 30일 뒤 자동 삭제하므로, 여기선 서버 디스크만 관리한다.
|
|
find "$BACKUP_DIR" -name "mirim-*.sql.gz" -mtime "+${LOCAL_KEEP_DAYS}" -delete
|
|
echo " 로컬 사본 ${LOCAL_KEEP_DAYS}일 초과분 정리 완료"
|
|
|
|
echo "[$(date '+%F %T')] 백업 성공 ✅"
|