mirim-app/deploy/restore-db.sh
AWESOMEDEV 44487d5746 feat(ops): DB 자동 백업 — 매일 S3 업로드, 30일 보관
- 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>
2026-07-17 08:13:37 +09:00

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"