배포 파이프라인 · 운영 (OPS-*)
전면 Cloudflare·운영 서버 0대 구조에서 "배포"는 곧 Worker/DO/스케줄/리소스 바인딩의 선언적 적용이다. 이 페이지는 그 deploy/operate 계약을 고정한다 — dev/staging/prod 3환경 격리, D1 전진 전용(forward-only) expand-first 마이그레이션, 시딩, feature flag 거버넌스, CI 게이트, 롤백·에러버짓 릴리스 게이트, cron 락, 인시던트 P1~P4, 그리고 사전 출시 리소스 확정·리네임 체크리스트. 모든 운영 목표 수치는 운영 데이터가 없는 진입 단계의 초기 확정치(설계)이며 운영 베이스라인으로 분기별 조정한다 — 달성치(achieved)는 기재하지 않는다. 본 페이지가 소유하는 ID는 OPS-* 뿐이며, 타 페이지 ID(FR/NFR/IR/OD)는 정의하지 않고 링크한다.
geofence.dwell_threshold_min에서 읽음) → feature flag는 "설계된 거버넌스, 미배선 메커니즘"이며 권위 출처는 미해소 결정 OD-007에 위임한다. ② 웹훅 구독·전달은 stub(handleNotifyBatch ack-only, 구독 테이블 부재) → 0004 마이그레이션 신설을 본 페이지가 요구한다. ③ 모든 wrangler D1/KV/R2 ID가 placeholder(0000…000a)이며 미프로비저닝, 'LogiNippon'은 전 레포 가칭 → 출시 차단 체크리스트 OPS-FINALIZE-001로 고정.
1. 환경 — dev/staging/prod 3환경 독립 바인딩
리소스 격리가 없으면 "staging에선 됐는데 prod에서 깨짐"과 dev 데이터의 prod 오염이 곧 사고가 된다. 따라서 각 환경은 별도 D1 DB·KV 네임스페이스·R2 버킷·Queues·Durable Object를 갖고, wrangler.jsonc의 [env.staging]/[env.production] 바인딩으로 분리한다(techspec delivery-pipeline.md). 빌드·배포·마이그레이션은 wrangler(공식 CLI) 중심으로 일관된다.
| 환경 | 목적 | 리소스 바인딩 | 승격·시크릿 |
|---|---|---|---|
| dev | 로컬·개인 개발. 시드 자격 로그인(DR-1180/OP-KAO-1, dev 전용) |
Miniflare/workerd 로컬 D1·KV·R2·Queues 에뮬레이션, 또는 개인 dev 네임스페이스. npm run db:migrate:local + npm run seed:local |
.dev.vars(로컬 시크릿, 레포 외부). 시드 비밀번호는 로컬 외 절대 사용 금지. |
| staging | 통합·e2e·제휴 PoC 리허설. prod와 동형(同型) 스키마 | 독립 D1/KV/R2/Queues/DO. main 머지가 자동 배포. 마이그레이션을 여기서 먼저 적용·e2e 검증 | wrangler secret put --env staging |
| prod | 실서비스 | 독립 바인딩, 보호된 승인 게이트. 태그/승인이 배포 트리거 | wrangler secret put --env production. CI 배포는 GitHub Secret CLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID 필요(없으면 배포 단계는 경고만 남기고 건너뜀, 빌드·테스트는 계속) |
환경별 시크릿(반드시 환경마다 분리 주입, 레포 하드코딩 금지). security-privacy 정책과 정합 — 시크릿은 wrangler secret으로만 주입한다(server/src/types/index.ts Env 바인딩):
| 시크릿 | 용도 | 환경별 분리 |
|---|---|---|
JWT_SIGNING_KEY | JWT(HS256) 서명·검증 (server/src/lib/auth.ts) | 환경마다 별도 키 — staging 토큰이 prod에서 통하면 안 됨 |
WEBHOOK_SIGNING_KEY | 웹훅 HMAC-SHA256 서명({timestamp}.{raw_body}) — FR-DLV-WH-001 | 환경별. 구독별 endpoint_secret은 별도(현재 구독 테이블 부재, §2 0004 신설) |
MAPS_API_KEY | Google Maps 거리/교통 — 룰 ETA(FR-ENG-ETA-001) | 환경별 쿼터·과금 분리 |
JMA_API_KEY | 기상청(JMA) — 날씨 보정 | 환경별 |
LINE_CHANNEL_TOKEN | LINE 알림 채널(인시던트·예외 통지) | 환경별 — staging 알림이 운영 채널로 새지 않게 |
2. D1 마이그레이션 — 전진 전용 expand-first
요구. D1 스키마 변경은 wrangler d1 migrations의 순차 번호·불변 SQL 파일로만 관리하며, 반드시 전진 전용(forward-only)이어야 한다(MUST) — 자동 down 마이그레이션에 의존하지 않는다. 잘못된 자동 롤백이 데이터를 손상시킬 위험이 down 부재 위험보다 크다. 문제가 생기면 새 전진 마이그레이션으로 교정(roll-forward)한다. 적용된 마이그레이션 파일은 수정하지 않는다(이미 적용된 환경과 어긋남) — 변경은 새 번호로. 컬럼 삭제·타입 변경·제약 강화는 expand-first 다단계로 수행한다(① 추가/백필 → ② 코드 전환 → ③ 구필드 제거)하여 배포–마이그레이션 순서에 무관(order-independent)하게 한다. 스키마 진실원은 DM-SCHEMA-001.
수용기준
- Given 빈 D1, When 전체 마이그레이션을 순서대로 적용, Then 결과 스키마가 DM-SCHEMA-001(model.md)과 일치한다(마이그레이션 테스트, staging 확인).
- Given 이미 적용된 마이그레이션 파일, When 그 내용을 수정, Then CI가 차단한다(체크섬/번호 불변 가드).
- Given prod 적용, When
wrangler d1 migrations apply --env production을 파이프라인의 명시 스텝으로 실행, Then 배포 직전/직후 순서가 의도적으로 정의되어 있다. - Given 컬럼 삭제가 필요, When expand-first 3단계 미적용으로 단일 마이그레이션에 DROP을 포함, Then 리뷰가 거부한다(다단계 강제).
- Given 대용량 백필, When 단일 트랜잭션 대량 쓰기, Then 배치 분할로 전환한다(D1 쓰기 한계 — NFR-COST-001 가드레일).
| 마이그레이션 | 내용 | 상태(as-built) |
|---|---|---|
0001_init.sql | D1 DDL — shipment/stop/event/carrier/geofence 핵심 스키마 | DONE |
0002_auth.sql | 인증 자격·API 키 테이블(PBKDF2 자격, refresh 로테이션) | DONE |
0003_geofence_membership.sql | 지오펜스 멤버십(geofence_cell interior/boundary) | DONE |
0004_webhook_subscriptions.sql | 신설 권고 — 웹훅 구독 테이블·endpoint_secret·전달상태. 현재 handleNotifyBatch는 ack-only stub이라 구독 영속이 부재(IR-WH-001·FR-DLV-WH-001 활성화의 전제) | ABSENT |
wrangler.jsonc DO migration(new_classes/renamed_classes) 태그를 함께 관리하되, 진실은 D1이라 DO 재생성은 복원 가능하다.
3. 시딩 — geofence_cell·event_code_map·핫 config
요구. 진입 단계 시드는 반드시 멱등(idempotent, UPSERT)이어야 하고 환경별로 분리 실행되어야 한다(MUST). 세 종류를 시드한다: ① 거점 지오펜스 셀(geofence_cell) + KV 역인덱스 — 거점 폴리곤 100~200개소를 polygonToCells로 H3 덮개 셀(interior/boundary)을 생성해 D1 geofence_cell과 KV cell→geofence_id[] 역인덱스를 채운다(지오펜스 등록·수정 시 동일 재생성 잡 재실행). ② 이벤트 정규화 매핑(event_code_map) — 대형 운송사(ヤマト·佐川·NX·セイノー) 코드/API 명칭 → canonical 매핑 시드, 미매핑 raw_code는 운영 중 보강. ③ 핫 config(KV CONFIG) — ETA 룰 파라미터(FR-ENG-ETA-001: 고속 80/도심 30/기본 50 km/h)·추적 손실 임계(45min)·荷待ち 임계(120min). 단, 핫 config는 시드되었으나 런타임에서 read되지 않는다(미배선 갭) — 권위 출처 결정은 OD-007에 위임한다.
수용기준
- Given 시드를 2회 연속 실행, When 두 번째 실행, Then 중복·충돌 행이 0(UPSERT 멱등).
- Given 거점 폴리곤 등록, When
polygonToCells재생성 잡, Then D1geofence_cell과 KV 역인덱스가 동일 셋으로 정합(FR-GEO-H3RES-001 해상도 정책 준수). - Given 미매핑 raw_code 유입, When 컨슈머 정규화, Then 알림→
event_code_map보강 경로가 동작(S8 — slo-catalog). - Given KV CONFIG 시드 존재, When ETA 컨슈머 실행, Then 현재는 KV를 read하지 않고 dwell만 D1
geofence.dwell_threshold_min에서 읽는다(갭으로 명시, OD-007·OPS-FLAG-001 추적).
4. Feature Flag 거버넌스 (deploy ≠ release)
요구. 코드 배포와 기능 활성화를 반드시 분리한다(deploy ≠ release, MUST). flag는 KV의 읽기 多·쓰기 少 핫 config로 운용한다. 키 네이밍: flag:<feature> 규약(예 flag:webhooks_enabled·flag:ml_eta_enabled). 기본 상태 정책: 신규 기능은 OFF로 배포 → 내부/일부 테넌트부터 ON → 점진 확대; 미정의 키는 OFF로 해석(fail-safe). 타게팅: 테넌트별·제휴 파트너(Phase 0)별 토글 권장(SHOULD). 킬 스위치: 문제 시 즉시 OFF가 코드 롤백보다 빠른 1차 완화(§6 롤백 1순위). 변경 감사: flag 변경은 누가·무엇을·언제·이전값→새값을 감사 로그에 남긴다(SR-AUDIT-001 — security-privacy).
수용기준
- Given 신규 기능 배포, When flag 미설정, Then 기능은 OFF(미정의 키=OFF 해석).
- Given 특정 테넌트만 ON, When 다른 테넌트 요청, Then 기능 미노출(타게팅 격리).
- Given flag 변경, When ON/OFF 전환, Then 감사 로그에 주체·시각·이전값→새값이 남는다.
- Given 인시던트, When 킬 스위치 OFF, Then 코드 롤백 없이 영향이 차단된다(완화 1순위).
flag:webhooks_enabled·flag:ml_eta_enabled를 KV에서 토글해도 코드 경로가 이를 읽지 않으므로 아직 효력이 없다. 따라서 deploy≠release를 실제로 구현하려면 ① KV CONFIG read 헬퍼 배선, ② config 권위(D1 vs KV) 결정이 선행되어야 한다 → OD-007(런타임 config 권위)에 위임. 본 카드는 "설계된 거버넌스 + 명시된 미배선 갭"으로 고정한다.
5. CI 게이트 — 커버리지·차단 스위트·계약 drift
요구. 머지·배포는 CI 게이트를 반드시 통과해야 한다(MUST): lint → typecheck(tsc --noEmit) → test(Vitest, @cloudflare/vitest-pool-workers·workerd 위 실행) → 마이그레이션 dry-run → wrangler deploy --env <env>. 커버리지 플로어: 핵심 로직(정규화·dedup·서명·H3·인증) 커버리지 하한을 두고 미달 시 차단(설계 — 초기 확정치, 베이스라인 조정). 차단 스위트(반드시 그린): 테넌트 격리("타 테넌트 리소스=404" 회귀, test/ 테넌트 격리)·dedup 멱등(같은 이벤트 중복 전달도 dedup_key로 1회만 — DM-DEDUP-001)·웹훅 서명 라운드트립(발송 서명↔수신 검증, 타임스탬프 ±300s 윈도 밖 거부 — FR-DLV-WH-001). contract drift-gate: 단일 @loginippon/contract(OpenAPI/AsyncAPI 추출 타입)와 실제 구현이 어긋나면 CI 차단 — IR-API-001(OpenAPI 단일원 + drift gate)에 연결.
수용기준
- Given 테넌트 격리 테스트가 fail, When PR, Then 머지 차단(격리 누락 = 보안 사고).
- Given dedup·서명 스위트 중 하나라도 fail, When CI, Then 배포 차단.
- Given OpenAPI 단일원과 server 핸들러 시그니처 불일치, When drift-gate, Then CI 실패(IR-API-001).
- Given 커버리지 플로어 미달, When CI, Then 차단(핵심 로직 한정).
- Given
CLOUDFLARE_API_TOKEN미설정, When 배포 단계, Then 경고만 남기고 건너뛰되 빌드·테스트는 계속 검증(현 server 동작).
6. 롤백 순서 · 에러버짓 릴리스 게이트
요구. 프로모션은 dev/preview → staging(머지) → prod(승인 + SLO 게이트) 순이며, prod 배포는 반드시 의도적 승인 단계를 둔다(MUST). 롤백 순서(빠른 것부터): ① feature flag OFF(킬 스위치, 가장 빠름 — 단 §4 미배선 갭 전제) → ② 이전 Worker 버전 재배포(wrangler 버전 롤백) → ③ 스키마는 전진 교정(down 아님 — OPS-MIGRATION-001). DO는 상태를 들고 있으므로 클래스 변경/롤백 시 migration 태그·하위호환을 확인하되, 진실은 D1이라 DO 재생성은 복원 가능하다. 에러버짓 릴리스 게이트: 가용성 SLO NFR-AVAIL-001(≥ 99.9% / 역월, 버짓 0.1%/월 ≈ 43분)이 소진되면 prod 승격을 차단하고 신규 기능보다 안정화·회귀 수정을 우선한다; 버짓 여유가 크면 변경 속도를 높인다. 진입 단계엔 버짓을 느슨하게 시작해 거짓 경보를 피한다(설계 — 베이스라인 조정).
수용기준
- Given 역월 다운타임이 43분 예산을 초과(에러버짓 소진), When prod 승격 시도, Then 릴리스 게이트가 차단하고 안정화 모드로 전환한다.
- Given 이상 감지, When 1차 완화, Then flag OFF가 코드 롤백보다 먼저 시도된다(가능한 경우).
- Given 코드 롤백, When 이전 Worker 버전 재배포, Then expand-first 덕에 스키마 down 없이 무결성이 유지된다.
- Given prod 승격, When 승인 단계, Then 명시적 사람·게이트 승인 기록이 남는다.
7. Cron 락 (as-built · JST 환산)
요구. Cron Triggers는 아래 as-built 스케줄로 고정한다(wrangler.jsonc, server/src/consumers/cron.ts). Cron 식은 UTC 기준이므로 일·월 배치는 JST(UTC+9) 환산 주석을 코드에 반드시 병기한다(MUST) — 荷待ち 법정 계산(DM-TS-001 +09:00)과 시각 해석이 어긋나면 안 된다.
| Cron 식 (UTC) | JST 환산 | 작업 | 상태 |
|---|---|---|---|
*/5 * * * * | 5분마다 | ETA 재계산 + 추적 손실 sweep(마지막 유효 핑 후 45min → TRACKING_LOST — FR-ENG-EXC-003) | sweepActive=빈 구현, handleEtaBatch=ack-only(룰 미구현) STUB |
0 17 * * * | = 02:00 JST 매일 | 일 荷待ち(NIMACHI) 리포트 배치 | CSV end-to-end DONE |
0 19 1 * * | = 04:00 JST 매월 1일 | 월 롤업(月次 롤업) | DONE(리포트 경로) |
수용기준
- Given
0 17 * * *, When 코드/주석 검토, Then "= 02:00 JST 매일" 주석이 병기되어 있다(UTC↔JST 명시). - Given
*/5sweep, When 마지막 유효 핑 후 45min 경과, ThenTRACKING_LOST생성(현재sweepActive빈 구현이라 미동작 — 갭 명시). - Given cron 식 변경, When PR, Then JST 환산 주석을 동반 갱신한다.
8. 인시던트 — 심각도 P1~P4 · MTTA/MTTR · 온콜
요구. 인시던트는 사용자 영향 기준으로 P1~P4 심각도를 판정하고, 각 심각도에 MTTA(인지까지)·MTTR(복구까지) 목표를 둔다(설계 — 진입 단계 초기 확정치, 운영 베이스라인으로 조정). 대응 절차는 감지 → 분류(triage, request_id로 영향 범위 좁히기) → 완화(롤백·flag OFF 우선, 근본 원인은 그다음) → 소통(영향 테넌트) → 사후(blameless postmortem, 에러버짓 반영). 진입 단계엔 경량 온콜(작은 팀)로 시작하되 P1/P2는 온콜 즉시 페이지(LINE 채널)한다. 각 시스템 클래스마다 런북(runbook)을 둔다.
| 심각도 | 정의(사용자 영향) | MTTA 목표 설계 | MTTR 목표 설계 | 온콜·동작 |
|---|---|---|---|---|
| P1 Critical | 전면 장애 — 인제스트/대시보드/가용성(NFR-AVAIL-001) 다수 테넌트 가시성 끊김, 데이터 손상·보안 침해 | ≤ 15분 | ≤ 1시간 | 온콜 즉시 페이지(LINE), 1차 완화 후 소통. 에러버짓 즉시 소진 평가 |
| P2 High | 주요 기능 저하 — 큐 백로그/지오펜스 lag 위반(NFR-PERF-002), 웹훅 대량 실패(NFR-RELY-001), 단일 테넌트 영향 | ≤ 30분 | ≤ 4시간 | 온콜 페이지(LINE/이메일). 핫스팟·소비자 스케일 점검 |
| P3 Medium | 제한적 영향 — 정규화 미스 급증(S8, slo-catalog), D1 쓰기/용량 임계 근접(NFR-COST-001), 우회 가능 | ≤ 4시간(업무시간) | ≤ 2영업일 | 이메일+대시보드. 다음 작업일 처리 |
| P4 Low | 경미 — 미매핑 raw_code 누적, 외관 결함, 백로그 항목 | 차기 분류 회의 | 백로그 계획 | 일일 요약/대시보드 |
수용기준
- Given 가용성 전면 장애, When 감지, Then P1로 분류되어 ≤ 15분 내 인지·온콜 페이지된다.
- Given 각 시스템 클래스(인제스트·큐·웹훅·DO·D1), When 인시던트, Then 해당 런북이 존재해 1차 완화 단계를 안내한다.
- Given P1/P2 종결, When 사후, Then blameless postmortem(원인·타임라인·재발 방지)과 에러버짓 반영이 기록된다.
온콜·에스컬레이션(진입 단계 경량, 팀 성장 시 정식화).
- 1차 온콜 엔지니어 → 미인지/미해결 시 2차 리드 → 3차 아키텍트. P1은 병렬 통지.
- 알림 피로 관리: 임계·디바운싱·중복 억제로 거짓 경보를 줄인다(제품 추적 손실 임계 튜닝과 같은 원칙).
- 보안 사건(인증 실패 폭증·키 폐기·웹훅 비활성)은 P1~P2 + 감사 로그(security-privacy).
9. 사전 출시 리소스 확정·리네임 체크리스트
요구. prod 출시 전에 아래 항목을 반드시 완료한다(MUST) — 미완료 시 prod 배포를 차단한다(출시 게이트). 현재 'LogiNippon'은 전 레포 가칭이고 모든 wrangler D1/KV/R2 ID는 placeholder(0000…000a)이며 미프로비저닝이다. 결정은 미해소 결정 페이지에 위임하되, 실행 체크리스트는 본 카드가 소유한다.
| # | 확정/리네임 항목 | 현재 상태 | 위임 결정 |
|---|---|---|---|
| 1 | 브랜드명 확정 — 가칭 'LogiNippon' → 정식명. server 하드코딩·web config.ts·console 다수 하드코딩 일괄 갱신, wrangler.jsonc의 name 및 D1/KV/R2/Queues 리소스명 | 가칭(전 레포) | OD-002 |
| 2 | Cloudflare 리소스 프로비저닝 — wrangler d1 create·kv namespace create·r2 bucket create·queues create 실행 후 출력 ID를 placeholder 0000…000a 자리표시자에 채움(env별) | placeholder·미프로비저닝 | OD-005 |
| 3 | web 사이트 값 확정 — SITE_URL·contactEmail·sitemap 예시값을 실값으로(web config.ts 브랜드 중앙화) | 예시값 | OD-002 |
| 4 | 환경별 시크릿 주입 — JWT/WEBHOOK/MAPS/JMA/LINE 키를 prod에 wrangler secret put --env production으로 주입(§1) | dev .dev.vars만 | — |
| 5 | 0004 웹훅 마이그레이션 적용 — 구독 테이블·endpoint_secret·전달상태(OPS-MIGRATION-001)로 ack-only stub 해소 | ABSENT | — |
| 6 | KV CONFIG 런타임 read 배선 — flag·ETA 속도 read 헬퍼(OPS-FLAG-001 효력화) | 시드만, 미read | OD-007 |
| 7 | METRICS 방출 배선 — writeDataPoint TODO 해소(SLI 측정·에러버짓 게이트 선행) | 바인딩만, 미방출 | slo-catalog OBS-METRICS-001 |
수용기준
- Given prod 배포 시도, When 체크리스트 항목 1~5 중 하나라도 미완, Then 출시 게이트가 차단한다.
- Given 리소스 프로비저닝 완료, When
wrangler.jsonc검토, Then0000…000aplaceholder가 남아 있지 않다. - Given 브랜드명 확정, When 전 레포 grep, Then 가칭 'LogiNippon' 하드코딩이 리소스명·사용자 노출 문자열에서 제거되었다.
근거·상호참조
- techspec: 08-operations/delivery-pipeline.md — 환경 격리·wrangler·전진 전용 마이그레이션·시딩·feature flag·롤백/프로모션·환경별 시크릿
- techspec: 08-operations/observability-slo.md — 에러버짓 릴리스 게이트·인시던트 대응·알림 채널
- techspec: 01-architecture/cloudflare-stack.md — KV(feature flag·핫 config) 용도
- techspec: 02-data-model/model.md — geofence_cell·event_code_map 시드 스키마
- techspec ADR: ADR-0008(D1) · ADR-0009(H3·polygonToCells) · ADR-0002(TS·wrangler)
- 코드(server):
server/README.md(빠른 시작·env 배포·프로비저닝·브랜드 가칭 주의) ·migrations/0001_init.sql·0002_auth.sql·0003_geofence_membership.sql·src/consumers/cron.ts·queue.ts·scripts/seed/·wrangler.jsonc(env별 바인딩, placeholder ID) · LogiNippon/server - 코드(web):
config.tsSITE_URL/contactEmail 예시값 · LogiNippon/web - TRD 내부: DM-SCHEMA-001·DM-DEDUP-001 · IR-API-001·IR-WH-001 · FR-DLV-WH-001·FR-ENG-EXC-003·FR-ENG-ETA-001·FR-GEO-H3RES-001 · NFR-AVAIL-001·NFR-PERF-002·NFR-RELY-001·NFR-COST-001
- TRD 내부: slo-catalog(OBS-METRICS-001·에러버짓 공식) · security-privacy(SR-AUDIT-001·시크릿) · regulatory(RR-LEGAL-001) · phase-gates-roadmap(단계 게이트) · OD-002·OD-005·OD-007·OD-004