- 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>
73 lines
3.3 KiB
Bash
73 lines
3.3 KiB
Bash
#!/bin/bash
|
|
# 이 파일이 하는 일: 백업 파일로 DB를 되돌린다.
|
|
#
|
|
# 학습 포인트 — "복원해 본 적 없는 백업은 백업이 아니다."
|
|
# 백업 스크립트를 짜두고 안심하다가, 정작 사고가 났을 때 복원이 안 되는 일이 실제로 자주 있다.
|
|
# (덤프가 비어 있었거나, 복원 명령을 아무도 몰랐거나, 권한이 없었거나…)
|
|
# 그래서 백업 스크립트를 만들면 복원 스크립트도 같이 만들고, 한 번은 실제로 돌려봐야 한다.
|
|
#
|
|
# 사용법:
|
|
# ./restore-db.sh # 백업 목록 보기
|
|
# ./restore-db.sh mirim-2026-07-17_030000.sql.gz # 그 파일로 복원
|
|
set -euo pipefail
|
|
|
|
BUCKET="awesomedev-mirim-backup-apne2"
|
|
CONTAINER="mirim-postgres"
|
|
DB_NAME="mirim"
|
|
DB_USER="mirim"
|
|
BACKUP_DIR="/home/ec2-user/backups"
|
|
|
|
# 인자가 없으면 어떤 백업이 있는지 보여주고 끝낸다.
|
|
if [ $# -eq 0 ]; then
|
|
echo "=== 서버 로컬 백업 ==="
|
|
ls -lh "$BACKUP_DIR"/mirim-*.sql.gz 2>/dev/null || echo " (없음)"
|
|
echo ""
|
|
echo "=== S3 백업 (최근 10개) ==="
|
|
aws s3 ls "s3://${BUCKET}/postgres/" | tail -10 || echo " (없음)"
|
|
echo ""
|
|
echo "사용법: $0 <파일이름>"
|
|
echo " 예: $0 mirim-2026-07-17_030000.sql.gz"
|
|
exit 0
|
|
fi
|
|
|
|
FILENAME="$1"
|
|
LOCAL_PATH="${BACKUP_DIR}/${FILENAME}"
|
|
|
|
# 로컬에 없으면 S3에서 받아온다.
|
|
if [ ! -f "$LOCAL_PATH" ]; then
|
|
echo "로컬에 없어 S3에서 내려받습니다: ${FILENAME}"
|
|
aws s3 cp "s3://${BUCKET}/postgres/${FILENAME}" "$LOCAL_PATH"
|
|
fi
|
|
|
|
# ⚠️ 되돌릴 수 없는 작업 앞에는 반드시 확인을 세운다.
|
|
# 학습 포인트: rm -rf, DROP TABLE, 복원처럼 "되돌리기 어려운 명령"은
|
|
# 사람이 한 번 더 눈으로 확인하게 만든다. 자동화의 예외를 두는 지점이다.
|
|
echo ""
|
|
echo "⚠️ 현재 DB(${DB_NAME})의 데이터를 백업 시점으로 되돌립니다."
|
|
echo " 파일: ${FILENAME}"
|
|
echo " 지금 DB에만 있는 최신 데이터(학생 진도·제출물)는 사라집니다."
|
|
read -p "정말 진행할까요? (yes 입력): " CONFIRM
|
|
if [ "$CONFIRM" != "yes" ]; then
|
|
echo "취소했습니다."
|
|
exit 0
|
|
fi
|
|
|
|
# 복원 전에 "지금 상태"를 한 번 더 백업해 둔다 — 복원이 잘못됐을 때 돌아올 자리.
|
|
# 학습 포인트: 복원도 실수할 수 있다. 되돌리기의 되돌리기를 준비해 두는 것이 프로다.
|
|
SAFETY="${BACKUP_DIR}/before-restore-$(date +%Y-%m-%d_%H%M%S).sql.gz"
|
|
echo "복원 전 현재 상태를 저장합니다: ${SAFETY}"
|
|
docker exec "$CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" | gzip > "$SAFETY"
|
|
|
|
# 복원: 기존 스키마를 지우고 덤프를 그대로 흘려 넣는다.
|
|
# 학습 포인트: pg_dump 결과는 그냥 SQL 문장 모음이라, psql에 파이프로 먹이면 그대로 재현된다.
|
|
echo "복원 중..."
|
|
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" > /dev/null
|
|
gunzip -c "$LOCAL_PATH" | docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" > /dev/null
|
|
|
|
echo "복원 완료 ✅"
|
|
echo "확인:"
|
|
docker exec "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -t -c \
|
|
"SELECT ' 사용자 '||count(*)||'명' FROM users UNION ALL SELECT ' 퀴즈문항 '||count(*)||'건' FROM quiz_question;"
|
|
echo ""
|
|
echo "백엔드를 재시작하세요: cd ~/mirim-app/deploy && docker compose -f docker-compose.prod.yml restart backend"
|