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
14 KiB
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-052054)
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_infodaemon-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)
전제 조건 (배포 시작 전 필수 확인)
-
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를 복호화할 수 없게 됨)
- 확인: 서비스 실행 환경에서
-
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)
- 검증 쿼리:
-
alembic 상태 사전 확인 — 현행 운영 DB의 alembic head가
20260628_0027(Step2 완료 후) 인지 확인:alembic current # 기대: 20260628_0027 (head) -
DB 백업 — Step2 마이그레이션 적용 전 운영 DB 전체 스냅샷 백업 (런북 1단계 참조).
-
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 윈도우 전용 — 코드 배포 전 수행]
- DB 백업: 운영 DB 전체 스냅샷 (
pg_dump) — 런북 1단계 참조 - Step1 upgrade (
alembic upgrade 20260628_0026):broker_credentials에daemon_id TEXT NULL컬럼 추가. 기존 PK(user_id, broker)유지. - backfill CLI:
pfhub credential backfill-daemon-id --user <username> --daemon-id <daemon_slug>— 기존 user-scoped 인증정보 행에 daemon_id 설정. 사용자별 반복.--dry-run선 확인 권장. - NULL 0건 검증:
SELECT COUNT(*) FROM broker_credentials WHERE daemon_id IS NULL;→ 반드시 0 확인. 1건이라도 있으면 Step2 진행 금지 (upgrade가 RuntimeError로 자동 거부함). - Step2 upgrade (
alembic upgrade 20260628_0027, 사장님 결재 필수): PK를(user_id, broker)→(daemon_id, broker)로 전환.daemon_id NOT NULL제약 추가. 비가역 작업. - Step2 스키마 검증: PK 전환·daemon_id NOT NULL·user_id 유지를 SQL 쿼리로 확인 — 런북 4단계 검증 SQL 참조.
[코드 배포]
- 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]
- 아래 Verification Checklist 항목 순서대로 실행.
Rollback Plan
트리거 조건
GET /healthHTTP 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 가능:
alembic downgrade 20260628_0026 # Step2 → Step1 상태
alembic downgrade 20260605_0025 # Step1 → 이전 상태 (daemon_id 컬럼 제거)
경고: Step2 downgrade는 조건부 가역이다. 백업 복원이 최후 수단.
pg_restore -h <DB_HOST> -U <DB_USER> -d <DB_NAME> -c backup_pre_bat021_<TIMESTAMP>.dump
롤백 영향 분석
- REQ-049 (
/v1/user_infodaemon-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 200PATCH /v1/admin/daemons/{daemon_id}(admin-token) — display_name 변경 → HTTP 200PATCH /v1/admin/daemons/{daemon_id}(admin-token, is_active=false) — 비활성화 → HTTP 200 + is_active=falseDELETE /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 200POST /v1/portfolios/{id}/ledger/trades(trade-write-token) → 기존 동작 유지GET /v1/portfolios/{id}/trades(read-token 이상) → HTTP 200GET /v1/portfolios/{id}/pnl/realized(read-token 이상) → HTTP 200
Known Issues (릴리즈 전 인지 사항)
- BAT-022 AC-3 통합 수준: REQ-052 AC-3(CLI·API·service-token 3경로 정합)이 unit-level 검증. 실 DB 통합 환경에서 재확인 권장(비가역 아님, 선택적).
owner 재할당 원자성(해소됨 — 오기 정정):update_daemon_owner()는 단일conn.transaction()트랜잭션으로 credential DELETE(선행) → owner UPDATE를 원자 처리한다(BAT-022 QC Major fix, 커밋83e6dde). 어느 한쪽 실패 시 전체 롤백되어 "owner만 바뀌고 구 인증정보 잔존"하는 leak 상태가 발생하지 않는다. 라이브 통합검증(2026-06-28)에서 owner 재할당 → 인증정보 무효화 PASS. 리스크 아님.- DELETE credential broker 고정:
DELETE /v1/daemons/{daemon_id}/credential은 broker="kis" 고정. 다중 broker 지원 시 별도 설계 필요(현재 단일 broker 가정 설계). - 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 결과 기입)
- Health Endpoint:
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:
- (배포 후 기입)