프로메테우스 메트릭 노출에 IP 필터와 설정 파싱 오류 수정
목차
운영 환경에서 뭔가 터졌을 때 제일 먼저 하는 짓이 로그 뒤지는 거라면, 아직 관찰 가능성 스택을 제대로 안 갖춘 거다. 로그만으론 "지금 이 순간 시스템 상태"를 볼 수가 없다. 메트릭이 있어야 JVM 힙이 얼마나 찼는지, GC가 얼마나 자주 도는지, HTTP 응답 시간이 어느 백분위에서 튀는지를 실시간으로 확인할 수 있다. 로그는 사후 부검이고, 메트릭은 심전도다.
이번 작업은 Spring Boot Actuator로 /actuator/prometheus 엔드포인트를 열고, Prometheus가 주기적으로 스크레이핑하는 구조를 정비한 것이다. Grafana datasource와 대시보드 갱신, Zipkin/Tempo 연동 엔드포인트 조정도 같이 묶어서 했다. 작업 자체는 크지 않았는데 중간에 YAML 파싱 문제로 한 번 막혔다.
Actuator 엔드포인트 노출 범위와 IP 필터
Spring Boot Actuator는 기본 제공하는 엔드포인트가 꽤 많다. health, info, metrics, prometheus, env, beans, heapdump... 이걸 다 열어두면 내부 구성 정보가 외부로 새나갈 수 있다. 특히 env나 beans는 애플리케이션 내부 구조를 거의 그대로 드러낸다. 그래서 노출 대상을 최소화하는 게 출발점이다.
management:
endpoints:
web:
exposure:
include: health,prometheus
metrics:
export:
prometheus:
enabled: true
health는 로드밸런서 헬스체크용, prometheus는 Prometheus 스크레이퍼 전용으로만 열었다. 이 두 개 이외엔 접근 자체가 안 된다.
그런데 이것만으론 충분하지 않다. /actuator/prometheus가 퍼블릭 인터넷에 열려 있으면 JVM 내부 상태, 스레드 수, 커넥션 풀 상황을 누구든 다 볼 수 있다. 보안 이슈이기도 하고, Prometheus 스크레이퍼 외의 불필요한 트래픽도 생긴다. Spring Security로 처리하는 방법도 있고, OncePerRequestFilter를 직접 구현하는 방법도 있는데, 이번엔 후자로 갔다. 허용 IP 목록을 설정 파일에서 읽어와서 목록에 없으면 403을 내려보내는 단순한 구조다.
@Value("${metrics.allowed-ips}")
private String allowedIpsRaw;
@PostConstruct
public void init() {
allowedIps = Arrays.stream(allowedIpsRaw.split(","))
.map(String::trim)
.collect(Collectors.toSet());
}
여기서 trim()이 핵심이었다. 이게 없어서 한 번 막혔다.
YAML 공백 파싱 문제, trim() 한 줄의 교훈
설정 파일에서 IP 목록을 ,로 구분해서 127.0.0.1, 10.0.0.1 이런 식으로 썼더니, 파싱 후 값이 " 10.0.0.1"이 되는 거다. 쉼표 뒤 공백이 그대로 살아있는 것. 필터에서 IP를 비교할 때 "10.0.0.1".equals(" 10.0.0.1")이 false라서 허용되어야 할 요청이 전부 막혔다.
처음엔 왜 막히는지 몰라서 필터 로직을 한참 뒤졌다. 로그에 비교 대상 IP를 찍어보니 앞에 공백이 붙어있었다. 원인 파악 이후엔 trim() 한 줄로 끝났는데, 이런 류 버그가 은근히 잡기 귀찮다. 값이 "있긴 한데 다른" 경우라서 NPE나 예외로 잡히지 않고 조용히 잘못 동작한다. 조용히 틀리는 버그가 시끄럽게 터지는 버그보다 더 나쁜 이유가 이거다.
외부에서 문자열 목록을 주입받을 때 파싱 시점에 trim()을 습관화하는 게 맞다. 설정 파일 작성 컨벤션을 강제하기 어려우니 코드에서 방어하는 게 현실적이다. YAML이든 properties든 마찬가지다. 특히 쉼표 구분 목록은 사람마다 공백 넣는 습관이 달라서 더 그렇다.
Grafana 대시보드, 뭘 봐야 하는가
Prometheus 스크레이핑이 안정되면 Grafana에서 뭘 볼지를 정해야 한다. 패널을 너무 많이 만들면 오히려 중요한 지표가 묻힌다. 기본으로 챙겨야 할 지표는 아래 정도다.
| 카테고리 | 지표 | 이유 |
|---|---|---|
| JVM 메모리 | heap used/committed, non-heap | 메모리 압박, 누수 감지 |
| GC | GC pause duration, GC count | Full GC 빈도와 STW 확인 |
| 스레드 | 활성/대기 스레드, thread pool queue depth | 병목 위치 파악 |
| HTTP | 요청 수, 에러율, 레이턴시 p95/p99 | 서비스 수준 확인 |
| DB 커넥션 풀 | active/idle/pending | DB 병목 조기 탐지 |
p95/p99 레이턴시는 평균보다 훨씬 의미있다. 평균이 100ms여도 p99가 2초라면 요청의 1%는 2초를 기다리는 거고, 트래픽이 많을수록 체감 불만이 쌓인다. 대시보드에 평균만 놓으면 이 1%를 영원히 모를 수 있다.
알림도 같이 설정해두는 게 맞다. 임계치 초과 시 슬랙으로 알림 보내도록 해두면 야간에 뭔가 튀더라도 빠르게 인지된다. Loki 연동까지 되면 에러 로그 발생 타임라인을 메트릭 그래프 위에 겹쳐서 볼 수 있어서 원인 추적 시간이 줄어든다. Zipkin/Tempo 트레이스까지 붙으면 "어느 서비스에서, 어느 구간에서 느려졌는지"까지 한 화면에서 파악이 된다.
메트릭-로그-트레이스 세 축을 다 갖추는 게 관찰 가능성의 기본이다. 이번 작업은 그 중 메트릭 쪽 기반을 다진 것이고, 작업 규모 자체는 작아도 나중에 "그 당시 JVM 상태가 어땠지"를 물어볼 수 있는 구조를 만드는 게 핵심이다. 문제가 생기고 나서 데이터를 모으기 시작하면 이미 늦다.
댓글 0
첫 댓글 달아줘.