관측성 도구를 설치한 뒤에는 애플리케이션의 데이터가 조회 화면까지 도착하는지 확인해야 합니다. Prometheus Pod가 Running이어도 ServiceMonitor selector가 맞지 않으면 애플리케이션 메트릭은 한 건도 수집되지 않습니다. Fluent Bit이 모든 노드에서 실행돼도 Loki 주소가 틀리거나 buffer가 가득 차면 로그는 유실됩니다.
Grafana에서 데이터가 보이지 않을 때 원인을 찾으려면, 메트릭과 로그가 어떤 경로로 수집되고 저장되는지 알아야 합니다. 이 글에서는 각 경로의 중간 결과를 확인하며 수집이 멈춘 지점을 좁혀 갑니다.
이 글은 네 가지 구성 요소를 하나의 데이터 흐름으로 연결합니다.
Kubernetes·애플리케이션 /metrics
→ Prometheus
→ PromQL·PrometheusRule
→ Alertmanager와 Grafana
컨테이너 stdout/stderr
→ 노드별 Fluent Bit Collector
→ Loki
→ LogQL과 Grafana앞 글에서 만든 telemetry-demo를 실제 수집 대상으로 사용합니다. 설치에 이어 타깃 발견, 시계열 생성, 로그 전달, 질의, 알림 상태까지 확인하는 것이 이 실습의 완료 기준입니다.
이 글에서 다루는 것
- kube-prometheus-stack의 구성 요소와 Prometheus Operator CRD
- Loki의 레이블 인덱싱과 Fluent Bit 노드 수집 패턴
- 2026년 현재 Loki·Fluent Bit Helm 차트 경로의 변화
- 영구 볼륨과 Secret을 사용한 교육용 단일 클러스터 설치
- ServiceMonitor로 애플리케이션
/metrics자동 발견 - PrometheusRule로 오류율 알림 생성
- Fluent Bit의 안정적인 레이블 설계와 파일 오프셋 저장
- Grafana에서 PromQL과 LogQL로 같은 앱을 교차 확인
- 설치·수집·저장·질의 단계별 장애 진단
- 운영 환경으로 확장할 때의 HA, 보존, 보안, 업그레이드 기준
실습 환경과 치환값
| 항목 | 실습값 | 설명 |
|---|---|---|
| 메트릭 Namespace | monitoring |
Prometheus, Grafana, Alertmanager |
| 로그 Namespace | logging |
Loki, Fluent Bit Collector |
| 애플리케이션 Namespace | observability-lab |
앞 글의 telemetry-demo |
| 교육용 StorageClass | nfs-csi |
Day 1 실습 연계용입니다. Prometheus local TSDB에는 지원되지 않습니다 |
| 선택적 Grafana 주소 | https://grafana.lab.example.com |
Gateway API로 외부 노출할 때만 사용 |
| Grafana 관리자 비밀번호 | ${GRAFANA_ADMIN_PASSWORD} |
셸 환경변수에서 Secret으로 주입 |
이 글에서 검증한 차트 조합은 updated 날짜 기준 다음과 같습니다.
| 차트 | 버전 | 배포 경로 |
|---|---|---|
| kube-prometheus-stack | 87.12.2 |
oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack |
| Loki community chart | 18.4.2 |
oci://ghcr.io/grafana-community/helm-charts/loki |
| Fluent Bit Collector | 1.0.9 |
oci://ghcr.io/fluent/helm-charts/fluent-bit-collector |
차트가 바뀌면 이 글의 values와 맞는지도 다시 확인해야 합니다. 따라서 사용할 버전을 values와 함께 고정하고, 업그레이드할 때는 각 major version의 migration note와 렌더링 결과를 검토합니다.
사전 점검
kubectl version
helm version
kubectl get storageclass nfs-csi
kubectl get nodes
kubectl -n observability-lab get deployment,service telemetry-demotelemetry-demo가 없다면 이전 글의 telemetry-demo.yaml을 먼저 적용합니다. 이 글은 Day 1과 연결하기 위해 nfs-csi를 교육용 공통값으로 사용하지만, Prometheus local TSDB용 StorageClass는 운영에서 별도로 선택해야 합니다. 아래 경고에 설명한 저장소 제약을 기준으로 각 구성 요소의 storageClassName을 정합니다.
구성 요소가 맡는 정확한 역할
kube-prometheus-stack
prometheus-community의 Helm 차트로 Kubernetes 메트릭 관측에 필요한 구성 요소를 묶습니다.
| 구성 요소 | 역할 |
|---|---|
| Prometheus Operator | CRD를 감시해 Prometheus·Alertmanager 구성을 생성 |
| Prometheus | target scrape, 로컬 TSDB, PromQL, 규칙 평가 |
| Alertmanager | 알림 그룹화·중복 제거·억제·수신자 라우팅 |
| Grafana | 대시보드와 탐색 UI |
| node-exporter | 노드 OS·하드웨어 메트릭 |
| kube-state-metrics | Kubernetes 오브젝트의 desired/current 상태 |
| 기본 규칙·대시보드 | Kubernetes 운영에 필요한 출발점 |
Prometheus Operator의 핵심 CRD는 다음과 같습니다.
| CRD | 사용 목적 |
|---|---|
ServiceMonitor |
레이블로 Service를 선택하고 이름 있는 port를 scrape |
PodMonitor |
Service 없이 Pod를 직접 선택 |
PrometheusRule |
recording rule과 alerting rule 선언 |
Probe |
blackbox exporter를 통한 외부 endpoint 점검 |
AlertmanagerConfig |
네임스페이스 단위 알림 경로 확장 |
이 구조 덕분에 애플리케이션 팀은 중앙 Prometheus 설정 파일을 직접 수정하지 않고 자신의 Namespace에 ServiceMonitor를 선언할 수 있습니다. 단, 중앙 운영자는 selector와 Namespace 범위를 통제해야 합니다.
Loki와 Fluent Bit
Kubernetes는 Pod 로그를 장기 저장하지 않습니다. 컨테이너가 stdout과 stderr에 기록하면 런타임이 노드의 CRI 로그 파일로 저장합니다. 노드가 사라지거나 파일이 회전하면 과거 로그에 접근할 수 없습니다.
Fluent Bit Collector는 DaemonSet으로 각 노드의 /var/log/containers/*.log를 tail하고 Kubernetes 메타데이터를 붙여 Loki로 전송합니다. Loki는 로그 본문 전체가 아니라 stream label을 중심으로 인덱싱합니다.
Pod의 stdout/stderr는 노드의 CRI 로그 파일에 기록됩니다.
Pod stdout/stderr → CRI 로그 파일로그 파일 기록
단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.
전체 단계 한눈에 읽기
- 컨테이너 로그가 파일로
Pod의 stdout/stderr는 노드의 CRI 로그 파일에 기록됩니다.
- Pod stdout/stderr → CRI 로그 파일: 로그 파일 기록
- 노드마다 Collector 배치
DaemonSet으로 실행되는 Fluent Bit Collector가 노드의 로그 파일을 수집합니다.
- CRI 로그 파일 → Fluent Bit Collector: 노드 로그 수집
- Loki에 로그 전송
Fluent Bit은 안정적인 labels와 함께 Loki에 로그를 push합니다.
- Fluent Bit Collector → Loki: push + 안정적 labels
- 로그와 메트릭 함께 조회
Grafana Explore에서 LogQL로 Loki 로그를 조회하고 Prometheus 메트릭과 함께 살펴봅니다. 저장소가 Grafana로 계속 push한다는 뜻은 아닙니다.
- Loki → Grafana Explore: LogQL 조회
- Prometheus → Grafana Explore: 메트릭 조회
도식 원문
flowchart LR
subgraph N["각 Kubernetes 노드"]
P["Pod stdout/stderr"] --> C["CRI 로그 파일"]
C --> F["Fluent Bit Collector<br/>DaemonSet"]
end
F -->|"push + 안정적 labels"| L["Loki"]
L --> G["Grafana Explore<br/>LogQL"]
M["Prometheus"] --> GLoki label에는 cluster, namespace, container처럼 값 종류가 제한된 항목만 둡니다. pod 이름도 배포마다 바뀌므로 이 글에서는 structured metadata로 보냅니다. request_id, 사용자 ID, trace ID를 label로 만들지 않습니다.
2026년 차트 경로를 확인해야 하는 이유
2026년 3월부터 오픈소스 Loki Helm 차트의 유지 위치가 grafana-community/helm-charts로 이동했습니다. 커뮤니티 차트 12.0.0부터 배포 모드 이름도 SingleBinary에서 Monolithic로 바뀌었습니다. values의 세부 키는 여전히 singleBinary를 사용하는 부분이 있으므로 deploymentMode: Monolithic과 혼동하지 않습니다.
Fluent Bit은 범용 차트 외에 노드 수집기와 중앙 집계기의 역할을 분리한 차트를 제공합니다. 이 글은 Kubernetes 로그 수집에 맞는 fluent-bit-collector를 사용합니다. 이 차트는 DaemonSet, CRI multiline parsing, Kubernetes metadata, 파일 기반 buffer를 목적에 맞게 제공합니다.
1단계: Namespace와 Grafana Secret 준비
kubectl create namespace monitoring --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace logging --dry-run=client -o yaml | kubectl apply -f -
: "${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD 환경변수를 먼저 설정하세요}"
kubectl -n monitoring create secret generic grafana-admin-credentials \
--from-literal=admin-user=admin \
--from-literal=admin-password="${GRAFANA_ADMIN_PASSWORD}" \
--dry-run=client -o yaml \
| kubectl apply -f -비밀번호 값을 명령문에 직접 적지 않습니다. 운영에서는 Keycloak OIDC를 Grafana에도 연결하고 로컬 관리자는 비상용으로 제한합니다. 이 Secret은 새 Grafana 데이터베이스의 초기 관리자 계정을 부트스트랩하는 용도입니다. 이미 데이터베이스가 초기화된 뒤 Secret만 바꿔도 기존 관리자 비밀번호가 자동 변경된다고 가정하지 않습니다.
2단계: kube-prometheus-stack 설치
다음을 kps-values.yaml로 저장합니다.
grafana:
admin:
existingSecret: grafana-admin-credentials
userKey: admin-user
passwordKey: admin-password
persistence:
enabled: true
type: pvc
storageClassName: nfs-csi
accessModes:
- ReadWriteOnce
size: 5Gi
additionalDataSources:
- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki-gateway.logging.svc.cluster.local
isDefault: false
editable: false
prometheus:
prometheusSpec:
replicas: 1
retention: 7d
retentionSize: 15GB
externalLabels:
cluster: lab
serviceMonitorSelectorNilUsesHelmValues: false
serviceMonitorNamespaceSelector: {}
podMonitorSelectorNilUsesHelmValues: false
podMonitorNamespaceSelector: {}
ruleSelectorNilUsesHelmValues: false
ruleNamespaceSelector: {}
# 교육 흐름을 위한 NFS 예시다. Prometheus TSDB 운영 저장소로 사용하지 않는다.
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: nfs-csi
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
alertmanager:
alertmanagerSpec:
replicas: 1
storage:
volumeClaimTemplate:
spec:
storageClassName: nfs-csi
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi중요 — 이 Prometheus NFS PVC는 교육용이며 공식 지원 구성이 아닙니다. Prometheus local TSDB는 WAL과 block compaction의 파일시스템 동작에 의존합니다. 공식 문서는 비 POSIX 파일시스템에서 복구할 수 없는 손상이 발생할 수 있다고 경고하고, NFS와 AWS EFS를 명시적으로 지원하지 않으며 신뢰성을 위해 local filesystem 사용을 강하게 권고합니다.
따라서
nfs-csiPVC가Bound이고 당장 수집이 되더라도 데이터 무결성과 복구 가능성이 보장되지 않습니다.운영에서는 Prometheus의
storageClassName만 신뢰할 수 있는 local persistent volume 또는 ext4/XFS 같은 POSIX 파일시스템을 제공하는 block volume용 StorageClass로 바꿉니다. 이 제한은 Prometheus local TSDB에 대한 것이며 Grafana·Alertmanager·Loki는 각 제품의 저장소 요구사항을 별도로 검토합니다.
선택자를 false로 설정한 것은 Helm release label이 없는 사용자 ServiceMonitor와 PrometheusRule도 수집하기 위해서입니다. Namespace selector의 빈 객체는 모든 Namespace를 대상으로 합니다. 이는 교육 클러스터에는 편리하지만 멀티테넌트 운영 환경에서는 아무 팀이나 고비용 scrape와 규칙을 만들 수 있다는 뜻이기도 합니다. 운영에서는 전용 label과 허용 Namespace selector, admission policy로 범위를 제한합니다.
설치 전에 렌더링을 확인합니다.
helm template kube-prometheus-stack \
oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack \
--version 87.12.2 \
--namespace monitoring \
--values kps-values.yaml \
> /tmp/kube-prometheus-stack-rendered.yaml
test -s /tmp/kube-prometheus-stack-rendered.yaml첫 설치 전에는 클러스터에 ServiceMonitor, PrometheusRule 같은 CRD가 아직 없으므로 전체 렌더링 결과에 대한 kubectl apply --dry-run이 실패할 수 있습니다. 여기서는 helm template이 오류 없이 완성되는지 먼저 검사합니다. CRD가 설치된 뒤 생성하는 사용자 정의 리소스는 아래에서 server-side dry-run으로 API 서버 검증까지 수행합니다.
실제 설치를 수행합니다.
helm upgrade --install kube-prometheus-stack \
oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack \
--version 87.12.2 \
--namespace monitoring \
--values kps-values.yaml \
--wait \
--timeout 15mkubectl -n monitoring get pods
kubectl -n monitoring get prometheus,alertmanager
kubectl get servicemonitors.monitoring.coreos.com --all-namespaces
kubectl get prometheusrules.monitoring.coreos.com --all-namespaces
kubectl -n monitoring get pvc예상 결과는 Prometheus Operator, Prometheus, Grafana, Alertmanager, kube-state-metrics, 각 노드의 node-exporter가 Ready이고 PVC가 Bound인 상태입니다.
3단계: 교육용 Monolithic Loki 설치
이 실습은 하나의 클러스터에서 흐름을 이해하기 위한 단일 replica와 filesystem 저장소를 사용합니다. 운영 로그 플랫폼은 이 구성 그대로 사용하지 않습니다. Loki 다중 replica와 장기 보존에는 외부 object storage, 장애 도메인 분리, ingest/query 용량 산정이 필요합니다.
다음을 loki-values.yaml로 저장합니다.
deploymentMode: Monolithic
loki:
auth_enabled: false
commonConfig:
replication_factor: 1
schemaConfig:
configs:
- from: "2024-04-01"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: loki_index_
period: 24h
storage:
type: filesystem
limits_config:
retention_period: 168h
allow_structured_metadata: true
volume_enabled: true
compactor:
retention_enabled: true
delete_request_store: filesystem
singleBinary:
replicas: 1
persistence:
enabled: true
storageClass: nfs-csi
size: 20Gi
gateway:
enabled: true
chunksCache:
enabled: false
resultsCache:
enabled: false
lokiCanary:
enabled: false
test:
enabled: false
backend:
replicas: 0
read:
replicas: 0
write:
replicas: 0
ingester:
replicas: 0
querier:
replicas: 0
queryFrontend:
replicas: 0
queryScheduler:
replicas: 0
distributor:
replicas: 0
compactor:
replicas: 0
indexGateway:
replicas: 0
bloomPlanner:
replicas: 0
bloomBuilder:
replicas: 0
bloomGateway:
replicas: 0from 날짜는 스키마가 적용되기 시작하는 기준일입니다. 기존 Loki의 스키마를 변경할 때는 과거 데이터를 덮어쓰지 말고 새로운 미래 날짜의 항목을 추가하는 migration 절차를 따릅니다.
helm template loki \
oci://ghcr.io/grafana-community/helm-charts/loki \
--version 18.4.2 \
--namespace logging \
--values loki-values.yaml \
> /tmp/loki-rendered.yaml
kubectl apply --dry-run=server -f /tmp/loki-rendered.yaml >/dev/null
helm upgrade --install loki \
oci://ghcr.io/grafana-community/helm-charts/loki \
--version 18.4.2 \
--namespace logging \
--values loki-values.yaml \
--wait \
--timeout 15mkubectl -n logging get pod,service,pvc
kubectl -n logging rollout status statefulset/loki --timeout=10m이어서 생성된 서비스 이름을 확인해 연결 대상과 대조합니다.
kubectl -n logging get service \
-o custom-columns='NAME:.metadata.name,PORTS:.spec.ports[*].port'이 글의 Grafana와 Fluent Bit 설정은 loki-gateway.logging.svc.cluster.local의 HTTP 80을 사용합니다.
4단계: Fluent Bit Collector 설치
새 Collector 차트는 Kubernetes container log input과 metadata processor를 기본으로 제공합니다. 전체 input을 다시 복사하지 않고 output과 buffer·모니터링만 명시합니다.
다음을 fluent-bit-values.yaml로 저장합니다.
storage:
enabled: true
hostPath: /var/fluent-bit/data
service:
enabled: true
serviceMonitor:
enabled: true
additionalLabels:
release: kube-prometheus-stack
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 300m
memory: 256Mi
config:
service:
log_level: info
flush: 1
pipeline:
filters:
- name: parser
match: "kube.*"
key_name: log
parser: json
reserve_data: on
preserve_key: off
outputs:
- name: loki
alias: loki-output
match: "kube.*"
host: loki-gateway.logging.svc.cluster.local
port: 80
labels: "job=fluent-bit,cluster=lab,namespace=$kubernetes['namespace_name'],container=$kubernetes['container_name']"
structured_metadata: "pod=$kubernetes['pod_name']"
line_format: json
compress: gzip
retry_limit: falseretry_limit: false는 일시적인 Loki 장애에서 무한 재시도를 허용합니다. 그래서 storage.enabled, disk 사용량, backlog와 output error 모니터링이 필수입니다. 백엔드가 오래 멈추면 노드 디스크가 무한하지 않으므로 운영에서는 buffer 상한과 데이터 유실 정책을 명시해야 합니다.
parser filter는 컨테이너 로그의 log 필드에 들어 있는 JSON을 status, path, request_id 같은 구조화 필드로 올립니다. 여러 로그 형식이 섞인 운영 환경에서는 태그·namespace·annotation으로 적용 범위를 좁히고 형식별 parser를 분리합니다.
이 구성은 auto_kubernetes_labels를 켜지 않습니다. 모든 Pod label을 Loki stream label로 자동 변환하면 배포 hash나 임시 label 때문에 카디널리티가 커질 수 있습니다. 필요한 안정적 label만 명시적으로 선택하고 Pod 이름은 structured metadata에 둡니다.
helm template fluent-bit-collector \
oci://ghcr.io/fluent/helm-charts/fluent-bit-collector \
--version 1.0.9 \
--namespace logging \
--values fluent-bit-values.yaml \
> /tmp/fluent-bit-rendered.yaml
kubectl apply --dry-run=server -f /tmp/fluent-bit-rendered.yaml >/dev/null
helm upgrade --install fluent-bit-collector \
oci://ghcr.io/fluent/helm-charts/fluent-bit-collector \
--version 1.0.9 \
--namespace logging \
--values fluent-bit-values.yaml \
--wait \
--timeout 10mDaemonSet의 desired와 ready가 노드 수와 일치해야 합니다.
kubectl -n logging get daemonset fluent-bit-collector
kubectl -n logging get pods \
-l app.kubernetes.io/name=fluent-bit-collector \
-o wide
kubectl -n logging logs daemonset/fluent-bit-collector --since=5m \
| grep -E 'error|warn|loki' || true5단계: telemetry-demo를 Prometheus에 연결
다음을 telemetry-demo-monitoring.yaml로 저장합니다.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: telemetry-demo
namespace: observability-lab
labels:
release: kube-prometheus-stack
spec:
namespaceSelector:
matchNames:
- observability-lab
selector:
matchLabels:
app.kubernetes.io/name: telemetry-demo
endpoints:
- port: metrics
path: /metrics
interval: 15s
scrapeTimeout: 5s
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: telemetry-demo
namespace: observability-lab
labels:
release: kube-prometheus-stack
spec:
groups:
- name: telemetry-demo.rules
rules:
- alert: TelemetryDemoHighErrorRate
expr: |
sum(rate(demo_http_requests_total{status="500"}[5m]))
/
clamp_min(sum(rate(demo_http_requests_total[5m])), 0.001)
> 0.05
for: 2m
labels:
severity: warning
service: telemetry-demo
annotations:
summary: telemetry-demo 5xx 비율이 5%를 초과했습니다
description: 최근 5분 오류율을 확인하고 애플리케이션 로그를 조회하세요.kubectl apply --dry-run=server -f telemetry-demo-monitoring.yaml >/dev/null
kubectl apply -f telemetry-demo-monitoring.yaml
kubectl -n observability-lab get servicemonitor,prometheusruleServiceMonitor의 endpoints[].port는 컨테이너 port 숫자가 아니라 Service port 이름 metrics입니다. 두 값이 다르면 ServiceMonitor가 해당 port를 찾지 못해 사용자 target이 나타나지 않을 수 있습니다.
6단계: 메트릭 수집 검증
Prometheus를 로컬로만 엽니다.
kubectl -n monitoring port-forward \
service/kube-prometheus-stack-prometheus 9090:9090다른 터미널에서 API를 조회합니다.
curl --fail --silent --get \
http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=up{namespace="observability-lab",service="telemetry-demo"}' \
| python3 -m json.tool
curl --fail --silent --get \
http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=demo_http_requests_total' \
| python3 -m json.tool첫 쿼리의 value가 1이고 두 번째 쿼리에 200·500 시계열이 보여야 합니다. Prometheus UI의 Status → Target health에서도 observability-lab/telemetry-demo target이 UP인지 확인합니다.
알림을 발생시키기 위해 앞 글처럼 정상과 실패 요청을 만듭니다.
kubectl -n observability-lab port-forward service/telemetry-demo 8080:8080for _ in $(seq 1 20); do
curl --silent --output /dev/null http://127.0.0.1:8080/
done
for _ in $(seq 1 5); do
curl --silent --output /dev/null http://127.0.0.1:8080/fail
done두 번 이상의 scrape와 for: 2m가 지난 뒤 Prometheus Alerts에서 TelemetryDemoHighErrorRate가 Pending을 거쳐 Firing이 되는지 확인합니다. 실습 traffic이 적으면 5분 rate window에서 결과가 늦게 보일 수 있으므로 요청을 한 번 더 생성합니다.
7단계: Loki와 로그 전달 검증
먼저 gateway를 통해 Loki HTTP API가 응답하는지 확인합니다.
kubectl -n logging port-forward service/loki-gateway 3100:80curl --fail --silent \
http://127.0.0.1:3100/loki/api/v1/status/buildinfo \
| python3 -m json.tool그다음 Loki HTTP API로 label을 확인합니다.
curl --fail --silent \
http://127.0.0.1:3100/loki/api/v1/labels \
| python3 -m json.tooljob, cluster, namespace, container가 보여야 합니다. request_id나 pod가 index label 목록에 없어야 이 글의 카디널리티 정책과 일치합니다.
로그를 직접 질의합니다. URL 인코딩은 curl --data-urlencode에 맡깁니다.
curl --fail --silent --get \
http://127.0.0.1:3100/loki/api/v1/query_range \
--data-urlencode 'query={job="fluent-bit",namespace="observability-lab"} |= "simulated failure"' \
--data-urlencode 'limit=20' \
| python3 -m json.tool응답의 stream과 values에 telemetry-demo의 500 로그가 포함되면 다음 전체 경로가 확인된 것입니다.
앱 stdout → 노드 CRI 로그 → Fluent Bit → Loki → LogQL8단계: Grafana에서 메트릭과 로그를 함께 보기
kubectl -n monitoring port-forward \
service/kube-prometheus-stack-grafana 3000:80브라우저에서 http://127.0.0.1:3000을 엽니다. 사용자 이름은 Secret에 넣은 admin, 비밀번호는 ${GRAFANA_ADMIN_PASSWORD}입니다.
Prometheus 확인
Explore에서 Prometheus 데이터소스를 선택하고 다음 PromQL을 실행합니다.
sum by (status) (rate(demo_http_requests_total[5m]))오류율은 다음처럼 계산합니다.
sum(rate(demo_http_requests_total{status="500"}[5m]))
/
clamp_min(sum(rate(demo_http_requests_total[5m])), 0.001)Loki 확인
데이터소스를 Loki로 바꾸고 다음 LogQL을 실행합니다.
{job="fluent-bit", namespace="observability-lab"} | json | status=500특정 문자열은 다음처럼 찾습니다.
{job="fluent-bit", namespace="observability-lab"} |= "simulated failure"메트릭에서 오류가 증가한 시간을 선택하고 같은 시간 범위의 로그를 보는 것이 핵심입니다. 두 데이터소스의 cluster, namespace, service 명명 규칙을 일치시키면 대시보드 data link와 derived field로 더 자연스럽게 이동할 수 있습니다.
선택: Grafana만 Gateway API로 노출
교육 실습에서는 port-forward로 접근 범위를 로컬에 한정합니다. 여러 사용자가 접근해야 한다면 Prometheus와 Alertmanager까지 외부에 직접 열기보다, 인증을 구성한 Grafana만 Gateway API에 연결합니다.
Day 1에서 만든 nginx-gateway/platform-gateway의 https listener는 gateway-access=true label이 있는 Namespace의 Route만 허용하며, *.lab.example.com 인증서를 사용합니다. 먼저 monitoring Namespace를 허용 대상으로 표시한 뒤 다음 내용을 grafana-httproute.yaml로 저장합니다.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: grafana
namespace: monitoring
spec:
parentRefs:
- name: platform-gateway
namespace: nginx-gateway
sectionName: https
hostnames:
- grafana.lab.example.com
rules:
- backendRefs:
- name: kube-prometheus-stack-grafana
port: 80kubectl label namespace monitoring gateway-access=true --overwrite
kubectl -n nginx-gateway get gateway platform-gateway
kubectl apply --dry-run=server -f grafana-httproute.yaml >/dev/null
kubectl apply -f grafana-httproute.yaml
kubectl -n monitoring get httproute grafana
kubectl -n monitoring describe httproute grafanaGateway의 PROGRAMMED가 True인지 확인하고, Route 상태에서 부모가 nginx-gateway/platform-gateway이며 Accepted=True, ResolvedRefs=True인지 확인합니다. 운영에서는 Grafana에도 Keycloak OIDC와 조직 역할 매핑을 적용하고 로컬 admin의 일상 사용을 중단합니다.
검증 체크리스트
- monitoring과 logging Namespace의 모든 필수 Pod가 Ready입니다.
- Prometheus·Grafana·Alertmanager·Loki PVC가
Bound입니다. - Prometheus target 목록에
telemetry-demo가UP으로 표시됩니다. demo_http_requests_total의 200·500 시계열이 증가합니다.TelemetryDemoHighErrorRate규칙이 로드되고 의도한 traffic에서 Pending/Firing으로 전환합니다.- Fluent Bit DaemonSet의 Ready 수가 대상 Linux 노드 수와 일치합니다.
- Loki API에서 로그를 찾을 수 있고
request_id가 index label이 아닙니다. - Grafana의 Prometheus와 Loki 데이터소스가 모두 정상입니다.
- 선택적 Grafana Route를 만들었다면 부모가
nginx-gateway/platform-gateway이고Accepted,ResolvedRefs가 모두True입니다. - Prometheus와 Alertmanager는 외부에 직접 공개하지 않았습니다.
장애 대응: 데이터가 보이지 않을 때 경로를 나눠 확인한다
Prometheus target 자체가 없다
kubectl -n observability-lab get service telemetry-demo --show-labels
kubectl -n observability-lab get servicemonitor telemetry-demo -o yaml
kubectl -n monitoring get prometheus -o yaml다음을 순서대로 비교합니다.
- Service의 label과 ServiceMonitor
selector.matchLabels - Service port 이름과 ServiceMonitor
endpoints.port - ServiceMonitor Namespace가 Prometheus Namespace selector에 포함되는지
- ServiceMonitor selector가 요구하는 release label
target은 있지만 DOWN이다
kubectl -n observability-lab get endpointslice \
-l kubernetes.io/service-name=telemetry-demo
kubectl -n observability-lab port-forward service/telemetry-demo 8080:8080
curl --fail http://127.0.0.1:8080/metricsEndpointSlice가 비어 있으면 Service selector와 Pod label 문제입니다. endpoint가 있는데 scrape가 실패하면 path, port, NetworkPolicy, TLS와 인증 설정을 봅니다.
컨트롤 플레인 target 일부가 DOWN이다
온프레미스 배포에서는 scheduler, controller-manager, etcd의 metrics endpoint가 loopback에만 bind되거나 인증이 필요할 수 있습니다. 이 문제를 해결하려고 endpoint를 무인증으로 모든 인터페이스에 노출하지 않습니다. RKE2 버전과 구성에 맞는 안전한 endpoint, 인증서, Service/Endpoint 설정을 검토하고 접근을 Prometheus로 제한합니다.
Loki 데이터소스가 연결되지 않는다
kubectl -n logging get service loki-gateway
kubectl -n logging get endpointslice \
-l kubernetes.io/service-name=loki-gateway
kubectl -n logging logs statefulset/loki --since=10mGrafana와 같은 namespace의 임시 Pod에서 DNS와 Loki API 응답을 확인할 수도 있습니다. 디버그 이미지를 운영 Pod에 임의 설치하기보다 허용된 임시 Pod를 사용하고 작업 후 삭제합니다.
kubectl -n monitoring run network-check \
--image=curlimages/curl:8.14.1 \
--restart=Never \
--rm -i \
-- curl --fail \
http://loki-gateway.logging.svc.cluster.local/loki/api/v1/status/buildinfoFluent Bit은 Running인데 로그가 없다
kubectl -n logging logs daemonset/fluent-bit-collector --since=10m
kubectl -n observability-lab logs deployment/telemetry-demo --tail=5앱 stdout에 로그가 있는지 먼저 확인합니다. 그다음 Fluent Bit 로그에서 Loki HTTP status, DNS, buffer 오류를 확인합니다. Container runtime의 로그 경로가 기본 /var/log와 다르면 Collector의 host volume과 input을 환경에 맞춰야 합니다.
Fluent Bit 재시작 뒤 중복 또는 누락이 크다
Collector가 tail offset을 기록할 writable storage를 갖는지 확인합니다.
kubectl -n logging get daemonset fluent-bit-collector -o yaml \
| grep -A8 -B3 '/var/fluent-bit/data'노드의 hostPath buffer는 같은 노드의 Pod 재시작에는 도움이 되지만 노드 자체 유실을 복구하는 중앙 queue는 아닙니다. 유실 허용치가 낮다면 aggregator 또는 durable message queue를 포함한 아키텍처를 별도로 설계합니다.
Loki 질의가 느리거나 series 제한 오류가 난다
label cardinality와 질의 범위를 확인합니다. 시간 범위를 줄이고 namespace, service 같은 선택적인 label로 먼저 stream을 좁힙니다. 모든 Pod label 자동 수집, request ID, URL, 사용자 ID를 label로 만든 구성이 없는지 확인합니다.
PVC가 Pending이다
kubectl -n monitoring get pvc
kubectl -n logging get pvc
kubectl describe storageclass nfs-csi
kubectl get events --all-namespaces --sort-by='.lastTimestamp' \
| tail -n 50StorageClass 이름, provisioner, NFS 서버 접근, quota를 확인합니다. PVC를 삭제하면 데이터가 사라질 수 있으므로, 재생성 여부는 장애 원인을 파악한 뒤 판단합니다. 다만 Prometheus PVC가 NFS에서 Bound가 되었다는 사실은 지원 가능성이나 TSDB 안전성을 증명하지 않습니다.
운영과 보안으로 확장하기
단일 replica 실습과 HA를 구분한다
이 글의 Prometheus, Alertmanager, Loki는 한 replica입니다. 운영에서는 장애 목표에 따라 Prometheus replica와 anti-affinity, Alertmanager cluster, Loki object storage와 다중 replica를 설계합니다. replica만 늘리고 같은 노드나 하나의 NFS 서버에 모두 배치하면 장애 도메인은 그대로입니다.
Prometheus replica마다 별도 TSDB가 생긴다는 점도 이해해야 합니다. 장기 보존과 멀티클러스터 전역 질의·중복 제거에는 별도 계층을 검토합니다.
Prometheus local TSDB를 NFS에 두지 않는다
운영 Prometheus의 짧은 기간 local TSDB는 노드의 local persistent volume 또는 신뢰할 수 있는 block storage를 ext4/XFS 같은 POSIX 파일시스템으로 마운트해 사용합니다. NFS와 EFS는 Prometheus local TSDB의 지원 저장소가 아니며, 공유 PVC 하나를 여러 replica가 함께 쓰는 구조도 만들지 않습니다. 각 replica는 독립된 TSDB와 WAL을 가져야 합니다.
노드 장애를 견디기 위해서는 서로 다른 장애 도메인에 Prometheus replica를 배치하고 각각 독립된 local/block volume을 줍니다. 더 긴 보존 기간과 중앙 질의가 필요하면, local TSDB를 네트워크 파일시스템으로 옮기기보다 remote_write를 수신하는 장기 저장 계층을 추가합니다. remote_write는 짧은 기간 local TSDB와 WAL을 없애는 설정이 아니라 장기 보존·통합을 보완하는 별도 경로이므로, 수신 지연·queue backlog·재시도·중복 제거·원격 계층 장애 시 동작을 함께 검증합니다.
파일시스템 Loki는 교육용으로 제한한다
단일 replica filesystem은 구조를 학습하기 쉽지만 수평 확장과 노드 장애에 취약합니다. 운영에서는 공식 지원 object storage와 TSDB schema를 사용하고, ingest·query 용량, retention, compactor, 테넌트 격리, 백업·복구를 검증합니다. 차트에 내장된 object storage 하위 차트를 장기 운영의 기본값으로 가정하지 않습니다.
수집 endpoint의 접근 권한을 제한한다
Prometheus, Alertmanager, Loki API는 내부 상태와 로그를 노출하고 설정에 따라 관리 기능도 제공합니다. 인터넷에 직접 공개하지 않습니다. Grafana는 SSO와 조직 역할을 적용하고, 내부 API 접근은 NetworkPolicy와 인증 프록시 또는 mTLS를 요구사항에 맞춰 구성합니다.
알림 수신자 비밀값을 Git에 넣지 않는다
Webhook URL, SMTP password, API token은 ${ALERT_WEBHOOK_TOKEN} 같은 Secret에서 주입합니다. AlertmanagerConfig를 팀에 위임할 때 허용 receiver와 Secret 참조 범위를 admission policy로 제한합니다. 알림 본문에도 토큰, 사용자 개인정보, 전체 로그 원문을 넣지 않습니다.
retention을 시간과 용량 두 축으로 관리한다
Prometheus에는 retention과 retentionSize, Loki에는 retention과 object storage lifecycle이 있습니다. 법적 보존, 장애 분석, 비용을 구분해 데이터 종류별 기간을 정합니다. PVC는 백업이 아닙니다. Grafana 대시보드와 알림 규칙은 Git으로 관리하고, 필요한 장기 데이터는 별도 저장·복구 전략을 둡니다.
관측 스택 자체에 알림을 만든다
다음 상태는 서비스 알림만큼 중요합니다.
- Prometheus target scrape 실패와 rule evaluation 실패
- Prometheus TSDB disk 여유와 compaction 오류
- Fluent Bit output retry, dropped record, buffer disk 사용량
- Loki ingest 4xx/5xx, query 오류, object storage 실패
- Alertmanager notification 실패
수집이 끊겼는데 대시보드가 조용한 상황을 정상으로 오해하지 않도록 absent()와 pipeline health 지표를 활용합니다.
차트 업그레이드는 migration 작업이다
버전을 고정하고 다음 순서를 따릅니다.
- release note와 major migration guide 검토
helm template결과를 Git diff로 확인- CRD 변경을 별도 검토하고 server-side dry-run
- staging에서 기존 PVC와 데이터 질의 검증
- 롤백 시 chart뿐 아니라 데이터 schema가 호환되는지 확인
특히 Helm은 기존 CRD를 일반 리소스처럼 자동 업그레이드하지 않는다는 제약이 있습니다. kube-prometheus-stack의 CRD 업그레이드 절차를 해당 버전 공식 문서에서 확인합니다.
요약
메트릭 수집은 kube-prometheus-stack이 제공하는 Prometheus Operator, Prometheus, Alertmanager, Grafana, exporter와 기본 규칙에서 시작합니다. 여기에 애플리케이션을 연결하려면 ServiceMonitor의 Service label·port name·Namespace selector가 모두 맞아야 합니다. target이 생성됐는지 확인하는 단계에서 이 연결을 검증합니다.
저장소는 수집 성공과 별도로 확인해야 합니다. 실습의 Prometheus NFS PVC는 공식 지원 구성이 아니므로, 운영 local TSDB는 local/block POSIX volume을 사용하고 장기 보존은 remote_write 계층으로 분리합니다.
로그 경로에서는 Fluent Bit Collector가 노드의 CRI 로그를 읽고, Loki가 안정적인 저카디널리티 label을 중심으로 로그를 저장·질의합니다. 2026년 현재 오픈소스 Loki 차트는 grafana-community 경로와 Monolithic 배포 모드를 사용하고, Fluent Bit은 노드 수집 전용 Collector 차트를 제공합니다. 설치할 때는 이 경로와 모드에 맞는 values를 사용합니다.
최종 확인은 Prometheus target → metric query → rule → Fluent Bit output → Loki API → Grafana LogQL까지 이어집니다. Pod Ready에 더해 각 단계에서 기대한 데이터와 상태가 나타나는지 확인하면 메트릭과 로그의 전체 수집 경로를 검증할 수 있습니다. 다만 이 실습의 단일 replica와 filesystem 저장은 교육용 구성입니다. 운영으로 확장하려면 HA, object storage, 인증, NetworkPolicy, 보존·복구, 업그레이드 전략을 함께 설계해야 합니다.
Day 2에서 인증·권한·관측성의 기반을 만들었습니다. 다음 글부터는 GitLab이 소스 변경을 어떻게 CI/CD와 GitOps 흐름으로 연결하는지 살펴봅니다.
← 이전 글: Kubernetes 관측성 지도: 메트릭·로그·트레이스를 함께 읽는 법
→ 다음 글: CI/CD에서 GitOps까지: GitLab은 어디까지 책임져야 하는가
공식 참고자료
- kube-prometheus-stack chart
- Prometheus Operator: Getting Started
- Prometheus Operator API reference
- Prometheus Storage
- Prometheus Alerting Rules
- Prometheus Alertmanager
- Grafana Loki: Install the monolithic Helm chart
- Grafana Loki Helm chart values
- Grafana Loki label cardinality
- Fluent Bit: New Collector and Aggregator Helm charts
- Fluent Bit Collector chart
- Fluent Bit Parser filter
- Fluent Bit Loki output
- Kubernetes Logging Architecture
- Gateway API HTTPRoute