배포 SSOT 아키텍처 — origin/main SHA = 배포 진실원천
원칙 (한 줄):
origin/main 의 commit SHA = 배포의 SSOT.working tree·로컬 main·실행 중 컨테이너는 전부 그 SHA 의 파생·일회용 투영이다.
AXE 가 docs·backlog(matrix-postgres, D-matrix-3)에 이미 적용 중인 SSOT 규율을 배포로 확장한 것. 결정 근거 = D-ops-42.
AI 요청 프롬프트
https://docs.axelabs.ai/ops/runbook/deploy-ssot 따라 배포 SSOT 아키텍처를 rollout 해줘.
진행:
1. 현재 상태 진단 (어느 컴포넌트 A–F 가 이미 있고 어느 게 없는지 + axe CLI 버전관리 여부 + 지금 라이브 SHA vs origin/main drift)
2. additive 원칙 확인 — 각 컴포넌트는 기존 동작과 공존하게 추가 (기존 axe deploy/ship 경로 안 깨고)
3. 각 컴포넌트 추가마다 --dry-run + passive-color 카나리 (flip 안 함) 로 검증, flip 은 게이트 (사용자 확인)
4. migration 포함 릴리스면 ephemeral DB clone 검증 게이트 통과 후에만 flip
5. cutover (기본값 flip + pre-push 훅/branch protection 가드 활성화) 는 escape hatch 포함 + 사용자 확인 후본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타.
페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감.
동기 — 공유 tree + 공유 main 의 세 고장
다중 동시 Claude Code 세션이 repo 당 (frame / blueprint / hive / index / cortex / matrix) 1 working tree + 1 main 을 공유한다. 여기서 세 고장이 동시에 난다:
| 고장 | 메커니즘 |
|---|---|
| push 거부 | 타 세션이 먼저 push → 로컬 main 이 origin 과 diverge → non-fast-forward → push blocked |
| WIP 혼재 | 서로 다른 feature 의 uncommitted 변경이 한 tree 에 누적 → 누구도 자기 slice 만 clean commit 불가 |
| dirty 누출 | axe deploy 가 dirty working tree 에서 이미지 빌드 → uncommitted 코드 prod 누출 + push 전 배포 가능 |
실측 (2026-06-04): 이미 배포까지 끝난 blueprint fix (commit b4067504) 가 main diverge + 타 세션 WIP 로 수 시간 push 불가 → 운영자가 손으로 reconcile (stash → rebase → push). 같은 병의 가장 깊은 사례 = 운영자 axe CLI (364KB) 자체가 버전 미관리 + in-place 편집 — 배포 도구인데 자기 자신은 SHA 추적 밖.
스테이지 파이프라인
blue/green 이 이미 canary/live/previous 3스테이지를 제공한다. 거기에 migration-validation 게이트 하나만 끼운다:
┌─────────┐ ┌────────────────────┐ ┌──────────────────────┐ ┌────────────┐ ┌──────────────────────┐
│ built │ ───► │ canary │ ───► │ [migration-validation │ ───► │ live │ ───► │ previous │
│ <svc>: │ │ passive color │ │ 게이트] │ │ active │ │ 직전 active │
│ <sha> │ │ 트래픽 0 │ │ ephemeral DB clone 에 │ │ flip │ │ = 즉시 롤백 타깃 │
└─────────┘ │ health 검증 │ │ migration 적용·검증 │ │ (alias swap)│ └──────────────────────┘
▲ └────────────────────┘ └──────────────────────┘ └────────────┘ │
│ │ fail │ rollback
│ build-from-SHA (clean checkout) ▼ ▼
pushed SHA only flip 거부 alias 되돌림 (5초)- built — pushed SHA 의 clean checkout 에서 이미지 빌드, 태그
<svc>:<sha>. push 안 된 SHA 는 여기 진입 거부. - canary — passive color 에 배포, 트래픽 0. health 검증만 (active 영향 0).
- migration-validation 게이트 — 스키마 변경 포함 시에만. prod DB 의 ephemeral clone 에 migration 적용·검증. fail = flip 거부.
- live — active color flip (alias swap). 사용자 다운타임 0.
- previous — 직전 active = 즉시 롤백 타깃 (alias 되돌림 5초, Blue/green deploy § Rollback).
컴포넌트 A–F — 각자 제거하는 고장
각 컴포넌트는 특정 고장 하나를 구조적으로 없앤다.
A. 작업 격리 — axe work <svc> <slug>
세션마다 origin/main 에서 분기한 git worktree (~/.worktrees/<svc>/<slug>). 정규 repo 는 fast-forward 전용 “main mirror” 로 강등 (손편집 금지 — mirror 는 origin 의 읽기 투영일 뿐).
제거하는 고장: 공유 tree 경합 · WIP 혼재 · stash 더미 = 발생 불가능. 각 세션이 자기 tree 에서 자기 slice 만 commit.
B. SHA 에서만 빌드
deploy 가 working tree 가 아니라 pushed SHA 의 clean checkout (git worktree --detach <sha> 또는 git archive) 에서 이미지를 빌드한다. 이미지 태그 = <svc>:<sha>. push 안 된 SHA 는 배포 거부.
제거하는 고장: deploy-before-push + dirty-tree 누출 = 구조적 불가능. 빌드 입력이 origin 에 도달한 SHA 로 한정되므로 uncommitted 코드가 prod 에 갈 경로 자체가 없음.
C. Deploy lock
서비스당 배포 직렬화. matrix-postgres pg_advisory_lock 우선 (이미 matrix DB 가 SSOT), 파일락 fallback (matrix down 시).
제거하는 고장: 두 세션이 같은 서비스 blue/green 을 동시에 flip → alias 경합 / 어느 color 가 active 인지 불명 = 불가능.
D. Provenance + drift
컨테이너에 org.axe.git_sha 라벨 부여. axe health / axe host 가 color 별 실행 SHA + origin/main 대비 drift 를 표시.
제거하는 고장: “지금 라이브가 git 과 일치하나” 가 silent 하게 불명이던 상태. 항상 답이 나옴 (drift 가시화).
E. 잘못된 경로 제거
axe deploy 가 더 이상 tree 입력을 받지 않는다 (B 가 SHA 입력으로 대체). git push origin main 직접 = pre-push 훅 + GitHub branch protection 으로 기계적 차단. axe ship 만 main 을 전진시킨다.
제거하는 고장: 현재는 “main 직접 push 금지” 가 honor-system (사회적 규약뿐, D-ops-16). 훅 + branch protection 으로 기계적 강제 전환.
F. 통합 ship 흐름 — axe ship
worktree 에서 한 명령으로:
fetch → origin/main 에 rebase → SHA 에서 build + test → push (main fast-forward)
→ 그 SHA 를 deploy (C 의 lock 하에) → main mirror fast-forward → shiplog 기록제거하는 고장: 오늘 운영자가 손으로 한 reconcile (fetch / stash / rebase / push / deploy / 정리) 전 단계가 한 명령으로 압축. b4067504 같은 incident 가 명령 한 줄로 끝남.
안전한 rollout — additive → canary → cutover
전부 비파괴. 진행 중 WIP 보존.
- additive — A–F 각 컴포넌트는 기존 동작과 공존하게 추가. 기존
axe deploy/axe ship경로를 깨지 않음. - canary 검증 — 각 컴포넌트를
--dry-run+ passive-color 카나리 (flip 안 함) 로 검증. active 트래픽 영향 0. - migration 게이트 — 스키마 변경 포함 릴리스는 ephemeral DB clone 검증 통과 후에만 flip.
- cutover — 기본값 flip 전환 + 가드 활성화 (pre-push 훅 / branch protection). escape hatch 포함 (응급 시 우회 경로). 게이트됨 (운영자 확인).
- 되돌림 — 문제 시 previous color alias 되돌림 (5초) + 가드 비활성화로 이전 운영 모델 복귀.
왜 별도 staging 환경을 안 쓰나
별도 staging 환경 (staging.axelabs.ai + 독립 DB + 독립 터널) 은 채택 안 함.
- AXE 는 단일 Mac mini 호스트. staging 환경을 띄워도 prod 와 같은 호스트 → 새 실패유형을 안 잡음 (호스트·네트워크·DB 엔진 동일).
- 비용 (컨테이너 2배 + DB 2배) 과 parity 유지 부담 (staging 이 prod 와 drift 하면 검증 가치 0) 만 증가.
- 실제로 막아야 할 위험 = (a) 깨진 코드가 트래픽 받기, (b) 깨지는 스키마 변경. (a) 는 passive-color 카나리 (트래픽 0 에서 health 검증) 가, (b) 는 migration-validation 게이트 가 이미 커버.
canary + migration-validation 이 staging 의 실효를 단일 호스트 비용 0 추가로 달성한다.
왜 migration-validation 이 본질인가
blue/green 의 유일한 사각이 스키마 마이그레이션이다.
- blue 와 green 은 DB 를 공유한다 (코드만 2벌, 데이터는 1벌).
- 따라서 green 용 마이그레이션을 적용하면 blue 도 즉시 그 스키마를 본다. 깨지는 변경 (컬럼 drop / 타입 변경 / NOT NULL 추가) 은 green flip 전에 이미 blue 를 오염시킨다.
- 즉 스키마 변경은 blue/green 으로 카나리가 원천적으로 불가능 — passive color 검증이 active 를 보호하지 못함.
migration-validation 게이트가 이 사각을 덮는다: flip 전에 prod DB 의 ephemeral clone (axe drill / backup 스냅샷 기계 재사용) 에 마이그레이션을 적용·검증. clone 에서 깨지면 flip 거부 → 공유 DB 는 손대지 않음. 대상 예 = blueprint 대기 migration 2개 (add_user_entra_oid, add_entity_legal_name), hive alembic.
관련
- D-ops-42 — 본 아키텍처 결정 근거 (컴포넌트 A–F 상세 + 실측 incident)
- Release flow (
axe ship) — F 의 ship 흐름이 확장하는 기존 release-gate (D-ops-16) - Blue/green deploy — 스테이지 파이프라인의 alias swap + 5초 rollback 메커니즘
- D-matrix-3 — matrix-postgres SSOT (C 의 advisory lock + backlog/roadmap/updates 작업 스테이지)