diff --git a/docs/releases/index.md b/docs/releases/index.md index 16fa2ae..d3a3b7d 100644 --- a/docs/releases/index.md +++ b/docs/releases/index.md @@ -10,3 +10,4 @@ | --- | --- | --- | --- | --- | --- | | REL-001 | app-service | released | BAT-001~016 (전체 통합) | 2026-05-21 | [rel-001](./rel-001_20260521_v1-api-full-surface.md) | | REL-002 | app-service | feedback-captured | BAT-017 (dcy-007 M1: 거래내역+실현PnL API) | 2026-06-19 | [rel-002](./rel-002_20260619_trade-history-and-pnl-api.md) | +| REL-003 | app-service | draft | BAT-018~022 (dcy-008/009/010/011: WAC조회+데몬목록+daemon-scoped 인증정보+관리API) | 2026-06-28 | [rel-003](./rel-003_20260628_daemon-credential-and-admin-api.md) | diff --git a/docs/releases/rel-003_20260628_daemon-credential-and-admin-api.md b/docs/releases/rel-003_20260628_daemon-credential-and-admin-api.md new file mode 100644 index 0000000..f45b11a --- /dev/null +++ b/docs/releases/rel-003_20260628_daemon-credential-and-admin-api.md @@ -0,0 +1,257 @@ +# REL-003: daemon-scoped 브로커 인증정보 + 관리 API (dcy-010 + dcy-011) + +- Status: draft +- Profile: app-service +- Included Batch: BAT-018 (REQ-043), BAT-019 (REQ-044, REQ-045), BAT-020 (REQ-046~051), BAT-021 (REQ-047 Step2 통합 및 런북), BAT-022 (REQ-052~054) + +--- + +## Scope + +### Included + +- **BAT-018** (dcy-008, batch-lite, release-candidate) — REQ-043: `GET /v1/portfolios/{id}/holdings/cost-basis` 엔드포인트 신설 (WAC/평단 조회) +- **BAT-019** (dcy-009, standard, confirmed) — REQ-044/045: `GET /v1/assets` 권한 완화(require_read) + `GET /v1/daemons` 신설 +- **BAT-020** (dcy-010, standard, release-candidate) — REQ-046~051: BrokerCredential daemon_id 도메인 모델 + Step1 마이그레이션 + backfill CLI + `/v1/user_info` daemon-scoped 확장 + CLI `--daemon-id` 옵션 + 보안 격리 +- **BAT-021** (dcy-010, standard, release-candidate) — REQ-047: Step2 동반 코드 수정(ensure_schema/upsert ON CONFLICT) + 실 PostgreSQL 통합검증 + Step2 런북 (`docs/runbooks/bat-021-req047-step2-migration-runbook.md`) +- **BAT-022** (dcy-011, standard, release-candidate) — REQ-052~054: 사용자 소유 스코프 인증정보 관리 API + admin 데몬 CRUD API + 인증정보 보안 격리(쓰기전용·secret 미반환·로그 미노출) +- **(동반 배포) dcy-035 WAC #15** (`3a6adc9`, REQ-041 change-req CHG-20260626-01) — `/pnl/realized`·holdings cost-basis의 **매수 수수료를 WAC cost basis에 편입**. 이미 origin/main 머지됨 → 이번 portfolio-hub 코드 배포(main)에 함께 실린다. trex-core 운영리포트(dcy-035, REL-010)와 짝을 이루는 멀티데이 WAC 정확값의 portfolio-hub 측. (별도 배포·재시작 불필요 — 동일 배포에 포함) + +### Excluded + +- REQ-047 Step2(PK 전환) 운영 DB 실제 적용 — **이번 릴리즈의 코드 배포 이전** ops 윈도우에서 별도 수행 (런북 `docs/runbooks/bat-021-req047-step2-migration-runbook.md` 엄수, 사장님 결재 게이트) +- trex-core 소비자 어댑터 (KISConfig fetch 경로) — 별도 trex-core Discovery (dcy-010 out-of-scope) +- trex-web UI (dcy-009) — trex-web repo 별도 진행 +- 서비스 토큰 발급·폐기 (dcy-011 out-of-scope) + +--- + +## 전제 조건 (배포 시작 전 필수 확인) + +1. **`CREDENTIAL_ENCRYPTION_KEY`** — 운영 환경 env에 Fernet key 값이 설정돼 있어야 한다. 미설정 시 인증정보 암·복호화 전체가 동작하지 않는다. + - 확인: 서비스 실행 환경에서 `echo $CREDENTIAL_ENCRYPTION_KEY | head -c 10` (공백이면 미설정) + - 최초 설정: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 결과를 `CREDENTIAL_ENCRYPTION_KEY`에 등록 + - **주의**: 기존 운영 인스턴스에 이미 설정돼 있으면 변경 금지 (기존 암호화 payload를 복호화할 수 없게 됨) + +2. **Step2 마이그레이션 완료 확인** — 코드 배포 전에 REQ-047 Step2(alembic revision `20260628_0027`) 적용이 완료되어야 한다. Step2 미적용 상태로 BAT-021 코드를 배포하면 `upsert_broker_credential`의 `ON CONFLICT (daemon_id, broker)` 절이 구 스키마(PK=`(user_id, broker)`)와 맞지 않아 인증정보 등록/회전 실패. + - 검증 쿼리: `SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'broker_credentials_pkey';` + - 기대값: `PRIMARY KEY (daemon_id, broker)` + +3. **alembic 상태 사전 확인** — 현행 운영 DB의 alembic head가 `20260628_0027` (Step2 완료 후) 인지 확인: + ```bash + alembic current + # 기대: 20260628_0027 (head) + ``` + +4. **DB 백업** — Step2 마이그레이션 적용 전 운영 DB 전체 스냅샷 백업 (런북 1단계 참조). + +5. **pytest 통과 확인** — main HEAD (`9fe6f3b`) 기준으로 `pytest` 전체가 통과해야 한다. + - BAT-022 기준 565 passed + 핫픽스 커밋 포함 최신 스위트 통과 확인 필요. + +--- + +## Rollout Plan + +> **배포 순서 엄수. 특히 Step2 미적용 상태에서 코드 배포하면 upsert 실패가 발생한다.** + +``` +[ops 윈도우] DB 백업 → Step1 upgrade → backfill CLI → NULL 0건 확인 + → Step2 upgrade (사장님 결재 필수) → Step2 스키마 검증 +[코드 배포] → portfolio-hub 서비스 rolling restart +[smoke] → 헬스체크 → 스모크 테스트 체크리스트 실행 +``` + +### 단계별 상세 + +**[ops 윈도우 전용 — 코드 배포 전 수행]** + +1. **DB 백업**: 운영 DB 전체 스냅샷 (`pg_dump`) — 런북 1단계 참조 +2. **Step1 upgrade** (`alembic upgrade 20260628_0026`): `broker_credentials`에 `daemon_id TEXT NULL` 컬럼 추가. 기존 PK `(user_id, broker)` 유지. +3. **backfill CLI**: `pfhub credential backfill-daemon-id --user --daemon-id ` — 기존 user-scoped 인증정보 행에 daemon_id 설정. 사용자별 반복. `--dry-run` 선 확인 권장. +4. **NULL 0건 검증**: `SELECT COUNT(*) FROM broker_credentials WHERE daemon_id IS NULL;` → 반드시 0 확인. 1건이라도 있으면 Step2 진행 금지 (upgrade가 RuntimeError로 자동 거부함). +5. **Step2 upgrade** (`alembic upgrade 20260628_0027`, **사장님 결재 필수**): PK를 `(user_id, broker)` → `(daemon_id, broker)`로 전환. `daemon_id NOT NULL` 제약 추가. **비가역 작업.** +6. **Step2 스키마 검증**: PK 전환·daemon_id NOT NULL·user_id 유지를 SQL 쿼리로 확인 — 런북 4단계 검증 SQL 참조. + +**[코드 배포]** + +7. **portfolio-hub 서비스 rolling restart**: freeze 커밋 `9fe6f3b` (main HEAD) 이미지로 교체. + - 포함 변경 모듈: `app/repositories/postgres.py` (ensure_schema·upsert ON CONFLICT), `app/api/v1_daemons.py` (admin CRUD·daemon 목록), `app/api/v1_credentials.py` (소유 스코프 관리 API), `app/api/v1_portfolios.py` (holdings/cost-basis), `app/api/v1_user_info.py` (daemon-scoped broker 전달), `app/cli/commands/credential.py` (--daemon-id·backfill), `app/domain/pnl.py` (calculate_current_wac) + +**[배포 후 smoke]** + +8. 아래 Verification Checklist 항목 순서대로 실행. + +--- + +## Rollback Plan + +### 트리거 조건 +- `GET /health` HTTP 200 실패 +- `GET /v1/user_info` (service token) HTTP 500 또는 broker 필드 누락 +- 인증정보 PUT/GET 연속 500 발생 +- 기존 API(`/v1/portfolios`, `/v1/assets`, `/v1/daemons`) 회귀 발생 + +### 코드 롤백 (1순위) + +BAT-022/021/020은 기존 기능의 additive 추가 및 REQ-049 동작 변경(brokers→broker 단일화)이 포함된다. 코드 롤백 시 REL-002 freeze 이미지(커밋 `d89f137`)로 복원하면 된다. DB는 Step2 이후 상태 그대로 둬도 무방 (코드가 구 스키마를 참조하지 않는 방향으로 하위호환 불가하여 DB 롤백 필요 여부를 판단). + +``` +portfolio-hub → REL-002 이미지(커밋 d89f137)로 rolling restart +``` + +### DB 롤백 (Step2 롤백 — 조건부) + +Step2 downgrade는 같은 user의 여러 daemon이 동일 broker를 쓰는 경우 RuntimeError로 자동 거부된다. 단일 daemon 1계좌 환경이면 downgrade 가능: + +```bash +alembic downgrade 20260628_0026 # Step2 → Step1 상태 +alembic downgrade 20260605_0025 # Step1 → 이전 상태 (daemon_id 컬럼 제거) +``` + +**경고**: Step2 downgrade는 조건부 가역이다. 백업 복원이 최후 수단. + +```bash +pg_restore -h -U -d -c backup_pre_bat021_.dump +``` + +### 롤백 영향 분석 +- REQ-049 (`/v1/user_info` daemon-scoped): brokers 리스트→단일 broker 변경. trex-core 소비자는 `daemon_id`만 읽으므로 무영향(소비자 인벤토리 확인 완료). +- REQ-052/053: HTTP API 추가분 — additive. 롤백 시 엔드포인트 제거만 발생. 기존 운영 영향 없음. +- REQ-043/044/045 (BAT-018/019): additive 추가·권한 완화. 롤백 시 신규 엔드포인트 제거 + GET /v1/assets가 require_admin으로 복원. + +--- + +## Verification Checklist + +### 배포 전 + +- [ ] Step2 완료 확인: `SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'broker_credentials_pkey';` → `PRIMARY KEY (daemon_id, broker)` +- [ ] alembic head 확인: `alembic current` → `20260628_0027 (head)` +- [ ] NULL 잔존 0건: `SELECT COUNT(*) FROM broker_credentials WHERE daemon_id IS NULL;` → `0` +- [ ] `CREDENTIAL_ENCRYPTION_KEY` 운영 환경 설정 확인 +- [ ] pytest 전체 통과 확인 (freeze 커밋 `9fe6f3b` 기준) +- [ ] DB 백업 완료 확인 (백업 파일 경로/사이즈 기록) + +### 배포 직후 + +#### 기본 헬스 + +- [ ] `GET /health` → HTTP 200, `db_enabled: true` + +#### admin 데몬 CRUD (REQ-053 — BAT-022) + +- [ ] `POST /v1/admin/daemons` (admin-token) — 데몬 생성 → HTTP 201 + `DaemonAdminResponse` (id, owner_user_id, daemon_slug, is_active) +- [ ] `GET /v1/admin/daemons` (admin-token) — 데몬 목록 → HTTP 200 + 데몬 목록 반환 +- [ ] `GET /v1/admin/daemons/{daemon_id}` (admin-token) → HTTP 200 +- [ ] `PATCH /v1/admin/daemons/{daemon_id}` (admin-token) — display_name 변경 → HTTP 200 +- [ ] `PATCH /v1/admin/daemons/{daemon_id}` (admin-token, is_active=false) — 비활성화 → HTTP 200 + is_active=false +- [ ] `DELETE /v1/admin/daemons/{daemon_id}` (admin-token) → HTTP 405 (하드 삭제 금지) +- [ ] 비admin user-token으로 admin CRUD 시도 → HTTP 403 + +#### 인증정보 관리 API (REQ-052 — BAT-022) + +- [ ] `PUT /v1/daemons/{daemon_id}/credential` (user-token, 소유 데몬) → HTTP 200 + 메타데이터(account_ref, credential_version, last_rotated_at, broker) 응답; **app_key/app_secret 미포함 확인 (필드 부재)** +- [ ] `GET /v1/daemons/{daemon_id}/credential` (user-token, 소유 데몬) → HTTP 200 + 메타데이터; **secret 미포함 확인** +- [ ] `DELETE /v1/daemons/{daemon_id}/credential` (user-token, 소유 데몬) → HTTP 204 +- [ ] 타 사용자 user-token으로 소유 스코프 외 데몬 인증정보 조회/수정 → HTTP 403 + +#### service-token `/v1/user_info` daemon-scoped (REQ-049 — BAT-020) + +- [ ] service-token(daemon_id 바인딩)으로 `GET /v1/user_info` → `broker` 단일 객체(account_ref, credential_payload 포함), `brokers` 리스트 키 없음 확인 +- [ ] `CREDENTIAL_ENCRYPTION_KEY`와 일치하는 키로 인증정보를 등록한 경우 `credential_payload` 복호화 값(app_key 포함) 반환 확인 +- [ ] daemon_id 없는 service-token으로 `/v1/user_info` → HTTP 403 +- [ ] user-token으로 `/v1/user_info` → HTTP 403 (REQ-051 보안 격리 회귀) + +#### WAC 조회 엔드포인트 (REQ-043 — BAT-018) + +- [ ] `GET /v1/portfolios/{id}/holdings/cost-basis` (read-token, 소유 portfolio) → HTTP 200 + `items`, `cost_basis_from` 포함 + +#### 자산/데몬 목록 (REQ-044/045 — BAT-019) + +- [ ] `GET /v1/assets` (read-token, non-admin) → HTTP 200 (권한 완화 확인) +- [ ] `GET /v1/daemons` (user-token) → HTTP 200 + 소유 데몬 목록 + +#### 기존 API 회귀 + +- [ ] `GET /v1/portfolios` (read-token 이상) → HTTP 200 +- [ ] `POST /v1/portfolios/{id}/ledger/trades` (trade-write-token) → 기존 동작 유지 +- [ ] `GET /v1/portfolios/{id}/trades` (read-token 이상) → HTTP 200 +- [ ] `GET /v1/portfolios/{id}/pnl/realized` (read-token 이상) → HTTP 200 + +--- + +## Known Issues (릴리즈 전 인지 사항) + +1. **BAT-022 AC-3 통합 수준**: REQ-052 AC-3(CLI·API·service-token 3경로 정합)이 unit-level 검증. 실 DB 통합 환경에서 재확인 권장(비가역 아님, 선택적). +2. ~~owner 재할당 원자성~~ **(해소됨 — 오기 정정)**: `update_daemon_owner()`는 **단일 `conn.transaction()` 트랜잭션**으로 credential DELETE(선행) → owner UPDATE를 원자 처리한다(BAT-022 QC Major fix, 커밋 83e6dde). 어느 한쪽 실패 시 전체 롤백되어 "owner만 바뀌고 구 인증정보 잔존"하는 leak 상태가 발생하지 않는다. 라이브 통합검증(2026-06-28)에서 owner 재할당 → 인증정보 무효화 PASS. **리스크 아님.** +3. **DELETE credential broker 고정**: `DELETE /v1/daemons/{daemon_id}/credential`은 broker="kis" 고정. 다중 broker 지원 시 별도 설계 필요(현재 단일 broker 가정 설계). +4. **REL-002 Known Issues 이월**: symbol 필터 edge 테스트, HTTP_422 deprecated 경고, 성능 미검증 — 여전히 비차단 잔여. + +--- + +## Release Execution Evidence + +- Release Status Transition Target: released + +### Deployment Facts + +- Executed At: + - (미실행 — 배포 전) +- Completed At: + - (미실행) +- Environment: + - portfolio-hub 운영 인스턴스 +- Operator: + - (ops 윈도우 담당자 기입) +- Revision or Artifact: + - 배포 커밋: `9fe6f3b` (main HEAD, 2026-06-28) + - alembic head (코드 배포 후 기대): `20260628_0027` +- Commands or Procedure: + - 상세는 위 Rollout Plan 참조 + - DB 마이그레이션: `docs/runbooks/bat-021-req047-step2-migration-runbook.md` 엄수 + - 코드 배포: portfolio-hub 서비스 rolling restart (freeze 커밋 `9fe6f3b`) + +### Profile-Specific Checks + +- app-service: + - Health Endpoint: + - (배포 후 기입) + - Startup Log Fields: + - (배포 후 기입) + - Request Log Fields: + - (배포 후 기입) + - Smoke or Manual Verification: + - (배포 후 Verification Checklist 결과 기입) + +### Rollback Outcome + +- Rollback Required: + - (배포 후 기입) +- Rollback Trigger: + - (배포 후 기입) +- Rollback Action: + - (배포 후 기입) + +### Release Decision Evidence + +- Released Decision: + - (배포 후 팀장 기입) +- Rationale: + - (배포 후 기입) +- Recorded By: + - (배포 후 기입) +- Recorded At: + - (배포 후 기입) + +--- + +## Feedback Handoff + +- Observation Summary: + - (배포 후 운영 관찰 기입) +- Discovery Input: + - trex-core 소비자 어댑터 (KISConfig fetch, dcy-010 out-of-scope) — 별도 trex-core Discovery (dcy-036/BAT-054로 진행, PR #35) +- REQ Input: + - (배포 후 기입) +- Change Request Input: + - (배포 후 기입) diff --git a/docs/runbooks/bat-021-req047-step2-migration-runbook.md b/docs/runbooks/bat-021-req047-step2-migration-runbook.md index f608154..a58930d 100644 --- a/docs/runbooks/bat-021-req047-step2-migration-runbook.md +++ b/docs/runbooks/bat-021-req047-step2-migration-runbook.md @@ -3,34 +3,67 @@ - Batch: BAT-021 - Discovery: dcy-010 - Date: 2026-06-28 -- 결재 게이트: 사장님 결재 필수 (비가역 PK 변경) +- 결재 게이트: 사장님 결재 필수 (비가역 PK 변경 — Step 4) +- 작업 예상 시간: 약 20~30분 (데이터량에 따라 상이) + +--- ## 전제 조건 - REQ-046 ~ REQ-051 코드(BAT-020 PR #17, d4acfd4)가 배포돼 있어야 한다. -- BAT-021 코드(ensure_schema 갱신, ON CONFLICT 수정)는 **Step2 적용 완료 이후** 배포한다(아래 배포 순서 준수). +- BAT-021 코드(ensure_schema 갱신, ON CONFLICT 수정)는 **Step4 적용 완료 이후** 배포한다(아래 배포 순서 준수). - 마이그레이션 전 DB 백업이 완료돼 있어야 한다. +- 이 런북의 모든 alembic 명령은 `PORTFOLIOHUB_DB_ENABLED=true` 및 DB 접속 환경변수가 설정된 셸에서 실행한다. + +--- ## 배포 순서 개요 ``` -백업 → Step1 upgrade → backfill CLI → NULL 0건 확인 → Step2 upgrade(결재) → 코드 배포 → 동작 확인 → 비상 시 롤백 +DB 백업 → 프리체크(alembic head·NULL 확인·락 확인) +→ Step1 upgrade(20260628_0026) → Step1 검증 +→ backfill CLI(pfhub credential backfill-daemon-id) → NULL 0건 확인(게이트) +→ Step2 upgrade(20260628_0027, 사장님 결재 후) → Step2 검증 +→ BAT-021 코드 배포(rolling restart) +→ 배포 후 동작 확인 ``` --- +## 순서 위반 실패 모드 (반드시 읽을 것) + +| 위반 | 증상 | +| --- | --- | +| **코드 배포를 Step2보다 먼저 수행** | `upsert_broker_credential`의 `ON CONFLICT (daemon_id, broker)` 절이 구 PK `(user_id, broker)`와 불일치 → 인증정보 등록·회전 시 `psycopg.errors.UndefinedColumn` 또는 `ProgrammingError` 발생 | +| **Step2를 backfill 없이 수행** | `alembic upgrade 20260628_0027` 내부 사전 검증 로직이 `daemon_id IS NULL` 행을 감지하고 `RuntimeError`로 자동 거부 (DB 변경 없음) | +| **Step1 없이 Step2 수행** | Step2 revision(`down_revision = 20260628_0026`)이 Step1 적용 여부를 검사하므로 alembic 체인 오류로 실패 | +| **백업 없이 Step2 수행** | 비가역 PK 변경 후 이슈 발생 시 복원 불가 — 반드시 백업 후 진행 | + +--- + ## 1단계: DB 백업 운영 DB에 마이그레이션을 적용하기 전 전체 스냅샷을 백업한다. ```bash -# 예: pg_dump 사용 -pg_dump -h -U -d -F c -f backup_pre_bat021_$(date +%Y%m%d%H%M%S).dump +pg_dump -h -U -d -F c \ + -f backup_pre_bat021_$(date +%Y%m%d%H%M%S).dump ``` +백업 완료 후 파일 크기와 종료 코드를 확인한다: +```bash +ls -lh backup_pre_bat021_*.dump +# 기대: 파일이 존재하고 크기가 0이 아닌 값 +echo $? # 기대: 0 +``` + +> **중단 기준**: 백업 실패(파일 미생성, 크기 0, exit code != 0) 시 이후 단계 진행 금지. 백업 성공 확인 후 진행. + --- -## 2단계: Step1 alembic upgrade (daemon_id NULL 컬럼 추가) +## 2단계: 사전 프리체크 (마이그레이션 시작 전) + +### 2-1: 현재 alembic head/stamp 확인 ```bash export TIMESCALEDB_HOST= @@ -40,62 +73,180 @@ export TIMESCALEDB_USER= export TIMESCALEDB_PASSWORD= export PORTFOLIOHUB_DB_ENABLED=true +alembic current +``` + +**기대값**: `20260605_0025 (head)` — REL-002 마이그레이션(`20260605_0025 ledger_consolidation`)까지 적용된 상태 + +> **주의**: alembic 상태가 없거나(`None`) 다른 revision인 경우 팀장에 보고 후 중단. `alembic stamp 20260605_0025` 조치가 필요한지 여부를 팀장이 판단한다. + +### 2-2: broker_credentials 현재 스키마 확인 + +```sql +-- 현재 컬럼 목록 확인 +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_name = 'broker_credentials' +ORDER BY ordinal_position; +-- 기대: daemon_id 컬럼이 없는 상태 (Step1 미적용) 또는 nullable인 상태 (Step1 적용 후) + +-- 현재 PK 확인 +SELECT constraint_name, constraint_type +FROM information_schema.table_constraints +WHERE table_name = 'broker_credentials' AND constraint_type = 'PRIMARY KEY'; +-- 기대: broker_credentials_pkey PRIMARY KEY +``` + +### 2-3: 기존 데이터 행 수 및 daemon_id NULL 현황 확인 + +```sql +-- 전체 행 수 (백업·복원 검증용으로 기록해 둘 것) +SELECT COUNT(*) AS total_rows FROM broker_credentials; + +-- NULL 현황 (Step1 이전에는 daemon_id 컬럼 자체가 없으므로 실패 정상) +-- Step1 적용 후 실행: +SELECT COUNT(*) AS null_daemon_id_rows FROM broker_credentials WHERE daemon_id IS NULL; +``` + +### 2-4: 활성 connection 및 락 확인 + +```sql +-- 현재 broker_credentials 테이블을 잡고 있는 락 확인 +SELECT pid, usename, application_name, state, wait_event_type, wait_event, query +FROM pg_stat_activity +WHERE query ILIKE '%broker_credentials%' AND state != 'idle'; +-- 기대: 결과 없음 (락 없음) + +-- 전체 활성 connection 수 확인 +SELECT COUNT(*) FROM pg_stat_activity WHERE state = 'active'; +``` + +> **중단 기준**: broker_credentials를 잡고 있는 활성 쿼리/락이 있으면 대기 또는 ops 윈도우 재조정. 서비스 배포 중단 확인 후 진행 권장. + +--- + +## 3단계: Step1 alembic upgrade (daemon_id NULL 컬럼 추가) + +```bash alembic upgrade 20260628_0026 ``` 예상 출력: ``` -INFO [alembic.runtime.migration] Running upgrade -> 20260628_0026, broker_credentials: daemon_id TEXT NULL 컬럼 추가 (REQ-047 Step1) +INFO [alembic.runtime.migration] Running upgrade 20260605_0025 -> 20260628_0026, broker_credentials: daemon_id TEXT NULL 컬럼 추가 (REQ-047 Step1) ``` -### 검증 +### Step1 검증 ```sql --- broker_credentials에 daemon_id TEXT NULL 컬럼이 존재해야 함 -\d broker_credentials --- 또는 -SELECT column_name, is_nullable FROM information_schema.columns +-- daemon_id TEXT NULL 컬럼이 존재해야 함 +SELECT column_name, data_type, is_nullable +FROM information_schema.columns WHERE table_name = 'broker_credentials' AND column_name = 'daemon_id'; --- 기대: daemon_id | YES (nullable) +-- 기대: daemon_id | text | YES (nullable) -- 기존 PK (user_id, broker) 유지 확인 -SELECT constraint_name, constraint_type FROM information_schema.table_constraints -WHERE table_name = 'broker_credentials' AND constraint_type = 'PRIMARY KEY'; --- 기대: broker_credentials_pkey | PRIMARY KEY +SELECT pg_get_constraintdef(oid) +FROM pg_constraint +WHERE conname = 'broker_credentials_pkey'; +-- 기대: PRIMARY KEY (user_id, broker) -- 기존 데이터 daemon_id=NULL 보존 확인 -SELECT COUNT(*) FROM broker_credentials; -- 기존 행 수 유지 +SELECT COUNT(*) AS total_rows FROM broker_credentials; +-- 기대: 2단계에서 확인한 total_rows와 동일 ``` +### Step1 롤백 절차 + +Step1에서 문제 발생 시: +```bash +alembic downgrade 20260605_0025 +``` + +예상 출력: +``` +INFO [alembic.runtime.migration] Running downgrade 20260628_0026 -> 20260605_0025, ... +``` + +롤백 검증: +```sql +SELECT column_name FROM information_schema.columns +WHERE table_name = 'broker_credentials' AND column_name = 'daemon_id'; +-- 기대: 결과 없음 (컬럼 제거됨) +``` + +> **중단 기준**: Step1 upgrade 실패(exit code != 0, 오류 메시지 출력) 시 즉시 downgrade 후 팀장에 보고. + --- -## 3단계: backfill CLI — daemon_id 매핑 +## 4단계: backfill CLI — daemon_id 매핑 -모든 기존 broker_credentials 행에 daemon_id를 설정한다. +Step1 적용 후 모든 기존 broker_credentials 행에 daemon_id를 설정한다. -```bash -# 사용자별로 실행 (daemon_id는 해당 user의 daemon id) -pfhub credential backfill-daemon-id --user --daemon-id +### 4-1: 대상 사용자/daemon 확인 -# --dry-run으로 먼저 확인 -pfhub credential backfill-daemon-id --user --daemon-id --dry-run -``` - -### 검증: NULL 0건 확인 +현재 user-scoped 인증정보(daemon_id IS NULL)가 있는 사용자와 그 데몬을 사전 파악한다: ```sql --- 이 쿼리가 0을 반환해야 Step2를 진행할 수 있다 +SELECT bc.user_id, u.username, bc.broker, bc.account_ref +FROM broker_credentials bc +LEFT JOIN users u ON bc.user_id = u.id +WHERE bc.daemon_id IS NULL; +``` + +각 (username, daemon_slug) 쌍을 확인한다. username은 포트폴리오 시스템에 등록된 사용자명, daemon_slug는 해당 사용자의 데몬 식별자(pfhub daemon list로 확인 가능). + +### 4-2: --dry-run으로 선 확인 + +```bash +# 각 사용자별로 실행 (daemon_id는 해당 user의 daemon slug 또는 UUID) +pfhub credential backfill-daemon-id \ + --user \ + --daemon-id \ + --dry-run +``` + +예상 출력: +``` +[dry-run] 대상 N건: + user_id= broker=kis → daemon_id= +{'status': 'dry_run', 'would_update': N} +``` + +### 4-3: 실제 백필 실행 + +```bash +pfhub credential backfill-daemon-id \ + --user \ + --daemon-id \ + --force +``` + +여러 사용자가 있으면 각 사용자별로 반복 실행한다. + +### 4-4: NULL 0건 게이트 — Step2 진행 전 필수 확인 + +```sql +-- 이 쿼리가 반드시 0을 반환해야 Step2를 진행할 수 있다 SELECT COUNT(*) FROM broker_credentials WHERE daemon_id IS NULL; -- 기대: 0 ``` **경고: NULL이 1건이라도 있으면 Step2 upgrade가 RuntimeError로 거부된다. 반드시 0건 확인 후 진행.** +> **중단 기준**: NULL 잔존 시 해당 행의 user와 daemon 매핑이 확인되지 않은 것이므로 팀장/사장님에게 보고 후 처리 방침 확인. 임의로 daemon을 지정하지 않는다. + --- -## 4단계: Step2 alembic upgrade (PK 전환) — 사장님 결재 필요 +## 5단계: Step2 alembic upgrade (PK 전환) — 사장님 결재 필수 -이 단계는 비가역 PK 변경이다. **사장님 결재 후에만 실행한다.** +> **CRITICAL**: 이 단계는 비가역 PK 변경이다. **사장님 결재 후에만 실행한다.** 결재 없이 실행 금지. + +Step2 이전 최종 체크리스트: +- [ ] DB 백업 완료 (1단계) +- [ ] Step1 적용·검증 완료 (3단계) +- [ ] NULL 0건 확인 완료 (4단계) +- [ ] 사장님 결재 획득 ```bash alembic upgrade 20260628_0027 @@ -106,88 +257,132 @@ alembic upgrade 20260628_0027 INFO [alembic.runtime.migration] Running upgrade 20260628_0026 -> 20260628_0027, broker_credentials: PK (user_id,broker) → (daemon_id,broker) 전환 (REQ-047 Step2) ``` -### 검증: Step2 완료 확인 +### Step2 검증 — 완료 확인 (필수) ```sql -- PK가 (daemon_id, broker)로 변경됐는지 확인 -SELECT constraint_name FROM information_schema.table_constraints -WHERE table_name = 'broker_credentials' AND constraint_type = 'PRIMARY KEY'; --- 기대: broker_credentials_pkey - SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'broker_credentials_pkey'; -- 기대: PRIMARY KEY (daemon_id, broker) -- daemon_id NOT NULL 확인 -SELECT column_name, is_nullable FROM information_schema.columns +SELECT column_name, is_nullable +FROM information_schema.columns WHERE table_name = 'broker_credentials' AND column_name = 'daemon_id'; -- 기대: daemon_id | NO (not nullable) --- user_id 컬럼 유지 확인 +-- user_id 컬럼 유지 확인 (감사/이력 목적, REQ-047 AC-4) SELECT column_name FROM information_schema.columns WHERE table_name = 'broker_credentials' AND column_name = 'user_id'; -- 기대: user_id 존재 + +-- 데이터 손실 없음 확인 +SELECT COUNT(*) AS total_rows FROM broker_credentials; +-- 기대: 2단계 total_rows와 동일 + +-- alembic head 갱신 확인 +-- (alembic 명령으로 확인) ``` ---- - -## 5단계: 코드 배포 (BAT-021 코드 포함) - -Step2 alembic upgrade가 완료된 후에 코드를 배포한다. - -포함 변경: -- `app/repositories/postgres.py`: - - `ensure_schema()` broker_credentials: daemon_id NOT NULL, PK=(daemon_id, broker) - - `_row_to_broker_credential()`: Step2 컬럼 순서 (daemon_id 선두) - - `upsert_broker_credential()`: ON CONFLICT (daemon_id, broker), fallback 제거 - - `list_broker_credentials()`: fallback 제거, Step2 컬럼 순서 - ---- - -## 6단계: 동작 확인 - ```bash -# pfhub CLI로 인증정보 조회/등록 테스트 -pfhub credential list --user --daemon-id - -# 서비스 헬스체크 -curl http:///health +alembic current +# 기대: 20260628_0027 (head) ``` ---- +### Step2 롤백 절차 (조건부 가역 — 주의) -## 롤백 절차 - -### Step2 롤백 (alembic downgrade) - -Step2 downgrade는 (user_id, broker) 중복이 없는 경우에만 가능하다. +Step2 downgrade는 같은 user의 여러 daemon이 동일 broker를 사용하는 경우 RuntimeError로 자동 거부된다. ```bash -# Step2 → Step1 상태로 복원 +# Step2 → Step1 상태로 복원 시도 alembic downgrade 20260628_0026 ``` -**경고**: 같은 user의 여러 daemon이 동일 broker를 사용하는 경우 downgrade가 RuntimeError로 거부된다. -이 경우 수동 데이터 정리 후 downgrade하거나 백업에서 복원한다. - -### Step1 롤백 - -```bash -# Step1 → 이전 상태로 복원 (daemon_id 컬럼 제거) -alembic downgrade 20260605_0025 +**경고**: 같은 user의 여러 daemon이 동일 broker를 사용하는 경우 downgrade가 다음과 같이 거부된다: ``` +RuntimeError: downgrade 불가: N개 (user_id, broker) 조합이 중복됩니다. +같은 user의 여러 daemon이 동일 broker를 사용하고 있어 +(user_id, broker) PK로 복원 시 데이터 손실이 발생합니다. +수동으로 데이터를 정리한 후 downgrade하세요. +``` + +이 경우 백업에서 복원해야 한다(아래 DB 복원 참조). ### DB 복원 (최후 수단) ```bash -pg_restore -h -U -d -c backup_pre_bat021_.dump +pg_restore -h -U -d -c \ + backup_pre_bat021_.dump +``` + +> **비가역 지점 강조**: Step2 PK 전환 완료 후에는 구 스키마(`PK=(user_id, broker)`)로의 롤백이 데이터 정리 없이 불가능할 수 있다. **백업 없이 Step2를 실행하면 복원이 불가능하다.** Step2 완료 후 추가 백업도 권장한다. + +--- + +## 6단계: BAT-021 코드 배포 (Step2 완료 이후) + +Step2 alembic upgrade 완료 후 코드를 배포한다. + +포함 변경: +- `app/repositories/postgres.py`: + - `ensure_schema()` broker_credentials: daemon_id NOT NULL, PK=(daemon_id, broker) 정합 + - `_row_to_broker_credential()`: Step2 컬럼 순서 (daemon_id 선두) + - `upsert_broker_credential()`: ON CONFLICT (daemon_id, broker), fallback 제거 + - `list_broker_credentials()`: fallback 제거, Step2 컬럼 순서 + +```bash +# portfolio-hub 서비스 rolling restart (freeze 커밋 9fe6f3b) +# 배포 방법은 운영 절차에 따름 (예: 컨테이너 재시작, systemd restart 등) ``` --- -## 주의사항 +## 7단계: 배포 후 동작 확인 -- Step2 이전에 BAT-021 코드를 배포하면 `ON CONFLICT (daemon_id, broker)` 절이 구 스키마와 맞지 않아 오류 발생. -- 배포 순서: Step1 → backfill → Step2 → 코드 배포 순서를 엄수한다. +```bash +# 헬스체크 +curl -s http:///health | python3 -m json.tool +# 기대: {"status": "ok", "db_enabled": true, ...} + +# pfhub CLI로 인증정보 조회 테스트 (Step2 이후 스키마 정합) +pfhub credential list --user --daemon-id +# 기대: 인증정보 1건, daemon_id가 설정된 상태 +``` + +--- + +## go/no-go 게이트 + +| 게이트 | 조건 | 판단 | +| --- | --- | --- | +| DB 백업 | 파일 생성 + 크기 > 0 + exit 0 | go / **NO-GO → 중단** | +| alembic current | `20260605_0025 (head)` (Step1 이전 기준) | go / **NO-GO → 팀장 보고** | +| 활성 락 없음 | broker_credentials 대상 활성 쿼리 없음 | go / **NO-GO → 대기 또는 재조정** | +| Step1 검증 | daemon_id TEXT NULL, PK=(user_id,broker) 유지 | go / **NO-GO → Step1 downgrade 후 팀장 보고** | +| NULL 0건 | `SELECT COUNT(*) ... WHERE daemon_id IS NULL` = 0 | go / **NO-GO → backfill 재수행 또는 팀장 보고** | +| 사장님 결재 | 명시적 결재 확인 | go / **NO-GO → Step2 실행 금지** | +| Step2 검증 | PK=(daemon_id,broker), NOT NULL, user_id 존재, 행 수 일치 | go / **NO-GO → 롤백 또는 DB 복원** | +| 배포 후 헬스 | GET /health → 200, db_enabled=true | go / **NO-GO → 코드 롤백** | + +### 중단 기준 요약 + +이 중 하나라도 발생하면 즉시 해당 단계를 중단하고 팀장에게 보고한다: +1. DB 백업 실패 +2. alembic current가 기대값과 다름 +3. broker_credentials 활성 락 존재 (ops 윈도우 재조정) +4. Step1 upgrade 실패 +5. NULL 잔존 (backfill 미완료) +6. 사장님 결재 미획득 상태에서 Step2 실행 시도 +7. Step2 upgrade 실패 +8. Step2 검증 쿼리 기대값 불일치 +9. 배포 후 헬스체크 실패 + +--- + +## 주의사항 요약 + +- Step2 이전에 BAT-021 코드를 배포하면 `ON CONFLICT (daemon_id, broker)` 절이 구 스키마와 맞지 않아 인증정보 upsert 오류 발생. +- 배포 순서: **Step1 → backfill → NULL 0건 확인 → Step2(결재) → 코드 배포** 순서를 엄수한다. - `ensure_schema()`는 `IF NOT EXISTS`를 사용하므로 Step2 적용 후 실행 시 테이블이 이미 존재해 스킵됨 (신규 인스턴스 초기화 시에만 적용됨). +- `CREDENTIAL_ENCRYPTION_KEY`가 운영 환경에 설정돼 있지 않으면 인증정보 암·복호화가 동작하지 않는다. 배포 전 반드시 확인.