vignette/docs/ops/server-migration-runbook-draft.md
2026-07-15 21:31:30 +09:00

14 KiB

서버 이관 런북 (초안) — 자택 개인 서버 → 학교 정보처 호스팅

초안 — 연구팀·정보처 협의 후 확정. 이 문서는 개발(트웬티온스) 몫인 기술 이관 절차 초안이다. 도메인·서버 사양·비용·보안 정책의 최종 결정 주체는 연구팀·한신대 정보처이며, 아래 값과 절차는 협의 결과에 맞춰 갱신한다. 스펙 근거: hanshin-meeting-actions-2026-07-13.md P3.

0. 요약 (한 장)

  • 현재 운영 형태: 윤찬 자택 Windows PC에서 scripts/start-public-runtime.ps1베어 프로세스(uvicorn + vite preview + 로컬 claude -p 게이트웨이)를 띄우고, Cloudflare 터널로 공개하며, Windows 작업 스케줄러 워치독이 살아있게 유지한다. → 이 경로 전체가 Windows 종속이라 리눅스로 그대로 못 옮긴다.
  • 이관 목표 형태: 학교 리눅스 서버에서 infra/docker-compose.yml 스택(web·api·db·proxy)을 그대로 기동. Docker 스택은 이식성을 전제로 설계돼 있어 Windows 런타임 스크립트 계층을 대부분 대체한다.
  • 가장 큰 이관 블로커 2개: ① 엔진 모드 claude_cli(개인 구독 CLI 인증) → claude_api(ANTHROPIC_API_KEY) 전환 필수. ② 하드코딩된 도메인/리다이렉트 URI/허용 호스트를 학교 도메인으로 교체(일부는 빌드 시점에 굽혀서 재빌드 필요).

1. 사전 요구사항 (이관 시작 전 확보)

1.1 정보처가 준비할 것

  • 리눅스 호스트: Docker Engine + Docker Compose v2 설치 가능한 서버(Ubuntu 22.04 LTS 등). 20명 파일럿 기준 최소 4 vCPU / 8GB RAM / 40GB SSD 권장(부하 검증 결과에 맞춰 조정).
  • 공개 도메인 2개: 웹용 1개 + API용 1개(예: vignette.hs.ac.kr, api-vignette.hs.ac.kr). 웹과 API는 별도 호스트네임이어야 한다(OAuth 콜백·CORS 설계 전제).
  • DNS A/AAAA 레코드: 위 두 도메인이 서버 공인 IP로 향하도록 설정.
  • TLS: 다음 중 하나.
    • (a) 서버에 공인 IP가 있으면 Caddy 자동 TLS(Let's Encrypt) 사용 — .envSITE_ADDRESS, ACME_EMAIL만 채우면 됨.
    • (b) 정보처가 기관 인증서를 강제하면 Caddy 앞단에 리버스 프록시를 두거나 인증서를 Caddy에 주입(협의 필요).
  • 아웃바운드 네트워크 허용: api.openai.com, api.anthropic.com(엔진 claude_api 모드), Google OAuth 엔드포인트로의 HTTPS egress.

1.2 연구팀·개발이 준비할 것

  • Google OAuth 클라이언트: Google Cloud Console에서 Authorized redirect URI에 https://api-<학교도메인>/auth/callback 추가(기존 chanpaca/18ka URI는 유지 또는 정리). Client ID/Secret 확보.
  • API 키: OPENAI_API_KEY(음성 STT/TTS), ANTHROPIC_API_KEY(엔진 claude_api 모드 — 아래 §2 참조).
  • 관리자·교수자 이메일 목록: AUTH_SUPER_ADMIN_EMAILS, AUTH_ADMIN_EMAILS, AUTH_TEACHER_EMAILS, 허용 도메인 AUTH_ALLOWED_EMAIL_DOMAINS(파일럿 참가자 도메인 포함).
  • 현재 운영 데이터 스냅샷: DB 덤프 + uploads 디렉터리(아래 §4).

2. 엔진 모드 주의 — claude_cli → claude_api (필수)

현재 운영은 ENGINE_MODE=claude_cli다. 이는 호스트에 상주하는 개인 claude CLI(개인 구독 OAuth 인증) 를 게이트웨이(http://host.docker.internal:9099)로 호출하는 방식이라 학교 서버에는 옮길 수 없다(개인 계정 인증에 묶임).

학교 서버에서는 반드시 다음으로 전환한다.

ENGINE_MODE=claude_api
ANTHROPIC_API_KEY=sk-ant-...      # 연구팀/기관 발급 키
# ENGINE_URL 은 claude_api 모드에서 사용하지 않음 (host.docker.internal 라인 무시됨)
  • 코드는 claude_api기본값으로 지원한다(apps/api/app/config.pyengine_mode 기본이 claude_api). 별도 개발 없이 env 전환으로 동작한다.
  • 비용: 회의 기록상 실사용 지난달 ~$10, 20명 액티브 시 최대 $100 이내 추정. 과금은 ANTHROPIC_API_KEY 계정으로 발생 → 연구팀/기관 계정 사용 권장. 관리자 UI에서 ADMIN_USAGE_BUDGET_USD로 예산 표시 가능.
  • claude_cli 게이트웨이(apps/api/engine_gateway/), 로컬 claude -p 상주풀, ENGINE_READY_TTL_SECONDS 튜닝은 모두 불필요해진다.

3. 이관 단계

3.1 코드 배치 + 환경파일 작성

git clone <저장소> vignette && cd vignette/infra
cp .env.example .env
# .env 편집 — 아래 §5 체크리스트의 "설정만 바꾸면 됨" 값을 학교 값으로 교체

배포 전 프리플라이트(읽기 전용 검증):

# 저장소 루트에서
python scripts/check-deploy-preflight.py --env-file infra/.env
# DB 앱롤까지 검증하려면 app-role DATABASE_URL 을 넘기고 --require-app-role

이 스크립트는 필수 env 누락, change-me 류 플레이스홀더, prod에서 켜지면 안 되는 플래그(AUTH_DEV_LOGIN_ENABLED 등), 잘못된 ENGINE_MODE를 잡아준다.

3.2 DB dump / restore

현재 DB(자택)에서 덤프를 뜬다. 접속 방식에 맞춰 하나 선택.

# (A) DB가 docker compose db 컨테이너인 경우 — 자택 서버에서
docker compose exec -T db pg_dump -U vignette_owner -Fc vignette > vignette-$(date +%F).dump

# (B) DB가 외부/NAS PostgreSQL인 경우 — 접속 문자열로 직접
pg_dump --format=custom --no-owner --no-privileges \
  --dbname="postgresql://vignette_owner:<PW>@<OLD_HOST>:5432/vignette" \
  --file=vignette-$(date +%F).dump

학교 서버에서 복원. 권장: 스키마 init 스크립트가 만든 빈 DB가 아니라, 완전히 빈 DB에 풀 덤프를 복원한다(스키마·데이터·RLS 정책을 덤프가 통째로 재현하도록).

# 새 호스트: db 컨테이너만 먼저 띄우되 init 스크립트와 충돌을 피하려면
#  - 신규 빈 볼륨 + init 스크립트로 스키마 생성 후 --data-only 복원, 또는
#  - init 없이 빈 DB 생성 후 풀 덤프 복원(권장, 아래)
docker compose up -d db
docker compose exec -T db psql -U vignette_owner -c "CREATE DATABASE vignette_new;"
cat vignette-YYYY-MM-DD.dump | docker compose exec -T db \
  pg_restore --no-owner --no-privileges --clean --if-exists \
  -U vignette_owner -d vignette_new
# 검증 후 vignette_new 를 정식 DB로 승격(또는 처음부터 vignette 로 복원)

⚠️ RLS/앱롤 주의: 스키마는 infra/db/init/01_extensions.sql … 06_session_evaluation.sql빈 볼륨 최초 기동 시 1회만 실행한다. 여기엔 RLS 정책(04_audit_eval_rls.sql)과 앱 전용 롤 생성(99_app_role.sh)이 포함된다. 풀 덤프 복원 시 롤/권한이 덤프에 없으면(=--no-owner --no-privileges 사용) 복원 후 99_app_role.sh 상당의 앱롤 GRANT를 다시 적용해야 API app-role(vignette_app)이 붙는다. 프리플라이트 --require-app-role로 확인.

3.3 persona / KB 데이터 이동

페르소나·평가·라이브코칭 KB는 저장소가 원본을 소유하고 스크립트로 DB에 동기화한다. DB 덤프에 이미 승인된 persona_card 행이 포함되지만, KB 소스팩은 코드 기준으로 재동기화한다.

# dry-run(기본): repo 소스팩 해시 vs 활성 kb.document 비교
python scripts/sync-persona-sources.py
# 실제 반영
python scripts/sync-persona-sources.py --apply
  • 페르소나 시드 재생성이 필요하면 python scripts/materialize-persona-seeds.py.
  • 원칙: prod에서는 AUTO_SEED_PERSONAS=false, ALLOW_SEED_PERSONA_FALLBACK=false 유지(승인된 DB 행만 사용). 추고록 원본 등 학교 재산 데이터는 repo·이미지에 넣지 않는다.

3.4 uploads 디렉터리 이동

사용자 업로드(교수자 자료 등)는 apiuploads 도커 볼륨(/app/uploads, USER_UPLOAD_DIR)에 있다.

# 자택: 볼륨 내용 tar 로 추출
docker run --rm -v vignette_apiuploads:/data -v "$PWD":/backup alpine \
  tar czf /backup/uploads-$(date +%F).tgz -C /data .
# 학교: 새 볼륨에 복원
docker run --rm -v vignette_apiuploads:/data -v "$PWD":/backup alpine \
  tar xzf /backup/uploads-YYYY-MM-DD.tgz -C /data

3.5 기동

cd infra
docker compose build            # 웹은 도메인이 빌드에 굽히므로 §5 빌드 인자 확인 후 빌드
docker compose up -d
docker compose ps               # web/api/db/proxy healthy 확인

4. 이식성 이슈 체크리스트

4.1 설정만 바꾸면 되는 것 (env / DNS / 콘솔 — 재빌드·코드수정 불필요)

항목 현재 하드코딩/기본값 이관 시 조치
API 리다이렉트 URI OAUTH_REDIRECT_URI=https://api-vignette.chanpaca.net/auth/callback 학교 API 도메인으로 교체 + Google Console에 등록
프론트 기준 URL FRONTEND_BASE_URL=https://vignette.chanpaca.net 학교 웹 도메인
콜백→프론트 매핑 FRONTEND_ORIGIN_MAP={api-vignette.chanpaca.net:…, api-vnet.18ka.net:…} 학교 api-호스트: https://웹-호스트
CORS 허용 CORS_ORIGINS=[chanpaca.net, 18ka.net, pages.dev, localhost…] 학교 웹 도메인만(로컬 항목 제거)
슈퍼관리자 AUTH_SUPER_ADMIN_EMAILS=[yunchan@twentyoz.kr, hoonjungkoo@hs.ac.kr] 운영 담당자 이메일
허용 도메인 AUTH_ALLOWED_EMAIL_DOMAINS=[hs.ac.kr, twentyoz.kr] 파일럿 참가 도메인
시크릿 POSTGRES_PASSWORD/APP_DB_PASSWORD/SESSION_SECRET(=change-me…) 랜덤 강값으로 교체
TLS/노출 SITE_ADDRESS=:80, ACME_EMAIL=(빈값) 학교 웹 도메인 + 관리자 메일
포트 HTTP_PORT=8080, HTTPS_PORT=8443 정보처 방화벽 정책에 맞춰
엔진 ENGINE_MODE=claude_cli claude_api + ANTHROPIC_API_KEY(§2)

4.2 고쳐야(재빌드/치환해야) 넘어가는 것

항목 위치 왜 블로커인가 조치
웹 허용 호스트 apps/web/vite.config.ts defaultAllowedHosts(chanpaca/18ka/ts.net 배열) vite preview가 Host 헤더를 검증 → 학교 도메인 누락 시 전체 화면이 403 블랭크. 빌드 시점 값이라 재빌드 필요 빌드 전 VITE_ALLOWED_HOSTS=<학교 웹 도메인> 환경변수로 주입(코드 수정 없이 가능)
웹 API 베이스 compose web 빌드 인자 VITE_API_BASE=${PUBLIC_API_BASE:-/api} 프론트가 붙는 API 경로가 빌드에 굽힘 Caddy 프록시로 /api 동일 오리진 유지 시 기본값 그대로 OK. 별도 API 도메인 직결이면 PUBLIC_API_BASE 지정 후 재빌드
엔진 인증 방식 ENGINE_MODE=claude_cli 런타임 경로 개인 구독 CLI 인증 → 학교서버 이식 불가 §2대로 claude_api 전환(코드 지원됨, env 전환)
Windows 런타임 계층 전체 scripts/*.ps1, .vbs, Windows 작업 스케줄러(install-public-runtime-task.ps1, register-boot-task.ps1), Get-CimInstance, winget 경로 Windows 전용. $Workspace="D:\workspace\vignette", $Python=%LOCALAPPDATA%\…, $Cloudflared, $USERPROFILE\.cloudflared 등 하드코딩 리눅스에서는 docker compose 스택 + systemd/cron 헬스체크로 대체(PowerShell 스크립트 미사용). Cloudflare 터널은 공인 IP+Caddy 자동TLS로 대체 권장
config.py 하드코딩 기본값 apps/api/app/config.pyoauth_redirect_uri/frontend_origin_map/cors_origins/auth_super_admin_emails 기본값에 chanpaca·18ka·특정 이메일 env로 override되지만, env 누락 시 자택 값으로 조용히 폴백될 위험 env를 반드시 채우고(§4.1), 이관 확정 후 기본값도 중립화 검토(별도 PR)

비밀 주입 방식: 현재는 infra/.env 평문 파일 한 장(요구변수는 compose ${VAR:?} 문법으로 미설정 시 기동 실패 → 누락 방어는 됨). 학교 서버에서는 .env 파일 권한 chmod 600 + repo 미포함 확인, 가능하면 정보처 시크릿 관리 수단으로 승격 협의. 시크릿 매니저/볼트는 현재 미사용.


5. 이관 후 검증 체크리스트

# 1) 헬스 — environment=prod, db=true, engine=true, engine_mode=claude_api 확인
curl -s https://api-<학교도메인>/health | jq
#   기대: {"status":"ok","environment":"prod","db":true,"engine":true,"engine_mode":"claude_api", ...}

# 2) dev-login 꺼짐 확인 — prod에서 AUTH_DEV_LOGIN_ENABLED=false 여야 함(preflight도 검증)
#    /auth/dev-login 류가 비활성인지, 로그인은 Google OAuth로만 되는지 확인

# 3) OAuth 왕복 — Google 로그인 → 콜백 → pending 승인 대기 → 슈퍼관리자 승인 → 온보딩 진입

# 4) /turn 스모크 — 세션 1개 생성 후 발화 1턴 왕복(내담자 응답 + 라이브 코칭 카드 수신)

# 5) 백엔드 pytest
cd apps/api && python -m pytest -q

# 6) (선택) 20 동시 세션 부하 — scripts/load-test-sessions.py 로 병목 재확인

추가 확인:

  • 음성 SSE/WSS 경로가 Caddy flush_interval -1로 버퍼링 없이 흐르는지(자막·실시간 피드백).
  • persona 목록이 승인 DB 행 기준으로 뜨는지(시드 폴백 아님).
  • uploads 업로드→변환→원본 파기 흐름(P4)이 실데이터로 동작하는지.

6. 롤백 / 주의

  • 이관은 자택 운영을 내리기 전에 학교 서버에서 병렬로 세워 검증하고, 검증 통과 후 DNS를 전환한다(다운타임 최소화).
  • DB 덤프는 이관 시점 스냅샷이다. 병렬 운영 중 자택에서 발생한 신규 데이터는 최종 컷오버 시점에 재덤프·재복원(또는 컷오버 동안 자택 쓰기 중지).
  • OAuth redirect URI를 학교 도메인으로 바꾸는 순간 기존 도메인 로그인은 깨질 수 있으니, 컷오버 타이밍을 연구팀과 합의.
  • 확정 후 이 문서의 "초안" 표기를 제거하고 DEPLOYMENT.md·SSOT 대시보드에 반영한다.