# 공개 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:55432`가 `vignette_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.ps1`은 `vignette-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를 통과했다. ## 전환 결과와 남은 확인 - [x] current-only Google 회기 1개와 직접 provenance 행을 격리 후보에 UUID remap하여 transaction merge한다. 기존 후보 계정/profile은 덮어쓰지 않고 auth session은 복사하지 않았다. - [x] 병합 후보의 계정·회기·턴·orphan·RLS·constraint·멱등 replay를 다시 고정한다. - [x] 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`이다. - [x] 윤찬의 공개 DB 전환 승인을 받는다. - [x] 전환 직전 현재 public-target dump SHA를 다시 확인한다. - [x] watchdog을 짧게 정지하고 현재 container를 보존 이름으로 바꾼 뒤, recovered named volume을 `vignette-dev-db:55432`에 연결한다. - [x] API 8001 재기동 후 public health/OpenAPI/auth/OAuth start를 확인한다. - [ ] 실제 사용자가 Google 재로그인한 뒤 본인 계정과 과거 소유 회기 목록이 보이는지 확인한다. 이 브라우저에는 기존 로그인 상태가 없어 계정 가장 없이 사용자 확인으로 남겼다. - [x] 실패 시 기존 container로 되돌리는 자동 rollback 경로를 두 차례 실증했다. 최종 전환은 통과했다. 공개 전환 뒤에도 원본·전환 직전 container와 모든 dump/volume은 사용자 확인과 안정화가 끝날 때까지 삭제하지 않는다.