<!-- canonical: https://docs.axelabs.ai/ops/runbook/deploy-ssot -->
<!-- source: content/ops/runbook/deploy-ssot.mdx -->

---
title: 배포 SSOT 아키텍처
description: origin/main 의 commit SHA = 배포의 진실원천. worktree 작업격리 + build-from-SHA + deploy lock + 스테이지 상태기계 + migration-validation 게이트로 다중 세션 배포 엉킴을 구조적으로 제거.
playbook: true
---

# 배포 SSOT 아키텍처 — `origin/main` SHA = 배포 진실원천

> **원칙 (한 줄)**: `origin/main 의 commit SHA = 배포의 SSOT.` working tree·로컬 main·실행 중 컨테이너는 전부 그 SHA 의 파생·일회용 투영이다.

AXE 가 docs·backlog(matrix-postgres, [D-matrix-3](/ops/decisions))에 이미 적용 중인 SSOT 규율을 **배포**로 확장한 것. 결정 근거 = [D-ops-42](/ops/decisions).

## 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](/ops/runbook/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](/ops/decisions)). 훅 + 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 보존.

1. **additive** — A–F 각 컴포넌트는 기존 동작과 **공존**하게 추가. 기존 `axe deploy` / `axe ship` 경로를 깨지 않음.
2. **canary 검증** — 각 컴포넌트를 `--dry-run` + passive-color 카나리 (flip 안 함) 로 검증. active 트래픽 영향 0.
3. **migration 게이트** — 스키마 변경 포함 릴리스는 ephemeral DB clone 검증 통과 후에만 flip.
4. **cutover** — 기본값 flip 전환 + 가드 활성화 (pre-push 훅 / branch protection). escape hatch 포함 (응급 시 우회 경로). 게이트됨 (운영자 확인).
5. **되돌림** — 문제 시 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](/ops/decisions) — 본 아키텍처 결정 근거 (컴포넌트 A–F 상세 + 실측 incident)
- [Release flow (`axe ship`)](/ops/runbook/release-flow) — F 의 ship 흐름이 확장하는 기존 release-gate ([D-ops-16](/ops/decisions))
- [Blue/green deploy](/ops/runbook/deploy) — 스테이지 파이프라인의 alias swap + 5초 rollback 메커니즘
- [D-matrix-3](/ops/decisions) — matrix-postgres SSOT (C 의 advisory lock + backlog/roadmap/updates 작업 스테이지)
