← 개발 로그 목록

website: CD 사고 전체 타임라인 정리와 드리프트 알람·OCI CLI 트러블슈팅 기록

/ 6분 분량 / 개발 로그

이번 커밋은 코드 변경이 아니라 `pm/docs/learnings.md`에 최근 겪은 CD 사고의 전체 타임라인과 그 과정에서 얻은 교훈 세 가지를 정리해서 추가한 문서 작업입니다. 커밋 메시지 그대로 "CD 사고 전체 타임라인 + 드리프트 알람 튜닝·OCI CLI 함정 기록"이라는 제목처럼, 07-11부터 07-12 새벽까지 이어진 인프라 사고를 되...

요약

변경된 파일은 pm/docs/learnings.md 하나뿐이고, 추가 4줄에 삭제는 없습니다. 다만 라인 수가 적다고 가벼운 내용은 아니고, 하루 이틀에 걸쳐 겪은 CD 3연속 실패 사고의 원인 분석, OCI Monitoring 알람 튜닝 과정, OCI CLI에서 겪은 인코딩 함정까지를 압축해서 기록한 항목입니다. 커밋 날짜는 2026-07-12 03:06 UTC, dev 브랜치에서 진행됐습니다.

배경 및 목적

07-11에 CD가 12:33, 14:35, 15:16(UTC) 세 번 연속으로 실패했습니다. 이걸 조사하다가 서버에서 git pull이 조용히 실패하고 있는 걸 발견해서 고쳤는데, 정작 헬스체크 실패의 진짜 원인은 그게 아니었습니다. docker-compose.yml은 그 몇 주간 diff가 0이었을 정도로 안 바뀌었으니, git pull이 안 되고 있었어도 실질적으로는 영향이 없었던 겁니다.

진짜 원인은 완전히 다른 곳에 있었습니다. admin-auth 파운데이션 커밋(84d7528, 07-11 01:00 KST, 즉 세 번의 CD 실패보다 먼저 머지된 커밋)이 들여온 JwtProvider가 stage 환경의 JWT_SECRET(184비트)으로 크래시 루프를 돌고 있었던 것 — HS256 알고리즘은 최소 256비트 시크릿을 요구하는데 그걸 못 채워서 서버가 계속 죽었던 겁니다.

이 문서 작업의 목적은 이 사고를 "왜 원인을 잘못 짚기 쉬웠는지"부터 "결국 어떻게 바로잡았는지"까지 시간순으로 기록해서, 다음에 비슷한 상황이 왔을 때 같은 실수를 반복하지 않기 위함입니다.

구현 내용

pm/docs/learnings.md의 "인프라 · CI/CD" 섹션에 세 개의 항목이 추가됐습니다.

1. 인과관계 착각에 대한 경고와 전체 타임라인

같은 날 두 개의 버그를 동시에 발견하면 "먼저 고친 게 원인이었겠지"라고 넘겨짚기 쉽다는 게 핵심 메시지입니다. 실제로는 commit 시각을 비교하고 파일 diff를 확인해야만 인과관계를 검증할 수 있다는 걸 이번 사고로 다시 확인했습니다. 함께 기록된 전체 타임라인은 이렇습니다.

CD 실패 3회 (07-11)
→ git pull 원인 규명·수정 PR #104 머지 (07-12 01:36)
→ 서버 동기화 완료
→ 드리프트 감지 PR #105 머지 (02:04)
→ 헬스체크 로그 가시화 PR #106 머지 (02:28)
→ 그 로그로 JWT_SECRET 184비트 발견
→ stage JWT_SECRET 교체 (512비트)
→ 재배포 성공 (run 29176978544, 02:34~02:36)
→ 배포 마커 파일이 드리프트 오탐 유발한 것 발견·수정 PR #107 머지 (02:40)
→ 알람 포맷 통일(ONS_OPTIMIZED)·pending-duration 5→3분
→ 테스트 드리프트로 왕복 검증
   (파일 생성 02:42:06 → FIRING 02:50:00 → 파일 제거 → OK 복귀 03:01:00)
   중 중복 알람 발견·정리

한 시간 반 남짓한 시간 동안 원인 규명, 수정, 알람 튜닝, 검증, 부작용 발견까지 계속 이어졌던 걸 알 수 있습니다.

2. OCI Monitoring 알람의 쿼리 윈도우 튜닝 한계

[Xm].max() 같은 쿼리 윈도우를 줄여서 순간적인 blip이 알람을 못 띄우게 막으려 했는데, 저빈도(5분 주기) 커스텀 메트릭에서는 이게 안 통한다는 걸 확인했습니다. 데이터 공백을 안 만들려면 윈도우를 최소 폴링 주기 이상(5~6분)으로 잡아야 하는데, 그러면 윈도우가 pending-duration(3분)보다 커져버립니다. 윈도우 안에 있는 단 하나의 값만으로도 pending-duration 조건을 만족시키기 충분하다는 거죠. 결국 윈도우를 줄여서 실제로 얻는 건 "문제 해소 후 OK로 복귀하는 속도"뿐이고, 오탐 방지는 애초에 메트릭 자체에서 "정상인데 순간적으로 잡히는 상태"를 빼는 방식으로 해야 한다는 결론입니다. 실제로는 배포 스크립트의 롤백 마커 파일을 .gitignore에 추가하는 식으로 해결했습니다.

3. OCI CLI(Windows)의 UnicodeEncodeError 함정

한글이 섞인 --body로 알람을 생성할 때, API 호출 자체는 서버에서 성공적으로 처리되는데 그 응답을 콘솔에 출력하는 단계에서 UnicodeEncodeError(cp949)로 클라이언트가 죽는 문제를 겪었습니다. 이걸 "명령 실패"로 착각해서 재시도하면 동일 조건의 알람이 중복 생성되고, 포맷 같은 후속 설정은 재시도로 얻은 새 ID에만 적용되면서 먼저 만들어진 유령 알람은 기본 RAW 포맷으로 계속 따로 알림을 보내는 상황이 벌어졌습니다.

기술적 의사결정

이번 문서 작업 자체에서 새로운 기술 선택이 있었던 건 아니지만, 사고 처리 과정에서 내린 판단 두 가지는 짚어둘 만합니다.

  • JWT_SECRET 길이 문제: HS256 최소 요구 사양(256비트)에 맞춰 184비트에서 512비트로 교체. 여유를 두고 512비트로 잡은 건 앞으로 비슷한 사고가 재발하지 않도록 하기 위한 선택으로 보입니다.
  • 알람 오탐 대응 방식: 쿼리 윈도우를 튜닝하는 대신, 오탐의 근본 원인인 배포 마커 파일을 메트릭 수집 대상에서 아예 제외(.gitignore 추가)하는 쪽을 택했습니다. 윈도우 튜닝은 "임시방편"이고 근본 원인 제거가 맞다는 판단이 깔려 있습니다.

배운 점 및 개선점

가장 크게 남은 교훈은 "방금 고친 버그가 지금 겪고 있는 증상의 원인이라는 보장은 없다"는 겁니다. 같은 날 여러 문제를 동시에 마주치면 먼저 눈에 띈 원인에 안도하고 넘어가기 쉬운데, commit 시각과 diff를 직접 대조하지 않으면 진짜 원인을 놓칠 수 있다는 걸 이번에 몸으로 확인했습니다.

또 하나는 도구를 믿지 말라는 것 — OCI CLI가 에러를 던졌다고 해서 서버 쪽 작업이 실패했다고 단정하면 안 되고, 재시도 전에 list로 실제 상태부터 확인해야 한다는 걸 중복 알람을 만들고 나서야 깨달았습니다. PYTHONIOENCODING=utf-8을 미리 설정해두는 것도 예방책으로 남겨뒀습니다.

앞으로는 알람이나 배포 자동화 스크립트를 건드릴 때 "결과를 눈으로 확인"하는 단계를 습관화하고, 사고가 나면 타임라인부터 먼저 정리한 뒤 원인 분석에 들어가는 순서를 지켜야겠다고 생각했습니다.