Skip to content

feat(infra): 운영 DB·오브젝트 백업 스크립트 + 일 1회 cron - #249

Merged
i3months merged 1 commit into
devfrom
feat/db-backup
Sep 26, 2026
Merged

i3months merged 1 commit into
devfrom
feat/db-backup

Conversation

@i3months

Copy link
Copy Markdown
Member

요약

점검 결과 백업이 하나도 없었습니다. 스크립트를 추가하고 운영에 설치·검증했습니다.

발견

확인 항목 결과
systemd 타이머 apt 업그레이드뿐
crontab (사용자·root) 비어 있음
덤프 파일 없음 (6월 git patch 하나뿐)

데이터가 단일 도커 볼륨에만 있었습니다 — PostgreSQL 13MB(사용자 5명, 세션 100건, 메시지 451건, 피드백 76건, 임베딩 57건), MinIO 246MB(이력서·음성·TTS·분석 원문).

배포 워크플로가 볼륨을 건드리진 않습니다(up -d --build + image prune뿐). 다만 infra/CLAUDE.md 가 초기화 절차로 docker compose down -v 를 안내하고, 같은 문서가 그걸 안티패턴으로 경고까지 합니다 — 위험을 알면서 안전망이 없는 상태였습니다.

변경 내용

infra/backup/backup.sh — pg_dump -Fc + MinIO 볼륨 tar.gz, 보존 14일, ~/backups 저장.

설계에서 신경 쓴 세 가지 (전부 오늘 겪은 함정의 재발 방지입니다):

1. 받은 즉시 검증합니다. pg_restore --list 로 복원 가능한 형식인지 + 테이블 데이터가 실제로 들어 있는지 확인하고, 아니면 실패로 끝냅니다. 0바이트나 깨진 파일을 성공으로 세면 "백업이 있다"는 착각만 남습니다.

2. MinIO tar 를 --user 로 돌립니다. 없이 돌리면 결과물이 root 소유가 되고, 보존 삭제(find -delete)가 stackup 유저 권한으로 조용히 실패해 아카이브가 무한정 쌓입니다. 시험 실행에서 실제로 root 소유로 나오는 걸 확인하고 고쳤습니다.

3. LAST_SUCCESS 마커를 남깁니다. cron 출력은 아무도 보지 않습니다. 백업이 멈춘 것과 정상인 것을 구분할 유일한 단서입니다.

검증 — 실제로 복원해봤습니다

임시 DB 로 받아 원본과 대조했습니다:

복원된 테이블 수: 20
  users                  원본 5      복원 5      OK
  interview_sessions     원본 100    복원 100    OK
  interview_messages     원본 451    복원 451    OK
  session_feedbacks      원본 76     복원 76     OK
  document_embeddings    원본 57     복원 57     OK
  analyzed_documents     원본 35     복원 35     OK
  resumes                원본 18     복원 18     OK
==> 복원 검증 통과

복원해보지 않은 백업은 백업이 아니라고 봐서 여기까지 했습니다.

운영 반영 상태 (이미 적용됨)

  • ~/stackup/infra/backup/backup.sh 설치
  • crontab: 0 4 * * * ... >> ~/backups/backup.log 2>&1
  • 첫 백업 생성 완료 (pg 688K + minio 246M, LAST_SUCCESS 기록됨)

이 PR 이 머지되면 deploy 의 rsync 가 같은 내용으로 덮어써 레포와 서버가 일치합니다.

⚠️ 한계 — 이걸로 못 막는 것

같은 디스크에 저장합니다. 실수 삭제·잘못된 마이그레이션·논리적 손상은 막지만 디스크·호스트 장애는 못 막습니다. 원격 복제(S3 또는 다른 호스트 rsync)가 다음 단계입니다.

그리고 스크립트가 실패하면 cron 은 조용합니다. LAST_SUCCESS 를 보는 사람이 없으면 결국 같은 함정이라, 헬스체크에 마커 신선도를 넣는 것을 후속으로 제안합니다(/api/system/health 에 backup 컴포넌트 추가). README 에 둘 다 명시했습니다.

리뷰어 체크포인트

  • 실행 시각 04:00 KST 가 적절한지 (백업 중 pg_dump 가 짧게 락을 잡습니다 — 13MB 라 1초 미만)
  • 보존 14일 / 저장 위치 ~/backups 가 적절한지
  • RabbitMQ 볼륨은 제외했습니다(큐 재선언 + 메시지 휘발성). 동의하시는지

🤖 Generated with Claude Code

점검 결과 **백업이 하나도 없었다.** systemd 타이머·crontab·덤프 파일 전부 없었고,
사용자 계정·면접 기록 90건·피드백 76건·이력서 파일이 도커 볼륨 단일 사본에만
존재했다. 배포 워크플로가 볼륨을 건드리진 않지만, infra/CLAUDE.md 가 초기화 절차로
안내하는 `docker compose down -v` 한 번이면 전부 사라진다 — 같은 문서가 그걸
안티패턴으로 경고까지 하면서 안전망은 없는 상태였다.

- infra/backup/backup.sh — pg_dump -Fc + MinIO 볼륨 tar.gz, 보존 14일
- **받은 즉시 검증한다**: pg_restore --list 로 복원 가능 형식인지 + 테이블 데이터가
  실제로 들어있는지 확인. 0바이트나 깨진 파일을 성공으로 세면 "백업이 있다"는
  착각만 남는다(오늘 고친 버그들과 같은 함정).
- MinIO tar 는 --user 로 실행. 없이 돌리면 결과물이 root 소유가 되고 보존 삭제가
  조용히 실패해 아카이브가 무한정 쌓인다.
- LAST_SUCCESS 마커 — cron 출력은 아무도 안 본다. 백업이 멈춘 것과 정상인 것을
  구분할 유일한 단서.
- 운영 서버에 설치 + crontab 등록(매일 04:00 KST) 완료, 첫 백업 생성됨.

실제 복원을 검증했다 — 임시 DB 로 받아 20개 테이블, users/sessions/messages/
feedbacks/embeddings/documents/resumes 행 수가 원본과 전부 일치.

한계: 같은 디스크라 논리적 사고만 막고 디스크·호스트 장애는 못 막는다.
원격 복제와 LAST_SUCCESS 신선도 헬스체크는 후속 과제(README 에 명시).
@i3months
i3months merged commit 4fd4886 into dev Sep 26, 2026
5 checks passed
i3months added a commit to i3months/stackup that referenced this pull request Oct 6, 2026
Team-StackUp#249 로 백업은 생겼지만 **실패하면 아무도 모르는** 상태였다. cron 출력은 읽는
사람이 없고 LAST_SUCCESS 를 들여다볼 사람도 없다 — 오늘 반복해서 당한
"조용히 멈춤" 패턴 그대로다.

실패 모드가 둘이고 잡는 방법이 다르다:

  1) 돌았는데 실패 (pg_dump 오류·디스크 부족) → 스크립트가 Discord 알림.
     팀이 이미 보는 채널이라 로그 파일보다 실제로 눈에 띈다.
  2) 아예 안 돎 (cron 죽음·재부팅) → 스크립트는 못 알린다. 돌지 않았으니까.
     백엔드 헬스체크가 밖에서 LAST_SUCCESS 나이를 본다(기본 36시간).

- infra/backup/backup.sh: fail() + ERR trap 에서 Discord 알림. 알림 실패는
  전부 삼켜 백업 결과를 덮지 않는다(curl -m 10, || true). cron 에는 환경변수가
  없으므로 .env 에서 웹훅 한 줄만 꺼내 쓴다(통째 source 하면 다른 값을 덮는다).
- deploy-app.yml: DISCORD_WEBHOOK_URL 을 운영 .env 에 주입(GitHub Secret 에만
  있어서 서버에서는 쓸 수 없었다).
- BackupHealthIndicator + /api/system/health 의 backup 컴포넌트.
  **전체 status 에는 반영하지 않는다** — 백업이 낡은 건 "복구 수단이 없다"이지
  "면접이 안 된다"가 아니다. aggregate 에 넣으면 업타임 감시가 헛울리고 진짜
  장애와 구분도 안 된다. ready 에서도 뺀다(트래픽 수용 가능 여부와 무관).
- compose: 백업 디렉토리를 backend 에 읽기 전용 마운트.

검증: 실패 시 exit 1 + LAST_SUCCESS 미갱신(그래야 헬스체크가 잡는다), 웹훅이
죽어도 지연 0초, 웹훅 미설정에서도 정상 동작.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant