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
389 lines
14 KiB
Markdown
389 lines
14 KiB
Markdown
# 운영 적용 런북: REQ-047 Step2 — broker_credentials PK 전환 (daemon_id, broker)
|
|
|
|
- Batch: BAT-021
|
|
- Discovery: dcy-010
|
|
- Date: 2026-06-28
|
|
- 결재 게이트: 사장님 결재 필수 (비가역 PK 변경 — Step 4)
|
|
- 작업 예상 시간: 약 20~30분 (데이터량에 따라 상이)
|
|
|
|
---
|
|
|
|
## 전제 조건
|
|
|
|
- REQ-046 ~ REQ-051 코드(BAT-020 PR #17, d4acfd4)가 배포돼 있어야 한다.
|
|
- BAT-021 코드(ensure_schema 갱신, ON CONFLICT 수정)는 **Step4 적용 완료 이후** 배포한다(아래 배포 순서 준수).
|
|
- 마이그레이션 전 DB 백업이 완료돼 있어야 한다.
|
|
- 이 런북의 모든 alembic 명령은 `PORTFOLIOHUB_DB_ENABLED=true` 및 DB 접속 환경변수가 설정된 셸에서 실행한다.
|
|
|
|
---
|
|
|
|
## 배포 순서 개요
|
|
|
|
```
|
|
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 -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단계: 사전 프리체크 (마이그레이션 시작 전)
|
|
|
|
### 2-1: 현재 alembic head/stamp 확인
|
|
|
|
```bash
|
|
export TIMESCALEDB_HOST=<DB_HOST>
|
|
export TIMESCALEDB_PORT=<DB_PORT>
|
|
export TIMESCALEDB_NAME=<DB_NAME>
|
|
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 20260605_0025 -> 20260628_0026, broker_credentials: daemon_id TEXT NULL 컬럼 추가 (REQ-047 Step1)
|
|
```
|
|
|
|
### Step1 검증
|
|
|
|
```sql
|
|
-- 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 | text | YES (nullable)
|
|
|
|
-- 기존 PK (user_id, broker) 유지 확인
|
|
SELECT pg_get_constraintdef(oid)
|
|
FROM pg_constraint
|
|
WHERE conname = 'broker_credentials_pkey';
|
|
-- 기대: PRIMARY KEY (user_id, broker)
|
|
|
|
-- 기존 데이터 daemon_id=NULL 보존 확인
|
|
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 후 팀장에 보고.
|
|
|
|
---
|
|
|
|
## 4단계: backfill CLI — daemon_id 매핑
|
|
|
|
Step1 적용 후 모든 기존 broker_credentials 행에 daemon_id를 설정한다.
|
|
|
|
### 4-1: 대상 사용자/daemon 확인
|
|
|
|
현재 user-scoped 인증정보(daemon_id IS NULL)가 있는 사용자와 그 데몬을 사전 파악한다:
|
|
|
|
```sql
|
|
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을 지정하지 않는다.
|
|
|
|
---
|
|
|
|
## 5단계: Step2 alembic upgrade (PK 전환) — 사장님 결재 필수
|
|
|
|
> **CRITICAL**: 이 단계는 비가역 PK 변경이다. **사장님 결재 후에만 실행한다.** 결재 없이 실행 금지.
|
|
|
|
Step2 이전 최종 체크리스트:
|
|
- [ ] DB 백업 완료 (1단계)
|
|
- [ ] Step1 적용·검증 완료 (3단계)
|
|
- [ ] NULL 0건 확인 완료 (4단계)
|
|
- [ ] 사장님 결재 획득
|
|
|
|
```bash
|
|
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 검증 — 완료 확인 (필수)
|
|
|
|
```sql
|
|
-- PK가 (daemon_id, broker)로 변경됐는지 확인
|
|
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
|
|
WHERE table_name = 'broker_credentials' AND column_name = 'daemon_id';
|
|
-- 기대: daemon_id | NO (not nullable)
|
|
|
|
-- 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 명령으로 확인)
|
|
```
|
|
|
|
```bash
|
|
alembic current
|
|
# 기대: 20260628_0027 (head)
|
|
```
|
|
|
|
### Step2 롤백 절차 (조건부 가역 — 주의)
|
|
|
|
Step2 downgrade는 같은 user의 여러 daemon이 동일 broker를 사용하는 경우 RuntimeError로 자동 거부된다.
|
|
|
|
```bash
|
|
# Step2 → Step1 상태로 복원 시도
|
|
alembic downgrade 20260628_0026
|
|
```
|
|
|
|
**경고**: 같은 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
|
|
```
|
|
|
|
> **비가역 지점 강조**: 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단계: 배포 후 동작 확인
|
|
|
|
```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`가 운영 환경에 설정돼 있지 않으면 인증정보 암·복호화가 동작하지 않는다. 배포 전 반드시 확인.
|