운영 인시던트 회복 (submission/identity 롤아웃 stuck) + 모니터링 강화 + SealedSecret 부채 해소 + 이월 정리
Sprint 130: 운영 인시던트 회복 + 부채 해소
Decisions
D1: submission /health 401 회귀 fix (Wave A-1)
Context: Sprint 121 PR #138 ("i18n 기반 구축")에서 신규 도입된 services/submission/src/common/middleware/gateway-context.middleware.ts가 req.path 사용. NestJS forRoutes('*') mount-strip으로 /health 요청 시 middleware 내부에서 /로 인식되어 X-Internal-Key 인증 강제 → probe 401 → liveness probe 실패 → 867회 재시작 (2일 5시간)
Choice: req.path → (req.originalUrl ?? req.url).split('?')[0] 변경 + 단위 테스트 7개 추가 (mount-strip 회귀 시뮬레이션)
Alternatives: forRoutes 패턴 변경 → 변경 범위 확대, 회귀 risk 큼 / probe path를 미들웨어 우회 경로로 변경 → ad-hoc, 다른 미들웨어에 동일 패턴 잠재
Context: aether-gitops f5f391d commit에서 SealedSecret에 신규 키 추가 시 gateway/github-worker만 갱신, identity-service-secrets 누락. Sprint 125 Wave C 코드(token-encryption.service.ts)가 키를 요구하나 cluster Secret 미반영 → 신 ReplicaSet CrashLoopBackOff 308회 (26시간)
Choice (트랙 1, 임시): gateway/github-worker가 사용 중인 동일 키를 cluster Secret에 직접 kubectl patch + rollout restart → 즉시 운영 회복. 사용자 영향 0 (재인증 불필요, DB 토큰 4건 보존)
Choice (트랙 2, 정식): SealedSecret 매니페스트 PR (#2)로 키 추가 → 그러나 컨트롤러 키 mismatch 부채로 unseal 실패 → Wave B-2(PR #3)에 흡수되어 GitOps 정합성 회복
Alternatives: 새 키 발급 → DB GitHub 토큰 4건 무효화, 사용자 재인증 강제 (선택 안 함)
PR: aether-gitops #2 (트랙 2 초안), 트랙 1은 kubectl patch 임시 → ADR-028(개발 서버 분리)에서 구조적 차단 결정
D3: SealedSecret 8개 일괄 재봉인 — 컨트롤러 키 mismatch 부채 해소 (Wave B-2)
Context: ~2026-04-02 sealed-secrets 컨트롤러 키 rotation (sealed-secrets-keyqvbr5 53d → sealed-secrets-keycdlrs 23d) 후 8개 SealedSecret 매니페스트 재봉인 미수행. cluster의 unsealed Secret은 rotation 이전 상태로 유지되어 운영 영향 없으나 매니페스트 변경 시 cluster 반영 차단 — Wave A-2 PR #2에서 노출됨
Choice: cluster의 평문 키를 메모리에서 직접 추출 → kubeseal --raw로 현재 active controller cert로 재봉인 → 8개 매니페스트의 encryptedData: 블록 일괄 교체. apiVersion/kind/metadata/template: 보존
Alternatives: 컨트롤러 옛 키 복원 → 인프라 복잡성 증가 / cluster Secret 직접 patch만 유지 → GitOps 정합성 영구 깨짐 (선택 안 함)
부수 효과: submission-service-secrets에 INTERNAL_KEY_AI_ANALYSIS 키 매니페스트 누락 발견 (cluster에는 존재) → 매니페스트로 흡수 = 추가 GitOps 정합성 회복 동시 달성
검증: 머지 후 kubectl get sealedsecret -n algosu의 8개 모두 Synced=True 도달, cluster Secret data sha256 hash 머지 전후 동일 (평문 값 불변), 운영 pod 재시작 0건
D4: AlertManager 룰 보강 + receiver 활성화 — 사고 재발 차단 (Wave B-1)
Context: Sprint 130 두 사고 모두 ArgoCD Health=Degraded 상태였으나 알림 없이 26h~2일 5시간 방치
충격적 발견 (Architect): alertmanager receiver: 'null' — 13개 기존 룰(PodRestartFrequent, ServiceDown, OOMKilled 등)이 정상 firing 중이었으나 모든 알림이 silent drop. Sprint 130 무알림의 진짜 root cause는 룰 부재가 아닌 배출구(receiver) 자체 비활성
When to Reuse: NestJS consumer.apply(...).forRoutes('*') 또는 와일드카드 라우트에서 req.path로 path 매칭 시. mount-strip으로 /로 인식되는 회귀 발생 가능 — req.originalUrl ?? req.url로 strip되지 않은 path 사용
검증 패턴: 단위 테스트 mock에 originalUrl/url 함께 주입 (production 시뮬레이션). path만 setter는 회귀 검출 못함
P2: SealedSecret 일괄 재봉인 (컨트롤러 키 rotation 후)
Where: aether-gitops/algosu/base/sealed-secrets/
When to Reuse: sealed-secrets 컨트롤러 키 rotation 시 또는 unseal 실패 SealedSecret 발견 시
Fix: 본 Sprint에서는 ADR-026 명명 오류 매핑으로 사후 영구 기록. 자동화는 Sprint 131+ 후보
G2: SealedSecret 컨트롤러 키 rotation 후 매니페스트 재봉인 절차 부재
Symptom: SealedSecret이 Status=False (no key could decrypt secret) 상태. cluster Secret은 rotation 이전 상태로 유지되어 운영 무영향 → 알림 없음 → 매니페스트 변경 시 cluster 반영 차단으로 노출
Fix: 본 Sprint Wave B-2로 일괄 재봉인. 자동화(CI job)는 Sprint 131+ 후보
G3: NestJS forRoutes('*') 단위 테스트가 mount-strip 회귀를 못 잡음
Symptom: spec.ts의 req.path mock 직접 주입으로 production 회귀 미검출
Root Cause: Express middleware mount 동작이 단위 테스트 환경에서 시뮬레이션되지 않음
Fix: originalUrl/url mock 함께 주입 (production 시뮬레이션). e2e 통합 테스트 추가는 후속 P3
G4: aether-gitops main 직접 push로 누락 commit이 PR review 우회
Symptom: f5f391d commit에서 identity-service-secrets 키 추가 누락. 단일 reviewer 흐름이 휴먼 에러 통과
Fix: ADR-027 (브랜치 규율 도입) 채택 시점에 구조적 차단
G5: 운영 cluster kubectl 직접 변경 환경
Symptom: Sprint 130 트랙 1에서 cluster Secret 직접 patch. GitOps 정합성 일시 깨짐 + 변경 추적 곤란
Fix: ADR-028 (개발 서버 분리 + 운영 read-only) 채택 시점에 구조적 차단
G6: alertmanager receiver: 'null'로 모든 알림 silent drop
Symptom: 13개 PrometheusRule이 정상 firing 중이었으나 운영자에게 통보 없음. ArgoCD Health=Degraded도 무알림. Sprint 130 두 사고 6일/26시간 방치 + sealed-secrets 컨트롤러 키 mismatch 23일 방치의 직접 원인
Root Cause: alertmanager.yaml의 receiver가 null로 설정 → 모든 라우팅이 폐기 destination으로 매핑. 룰 점검만 했다면 못 찾았을 함정 (룰 inventory는 정상이었기 때문)