Prometheus를 실무 흐름으로 이해하기
Prometheus + Grafana로 서버와 애플리케이션을 모니터링하세요. 메트릭 수집, Exporter 생태계, PromQL, Grafana 대시보드 프로비저닝, AlertManager 알림 라우팅, kube-prometheus-stack 설치·CRD 확장·운영까지 완전 가이드. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
Prometheus + Grafana로 서버와 애플리케이션을 모니터링하세요. 메트릭 수집, Exporter 생태계, PromQL, Grafana 대시보드 프로비저닝, AlertManager 알림 라우팅, kube-prometheus-stack 설치·CRD 확장·운영까지 완전 가이드.
Prometheus + Grafana로 서버와 애플리케이션을 모니터링하세요. 메트릭 수집, Exporter 생태계, PromQL, Grafana 대시보드 프로비저닝, AlertManager 알림 라우팅, kube-prometheus-stack 설치·CRD 확장·운영까지 완전 가이드. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
설치 명령을 외우기보다 트래픽, 런타임, 관측, 장애 대응이 어떤 순서로 이어지는지 파악합니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 Prometheus를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
Prometheus를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
| 신호 | 도구 | 질문 |
|---|---|---|
| Metrics (메트릭) | Prometheus, Grafana | 지금 얼마나 많은 요청이 오는가? |
| Logs (로그) | Loki, ELK | 무엇이 잘못되었는가? |
| Traces (추적) | Jaeger, Tempo | 어디서 느린가? |
여기서는 Docker Compose 설치을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
services:
prometheus:
image: prom/prometheus:latest
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./alert_rules.yml:/etc/prometheus/alert_rules.yml
- promdata:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --web.enable-lifecycle # curl -X POST 로 재시작 없이 설정 리로드
grafana:
image: grafana/grafana:latest
ports: ["3000:3000"]
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafdata:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning # 데이터소스·대시보드 as Code
alertmanager:
image: prom/alertmanager:latest
ports: ["9093:9093"]
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
node-exporter:
image: prom/node-exporter:latest
ports: ["9100:9100"]
volumes:
promdata:
grafdata:global:
scrape_interval: 15s # 기본 수집 주기
evaluation_interval: 15s # 레코딩/알림 규칙 평가 주기
rule_files:
- "alert_rules.yml" # 레코딩 규칙 + 알림 규칙
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: node
static_configs:
- targets: ["node-exporter:9100"]
- job_name: myapp
metrics_path: /metrics
static_configs:
- targets: ["myapp:8080"]
labels: { env: production, team: backend }docker compose up -d
# 수집 대상(Target) 상태 확인 — UP이어야 정상
curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health}'
# 설정 파일만 리로드 (컨테이너 재시작 없이 반영, --web.enable-lifecycle 필요)
curl -X POST localhost:9090/-/reload
# 룰 파일 문법 검증 (배포 전 CI에서 실행 권장)
docker run --rm -v $(pwd):/rules prom/prometheus:latest \
promtool check rules /rules/alert_rules.yml여기서는 Exporter 생태계 & 커스텀 메트릭을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from prometheus_client import Counter, Histogram, start_http_server
import time
# Counter — 누적 증가만 하는 값 (요청 수, 에러 수)
REQUEST_COUNT = Counter(
"http_requests_total", "Total HTTP requests",
["method", "endpoint", "status"]
)
# Histogram — 분포 측정 (응답시간 p50/p95/p99 계산용 버킷)
REQUEST_LATENCY = Histogram(
"http_request_duration_seconds", "Request latency",
["endpoint"], buckets=[0.05, 0.1, 0.3, 0.5, 1, 2, 5]
)
def handle_request(endpoint: str):
start = time.time()
try:
... # 실제 처리 로직
REQUEST_COUNT.labels("GET", endpoint, "200").inc()
finally:
REQUEST_LATENCY.labels(endpoint).observe(time.time() - start)
start_http_server(8080) # /metrics 엔드포인트 자동 노출# blackbox_exporter — 외부 관점에서 엔드포인트 가용성을 확인 (synthetic monitoring)
# blackbox.yml — exporter 자체 설정
modules:
http_2xx:
prober: http
timeout: 5s
http:
valid_status_codes: [200]
method: GET
---
# prometheus.yml에 추가할 scrape job — 실제 타깃은 params.target으로 전달
scrape_configs:
- job_name: blackbox
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://api.example.com/health
- https://example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115 # 실제 프로브를 수행하는 exporter 주소로 리라우팅| Exporter | 수집 대상 | 기본 포트 |
|---|---|---|
| node_exporter | 호스트 CPU · 메모리 · 디스크 · 네트워크 | 9100 |
| cAdvisor | 컨테이너별 리소스 사용량 (Docker/K8s) | 8080 |
| blackbox_exporter | HTTP/TCP/ICMP 프로브 — 엔드포인트 가용성·응답시간 | 9115 |
| mysqld_exporter / postgres_exporter | DB 커넥션·슬로우쿼리·복제 지연 | 9104 / 9187 |
| redis_exporter | Redis 메모리·커맨드 처리량·hit rate | 9121 |
| kube-state-metrics | K8s 오브젝트 상태 (Pod/Deployment/PVC 등) | 8080 |
여기서는 헬스체크 vs 메트릭 — Probe와 Actuator을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
management:
endpoints:
web:
exposure:
include: health, prometheus, metrics # 필요한 엔드포인트만 명시적으로 노출
endpoint:
health:
probes:
enabled: true # /actuator/health/liveness, /actuator/health/readiness를 K8s용으로 분리
prometheus:
metrics:
export:
enabled: true # micrometer 메트릭을 Prometheus 형식(/actuator/prometheus)으로 노출spec:
containers:
- name: myapp
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 10
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
periodSeconds: 5
---
# prometheus.yml (또는 ServiceMonitor)는 /actuator/prometheus를 별도로 scrape
scrape_configs:
- job_name: myapp
metrics_path: /actuator/prometheus
static_configs:
- targets: ["myapp:8080"]# 최근 15분간 재시작이 3회 이상 발생한 컨테이너 — liveness probe 실패가 반복되고 있다는 신호
increase(kube_pod_container_status_restarts_total[15m]) >= 3| 항목 | Liveness/Readiness Probe | Prometheus 메트릭 |
|---|---|---|
| 호출 주체 | kubelet (K8s 내부) | Prometheus 서버 (외부에서 주기적 scrape) |
| 값의 형태 | 이진 — 성공/실패(UP/DOWN) | 시계열 수치 — 추세·비율·분포 계산 가능 |
| 실패 시 결과 | 컨테이너 재시작 또는 Service에서 즉시 제외 | 즉각적인 조치 없음 — 규칙에 따라 Alertmanager가 알림 |
| 목적 | "지금 이 순간 이 인스턴스가 정상인가" | "시간에 따라 시스템이 어떻게 변하고 있는가" |
여기서는 PromQL 기초 & 레코딩 규칙을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# 초당 HTTP 요청 수 (5분 평균)
rate(http_requests_total[5m])
# 에러율 (4xx + 5xx)
sum(rate(http_requests_total{status=~"4..|5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
# 엔드포인트별 p95 응답시간
histogram_quantile(0.95, sum by (le, endpoint) (rate(http_request_duration_seconds_bucket[5m])))
# CPU 사용률
100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# 최근 1시간 내 재시작한 Pod (K8s)
increase(kube_pod_container_status_restarts_total[1h]) > 0groups:
- name: myapp.recording-rules
interval: 30s
rules:
# 자주 조회하는 무거운 쿼리를 미리 계산해 job:metric:operation 이름 규칙으로 저장
- record: job:http_requests:rate5m
expr: sum by (job) (rate(http_requests_total[5m]))
- record: job:http_errors:ratio5m
expr: |
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/ sum by (job) (rate(http_requests_total[5m]))
- record: job:http_request_duration:p95_5m
expr: histogram_quantile(0.95, sum by (job, le) (rate(http_request_duration_seconds_bucket[5m])))| 함수/연산 | 용도 | 주의점 |
|---|---|---|
| rate(v[5m]) | 5분 구간의 초당 평균 증가율 | counter 전용. 카운터 리셋을 자동 보정 |
| irate(v[5m]) | 가장 최근 두 샘플만으로 순간 증가율 계산 | 그래프 노이즈가 큼 — 대시보드보다 알림 임계값에는 부적합 |
| histogram_quantile() | histogram 버킷으로 백분위수(p95/p99) 근사 계산 | buckets 경계값 설계가 정확도를 좌우 |
| sum by (label)(...) | 특정 라벨 기준으로만 그룹핑해 합산 | by와 without을 헷갈리면 카디널리티 폭발 위험 |
여기서는 Grafana 연동을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: 15s # global.scrape_interval과 맞춰두면 그래프가 자연스러움
httpMethod: POSTapiVersion: 1
providers:
- name: default
folder: "Services"
type: file
updateIntervalSeconds: 30 # 파일 변경을 주기적으로 감지해 자동 리로드
options:
path: /etc/grafana/provisioning/dashboards/files
foldersFromFilesStructure: true{
"title": "MyApp - RED Metrics",
"templating": {
"list": [
{ "name": "job", "type": "query", "query": "label_values(http_requests_total, job)" }
]
},
"panels": [
{
"title": "Request Rate",
"type": "timeseries",
"targets": [
{ "expr": "sum(rate(http_requests_total{job=\"$job\"}[5m]))", "legendFormat": "req/s" }
]
},
{
"title": "Error Ratio",
"type": "timeseries",
"targets": [
{ "expr": "job:http_errors:ratio5m{job=\"$job\"}", "legendFormat": "error %" }
],
"fieldConfig": { "defaults": { "unit": "percentunit", "thresholds": { "steps": [
{ "color": "green", "value": 0 }, { "color": "red", "value": 0.05 }
]}}}
},
{
"title": "P95 Duration",
"type": "timeseries",
"targets": [
{ "expr": "job:http_request_duration:p95_5m{job=\"$job\"}", "legendFormat": "p95" }
]
}
]
}# 대시보드가 정상 로드됐는지 API로 확인
curl -s -u admin:admin localhost:3000/api/search?query=RED | jq '.[].title'
# 대시보드 JSON을 코드로 export (버전 관리에 커밋)
curl -s -u admin:admin localhost:3000/api/dashboards/uid/<uid> | jq '.dashboard' > myapp-red-metrics.json# 클러스터가 여러 개면, 각 Prometheus가 자기 자신을 어느 클러스터인지 스스로 표시해야
# 나중에 Grafana나 중앙 저장소에서 클러스터별로 구분·필터링할 수 있습니다.
global:
external_labels:
cluster: prod-seoul # 클러스터마다 고유한 이름 (prod-seoul, prod-tokyo, staging ...)
region: ap-northeast-2# 방식 A — 클러스터별로 Prometheus 데이터소스를 따로 등록
# 대시보드 상단의 데이터소스 드롭다운으로 클러스터를 전환하는 구조
apiVersion: 1
datasources:
- name: Prometheus-Seoul
uid: prom-seoul
type: prometheus
access: proxy
url: http://prometheus-seoul.monitoring.svc:9090
jsonData: { timeInterval: 15s }
- name: Prometheus-Tokyo
uid: prom-tokyo
type: prometheus
access: proxy
url: http://prometheus-tokyo.monitoring.svc:9090
jsonData: { timeInterval: 15s }{
"templating": {
"list": [
{
"name": "datasource",
"type": "datasource",
"query": "prometheus",
"current": { "text": "Prometheus-Seoul", "value": "prom-seoul" }
},
{
"name": "cluster",
"type": "query",
"datasource": "${datasource}",
"query": "label_values(up, cluster)",
"refresh": 2
}
]
},
"panels": [
{
"title": "Cluster Node Count",
"type": "stat",
"datasource": "${datasource}",
"targets": [
{ "expr": "count(up{job=\"node\", cluster=\"$cluster\"} == 1)" }
]
}
]
}# kube-prometheus-stack — grafana.com의 검증된 클러스터 대시보드를 gnetId로 자동 로드
# (Kubernetes 통합 섹션의 helm install -f values.yaml 에 추가)
grafana:
dashboardProviders:
dashboardproviders.yaml:
apiVersion: 1
providers:
- name: community
orgId: 1
folder: "Community"
type: file
options:
path: /var/lib/grafana/dashboards/community
dashboards:
community:
k8s-cluster-monitoring:
gnetId: 315 # Kubernetes cluster monitoring (via Prometheus) — 클러스터 전체 개요
revision: 3
datasource: Prometheus
node-exporter-full:
gnetId: 1860 # Node Exporter Full — 노드별 CPU/메모리/디스크/네트워크 상세
revision: 37
datasource: Prometheus
# revision은 grafana.com/grafana/dashboards/<gnetId> 페이지에서 최신 값 확인 후 갱신# Docker Compose 환경 등 UI로 직접 임포트할 때
# Grafana → Dashboards → New → Import → ID 입력 (315 또는 1860) → 데이터소스 선택
# 이미 설치된 커뮤니티 대시보드를 API로 확인
curl -s -u admin:admin localhost:3000/api/search?query=Kubernetes | jq '.[].title'
# 클러스터 목록 확인 (external_labels.cluster가 붙은 모든 클러스터)
curl -s -u admin:admin --data-urlencode 'query=label_values(up, cluster)' \
localhost:3000/api/datasources/proxy/uid/prom-seoul/api/v1/query| 구성 요소 | 역할 | 파일 위치 (기본값) |
|---|---|---|
| Data source provisioning | Prometheus 등 데이터소스를 UI 클릭 없이 자동 등록 | /etc/grafana/provisioning/datasources/*.yml |
| Dashboard provisioning | 대시보드 JSON을 폴더 기준으로 자동 로드 | /etc/grafana/provisioning/dashboards/*.yml |
| Template variable | $job, $instance 등으로 대시보드를 파라미터화 | 대시보드 JSON의 templating.list |
| Folder / 권한 | 팀·서비스별 대시보드 폴더 분리, RBAC으로 편집 권한 제한 | Grafana 조직 설정 |
| Multi-cluster datasource | 클러스터별 Prometheus를 별도 데이터소스로 등록해 상단 드롭다운으로 전환 | provisioning/datasources/*.yml |
| Community 대시보드 임포트 | 검증된 커뮤니티 대시보드를 grafana.com ID로 즉시 가져오기 | Dashboards → New → Import (또는 gnetId 프로비저닝) |
여기서는 AlertManager 알림을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
groups:
- name: myapp.alerts
rules:
- alert: HighErrorRate
expr: job:http_errors:ratio5m > 0.05
for: 5m # 5분 이상 지속돼야 발화 — 순간 스파이크로 인한 오탐 방지
labels:
severity: critical
annotations:
summary: "{{ $labels.job }} 에러율 {{ $value | humanizePercentage }}"
runbook_url: "https://wiki.internal/runbooks/high-error-rate"
- alert: HighMemoryUsage
expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} 메모리 사용률 90% 초과"
- alert: PodCrashLooping
expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.namespace }}/{{ $labels.pod }} 반복 재시작 중"route:
receiver: default-webhook
group_by: [alertname, job]
group_wait: 30s # 그룹 생성 후 30초 동안 관련 알림을 더 모아서 발송
group_interval: 5m # 같은 그룹에 새 알림이 추가됐을 때 재발송 최소 간격
repeat_interval: 4h # 미해결 알림 재발송 주기
routes:
- matchers: ["severity = critical"]
receiver: slack-critical
continue: false
- matchers: ["severity = warning"]
receiver: slack-warning
inhibit_rules:
# critical 알림이 켜져 있으면 같은 job의 warning 알림은 노이즈이므로 억제
- source_matchers: ["severity = critical"]
target_matchers: ["severity = warning"]
equal: [job]
receivers:
- name: default-webhook
webhook_configs:
- url: http://ops-bot:9000/alert
- name: slack-critical
slack_configs:
- api_url: "${SLACK_WEBHOOK_URL}"
channel: "#alerts-critical"
title: "{{ .CommonAnnotations.summary }}"
send_resolved: true
- name: slack-warning
slack_configs:
- api_url: "${SLACK_WEBHOOK_URL}"
channel: "#alerts-warning"
send_resolved: true# 설정 문법 검증 (배포 전 CI에서 실행)
amtool check-config alertmanager.yml
# 현재 활성화된 알림 목록 확인
amtool alert query --alertmanager.url=http://localhost:9093
# 점검 시간 동안 특정 알림 음소거 (2시간)
amtool silence add alertname=HighMemoryUsage instance=web-01 \
--alertmanager.url=http://localhost:9093 \
--duration=2h --comment="정기 점검"
# 활성 silence 목록 확인 / 조기 해제
amtool silence query --alertmanager.url=http://localhost:9093
amtool silence expire <silence-id> --alertmanager.url=http://localhost:9093| 개념 | 역할 |
|---|---|
| route | 알림 라벨을 기준으로 어느 receiver로 보낼지 결정하는 트리 구조 |
| receiver | Slack, Email, PagerDuty, Webhook 등 실제 알림을 받는 채널 |
| group_by / group_wait | 같은 그룹의 알림을 모아 한 번에 발송 — 알림 폭주(alert storm) 방지 |
| repeat_interval | 해결되지 않은 알림을 재발송하는 주기 |
| inhibit_rule | 심각한 알림(critical)이 활성화되면 관련된 하위 알림(warning)을 억제 |
| silence | 점검·배포 등 예상된 상황에서 특정 알림을 일정 기간 수동으로 음소거 |
여기서는 CRD(Custom Resource Definition)란?을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# kube-prometheus-stack이 실제로 등록한 CRD 목록 확인
kubectl get crd | grep monitoring.coreos.com
# alertmanagerconfigs.monitoring.coreos.com
# alertmanagers.monitoring.coreos.com
# podmonitors.monitoring.coreos.com
# probes.monitoring.coreos.com
# prometheuses.monitoring.coreos.com
# prometheusrules.monitoring.coreos.com
# servicemonitors.monitoring.coreos.com
# thanosrulers.monitoring.coreos.com
# CRD가 정의한 스키마(필드 구조)를 kubectl explain으로 그대로 조회 가능 — Pod 보듯 동일하게 동작
kubectl explain servicemonitor.spec
kubectl explain servicemonitor.spec.endpoints
# CRD 하나를 자세히 보면 group/version/kind와 openAPIV3Schema(필드 검증 규칙)로 구성돼 있음
kubectl get crd servicemonitors.monitoring.coreos.com -o yaml | head -30
# 이 CRD를 만든 Controller(Prometheus Operator)가 실제로 떠 있는지 확인
# — Controller가 없으면 CR을 apply해도 etcd에 저장만 될 뿐 아무 일도 일어나지 않음
kubectl get deploy -n monitoring -l app=kube-prometheus-stack-operator# 개념 이해를 위한 최소 예시 — "Website"라는 가상의 리소스 종류를 새로 등록
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: websites.example.com # <복수형>.<group>
spec:
group: example.com
scope: Namespaced
names:
plural: websites
singular: website
kind: Website # 이제부터 kubectl get website 가 가능해짐
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema: # CR이 반드시 지켜야 할 필드 구조 (spec.url은 string 필수)
type: object
properties:
spec:
type: object
required: ["url"]
properties:
url:
type: string
---
# 위 CRD를 apply한 뒤에는 아래처럼 CR 인스턴스를 만들 수 있음
apiVersion: example.com/v1
kind: Website
metadata:
name: aidevops
spec:
url: https://www.aidevops.kr
# 주의: 이 CR을 apply해도 실제로 웹사이트를 배포/점검하는 동작은 없음.
# 그 동작을 수행하려면 Website CR을 watch하는 Controller를 별도로 만들어 배포해야 함
# (kube-prometheus-stack에서는 이 Controller 역할을 Prometheus Operator가 대신 해줌)| 용어 | 의미 |
|---|---|
| CRD (Custom Resource Definition) | 새 리소스 종류(kind)를 K8s API에 등록하는 스키마 정의 그 자체 — "명세서" |
| CR (Custom Resource) | CRD가 정의한 스키마를 따르는 실제 인스턴스 — kubectl apply한 YAML 한 건 한 건 |
| Controller | CR의 생성·변경·삭제를 watch하며 원하는 상태(desired state)로 맞춰주는 프로그램 |
| Operator | CRD + 전용 Controller를 묶어 특정 소프트웨어의 운영 지식을 자동화한 것 (예: Prometheus Operator) |
| 내장 리소스와의 차이 | Pod/Service는 K8s 코어에 하드코딩된 리소스, CR은 누구나 추가할 수 있는 "플러그인 리소스" |
여기서는 설치 전 준비 — 사전 요구사항 & 클러스터 사이징을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# 1) 클러스터·Helm 버전 확인 — kube-prometheus-stack 최신 버전은 Helm 3.8+ 요구
kubectl version --short
helm version
# 2) StorageClass가 실제로 존재하는지 확인 (values.yaml의 storageClassName과 반드시 일치해야 함)
kubectl get storageclass
# NAME (default) PROVISIONER ...
# fast-ssd ebs.csi.aws.com ... ← 이 이름을 values.yaml에 그대로 사용
# 3) 노드 자원 여유 확인 — Prometheus 하나만으로도 최소 500m CPU / 2Gi 메모리를 선점
kubectl top nodes
kubectl describe nodes | grep -A5 "Allocated resources"
# 4) monitoring 네임스페이스에 리소스 상한이 걸려 있지 않은지 확인
kubectl get resourcequota -n monitoring 2>/dev/null || echo "쿼터 없음 (정상)"
# 5) CRD를 kubectl apply -f로 직접 적용하면 흔히 나는 오류 —
# "metadata.annotations: Too long: must have at most 262144 bytes"
# kube-prometheus-stack의 CRD는 용량이 커서 kubectl apply의 last-applied-configuration
# annotation 저장 한도를 초과합니다. helm install은 이 문제를 우회해 CRD를 등록하므로,
# 부득이 수동으로 적용해야 한다면 반드시 --server-side를 사용하세요.
kubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/crds/crd-prometheusrules.yamlapiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kube-prometheus-stack
namespace: argocd
spec:
project: default
source:
repoURL: https://prometheus-community.github.io/helm-charts
chart: kube-prometheus-stack
targetRevision: 65.5.1 # 버전을 반드시 고정 — Chart.yaml appVersion과 별개로 관리됨
helm:
valueFiles: ["values-production.yaml"] # 같은 Git 저장소의 values 파일 참조
destination:
server: https://kubernetes.default.svc
namespace: monitoring
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: ["CreateNamespace=true"]| 클러스터 규모 | Prometheus 리소스 권장값 | retention | 비고 |
|---|---|---|---|
| 소규모 (~20 노드, ServiceMonitor 수십 개) | requests: 500m CPU / 2Gi | 15d | 기본값에서 크게 벗어나지 않아도 무방 |
| 중규모 (20~100 노드) | requests: 1~2 CPU / 4~8Gi | 30d | retentionSize를 함께 설정해 볼륨 초과를 사전 방지 |
| 대규모 (100+ 노드, 카디널리티 높음) | requests: 4 CPU+ / 16Gi+ | 15d + remoteWrite | 단일 Prometheus 한계에 근접 — Thanos/Mimir로 샤딩·장기 보관 분리 검토 |
여기서는 Kubernetes 통합 — kube-prometheus-stack을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# 1) Helm repo 등록
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# 2) 버전 확인 — 프로덕션은 반드시 버전을 고정해서 설치 (latest 금지)
helm search repo prometheus-community/kube-prometheus-stack --versions | head -5
# 3) 설치 (CRD는 차트에 포함되어 함께 설치됨)
helm install kube-prom prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--version 65.5.1 \
-f values.yaml
# 4) 설치 확인 — Prometheus/Alertmanager/Grafana/Operator/node-exporter/kube-state-metrics 모두 Running
kubectl get pods -n monitoring
kubectl get prometheus,alertmanager -n monitoring # Operator가 생성한 CR 상태 확인
# 5) Grafana 접속 (admin 계정 초기 비밀번호)
kubectl get secret kube-prom-grafana -n monitoring \
-o jsonpath="{.data.admin-password}" | base64 -d; echo
kubectl port-forward -n monitoring svc/kube-prom-grafana 3000:80prometheus:
prometheusSpec:
retention: 30d # 로컬 TSDB 보관 기간
retentionSize: "45GB" # 볼륨 대비 여유를 두고 용량 상한도 함께 설정
resources:
requests: { cpu: 500m, memory: 2Gi }
limits: { memory: 4Gi } # CPU limit은 스로틀링 유발 — 보통 미설정 권장
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: fast-ssd
resources: { requests: { storage: 50Gi } }
# false로 설정하면 release 라벨과 무관하게 클러스터 전체 ServiceMonitor/PodMonitor/Probe/Rule을 인식
serviceMonitorSelectorNilUsesHelmValues: false
podMonitorSelectorNilUsesHelmValues: false
probeSelectorNilUsesHelmValues: false
ruleSelectorNilUsesHelmValues: false
additionalScrapeConfigs: [] # ServiceMonitor로 커버 안 되는 정적 타깃 추가 시 사용
# remoteWrite: # 장기 보관이 필요하면 Thanos/Mimir/Grafana Cloud 등으로 원격 저장
# - url: https://mimir.example.com/api/v1/push
# 관리형 K8s(EKS/GKE/AKS)는 컨트롤 플레인 메트릭에 접근 불가 — 기본 Off 권장 (Pending Target 방지)
kubeControllerManager: { enabled: false }
kubeScheduler: { enabled: false }
kubeEtcd: { enabled: false }
kubeProxy: { enabled: false }
alertmanager:
alertmanagerSpec:
resources:
requests: { cpu: 100m, memory: 256Mi }
storage:
volumeClaimTemplate:
spec:
resources: { requests: { storage: 5Gi } }
grafana:
admin:
existingSecret: grafana-admin-credentials # 평문 비밀번호 대신 별도 Secret 참조 권장
persistence: { enabled: true, size: 10Gi }
sidecar:
dashboards: { enabled: true, label: grafana_dashboard } # ConfigMap 라벨 기반 대시보드 자동 로드
datasources: { enabled: true }
ingress:
enabled: true
ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts: ["grafana.aidevops.kr"]
tls:
- secretName: grafana-tls
hosts: ["grafana.aidevops.kr"]
defaultRules:
rules:
etcd: false # 컨트롤 플레인 규칙도 관리형 K8s에서는 대부분 비활성화 대상
kubeScheduler: false| 구성 요소 | 종류 | 역할 |
|---|---|---|
| Prometheus Operator | Deployment | CRD를 감시해 Prometheus/Alertmanager StatefulSet과 설정을 자동 생성·갱신 |
| Prometheus | StatefulSet (Operator가 생성) | 메트릭 저장 엔진 — 직접 만들지 않고 Prometheus CRD로 선언 |
| Alertmanager | StatefulSet (Operator가 생성) | 알림 그룹핑·중복 제거·라우팅 — Alertmanager/AlertmanagerConfig CRD로 선언 |
| Grafana | Deployment | 대시보드 시각화. sidecar 컨테이너가 라벨 붙은 ConfigMap을 감지해 자동 로드 |
| node-exporter | DaemonSet | 모든 노드의 CPU/메모리/디스크/네트워크 OS 메트릭 노출 |
| kube-state-metrics | Deployment | Pod/Deployment/PVC 등 K8s API 오브젝트 상태를 메트릭으로 변환 |
| kubelet / cAdvisor | 기존 kubelet 내장 | 컨테이너 리소스 사용량 — 차트가 기본 scrape 설정을 자동 구성 |
| 기본 PrometheusRule/대시보드 | CRD + ConfigMap | kube-apiserver, etcd, kubelet 등에 대한 검증된 알림·대시보드가 기본 포함 |
여기서는 CRD로 수집·알림 확장 — ServiceMonitor · PodMonitor · Probe을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# Operator가 이 리소스를 보고 자동으로 myapp을 scrape 대상에 추가
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: myapp
namespace: production
labels:
release: kube-prom # serviceMonitorSelectorNilUsesHelmValues: true일 때만 필요
spec:
selector:
matchLabels:
app: myapp # 대상 Service의 라벨
endpoints:
- port: metrics # 대상 Service의 포트 이름
path: /metrics
interval: 15s# 안정적인 Service 없이 Pod를 직접 스크랩 (예: Job/배치성 워크로드, 사이드카 메트릭)
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: myapp-batch
namespace: production
spec:
selector:
matchLabels:
app: myapp-batch
podMetricsEndpoints:
- port: metrics
path: /metrics
interval: 30s# 외부/내부 엔드포인트 가용성 원격 점검 — blackbox-exporter 별도 설치 필요
# helm install blackbox-exporter prometheus-community/prometheus-blackbox-exporter -n monitoring
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: aidevops-uptime
namespace: monitoring
labels:
release: kube-prom
spec:
jobName: blackbox-http
interval: 30s
module: http_2xx
prober:
url: blackbox-exporter-prometheus-blackbox-exporter.monitoring.svc:19115
targets:
staticConfig:
static:
- https://www.aidevops.kr
- https://www.aidevops.kr/learn/prometheus/# alert_rules.yml을 수동 마운트하는 대신 CRD로 선언 — GitOps로 관리 가능
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: myapp-alerts
namespace: production
labels:
release: kube-prom
spec:
groups:
- name: myapp.recording
rules:
- record: job:http_errors:ratio5m
expr: sum(rate(http_requests_total{status=~"5.."}[5m])) by (job) / sum(rate(http_requests_total[5m])) by (job)
- name: myapp.alerts
rules:
- alert: HighErrorRate
expr: job:http_errors:ratio5m{namespace="production"} > 0.05
for: 5m
labels: { severity: critical }
annotations:
summary: "{{ $labels.job }} 에러율 5% 초과"
- alert: ProbeFailing
expr: probe_success{job="blackbox-http"} == 0
for: 5m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }} 헬스체크 실패"# grafana_dashboard 라벨을 붙이면 sidecar가 클러스터 전체 네임스페이스에서 자동 감지해 로드
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-dashboard
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
myapp-red-metrics.json: |
{ "title": "MyApp - RED Metrics", "panels": [] }여기서는 검증 · 업그레이드 · 트러블슈팅을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# Target 상태 확인 (모두 up이어야 정상)
kubectl port-forward -n monitoring svc/kube-prom-kube-prome-prometheus 9090:9090 &
curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health}'
# Operator가 생성한 Prometheus CR이 정상 반영됐는지 확인
kubectl get prometheus -n monitoring -o jsonpath='{.items[0].status.conditions}' | jq
# 어떤 ServiceMonitor/PodMonitor/Rule을 Operator가 실제로 인식했는지 확인
kubectl get servicemonitors,podmonitors,prometheusrules -A
# PrometheusRule 문법 검증 (배포 전 CI에서 실행 권장)
kubectl get prometheusrule myapp-alerts -n production -o jsonpath='{.spec}' > /tmp/rule.yaml
promtool check rules /tmp/rule.yaml# 업그레이드 — values.yaml 변경 후 재적용
helm upgrade kube-prom prometheus-community/kube-prometheus-stack \
--namespace monitoring -f values.yaml
# 특정 버전으로 업그레이드 시 CRD 변경 여부를 먼저 확인 (major 버전 업은 CRD 스키마가 바뀔 수 있음)
helm show crds prometheus-community/kube-prometheus-stack --version 65.5.1 | grep "kind: CustomResourceDefinition" -A2
# 완전 제거 — Helm은 CRD를 지우지 않으므로 재설치 충돌 방지를 위해 수동 삭제 필요
helm uninstall kube-prom --namespace monitoring
kubectl delete crd -l app.kubernetes.io/part-of=kube-prometheus-stack| 증상 | 원인 | 해결 |
|---|---|---|
| ServiceMonitor를 만들었는데 Target에 안 뜸 | label이 selector와 불일치하거나 selectorNilUsesHelmValues가 true | ServiceMonitor에 release 라벨 추가 또는 values.yaml에서 false로 전환 |
| kube-controller-manager/kube-scheduler Target이 계속 down | 관리형 K8s는 컨트롤 플레인 미노출 | values.yaml에서 kubeControllerManager/kubeScheduler.enabled: false |
| PVC가 Pending에서 멈춤 | 지정한 storageClassName이 클러스터에 없음 | kubectl get storageclass로 실제 이름 확인 후 values.yaml 수정 |
| Grafana sidecar가 대시보드를 못 찾음 | ConfigMap 라벨이 grafana.sidecar.dashboards.label과 불일치 | 라벨 값(기본 grafana_dashboard)과 네임스페이스 감시 범위 확인 |
| helm uninstall 후 재설치가 실패함 | 이전 설치의 CRD/PVC가 잔존 | kubectl delete crd, kubectl delete pvc -n monitoring 로 정리 후 재설치 |
Prometheus 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | Prometheus 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 Prometheus 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
Prometheus 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |