# task-2988 — InsuRo 백엔드 배포 원자화: 워킹트리 직접 참조 제거 + release 고정

**레벨**: Lv.3 (프로덕션 인프라) · **팀**: dev7-team (이참나) · **회장 승인 완료** (2026-08-20)

## 배경 — 실제로 사고가 났다
2026-08-20, 다음 상태가 **16시간 넘게** 지속됐다.
- 디스크(워킹트리) HEAD = `74679f2` (워크벤치 백엔드 3,015줄 포함, 프로덕션 미검증)
- 실제 구동 중인 프로세스가 로드한 코드 = `49c17a9`
- `insuro-api.service` 는 `Restart=always` · `RestartSec=5`

즉 **프로세스가 어떤 이유로든 죽으면 5초 뒤 검증되지 않은 3,015줄이 자동으로 라이브가 되는 상태**였다.
"아직 배포 안 함"이 아니라 **"크래시가 배포 버튼이 된 상태"**다.
ANU 가 디스크를 `49c17a9` 로 detached 고정해 임시 차단했다(현재 상태).

## 근본 결함 (ANU·Codex 합의 진단)
병렬 개발이나 git 버전관리의 실패가 **아니다**. main 은 깨끗했고 커밋 소실도 0이었다.
결함은 배포 계층에 있으며, 셋이 겹쳤다.
1. **라이브 서비스가 git 워킹트리를 직접 참조**한다. 실행 아티팩트가 불변 산출물이 아니라 "언제든 바뀌는 폴더"다.
2. **`git pull` 과 재기동이 원자적으로 묶여 있지 않다.** pull 만 하면 메모리와 디스크가 갈라진다.
3. **`Restart=always` 가 크래시를 비통제 배포 트리거로 만든다.** 재시작이 "승인된 버전"이 아니라 "그때 폴더에 있던 것"을 집는다.

## 현재 환경 (실측)
- `insuro-api.service` = **user 스코프** systemd. 반드시 `systemctl --user` 로 다룰 것(system 스코프는 빈 값 반환 → 오판 유발).
- `WorkingDirectory=/home/jay/projects/InsuRo/server`
- `ExecStart=/home/jay/.local/bin/uvicorn main:app --port 8001 --host 0.0.0.0`
- `Restart=always` · `RestartSec=5`
- `/api/status` 가 `sha` · `sha_short` · `started_at` · `uptime_sec` · `db` 를 반환한다. **판정에 이걸 쓴다.**
- 저장소는 현재 **detached HEAD `49c17a9`**. 복귀는 `git checkout main`.

## 목표 (우선순위 순)
### P1 — 서비스가 워킹트리를 직접 보지 않게 한다
- `/releases/<sha>/` 형태로 배포하고 **symlink(예: `current`) 교체**로 전환한다.
- `WorkingDirectory` 는 symlink 를 가리킨다.
- 이것 하나로 결함 1번과 3번이 동시에 닫힌다. **가장 중요하다.**

### P2 — 배포를 원자적으로 만든다
- fetch → checkout → 의존성 설치 → 빌드 → **검증**까지 끝낸 뒤 **마지막에만** symlink 를 전환한다.
- 어느 단계든 실패하면 **현재 release 를 그대로 유지**한다(전환하지 않는다).
- 배포 스크립트 1개로 제공한다. 사람이 여러 명령을 순서대로 치는 방식은 금지 — 그게 이번 사고의 원인이다.

### P3 — 크래시 재시작이 새 코드를 집지 못하게 한다
- 재시작은 **현재 승인된 release** 만 로드해야 한다.
- symlink 방식이면 자연히 충족되지만, **실제로 그런지 검증**해야 한다(아래 검증 4번).

### P4 — 안전장치
- 배포 직후 헬스체크(`/api/status`) 실패 시 **자동 롤백**(직전 release 로 symlink 복귀 + 재기동).
- 승인 없는 `git pull` 이 라이브에 영향을 주지 못하게 한다(P1 이 되면 자동 충족).
- 이전 release 를 최소 3개 보관해 즉시 롤백 가능하게 한다.

## 무중단 요구
- 작업 중 서비스가 죽으면 안 된다. 현재 uptime 이 16시간 이상이며 정상 동작 중이다.
- 전환은 **1회의 계획된 재기동**으로 끝낸다. 재기동 전후로 `/api/status` 의 `sha`·`db` 를 대조한다.
- 전환 시점은 구현·검증이 모두 끝난 뒤 **ANU 에 보고하고 승인받은 뒤** 실행한다. 임의로 재기동하지 말 것.

## 검증 (전부 실측 증거 필요)
1. 배포 스크립트로 특정 sha 를 배포 → `/api/status` 의 `sha` 가 그 값과 **일치**하는지 확인.
2. **중간 실패 주입**: 빌드나 검증 단계를 일부러 실패시켜, symlink 가 전환되지 **않고** 기존 release 가 유지되는지 확인.
3. **롤백 실습**: 직전 release 로 되돌려 `/api/status` sha 가 바뀌는지 확인.
4. **★ 크래시 시뮬레이션**: 워킹트리를 다른 커밋으로 바꿔둔 상태에서 프로세스를 강제 종료 → 5초 뒤 자동 재시작된 프로세스가 **승인된 release 의 sha 를 보고**하는지 확인. 이번 사고가 재현되지 않음을 증명하는 핵심 테스트다.
5. 헬스체크 실패 시 자동 롤백이 실제로 도는지 확인.

## 범위 · 주의
- 대상: `/home/jay/projects/InsuRo` 와 `insuro-api.service` 유닛 파일, 배포 스크립트.
- **다른 태스크와 충돌 주의**: task-2985(CustomerChat) · task-2986(실손 엔진·계산기 화면)이 동시 진행 중이다.
  애플리케이션 소스는 건드리지 말 것. 이 태스크는 **배포 방식만** 바꾼다.
- 프론트엔드는 CF Pages 별도 배포라 이 태스크 범위 밖이다.
- 되돌리기 어려운 조작(유닛 파일 교체, symlink 전환)은 **실행 전 현재 상태를 백업**하고, 롤백 절차를 보고서에 먼저 적은 뒤 실행한다.

## allowed_resources (본 task의 capability)

```yaml
allowed_resources:
  paths:
    - "scripts/deploy/**"
    - "ops/**"
    - "docs/deploy/**"
    - "memory/reports/task-2988.md"
  forbidden_paths:
    - "src/**"
    - "server/silson/**"
    - "server/main.py"
    - ".github/**"
  commands:
    - "systemctl --user"
    - "curl"
    - "git"
  merge_policy: "tiered"
  ttl_hours: 72
```

> 유닛 파일(`~/.config/systemd/user/insuro-api.service`)과 `/home/jay/releases/**` 는 저장소 밖이라 위 목록에 없다. 이 두 곳은 이 태스크의 정당한 작업 대상이며, 변경 전후 상태를 보고서에 기록한다.

## 완료 조건
P1~P4 구현 · 검증 5종 전부 실측 증거 첨부(특히 4번) · 무중단 유지 · 롤백 절차 문서화 · 전환은 ANU 승인 후 1회

## 보고
`memory/reports/task-2988.md` 작성 후 표준 완료 콜백 등록. 콜백 프롬프트 **UTF-8 3900 bytes 이하**.