1. 개요
'비빔밥 SE' 4주차 세미나를 준비하면서 쿠버네티스(K8s) 환경의 '정석' Observability 스택 구축에 도전했다.
'올인원(loki-stack)' 대신, 현업의 표준인 kube-prometheus-stack (Prometheus + Grafana)과 grafana/loki (Loki + Promtail)를 '분리 설치'하는 방식을 혼자서 연습 해보고 싶다는 생각을 했다.
이 결정은 우리를 Helm 검증 로직, K8s 네트워킹, 그리고 Operator 패턴의 세계로 '강제로' 밀어 넣었다.
이 포스트는 30번이 넘는 설치 실패 속에서 마주했던 가장 치명적인 두 개의 벽, 즉 'NetworkPolicy 방화벽'과 'ServiceMonitor 허가증'에 대한 상세한 트러블슈팅 기록이다.
(참고로 비빔밥 실습은 최대한 단순히 하기 위해서 loki-stack으로 설치를 진행했다)
문제 1: Grafana-Loki 연동 - "Unable to connect"
Loki 설치 지옥을 통과한 뒤, kube-prometheus-stack을 설치하고 Grafana UI에 접속했다. 하지만 Loki 데이터 소스를 연동하려는 순간, 차가운 에러 메시지가 우리를 맞이했다.
Unable to connect with Loki. Please check the server logs for more details.
Grafana Pod가 Loki Pod와 통신 자체를 하지 못하는 상황이었다. kubectl exec로 Grafana Pod에 접속해 curl을 날려봐도 Connection timed out이 발생했다.
원인: kube-prometheus-stack의 기본 'NetworkPolicy' (방화벽)
"정석" 스택은 '보안'이 기본값이다. kube-prometheus-stack은 설치 시, monitoring 네임스페이스에 K8s의 NetworkPolicy 리소스를 '자동으로' 설치한다. 그리고 이 정책의 기본값은 'Deny All' (모두 차단)이다.
개념: Kubernetes NetworkPolicy

K8s의 NetworkPolicy는 Pod 간의 트래픽을 제어하는 L3/L4 방화벽이다. podSelector를 사용해 특정 Pod 그룹을 지정하고, ingress(수신) 및 egress(송신) 규칙을 정의한다. '정석' 스택이 설치한 'Deny All' 정책은, 명시적으로 '허용'된 Pod 외에는 그 어떤 Pod와도 통신을 막는다.
분석
이 방화벽 정책으로 인해, Grafana Pod (prometheus-grafana-xxx)가 Loki Pod (loki-headless 또는 loki-gateway)로 3100번 포트 통신을 시도하자, NetworkPolicy 컨트롤러가 이 트래픽을 가로채고 차단버린 것이다.
해결: 'IaC'로 방화벽 비활성화 및 '자동' 연동
우리는 이 문제를 "가장 현업다운" 방식인 'Infrastructure as Code' (IaC)로 해결했다. Grafana UI에서 수동으로 클릭하는 대신, kube-prometheus-stack 설치 시 prometheus-values.yaml 파일을 -f 플래그로 전달하여 '방화벽'을 끄고 'Loki'를 '자동' 등록시켰다.
prometheus-values.yaml
# 1. Grafana가 생성하는 '방화벽'을 비활성화한다.
grafana:
networkPolicy:
enabled: false # "Deny All 방화벽을 생성하지 마라."
# 2. Grafana에 'Loki' 데이터 소스를 '코드로' 자동 등록한다.
additionalDataSources:
- name: Loki (IaC-Auto)
type: loki
# 3. 'SingleBinary' 모드의 '진짜' K8s 내부 DNS URL
url: <http://loki-headless.monitoring.svc.cluster.local:3100>
access: server # 'Server' 모드 (Grafana Pod가 직접 접속)
여기서 access: server는 "네 PC(브라우저)가 아니라, Grafana '서버'(Pod)가 K8s '내부' DNS (.svc.cluster.local)로 직접 접속해라"라는 뜻이다. 이 IaC 접근 방식을 통해 우리는 K8s의 '보안 기본값'을 명시적으로 제어했다.
문제 2: Spring Boot actuator 인식 불가 (ServiceMonitor)
모니터링 스택 연동에 성공한 뒤, my-spring-app을 배포했다. 하지만 Grafana에서 app="my-spring-app"으로 필터링하자, up 외에는 아무 메트릭도 보이지 않았다.
원인 1: Prometheus Operator는 'annotations'를 '무시'한다
우리는 '구식' 방법처럼 Service YAML에 prometheus.io/scrape: 'true' 주석(Annotation)을 달았다. 하지만 정석 kube-prometheus-stack의 Prometheus Operator는 이 주석을 '무시'한다.
개념: Prometheus Operator와 CRD (ServiceMonitor)

Prometheus Operator는 K8s의 'Operator Pattern'을 구현한 것이다.
이는 K8s API를 확장하여 ServiceMonitor라는 '커스텀 리소스(CRD)'를 사용한다. Operator는 Service의 annotations를 일일이 스캔하는 '비효율적인' 대신, 오직 kind: ServiceMonitor라는 '특별한 허가증(CRD)' YAML이 생성되는지만 감시(Watch)한다.
ServiceMonitor가 없었기에, 우리 앱은 Prometheus에게 '감지되지 않은 거였다.
해결 1: ServiceMonitor (허가증) YAML 배포
우리는 my-app-monitor.yaml이라는 '허가증'을 monitoring 네임스페이스(Prometheus가 있는 곳)에 배포했다.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-spring-app-monitor
namespace: monitoring # Prometheus가 감시하는 네임스페이스
...
4. 문제 3: ServiceMonitor의 2단계 라벨과 Scrape 경로
ServiceMonitor를 배포했지만, 여전히 Prometheus 'Targets' 페이지에 우리 앱이 나타나지 않았다. '허가증'을 냈는데도 인식이 안 되는, 더 복잡한 문제였다.
원인 1: 라벨 매칭 불일치 문제
ServiceMonitor는 2단계의 라벨 매칭이 필요했다.
- Operator -> Monitor: Prometheus Operator가 이 ServiceMonitor '허가증' 자체를 '발견'하기 위한 '명찰'이 필요했다. (e.g., labels: {release: prometheus})
- Monitor -> Service: ServiceMonitor가 my-spring-app-svc라는 Service를 '발견'하기 위한 '명찰'이 필요했다. (e.g., spec.selector.matchLabels: {app: my-spring-app})
우리는 이 두 가지 라벨 매칭을 모두 놓쳤다.
원인 2: 매트릭 엔드포인트 불일치
설사 라벨 매칭이 되었더라도 문제가 있었다. Prometheus는 기본적으로 /metrics 엔드포인트를 긁으러 간다. 하지만 우리 Spring Boot 앱은 /actuator/prometheus에 메트릭을 노출하고 있었다. 기본 엔드포인트 주소가 달랐다.
해결 2: ServiceMonitor에 정확한 라벨과 엔드포인트 명시
우리는 ServiceMonitor YAML을 다음과 같이 수정하여 2단계 라벨과 엔드포인트 경로를 모두 명시했다.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-spring-app-monitor
namespace: monitoring
labels:
release: prometheus # 1. Operator가 나를 찾을 수 있게 '명찰' 부착
spec:
selector:
matchLabels:
app: my-spring-app # 2. 내가 찾아야 할 Service의 '명찰'
endpoints:
- port: "http-8080"
path: /actuator/prometheus # 3. /metrics 말고, /actuator/prometheus로 안내
interval: 15s
이 YAML을 apply하는 순간, Prometheus의 'Targets' 페이지에 my-spring-app이 UP 상태로 등록되는 것을 확인할 수 있었다.
5. 배운점 정리
- NetworkPolicy: '정석' 스택은 '보안(Deny All)'이 기본값이라는 것.
- IaC (Infrastructure as Code): additionalDataSources로 데이터 소스를 '코드로' 관리하는 법.
- Prometheus Operator: annotations를 '무시'하며, ServiceMonitor (CRD)라는 '허가증'이 '정석'이라는 것.
- Label Matching: Operator -> Monitor, Monitor -> Service로 이어지는 2단계 라벨 매칭이 핵심이라는 것.
- Scrape Config: path로 '정확한 문'을 알려줘야 한다는 것.
댓글