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
운영 적용 런북: 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 → 코드 롤백 |
중단 기준 요약
이 중 하나라도 발생하면 즉시 해당 단계를 중단하고 팀장에게 보고한다:
- DB 백업 실패
- alembic current가 기대값과 다름
- broker_credentials 활성 락 존재 (ops 윈도우 재조정)
- Step1 upgrade 실패
- NULL 잔존 (backfill 미완료)
- 사장님 결재 미획득 상태에서 Step2 실행 시도
- Step2 upgrade 실패
- Step2 검증 쿼리 기대값 불일치
- 배포 후 헬스체크 실패
주의사항 요약
- Step2 이전에 BAT-021 코드를 배포하면
ON CONFLICT (daemon_id, broker)절이 구 스키마와 맞지 않아 인증정보 upsert 오류 발생. - 배포 순서: Step1 → backfill → NULL 0건 확인 → Step2(결재) → 코드 배포 순서를 엄수한다.
ensure_schema()는IF NOT EXISTS를 사용하므로 Step2 적용 후 실행 시 테이블이 이미 존재해 스킵됨 (신규 인스턴스 초기화 시에만 적용됨).CREDENTIAL_ENCRYPTION_KEY가 운영 환경에 설정돼 있지 않으면 인증정보 암·복호화가 동작하지 않는다. 배포 전 반드시 확인.