외부 Ceph 클러스터 지표를 Prometheus로 수집하기 — 정적 scrape vs Rook ServiceMonitor
쿠버네티스 밖 베어메탈에 따로 구축한 Ceph 클러스터의 헬스·OSD·용량 지표를, 이미 운영 중인 Prometheus에 시계열로 쌓는 작업을 정리한다. 식별 정보는 문서용 대역과 가명으로 바꿨다 — 스토리지 노드 node-s1/s2/s3 = 192.0.2.11/12/13, Ceph public network 192.0.2.0/24, OSD 복제망 198.51.100.0/24, 관리 노드 node-a1, SSH 계정 user.
대상: Ceph를 Rook external 모드로 쿠버네티스에 붙여 쓰면서 모니터링 스택은 이미 갖고 있는 운영자
경로는 두 가지다. 하나는 Ceph 매니저가 노출하는 지표를 Prometheus의 정적 수집 대상으로 직접 등록하는 방법이고, 다른 하나는 Rook에게 외부 매니저 주소를 알려주고 ServiceMonitor를 만들게 하는 방법이다. 지표를 노출하는 쪽은 두 방법이 같고, 그 지표에 Prometheus를 연결하는 쪽만 다르다.
graph LR
subgraph BM["베어메탈 Ceph (쿠버네티스 밖)"]
MGR["active mgr
prometheus 모듈
:9283/metrics"]
SB["standby mgr
:9283 → 503"]
end
subgraph K8S["쿠버네티스 (monitoring)"]
A["방법 A
additionalScrapeConfigs
static_configs"]
B["방법 B
rook-ceph-mgr-external
Service + ServiceMonitor"]
P["Prometheus"]
end
MGR --> A
SB --> A
MGR --> B
SB --> B
A --> P
B --> P
style MGR fill:#e8f5e9,stroke:#2e7d32
style SB fill:#eceff1,stroke:#607d8b
style A fill:#fff3e0,stroke:#e65100
style B fill:#e3f2fd,stroke:#1565c0
style P fill:#fce4ec,stroke:#c62828
되돌리기 어려운 작업은 없다. 매니저 모듈 활성화와 수집 설정 추가뿐이고, [08]에 원복 명령을 적었다. 다만 9283 포트를 스토리지망에 여는 일이므로 방화벽 정책은 미리 확인한다.
[00] 용어 세 개
[00-1] mgr (Ceph Manager)
Ceph 클러스터의 관리 데몬이다. mon이 클러스터 상태 합의를 맡는다면, mgr는 그 상태를 모아 바깥에 보여주는 일을 한다. 대시보드, 오케스트레이터, 지표 노출이 전부 mgr 위의 모듈로 동작한다. active 하나와 standby 하나 이상으로 이중화된다.
[00-2] prometheus 모듈
mgr에 기본 포함된 모듈이다. 활성화하면 active mgr가 :9283/metrics로 Prometheus 형식의 지표를 낸다. 별도 exporter를 설치하지 않아도 되고, mon·OSD·pool·PG 지표가 한 엔드포인트에서 나온다.
[00-3] Rook external 모드
Rook operator가 Ceph를 소유하지 않고 클라이언트로만 붙는 모드다. OSD를 만들지 않고, 기존 Ceph의 자격증명과 mon 주소를 가져와 CSI 드라이버로 동적 프로비저닝만 제공한다. 이 모드에서는 Ceph 데몬이 쿠버네티스 파드가 아니므로, Prometheus Operator가 자동으로 찾아낼 대상 자체가 없다.
[01] 현재 시스템 상황
Ceph는 전담 베어메탈 3노드에 cephadm으로 구축했다. Quincy 17.2.8, mon 3대 쿼럼, 노드당 OSD 1개다. 클라이언트망 192.0.2.0/24와 OSD 복제망 198.51.100.0/24를 분리했고, pool은 general(RBD 범용), vms, volumes, backups, CephFS용 general-multi-attach-{data,metadata}를 쓴다.
구축 당시 기록한 ceph -s 출력이다.
1
2
3
4
5
6
7
8
9
10
11
12
cluster:
id: 9b3c1d7a-...
health: HEALTH_OK
services:
mon: 3 daemons, quorum node-s1,node-s2,node-s3
mgr: node-s1.xxxx(active), standbys: 1
mds: 1/1 daemons up, 2 standby
osd: 3 osds: 3 up, 3 in
data:
pools: 7 pools, 193 pgs
usage: ... 49 TiB avail
pgs: 193 active+clean
쿠버네티스는 Rook external 모드로 이 클러스터에 붙어 있다. rook-ceph-external 네임스페이스에 CephCluster가 Connected 상태로 있고, StorageClass general(RBD)이 default, general-multi-attach(CephFS)가 RWX용이다.
모니터링 스택은 이미 돌고 있다. 관리 노드 node-a1에 kube-prometheus-stack이 올라가 있고, Prometheus는 100Gi PVC를 쓴다. Grafana는 operator 방식으로 설치해 대시보드 6종을 provisioning 했다. exporter는 node-exporter, kube-state-metrics를 포함해 OpenStack·MySQL·memcached·libvirtd·blackbox까지 붙어 있다.
[01-1] 빠진 것
여기에 빠진 것이 Ceph다. 대시보드 6종 어디에도 Ceph 패널이 없고, Prometheus 수집 대상 목록에도 Ceph가 없다. Ceph 상태를 보는 경로는 세 가지인데 셋 다 사람이 그 순간에 직접 들여다보는 방식이다.
| 경로 | 명령 또는 주소 | 한계 |
|---|---|---|
| 스토리지 노드에서 CLI | ssh user@192.0.2.11 'sudo ceph -s' |
사람이 칠 때만 값이 나온다 |
| Ceph Dashboard | https://192.0.2.11:8443 |
이력 보존 기간이 짧고 알림 연동이 별개다 |
| 쿠버네티스에서 CR 조회 | kubectl -n rook-ceph-external get cephcluster |
operator가 갱신한 요약 한 줄뿐이다 |
이력이 없으니 “언제부터 이랬는지”를 답할 수 없고, 알림이 없으니 OSD 하나가 빠져도 다음 점검 때까지 모른다.
[01-2] 의존 방향 문제
Prometheus의 100Gi PVC는 StorageClass general, 즉 이 Ceph 위에 있다. Ceph가 흔들리면 그 사실을 기록해야 할 Prometheus가 먼저 영향을 받는다.
이 순환 의존은 지표 수집을 붙인다고 사라지지 않는다. 다만 Ceph 지표를 Prometheus 안에 두면 최소한 장애 직전까지의 기록은 남고, 알림 경로를 Alertmanager 밖(메일·메신저)으로 빼두면 사후 추적이 가능해진다.
[02] 필요한 작업
세 단계다.
- 노출: Ceph 쪽에서 지표를 HTTP로 낸다. mgr의 prometheus 모듈을 켜는 것으로 끝난다. 두 방법의 공통 전제다.
- 연결: Prometheus가 그 엔드포인트를 수집 대상으로 삼는다. 여기서 방법 A와 B가 나뉜다.
- 활용: Grafana 대시보드와 PrometheusRule 알림을 추가한다.
연결 단계가 나뉘는 이유는 외부 Ceph의 성질 때문이다. Prometheus Operator는 쿠버네티스 Service를 기준으로 수집 대상을 찾는다. Ceph 데몬은 쿠버네티스 밖에 있으므로 가리킬 Service가 없다. 방법 A는 Service 없이 정적 주소를 직접 적어 넣고, 방법 B는 Rook에게 외부 주소를 가리키는 Service를 대신 만들게 한다.
네트워크 도달성도 먼저 확인할 항목이다. Prometheus 파드가 뜨는 노드에서 192.0.2.0/24의 9283으로 나갈 수 있어야 한다. 노드마다 스토리지망 경로가 다른 환경이라면 이 확인을 건너뛰지 않는다. 수집 대상만 등록해두고 전부 down으로 뜨는 상황의 대부분이 라우팅 문제다.
[03] 공통 준비 — mgr prometheus 모듈
[03-1] 모듈 활성화
active mgr가 있는 노드가 아니어도 되고, 어느 노드에서 쳐도 클러스터 전체에 적용된다.
1
2
ssh user@192.0.2.11 'sudo ceph mgr module enable prometheus'
ssh user@192.0.2.11 'sudo ceph mgr services'
예상 출력:
1
2
3
4
{
"dashboard": "https://192.0.2.11:8443/",
"prometheus": "http://192.0.2.11:9283/"
}
prometheus 항목이 나오면 active mgr가 엔드포인트를 열었다는 뜻이다. 이 출력에는 active mgr의 주소만 나온다.
[03-2] 동작 값 명시 — standby mgr를 어떻게 다룰 것인가
1
2
3
4
5
ssh user@192.0.2.11 'sudo ceph config set mgr mgr/prometheus/server_addr 0.0.0.0
sudo ceph config set mgr mgr/prometheus/server_port 9283
sudo ceph config set mgr mgr/prometheus/scrape_interval 30
sudo ceph config set mgr mgr/prometheus/standby_behaviour error
sudo ceph config set mgr mgr/prometheus/standby_error_status_code 503'
앞의 세 줄은 기본값을 명시하는 것에 가깝다. 뒤의 두 줄이 실제로 동작을 바꾸므로 이유를 적어둔다.
standby_behaviour의 기본값은 standby mgr가 요청을 받으면 active mgr로 리다이렉트하는 것이다. Prometheus는 리다이렉트를 따라가므로, standby 주소를 수집 대상에 넣어두면 active의 지표를 한 번 더 받게 된다. 같은 시계열이 instance 라벨만 다른 채로 두 벌 들어오고, sum() 계열 집계가 그만큼 부풀려진다.
error로 바꾸면 standby는 리다이렉트 대신 503을 돌려주고, 그 대상은 Prometheus에서 down으로 남는다. 중복 집계와 down 표시 중에서는 후자가 낫다. mgr가 페일오버하면 up인 대상이 자동으로 옮겨간다.
[03-3] 노출 확인
1
ssh user@192.0.2.11 'curl -s http://192.0.2.11:9283/metrics | head -6; echo ---; curl -s http://192.0.2.11:9283/metrics | grep -c "^ceph_"'
예상 출력:
1
2
3
4
5
6
7
8
# HELP ceph_health_status Cluster health status
# TYPE ceph_health_status untyped
ceph_health_status 0.0
# HELP ceph_mon_quorum_status Monitors in quorum
# TYPE ceph_mon_quorum_status gauge
ceph_mon_quorum_status{ceph_daemon="mon.node-s1"} 1.0
---
1842
ceph_health_status는 0이 HEALTH_OK, 1이 HEALTH_WARN, 2가 HEALTH_ERR다. 지표 개수는 pool과 OSD 수에 비례하므로 환경마다 다르다.
[03-4] 쿠버네티스 쪽 도달성 확인
Prometheus가 뜨는 노드에서 나가는지 봐야 하므로 파드로 확인한다.
1
2
3
4
kubectl -n monitoring run nettest --rm -i --restart=Never \
--overrides='{"spec":{"nodeSelector":{"role":"admin-node"}}}' \
--image=curlimages/curl -- \
curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://192.0.2.11:9283/metrics
예상 출력:
1
2
200
pod "nettest" deleted
000이 나오면 라우팅이나 방화벽이다. 수집 설정을 추가하기 전에 이것부터 푼다.
[04] 방법 A — 정적 수집 대상 등록
kube-prometheus-stack의 additionalScrapeConfigs에 Prometheus 원본 형식의 job을 넣는다. Rook을 거치지 않으므로 Ceph와 Prometheus만 있으면 성립한다.
[04-1] 수집 설정 작성
1
2
3
4
5
6
7
8
9
10
# ceph-scrape.yaml
- job_name: ceph-mgr
scrape_interval: 30s
static_configs:
- targets:
- 192.0.2.11:9283
- 192.0.2.12:9283
- 192.0.2.13:9283
labels:
cluster: onprem
세 노드를 모두 넣는 이유는 mgr 페일오버 때문이다. active가 어느 노드로 옮겨가든 그 대상이 up이 된다. cluster 라벨 값은 기존 대시보드 변수와 맞춘다. 이 값이 다르면 대시보드의 클러스터 선택 항목에 Ceph 지표가 따로 뜬다.
[04-2] Secret 등록과 Helm 반영
1
2
kubectl -n monitoring create secret generic additional-scrape-configs \
--from-file=ceph-scrape.yaml --dry-run=client -o yaml | kubectl apply -f -
1
2
3
4
5
6
# kube-prometheus-stack values 추가분
prometheus:
prometheusSpec:
additionalScrapeConfigs:
name: additional-scrape-configs
key: ceph-scrape.yaml
1
2
helm -n monitoring upgrade kube-prometheus-stack \
prometheus-community/kube-prometheus-stack --reuse-values -f values-ceph.yaml
Operator가 Prometheus 설정을 다시 만들고 파드가 재시작한다. 설정 반영은 수십 초 걸린다.
[04-3] 확인
Prometheus의 타깃 API로 본다. NodePort는 기존 스택이 쓰던 31010이다.
1
2
curl -s http://192.0.2.20:31010/api/v1/targets \
| jq -r '.data.activeTargets[] | select(.labels.job=="ceph-mgr") | "\(.labels.instance)\t\(.health)\t\(.lastError)"'
예상 출력:
1
2
3
192.0.2.11:9283 up
192.0.2.12:9283 down server returned HTTP status 503 Service Unavailable
192.0.2.13:9283 down server returned HTTP status 503 Service Unavailable
| 결과 | 해석 |
|---|---|
up 1개 + down 2개 |
정상. [03-2]에서 설정한 standby 동작의 결과다 |
셋 다 down
|
도달성 문제. [03-4]로 돌아간다 |
셋 다 up
|
standby_behaviour가 반영되지 않아 중복 수집 중이다 |
[04-4] 장단점
장점: Rook 버전이나 CephCluster CR 스키마에 의존하지 않는다. Ceph를 쓰는 다른 소비자(예: OpenStack에서 직접 RBD 사용)만 있고 쿠버네티스 연동이 없는 환경에도 그대로 쓸 수 있다.
단점: 주소가 Secret 안에 하드코딩된다. 스토리지 노드를 늘리거나 IP를 바꾸면 이 파일을 직접 고쳐야 한다. 클러스터 재구축 절차에 이 Secret 생성 단계를 따로 적어두지 않으면 빠뜨리기 쉽다.
[05] 방법 B — Rook에게 ServiceMonitor를 맡기기
CephCluster CR에 외부 mgr 주소를 알려주면, Rook operator가 그 주소를 가리키는 Service와 Endpoints를 만들고 ServiceMonitor까지 함께 생성한다. 그 뒤로는 Prometheus Operator가 평소 하던 대로 대상을 찾아간다.
[05-1] 현재 설정과 operator 버전 확인
external monitoring 필드는 Rook operator가 처리하므로, 쓰고 있는 버전이 이 필드를 다루는지 보는 것이 첫 단계다.
1
2
kubectl -n rook-ceph get deploy rook-ceph-operator -o jsonpath='{..image}{"\n"}'
kubectl -n rook-ceph-external get cephcluster rook-ceph-external -o jsonpath='{.spec.monitoring}{"\n"}'
예상 출력:
1
2
rook/ceph:v1.13.7
{"enabled":false}
[05-2] Rook values 수정
CephCluster는 Helm 릴리스가 관리하므로 values에 넣는다. CR을 직접 patch하면 다음 helm upgrade에서 되돌아간다.
1
2
3
4
5
6
7
8
9
# rook-values-external.yaml 추가분
cephClusterSpec:
monitoring:
enabled: true
externalMgrEndpoints:
- ip: 192.0.2.11
- ip: 192.0.2.12
- ip: 192.0.2.13
externalMgrPrometheusPort: 9283
1
2
helm -n rook-ceph-external upgrade rook-ceph-cluster rook-release/rook-ceph-cluster \
--set operatorNamespace=rook-ceph -f rook-values-external.yaml
[05-3] ServiceMonitor 선택 조건 풀기
kube-prometheus-stack은 기본적으로 자기 릴리스 라벨이 붙은 ServiceMonitor만 고른다. Rook이 만든 ServiceMonitor에는 그 라벨이 없으므로, 선택 조건을 풀어주지 않으면 리소스는 생겼는데 수집은 안 되는 상태가 된다.
1
2
3
4
5
# kube-prometheus-stack values 추가분
prometheus:
prometheusSpec:
serviceMonitorSelectorNilUsesHelmValues: false
serviceMonitorNamespaceSelector: {}
[05-4] 확인
1
kubectl -n rook-ceph-external get svc,endpoints,servicemonitor | grep mgr-external
예상 출력:
1
2
3
service/rook-ceph-mgr-external ClusterIP <ClusterIP> <none> 9283/TCP 45s
endpoints/rook-ceph-mgr-external 192.0.2.11:9283,192.0.2.12:9283,192.0.2.13:9283 45s
servicemonitor.monitoring.coreos.com/rook-ceph-mgr-external 45s
Endpoints에 세 주소가 다 들어 있으면 Rook이 values를 그대로 반영한 것이다. 그다음 Prometheus가 실제로 그 ServiceMonitor를 집었는지 본다.
1
2
curl -s http://192.0.2.20:31010/api/v1/targets \
| jq -r '.data.activeTargets[] | select(.labels.job|test("mgr-external")) | "\(.labels.instance)\t\(.health)"'
예상 출력:
1
2
3
192.0.2.11:9283 up
192.0.2.12:9283 down
192.0.2.13:9283 down
출력이 비어 있으면 ServiceMonitor 선택 조건이 아직 좁은 것이다. [05-3]의 설정과 Prometheus 파드 재시작 여부를 다시 본다.
[05-5] 장단점
장점: 주소가 Rook values 한 곳에 모인다. Rook external 연결 자체가 이미 Helm으로 관리되고 있으므로, 재구축 시 같은 values 파일 하나만 챙기면 지표 수집도 함께 복원된다. 라벨링도 Prometheus Operator의 일반 경로를 따른다.
단점: Rook operator 버전에 의존한다. external 모드에서 Ceph 자격증명을 추출하는 스크립트가 --monitoring-endpoint 인자를 함께 받는 구조라, 그 값과 CR의 externalMgrEndpoints가 어긋나 있으면 어느 쪽이 실제로 쓰이는지 판단하기 어렵다. 처음 연결할 때 넘긴 값이 mon 주소였는지 mgr 주소였는지 확인해두는 편이 낫다. 두 주소가 같은 노드라도 의미가 다르다.
[06] 선택 기준
두 방법은 배타적이지 않지만 동시에 켜면 같은 지표를 두 벌 받는다. 하나만 고른다.
| 상황 | 선택 |
|---|---|
| Ceph 소비자가 쿠버네티스만이 아니다 (OpenStack 등 직접 RBD 사용) | A |
| Rook external 연결을 Helm values로 이미 형상 관리하고 있다 | B |
| Rook operator 버전을 당분간 고정해둬야 한다 | A |
| 스토리지 노드 증설이 예정돼 있다 | B |
| 모니터링 스택과 스토리지 연동을 서로 독립적으로 바꾸고 싶다 | A |
정하기 애매하면 A로 시작한다. 설정이 Secret 하나와 values 네 줄이라 되돌리기 쉽고, 나중에 B로 옮길 때 지표 이름과 대시보드는 그대로 쓴다. 바뀌는 것은 job 라벨뿐이다.
[07] 대시보드와 알림
[07-1] 확인할 지표
| 지표 | 의미 |
|---|---|
ceph_health_status |
0=OK, 1=WARN, 2=ERR |
ceph_mon_quorum_status |
mon별 쿼럼 참여 여부 |
ceph_osd_up, ceph_osd_in
|
OSD별 기동·데이터 수용 상태 |
ceph_cluster_total_bytes, ceph_cluster_total_used_raw_bytes
|
전체 용량과 원시 사용량 |
ceph_osd_apply_latency_ms, ceph_osd_commit_latency_ms
|
OSD 지연 |
한 번 조회해본다.
1
2
curl -sG http://192.0.2.20:31010/api/v1/query \
--data-urlencode 'query=ceph_health_status' | jq -r '.data.result[] | "\(.metric.instance)\t\(.value[1])"'
예상 출력:
1
192.0.2.11:9283 0
[07-2] PrometheusRule 알림
ServiceMonitor와 같은 이유로 release 라벨이 필요하다. 이 라벨이 없으면 규칙 파일이 Prometheus에 로드되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: ceph-external
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
groups:
- name: ceph
rules:
- alert: CephHealthError
expr: ceph_health_status == 2
for: 5m
- alert: CephHealthWarn
expr: ceph_health_status == 1
for: 30m
- alert: CephOsdDown
expr: ceph_osd_up == 0
for: 5m
- alert: CephClusterNearFull
expr: ceph_cluster_total_used_raw_bytes / ceph_cluster_total_bytes > 0.85
for: 30m
- alert: CephMetricsMissing
expr: absent(ceph_health_status)
for: 10m
마지막 규칙이 실질적으로 중요하다. standby_behaviour: error 때문에 대상 일부가 상시 down이므로, 흔히 쓰는 up == 0 조건은 이 환경에서 상시 발화한다. 지표가 아예 안 들어오는 상황을 잡으려면 absent()로 지표 존재 자체를 본다.
CephHealthWarn의 for를 30분으로 둔 이유는 pool autoscale 관련 경고처럼 무해한 HEALTH_WARN이 짧게 스쳐가는 경우가 있어서다. 처음에는 넉넉히 두고, 어떤 경고가 실제로 뜨는지 한 주 정도 보고 나서 조인다.
[07-3] Grafana 대시보드
기존 대시보드와 같은 방식으로 등록한다. JSON을 ConfigMap으로 만들고 GrafanaDashboard CR로 참조하는 형태다. 다만 Ceph 계열 커뮤니티 대시보드는 패널 수가 많아 JSON이 큰 편이므로, kubectl apply의 last-applied 어노테이션 한도(256KB)를 넘는 파일은 kubectl create configmap으로 등록한다.
[08] 원복
방법 A:
1
2
kubectl -n monitoring delete secret additional-scrape-configs
# values에서 additionalScrapeConfigs 블록 제거 후 helm upgrade
방법 B:
1
2
3
cephClusterSpec:
monitoring:
enabled: false
공통:
1
ssh user@192.0.2.11 'sudo ceph mgr module disable prometheus'
모듈을 끄면 9283 엔드포인트가 닫힌다. Ceph 클러스터 동작과 데이터에는 영향이 없다.
[09] 정리
- 지표 노출은 mgr의 prometheus 모듈 하나로 끝난다. 별도 exporter를 설치할 일이 없다.
- 나뉘는 부분은 Prometheus를 그 엔드포인트에 연결하는 방식이다. 정적 수집 설정은 Rook과 무관하게 성립하고, Rook external monitoring은 설정을 Helm values 한 곳에 모아준다.
- standby mgr를 어떻게 다룰지 먼저 정한다. 리다이렉트를 그대로 두면 지표가 두 벌 들어오고,
error로 바꾸면 대상 일부가 상시down으로 보인다. 알림 조건은 그 선택에 맞춘다. - Prometheus Operator의 선택 조건은 ServiceMonitor와 PrometheusRule 양쪽에서 한 번씩 문제가 된다. 리소스는 만들어졌는데 아무 일도 안 일어나면 라벨과 selector부터 본다.
- 모니터링 스택 자신의 저장소가 Ceph 위에 있다면 그 의존은 지표를 붙여도 남는다. 알림 전달 경로만큼은 Ceph 밖으로 빼둔다.
참고 자료
- Ceph — Prometheus Module · Ceph Manager Daemon
- Rook — External Cluster · Monitoring
- Prometheus Operator — ServiceMonitor · Additional Scrape Configuration
- 관련 글: 격리된 BMC/IPMI 망에서 서버 하드웨어 원격 모니터링 구축기
환경: Ceph Quincy 17.2.8 (cephadm, 베어메탈 3노드) / Rook v1.13.x external 모드 / kube-prometheus-stack. 2026-08-20 기준 문서이며, Rook의 monitoring 필드 스키마는 operator 버전에 따라 달라진다.