191 lines
14 KiB
Markdown
191 lines
14 KiB
Markdown
# 서버 이관 런북 (초안) — 자택 개인 서버 → 학교 정보처 호스팅
|
|
|
|
> **초안 — 연구팀·정보처 협의 후 확정.** 이 문서는 개발(트웬티온스) 몫인 *기술 이관 절차* 초안이다.
|
|
> 도메인·서버 사양·비용·보안 정책의 최종 결정 주체는 연구팀·한신대 정보처이며, 아래 값과 절차는 협의 결과에 맞춰 갱신한다.
|
|
> 스펙 근거: [`hanshin-meeting-actions-2026-07-13.md`](./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) 사용 — `.env`의 `SITE_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`)로 호출하는 방식이라 **학교 서버에는 옮길 수 없다**(개인 계정 인증에 묶임).
|
|
|
|
학교 서버에서는 반드시 다음으로 전환한다.
|
|
|
|
```dotenv
|
|
ENGINE_MODE=claude_api
|
|
ANTHROPIC_API_KEY=sk-ant-... # 연구팀/기관 발급 키
|
|
# ENGINE_URL 은 claude_api 모드에서 사용하지 않음 (host.docker.internal 라인 무시됨)
|
|
```
|
|
|
|
- 코드는 `claude_api`를 **기본값으로 지원**한다(`apps/api/app/config.py`의 `engine_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 코드 배치 + 환경파일 작성
|
|
```bash
|
|
git clone <저장소> vignette && cd vignette/infra
|
|
cp .env.example .env
|
|
# .env 편집 — 아래 §5 체크리스트의 "설정만 바꾸면 됨" 값을 학교 값으로 교체
|
|
```
|
|
배포 전 프리플라이트(읽기 전용 검증):
|
|
```bash
|
|
# 저장소 루트에서
|
|
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(자택)에서 덤프를 뜬다. 접속 방식에 맞춰 하나 선택.
|
|
|
|
```bash
|
|
# (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 정책을 덤프가 통째로 재현하도록).
|
|
|
|
```bash
|
|
# 새 호스트: 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 소스팩은 코드 기준으로 재동기화한다.
|
|
```bash
|
|
# 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`)에 있다.
|
|
```bash
|
|
# 자택: 볼륨 내용 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 기동
|
|
```bash
|
|
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.py`의 `oauth_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. 이관 후 검증 체크리스트
|
|
|
|
```bash
|
|
# 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`](../DEPLOYMENT.md)·SSOT 대시보드에 반영한다.
|