Files
portfolio-hub/docs/runbooks/bat-021-req047-step2-migration-runbook.md
T
genichinandClaude Opus 4.8 9d98d47e2d 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
2026-06-28 23:02:35 +09:00

14 KiB

운영 적용 런북: 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에 마이그레이션을 적용하기 전 전체 스냅샷을 백업한다.

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

백업 완료 후 파일 크기와 종료 코드를 확인한다:

ls -lh backup_pre_bat021_*.dump
# 기대: 파일이 존재하고 크기가 0이 아닌 값
echo $?  # 기대: 0

중단 기준: 백업 실패(파일 미생성, 크기 0, exit code != 0) 시 이후 단계 진행 금지. 백업 성공 확인 후 진행.


2단계: 사전 프리체크 (마이그레이션 시작 전)

2-1: 현재 alembic head/stamp 확인

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 현재 스키마 확인

-- 현재 컬럼 목록 확인
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 현황 확인

-- 전체 행 수 (백업·복원 검증용으로 기록해 둘 것)
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 및 락 확인

-- 현재 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 컬럼 추가)

alembic upgrade 20260628_0026

예상 출력:

INFO  [alembic.runtime.migration] Running upgrade 20260605_0025 -> 20260628_0026, broker_credentials: daemon_id TEXT NULL 컬럼 추가 (REQ-047 Step1)

Step1 검증

-- 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에서 문제 발생 시:

alembic downgrade 20260605_0025

예상 출력:

INFO  [alembic.runtime.migration] Running downgrade 20260628_0026 -> 20260605_0025, ...

롤백 검증:

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)가 있는 사용자와 그 데몬을 사전 파악한다:

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으로 선 확인

# 각 사용자별로 실행 (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: 실제 백필 실행

pfhub credential backfill-daemon-id \
  --user <username> \
  --daemon-id <daemon_slug> \
  --force

여러 사용자가 있으면 각 사용자별로 반복 실행한다.

4-4: NULL 0건 게이트 — Step2 진행 전 필수 확인

-- 이 쿼리가 반드시 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단계)
  • 사장님 결재 획득
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 검증 — 완료 확인 (필수)

-- 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 명령으로 확인)
alembic current
# 기대: 20260628_0027 (head)

Step2 롤백 절차 (조건부 가역 — 주의)

Step2 downgrade는 같은 user의 여러 daemon이 동일 broker를 사용하는 경우 RuntimeError로 자동 거부된다.

# 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 복원 (최후 수단)

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 컬럼 순서
# portfolio-hub 서비스 rolling restart (freeze 커밋 9fe6f3b)
# 배포 방법은 운영 절차에 따름 (예: 컨테이너 재시작, systemd restart 등)

7단계: 배포 후 동작 확인

# 헬스체크
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가 운영 환경에 설정돼 있지 않으면 인증정보 암·복호화가 동작하지 않는다. 배포 전 반드시 확인.