From 94426240d64e31c6ec5f7f9e1f65ca1054ebca10 Mon Sep 17 00:00:00 2001 From: jmj Date: Sat, 26 Sep 2026 15:45:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(infra):=20=EC=9A=B4=EC=98=81=20DB=C2=B7?= =?UTF-8?q?=EC=98=A4=EB=B8=8C=EC=A0=9D=ED=8A=B8=20=EB=B0=B1=EC=97=85=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20+=20=EC=9D=BC=201?= =?UTF-8?q?=ED=9A=8C=20cron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 점검 결과 **백업이 하나도 없었다.** 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 에 명시). --- infra/backup/README.md | 61 ++++++++++++++++++++++++++++++++++++++++++ infra/backup/backup.sh | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 infra/backup/README.md create mode 100755 infra/backup/backup.sh diff --git a/infra/backup/README.md b/infra/backup/README.md new file mode 100644 index 00000000..16b4f496 --- /dev/null +++ b/infra/backup/README.md @@ -0,0 +1,61 @@ +# 운영 백업 + +2026-09-26 이전까지 **백업이 하나도 없었다.** 사용자 계정·면접 기록·피드백·이력서 파일이 +전부 도커 볼륨 단일 사본에만 있었고, `docker compose down -v`(이 레포가 초기화 절차로 +안내하는 명령) 한 번이면 전부 사라지는 상태였다. + +## 무엇을 받는가 + +| 대상 | 방식 | 크기(2026-09-26) | +|---|---|---| +| PostgreSQL | `pg_dump -Fc` (압축 + 선택 복원 가능) | 688 KB | +| MinIO 오브젝트 | 볼륨 tar.gz (이력서·음성·TTS·분석 원문) | 246 MB | + +RabbitMQ 볼륨은 받지 않는다 — 큐는 재선언되고 메시지는 휘발성이다. + +## 언제 도는가 + +`crontab` 매일 04:00 KST. 보관 14일(`BACKUP_RETENTION_DAYS`), 저장 위치 `~/backups` +(`BACKUP_DIR`). + +```cron +0 4 * * * /home/stackup/stackup/infra/backup/backup.sh >> /home/stackup/backups/backup.log 2>&1 +``` + +## 백업이 살아 있는지 확인 + +cron 출력은 아무도 보지 않으므로 **성공 시각 마커**를 남긴다. 이게 하루 이상 낡았으면 +백업이 멈춘 것이다. + +```bash +cat ~/backups/LAST_SUCCESS # 예: 2026-09-26T15:44:58+09:00 +ls -lh ~/backups/ +tail -20 ~/backups/backup.log +``` + +## 복원 + +```bash +# PostgreSQL — 임시 DB 로 먼저 받아 확인한 뒤 교체하는 것을 권장 +docker exec stackup-postgres psql -U stackup -d postgres -c "CREATE DATABASE restore_test" +docker exec -i stackup-postgres pg_restore -U stackup -d restore_test --no-owner < ~/backups/pg-.dump +docker exec stackup-postgres psql -U stackup -d restore_test -c "SELECT count(*) FROM interview_sessions" + +# MinIO — 컨테이너를 내리고 볼륨에 풀어 넣는다 +docker compose stop minio +docker run --rm -v stackup_minio_data:/dst -v ~/backups:/src busybox \ + sh -c "rm -rf /dst/* && tar xzf /src/minio-.tar.gz -C /dst" +docker compose start minio +``` + +2026-09-26 에 실제 복원을 검증했다 — 20개 테이블, `users`/`interview_sessions`/ +`interview_messages`/`session_feedbacks`/`document_embeddings`/`analyzed_documents`/ +`resumes` 행 수가 원본과 전부 일치. + +## 한계 — 아직 못 막는 것 + +**같은 디스크에 저장한다.** 실수 삭제·잘못된 마이그레이션·논리적 손상은 막지만 +**디스크·호스트 장애는 못 막는다.** 원격 복제(S3·다른 호스트 rsync)는 후속 과제다. + +또한 스크립트가 실패하면 cron 은 조용하다. `LAST_SUCCESS` 를 주기적으로 보는 사람이 +없으면 결국 같은 함정이므로, 헬스체크에 마커 신선도를 넣는 것이 다음 개선이다. diff --git a/infra/backup/backup.sh b/infra/backup/backup.sh new file mode 100755 index 00000000..50117be6 --- /dev/null +++ b/infra/backup/backup.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# StackUp 운영 백업 — PostgreSQL 덤프 + MinIO 오브젝트 아카이브. +# +# 왜 있나: 2026-09-26 점검 시점까지 백업이 **하나도 없었다**. 모든 사용자 계정·면접 +# 기록·피드백·이력서 파일이 도커 볼륨 단일 사본에만 존재했고, `docker compose down -v` +# 한 번이면 전부 사라지는 상태였다(infra/CLAUDE.md 가 그 명령을 초기화 절차로 안내한다). +# +# 한계: 같은 디스크에 저장하므로 **논리적 사고**(실수 삭제·잘못된 마이그레이션·손상)만 +# 막는다. 디스크·호스트 장애는 못 막는다 — 원격 복제는 후속 과제(docs/operations.md). +set -Eeuo pipefail + +BACKUP_DIR="${BACKUP_DIR:-$HOME/backups}" +RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}" +PG_CONTAINER="${PG_CONTAINER:-stackup-postgres}" +PG_USER="${PG_USER:-stackup}" +PG_DB="${PG_DB:-stackup}" +MINIO_VOLUME="${MINIO_VOLUME:-stackup_minio_data}" + +stamp="$(date +%Y%m%d-%H%M%S)" +log() { echo "[$(date +%H:%M:%S)] $*"; } +fail() { log "FAILED: $*"; exit 1; } + +mkdir -p "$BACKUP_DIR" + +# ── PostgreSQL ──────────────────────────────────────────────────────────────── +# -Fc(custom) 은 압축 + pg_restore 로 선택 복원이 가능하다. 평문 SQL 보다 낫다. +pg_file="$BACKUP_DIR/pg-$stamp.dump" +log "pg_dump -> $(basename "$pg_file")" +docker exec "$PG_CONTAINER" pg_dump -U "$PG_USER" -Fc "$PG_DB" > "$pg_file" \ + || fail "pg_dump 실패" + +# 덤프가 실제로 복원 가능한 형식인지 즉시 검증한다 — 0바이트나 깨진 파일을 +# 성공으로 세면 백업이 있다는 착각만 남는다(오늘 고친 버그들과 같은 함정). +docker exec -i "$PG_CONTAINER" pg_restore --list > /dev/null < "$pg_file" \ + || fail "덤프 검증 실패 — 복원 불가능한 파일" +pg_tables=$(docker exec -i "$PG_CONTAINER" pg_restore --list < "$pg_file" | grep -c "TABLE DATA" || true) +[ "$pg_tables" -gt 0 ] || fail "덤프에 테이블 데이터가 없다 (tables=$pg_tables)" + +# ── MinIO (이력서·음성·TTS·분석 원문) ────────────────────────────────────────── +minio_file="$BACKUP_DIR/minio-$stamp.tar.gz" +log "minio archive -> $(basename "$minio_file")" +# --user 없이 돌리면 결과물이 root 소유가 되고, 아래 보존 삭제(find -delete)가 +# 조용히 실패해 아카이브가 무한정 쌓인다. +docker run --rm --user "$(id -u):$(id -g)" \ + -v "$MINIO_VOLUME":/src:ro -v "$BACKUP_DIR":/dst busybox \ + tar czf "/dst/$(basename "$minio_file")" -C /src . || fail "MinIO 아카이브 실패" +tar tzf "$minio_file" > /dev/null || fail "MinIO 아카이브 검증 실패" + +# ── 보존 ────────────────────────────────────────────────────────────────────── +deleted=$(find "$BACKUP_DIR" -maxdepth 1 -name 'pg-*.dump' -o -name 'minio-*.tar.gz' \ + | wc -l) +find "$BACKUP_DIR" -maxdepth 1 \( -name 'pg-*.dump' -o -name 'minio-*.tar.gz' \) \ + -mtime +"$RETENTION_DAYS" -delete + +# 성공 시각 마커. cron 출력은 아무도 안 보므로, "언제 마지막으로 성공했는가" 를 +# 파일 하나로 남겨 둔다 — 백업이 조용히 멈춘 것과 정상인 것을 구분할 유일한 단서다. +date -Iseconds > "$BACKUP_DIR/LAST_SUCCESS" + +log "done. pg=$(du -h "$pg_file" | cut -f1) (tables=$pg_tables), minio=$(du -h "$minio_file" | cut -f1), 보관=$deleted개, 보존=${RETENTION_DAYS}일"