docs(rel-003): M3 daemon-scoped 크레덴셜 릴리즈 문서 + Step2 ops 런북
REL-003(dcy-010/011 + WAC #15 동반) draft + Step2 마이그레이션 핸드오프(프리체크·롤백·go/no-go). 내일 ops 윈도우용. 미푸시(사장님 결재 시 release와 함께 push). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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 <username> --daemon-id <daemon_slug>` — 기존 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 <DB_HOST> -U <DB_USER> -d <DB_NAME> -c backup_pre_bat021_<TIMESTAMP>.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:
|
||||
- (배포 후 기입)
|
||||
@@ -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 <DB_HOST> -U <DB_USER> -d <DB_NAME> -F c -f backup_pre_bat021_$(date +%Y%m%d%H%M%S).dump
|
||||
pg_dump -h <DB_HOST> -U <DB_USER> -d <DB_NAME> -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=<DB_HOST>
|
||||
@@ -40,62 +73,180 @@ export TIMESCALEDB_USER=<DB_USER>
|
||||
export TIMESCALEDB_PASSWORD=<DB_PASS>
|
||||
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 <prev> -> 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 <username> --daemon-id <daemon_slug>
|
||||
### 4-1: 대상 사용자/daemon 확인
|
||||
|
||||
# --dry-run으로 먼저 확인
|
||||
pfhub credential backfill-daemon-id --user <username> --daemon-id <daemon_slug> --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 <username> \
|
||||
--daemon-id <daemon_slug> \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
예상 출력:
|
||||
```
|
||||
[dry-run] 대상 N건:
|
||||
user_id=<uuid> broker=kis → daemon_id=<daemon_uuid>
|
||||
{'status': 'dry_run', 'would_update': N}
|
||||
```
|
||||
|
||||
### 4-3: 실제 백필 실행
|
||||
|
||||
```bash
|
||||
pfhub credential backfill-daemon-id \
|
||||
--user <username> \
|
||||
--daemon-id <daemon_slug> \
|
||||
--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 <username> --daemon-id <daemon_id>
|
||||
|
||||
# 서비스 헬스체크
|
||||
curl http://<SERVICE_HOST>/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 <DB_HOST> -U <DB_USER> -d <DB_NAME> -c backup_pre_bat021_<TIMESTAMP>.dump
|
||||
pg_restore -h <DB_HOST> -U <DB_USER> -d <DB_NAME> -c \
|
||||
backup_pre_bat021_<TIMESTAMP>.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://<SERVICE_HOST>/health | python3 -m json.tool
|
||||
# 기대: {"status": "ok", "db_enabled": true, ...}
|
||||
|
||||
# pfhub CLI로 인증정보 조회 테스트 (Step2 이후 스키마 정합)
|
||||
pfhub credential list --user <username> --daemon-id <daemon_slug>
|
||||
# 기대: 인증정보 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`가 운영 환경에 설정돼 있지 않으면 인증정보 암·복호화가 동작하지 않는다. 배포 전 반드시 확인.
|
||||
|
||||
Reference in New Issue
Block a user