운영 인시던트 회복 (submission/identity 롤아웃 stuck) + 모니터링 강화 + SealedSecret 부채 해소 + 이월 정리
날짜
영향도치명
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사용. NestJSforRoutes('*')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, 다른 미들웨어에 동일 패턴 잠재 - Code Paths:
services/submission/src/common/middleware/gateway-context.middleware.ts:64, spec.ts (15→22 tests) - PR: #157 (
8800cfc)
D2: identity GITHUB_TOKEN_ENCRYPTION_KEY 누락 회복 (Wave A-2)
- Context: aether-gitops
f5f391dcommit에서 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-keyqvbr553d →sealed-secrets-keycdlrs23d) 후 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건 - PR: aether-gitops #3 (
f458d55)
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) 자체 비활성 - Choice:
- AlertManager receiver를
null→discord-default(warning, 1h repeat) +discord-critical(critical, 30m repeat, @here)으로 활성화 - PrometheusRule 매니페스트에 6개 룰 신규 추가:
KubePodCrashLooping,KubeDeploymentReplicasMismatch,KubeDeploymentRolloutStuck,ArgoCDAppDegraded,ArgoCDAppOutOfSync,ArgoCDAppSyncFailed - alertmanager v0.28.1 업그레이드, 기존
identity-discord-secretwebhook 재사용 (신규 SealedSecret 불필요)
- AlertManager receiver를
- 검증: promtool/amtool/kustomize 모두 SUCCESS. PR 머지 즉시 Sprint 130 잔존 firing(
PodRestartFrequent2건)이 Discord 채널에 도달 → receiver 활성화 검증 동시 달성 - PR: aether-gitops #4 (Sprint 130 명명 채택 시작 PR)
D5: 메모리/스프린트 윈도우 정정 (Wave D-3)
- Context: Sprint 92 종료 후 9일간
sprint-window.md미갱신./start시점에 sprint number 오인 (Sprint 93 → 실제 130) - Choice: sprint-window.md를 [1]=Sprint 129 / [2]=Sprint 130으로 정정. MEMORY.md 표에 Sprint 93~129 묶음 아카이브 행 추가. PR/브랜치/commit의
sprint-93명명은 사후 정정 곤란하므로 ADR-026 명명 오류 매핑 표로 영구 기록 - Alternatives: 모든 PR/commit message 강제 정정 (rebase) → 머지된 history 변경 위험, ADR 매핑으로 충분
D6: 구조적 가드 ADR 제안 (Wave C-2 + 개발 서버 분리)
- Choice: 두 ADR을 proposed 상태로 작성. Sprint 131에서 구현 결정 + 채택
- ADR-027: aether-gitops 브랜치 규율 (작업 브랜치 + PR + auto-merge로 main 직접 push 차단)
- ADR-028: 개발 서버(k3d/별도 dev cluster) 분리 (운영 직접 수정 안티패턴 구조적 차단)
D7: 인시던트 종합 ADR (Wave E-1)
- Choice: ADR-026 작성 — 타임라인, 근본 원인 4가지, 학습, 명명 오류 매핑 영구 기록
Patterns
P1: NestJS forRoutes('*') middleware의 req.path 회피 — req.originalUrl 사용
- Where:
services/submission/src/common/middleware/gateway-context.middleware.ts:64 - 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 발견 시
- Steps:
- cluster의 평문 키를 메모리(셸 변수)로만 추출:
kubectl get secret -n <ns> <name> -o json | jq -r '.data | to_entries[] | "\(.key)=\(.value | @base64d)"' - 각 키-값을
echo -n "$VAL" | kubeseal --raw --namespace <ns> --name <secret>로 봉인 - 매니페스트
encryptedData:블록만 교체 (apiVersion/kind/metadata/template 보존) kubeseal --validate로 dry-run unseal 확인- 평문 값 디스크/로그/PR/commit message 노출 금지, 작업 후 history clear
- cluster의 평문 키를 메모리(셸 변수)로만 추출:
P3: cluster Secret data hash 비교로 평문 값 불변 검증
- Where: SealedSecret 재봉인 PR 머지 검증 단계
- Steps:
- 머지 전:
kubectl get secret -n <ns> <name> -o json | jq -S '.data' | sha256sum로 baseline 캡처 - 머지 후 ArgoCD sync 완료 대기
- 다시 hash 계산 → diff 비교
- 동일하면 평문 값 불변 → cluster 영향 없음 확정
- 머지 전:
P4: 운영 핫픽스 트랙 1 + 트랙 2 패턴
- When to Reuse: GitOps 흐름이 막힌 상태에서 운영 회복 시급한 경우
- 트랙 1 (즉시 회복): cluster 직접 patch — 단 ADR로 별도 기록 + 메모리
feedback_avoid_prod_direct_edit.md정합 유지 - 트랙 2 (사후 정합성 회복): GitOps PR로 cluster 자국을 매니페스트로 흡수 — 트랙 1과 같은 Sprint 안에서 처리
Metrics
- Commits (AlgoSu): 1건 (PR #157)
- Commits (aether-gitops): 2건 (PR #2, #3)
- 매니페스트 변경: SealedSecret 8개 + identity 추가 키 1개 + submission INTERNAL_KEY_AI_ANALYSIS 흡수 1개
- 코드 변경: middleware 1줄 + 테스트 7개 추가
- 새 ADR: 3건 (ADR-026, ADR-027, ADR-028)
- 새 메모리: 2건 (
feedback_critic_unavailable.md,feedback_avoid_prod_direct_edit.md) - 회복 시간: 운영 stuck 26시간/2일 5시간 → Sprint 130 인지 후 약 4시간 내 완전 회복
- 사용자 영향: 0명 (구버전 트래픽 처리, 재인증 불필요)
- ArgoCD: Degraded → Healthy