vignette/docs/ops/public-db-recovery-rehearsal-2026-08-07.md
Yun Chan 16e791e044 G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

8.3 KiB

공개 DB 복구 리허설과 전환 게이트 — 2026-08-07

현재 상태

  • 2026-08-07 18:26 KST owner 승인에 따라 공개 DB를 복구 named volume으로 전환했다. 공개 API api-vignette.chanpaca.net은 이 Windows 호스트의 127.0.0.1:8001로 들어오며, vignette-dev-db:55432vignette_recovered_prod_20260807을 사용한다.
  • 18:30 KST 공개 경로 재검증 결과 owner 기준 app_user=84, sessions=30, turns=705, Google 계정 16, Google 소유 회기 30, orphan session 0이다. local/public health는 모두 environment=prod, db=true, engine=true, engine_mode=claude_cli이고 OpenAPI path는 119개다.
  • /auth/config의 Google OAuth는 configured, dev login은 disabled이며, 미인증 /auth/me는 401, Google login start는 accounts.google.com으로 302다. 공개 로그인 화면도 실제 브라우저에서 렌더됐다.
  • 복구 후보에는 유효한 auth_session을 복사하지 않았으므로 모든 사용자는 Google 재로그인이 필요하다. 계정·회기 원장은 복구됐지만 기존 로그인 쿠키가 계속 유효하다는 의미는 아니다.
  • 전환 직전 DB는 vignette-dev-db-pre-recovery-20260807-182642로 중지 보존했다. 자동 rollback을 검증한 앞선 실패 후보 2개도 중지 보존했으며, 어떤 원본 container/volume도 삭제하지 않았다.
  • VignettePublicRuntimeWatchdog를 다시 활성화했고 18:30 KST check-only에서 engine, API, web, cloudflared, public API가 모두 healthy였다.
  • active recovered DB와 cutover 스크립트는 restart=unless-stopped를 사용해 Docker 재기동 뒤에도 같은 named volume을 자동 재연결한다. 이 변경은 실행 중 container를 재시작하지 않고 적용했다.
  • 전환 전 DB는 2026-08-06 20:36 KST에 만들어진 익명 PGDATA였다. owner 기준 app_user=706, sessions=448, turns=204이지만, 대부분은 개발/E2E fixture였다.
  • RLS context 없이 app role로 조회한 sessions=0, turns=0은 삭제 증거가 아니다. owner 조회에는 행이 있고 pg_stat_user_tables.n_tup_del=0, 보존 로그의 대상 테이블 DELETE/TRUNCATE/DROP도 0건이다.
  • 운영에 가장 가까운 분리 원본은 detached Docker volume 08413cd7…cceba8이다. 원본은 읽기 전용으로 조사했으며 app_user=84, Google account 16, Google-owned sessions 29, turns 705, feedback 2,895가 있다. 마지막 사용자 갱신 흔적은 새 DB 생성보다 약 14시간 40분 앞선 2026-08-06 05:55 KST다.
  • 현재 DB에는 이 원본에 없는 Google 회기 1개가 2026-08-07 07:55 KST에 생성돼 있다. 테스트라고 단정하지 않고 current-only delta로 보존한다. 이 회기는 종료 상태이며 턴 0, Alliance Pulse 1, measurement 9, self-assessment 1, state 1, summary 1, model run 2를 가진다.

복구 가능성 증거

  • 사고 전 후보 dump: D:\workspace\vignette-backups\vignette-before-test-data-cleanup-20260715-095617.dump
  • 현재 공개 대상 보존 dump: D:\workspace\vignette-recovery\current-public-target-20260807-161953.dump
    • SHA-256 ceaf52230a8cd5dfc408c27728c894ca7290cc47d7e9a9d82e0e18e7239db145
  • 옛 원본 보존 dump: D:\workspace\vignette-recovery\recovered-original-20260807-161953.dump
    • SHA-256 9e5f2185f146a31a322dbfc18b2fa1c3bfef64e951d58b8ff8d4aef0dc26a94a
  • 격리 복구 후보:
    • container vignette-recovered-prod-candidate
    • named volume vignette_recovered_prod_20260807
    • localhost 55433
    • detached 원본과 current-only Google 회기 1개를 transaction merge
    • migrations 01~16 적용, 최신 07·16 ON_ERROR_STOP 단일 transaction 재적용 통과
    • owner 무결성: users 84, sessions 30, turns 705, Google sessions 30
    • orphan·external ID 중복·case/session_no 중복·시간 역전 0
    • 같은 source 재실행 already-applied, 추가 삽입 0
    • learner RLS simulation: 기대 session 6, app role 관측 session 6
    • post-merge dump: D:\workspace\vignette-recovery\candidate-post-delta-20260807-172059.dump
    • SHA-256 d24eaa753526dff77d889ed2a48477d4df4bc2b854878543f89f4db6f6502e32
    • 비식별 merge manifest: docs/ops/evidence/recovery-delta-merge-manifest-2026-08-07.json

백업과 복원 리허설

  • scripts/backup-vignette-db.ps1은 running container와 readiness를 먼저 확인하고, custom-format dump를 container 내부에서 pg_restore --list로 검증한 뒤에만 .partial을 최종 dump로 원자 이동한다.
  • 게시 manifest는 container ID, dump 크기, SHA-256, 검증 방식을 포함하고 DB 본문·비밀은 포함하지 않는다.
  • 2026-08-07 현재 public-target 백업: D:\workspace\vignette-backups\vignette-dev-db-vignette-20260807-080647Z.dump
    • SHA-256 c39fb23ad4845275ccf1fbd659a18bc57da768d70fa16ad8ed917551bc089cf3
    • 외부 포트 없는 일회용 DB에 pg_restore --exit-on-error 성공
    • 129 tables, 9,646 rows, users 706, sessions 448, turns 204
    • session→user/turn→session orphan 0, invalid index 0
    • temporary container/volume cleanup 뒤 잔여 0
  • 전환 직전 public-target 보존 dump: D:\workspace\vignette-backups\vignette-dev-db-vignette-20260807-091057Z.dump
    • SHA-256 6b84d5c8fd82edd1c40f407e4b5d28e5c32ea6c00c953aaeb2ff5551f4e8af1f
  • engine 설정을 현재 공개 런타임과 맞춘 최종 복구 DB 보존 dump: D:\workspace\vignette-recovery\vignette-recovered-prod-candidate-vignette-20260807-092630Z.dump
    • SHA-256 d7bcc396972482ea7691b3d25f923f92bb4e05c3a54fdc7d50954144a48868ed
  • 공개 전환 후 active recovered DB 보존 dump: D:\workspace\vignette-backups\vignette-dev-db-vignette-20260807-095511Z.dump
    • SHA-256 660695c9b3590198de35d6027b70b2df8ffb5a684ccd1572415b8b2a3865bb88
    • custom archive TOC 검증과 원자 게시 manifest 생성 완료
  • 예약 실행, 실패 알림, NAS/off-host 암호화 복제, restore drill 주기와 보존 정책은 별도 운영 게이트다.

확인된 재발 메커니즘과 패치

기존 scripts/dev-up.ps1vignette-dev-db가 실행 중이 아니면 컨테이너를 제거하고 PGDATA mount 없는 새 컨테이너를 만들었다. 기존 익명 volume은 삭제되지 않지만 서비스가 새 빈 volume을 보게 되는 형태가 현재 결과와 일치한다. 보존된 Docker event가 없어 실행 actor를 단정하지는 않는다.

재발 방지 패치는 다음 계약으로 바꿨다.

  1. 정지된 기존 DB는 제거하지 않고 docker start를 우선한다.
  2. 새 DB는 고정 named volume vignette-dev-db-pgdata를 명시한다.
  3. 일반 dev-up에는 container/volume 삭제 경로가 없다.
  4. 시작·readiness 실패 시 container와 volume을 보존하고 fail closed한다.
  5. focused pytest 5 passed, Ruff, Windows PowerShell 5.1 AST parser, diff check를 통과했다.

전환 결과와 남은 확인

  • current-only Google 회기 1개와 직접 provenance 행을 격리 후보에 UUID remap하여 transaction merge한다. 기존 후보 계정/profile은 덮어쓰지 않고 auth session은 복사하지 않았다.
  • 병합 후보의 계정·회기·턴·orphan·RLS·constraint·멱등 replay를 다시 고정한다.
  • post-merge dump의 disposable clone에서 G4/G5 DB-backed 브라우저 폐루프를 확인한다. desktop 2/2·mobile 2/2, route mock 0, G4/G5 실제 POST·read-model reload·멱등 재확인, UUID 노출·마이크 호출·중복 원장 0을 확인했다. 증거 SHA는 7c2336bf…7cc93이다.
  • 윤찬의 공개 DB 전환 승인을 받는다.
  • 전환 직전 현재 public-target dump SHA를 다시 확인한다.
  • watchdog을 짧게 정지하고 현재 container를 보존 이름으로 바꾼 뒤, recovered named volume을 vignette-dev-db:55432에 연결한다.
  • API 8001 재기동 후 public health/OpenAPI/auth/OAuth start를 확인한다.
  • 실제 사용자가 Google 재로그인한 뒤 본인 계정과 과거 소유 회기 목록이 보이는지 확인한다. 이 브라우저에는 기존 로그인 상태가 없어 계정 가장 없이 사용자 확인으로 남겼다.
  • 실패 시 기존 container로 되돌리는 자동 rollback 경로를 두 차례 실증했다. 최종 전환은 통과했다.

공개 전환 뒤에도 원본·전환 직전 container와 모든 dump/volume은 사용자 확인과 안정화가 끝날 때까지 삭제하지 않는다.