← 개발 로그 목록

website: 알람 상태 조회 화면을 만들다가 인스턴스 이름부터 정리하게 된 이야기

/ 8분 분량 / 개발 로그

OCI Monitoring이 판정하는 알람 9개(디스크·메모리·백업·git 드리프트·이메일 실패·ERROR 로그)의 지금 상태를 어드민에서 바로 볼 수 있게 만들었고, 그 과정에서 드러난 인스턴스 이름 혼동 문제를 같이 정리해서 dev를 main에 동기화한 PR이다.

지금까지는 알람이 울리면 OCI가 이메일로 보내주는 게 전부였다. 문제는 "지금 뭔가 FIRING 중인가"를 확인하려면 메일함을 뒤지거나 OCI 콘솔에 직접 들어가야 했다는 점이다. 마침 어드민 대시보드에 배포 이력·시스템 지표 페이지가 있었으니, 여기에 알람 상태도 같이 보여주면 된다고 판단했다.

페이지부터 갈라야 했다

기존 /admin/infra 안에 배포 이력과 시스템 지표가 같이 들어 있었는데, 알람 상태까지 얹으면 한 화면이 너무 커진다. 그래서 /admin/infra(배포 이력), /admin/infra/metrics(시스템 지표), /admin/infra/alarms(알람 상태) 세 개로 나눴다. 나누고 보니 사이드바 "현재 위치" 판정이 /admin/infra와 /admin/infra/metrics가 접두어 관계라 서로 겹칠 뻔했다 — 가장 구체적으로 매칭되는 항목만 활성화하도록 isCurrentPath 로직도 같이 손봤다.

알람 상태를 어디서 가져올지

알람의 FIRING/OK 판정은 로컬에서 재현할 수 없다. threshold·평가 주기·지속시간을 계산해서 내리는 OCI Monitoring의 판정 그 자체라, 로컬에서 흉내 내려면 그 로직을 통째로 다시 짜야 하고 그러면 오탐/미탐 버그를 새로 만들 위험이 있다. 그러니 "OCI를 거칠지 말지"는 고민할 게 아니고 "어디서 거칠지"만 정하면 됐다.

이전에 CPU·메모리·디스크 시계열을 어드민에 붙일 때(#451)도 같은 갈림길이 있었다. 백엔드 컨테이너가 직접 OCI SDK로 조회하는 안은 새 의존성과 새 IAM read 권한이 필요해서 보류했었고, 이번에도 같은 이유로 피했다. 대신 호스트 크론(snapshot-alarm-status.py)이 instance principal로 list_alarms_status를 5분마다 호출해서 로컬 JSON Lines 파일에 한 줄씩 남기고, 백엔드는 그 파일의 최신 한 줄만 읽는 구조를 그대로 재사용했다. 백엔드엔 아무것도 새로 안 얹고, 이미 신뢰하는 venv 하나만 확장하면 됐다.

response = client.list_alarms_status(
    compartment_id=compartment_id,
    compartment_id_in_subtree=True,
)

다만 이 스크립트는 실제 서버에 아직 반영이 안 됐다. IAM 정책이 지금 use metrics만 허용해서 알람 상태를 읽으려면 read alarms 권한이 별도로 필요한데, 이건 인프라 오너 확인이 필요한 부분이라 미결로 남겨뒀다.

만들면서 나온 잔가지들

AlarmStatusPanel 컴포넌트를 짜다가 CI에서 react-hooks/purity 규칙에 걸렸다. Date.now()를 렌더 함수 안에서 직접 불러서 "마지막 확인이 15분 넘었는지" 판단했는데, 이러면 리렌더마다 값이 계속 바뀌어 impure해진다. fetch 응답이 온 시점의 콜백 안에서 한 번만 계산해 state로 저장하는 걸로 고쳤다.

fix(frontend): AlarmStatusPanel의 Date.now() 렌더 중 직접 호출 제거

화면을 만들어놓고 보니 FIRING 알람과 OK 알람이 뒤섞여 있어서 뭐가 문제인지 한눈에 안 들어왔다. FIRING을 심각도 순으로 위에 모으고 알람 이름 키워드로 매칭한 안내 문구(예: "디스크 사용률이 80%를 5분 넘게 유지 중이에요")를 붙였다. OK 알람은 접어서 기본으로 숨겼다. 배포 이력 페이지의 actionGuidance와 같은 패턴이라 그대로 따라갔다.

그리고 화면을 붙여놓고 보니 실제로 git 드리프트 알람이 자꾸 FIRING 상태로 잡히는 게 보였다. 서버에 남아있던 nginx.conf.bak-* 백업 파일이 untracked라 드리프트로 인식되고 있었다. nginx.conf 본체와 같은 이유로 gitignore에 백업본도 같이 넣어서 정리했다.

이름이 문제였다

화면을 붙여놓고 실제로 들여다보니, 디스크·메모리·git 드리프트 알람 이름이 전부 "likelion-prod ..."로 붙어 있었다. 그런데 이 인스턴스 하나가 stage와 prod 컨테이너를 같이 호스팅한다. DB 백업이나 이메일 실패처럼 진짜 환경별로 값이 갈리는 알람과, 호스트 전체를 가리키는 알람이 이름만 보면 구분이 안 됐던 것이다. 이게 실제로 알람 상태 화면에서 눈에 띄었다.

그래서 OCI 인스턴스의 표시 이름을 "likelion-prod"에서 "likelion-server"로 바꿨다. OCID는 그대로라 재부팅이 필요 없는 순수 라벨 변경이었다. 호스트 전체를 가리키는 알람 3개(디스크·메모리·git 드리프트)의 이름도 같이 바꾸고, custom_likelion 네임스페이스로 메트릭을 보내는 push-*.py 스크립트 6개의 resourceDisplayName 차원도 맞췄다. 어차피 알람 판정 자체는 resourceId로 하니 이 값은 "어느 호스트가 보냈나"를 나타내는 라벨일 뿐이라 영향은 없었다.

정리하고 나니 규칙이 명확해졌다: likelion-server는 이 알람들을 실제로 실행·측정하는 물리 호스트 하나(stage·prod를 같이 서빙), likelion-prod/likelion-stage는 그 알람이 가리키는 환경(백업·이메일·ERROR 로그처럼 환경별로 값이 갈리는 것들만). 이 구분을 어드민 화면에도 범례로 남겨서, 처음 보는 사람이 헷갈리지 않게 해뒀다.

알람 튜닝 모델 문서화

작업하면서 참고하던 pending-duration(P), evaluation window(W), 크론 주기(C) 같은 개념이 팀에서 바로 안 읽힐 것 같았다. 이전에 실제 사고(2026-07-12, 롤백 마커 파일이 gitignore 누락으로 오탐을 일으킨 건)를 분석하며 정리해둔 C/W/R/P 공식이 있었는데, 알파벳 표기만으로는 온보딩할 때 설명하기가 어려웠다. 그래서 감시카메라 사진첩 비유와 한글 용어(촬영주기·관찰범위·재확인주기·벨조건시간)로 다시 풀어 쓰고, ASCII 다이어그램으로 "사진 한 장으로 오탐이 나는 경우"와 "진짜 두 번 연속 나빠서 정상 발동하는 경우"를 나란히 비교했다.

이 예시 중 실제 사고 데이터로 검증된 것과, k_min 공식에서 파생만 시킨 것을 구분해서 각주로 남겼다 — 나중에 이 문서만 보고 "이게 실측인지 이론인지" 헷갈리지 않게 하려는 목적이었다. 기반 원리는 OCI 공식 Monitoring 문서 원문과 대조해서 확인해뒀다.

이 흐름 중에 PM 요청으로 stage/prod 분리 회고 블로그 초안을 잠깐 추가했다가, 발행 전 검토가 더 필요해서 바로 되돌렸다(Revert "docs(pm): stage/prod 분리 설계 회고 블로그 초안 추가"). 대신 이번 작업에서 실제로 배운 것 두 가지 — 호스트 이름에 환경 이름을 그대로 쓰면 헷갈린다는 것, Windows OCI CLI가 --display-name에서 한글을 cp949로 잘못 읽어 조용히 깨진 문자열을 저장한다는 것 — 을 pm 학습 문서에 짧게 남겼다.

동기화

이 네 개의 작업(#471~#474)이 각각 CI를 통과하고 dev에 순서대로 머지된 뒤, 이번 PR로 dev를 main에 반영했다. DB 마이그레이션은 없고 알람 상태는 파일 기반이라 Flyway 리스크도 없어서, 각 PR 병합 전 CI만 확인하고 4분 만에 머지했다. 다만 alarm-status 마운트와 크론 등록은 아직 실서버에 반영되지 않은 상태라, 이후 인프라 오너 확인을 거쳐 IAM 정책 추가와 크론 등록이 남아있다.