LabHub
배우기 러닝패스 코스

Grafana — ダッシュボードは問いだ

問い一つに答えるダッシュボードを作る

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

진짜 Grafana 를 띄워 진짜 Prometheus 를 붙이고, 질문 하나에 답하는 대시보드를 처음부터 만듭니다. 다 만든 뒤에는 그것을 프로비저닝 파일로 굳혀, 이 Grafana 를 지워도 같은 화면을 다시 세울 수 있게 만듭니다.

이 실습은 화면이 있는 첫 실습입니다. Grafana 를 http://127.0.0.1:3000 으로 띄우면 터미널 위쪽의 웹 미리보기 단추로 실제 Grafana 화면을 열 수 있습니다. 패널은 그 화면에서 클릭으로 만들어도 되고 API 로 올려도 됩니다 — 채점기는 어느 쪽으로 만들었는지 묻지 않고 Grafana 에 올라간 결과만 봅니다.

왜 중요한가

대시보드는 그림이 아니라 질문에 답하는 도구입니다. 그래서 이 실습은 패널을 그리는 순서가 아니라 질문 → 쿼리 → 패널 → 알림 → 파일 순서로 갑니다.

마지막 단계가 특히 중요합니다. 클릭으로 만든 대시보드는 Grafana 의 데이터베이스 안에만 있어서, 그 데이터베이스가 사라지면 함께 사라지고 누가 언제 무엇을 고쳤는지도 남지 않습니다. 실무에서 "그때 그 대시보드" 를 못 찾는 일은 거의 언제나 이 이유입니다.

이 파드에는 shop-api 라는 가상의 서비스 지표가 12시간치 들어 있습니다. http_requests_total 에는 handler 라벨이 넷(/api/orders, /api/search, /api/users, /healthz) 있고, 응답시간은 http_request_duration_seconds_bucket 히스토그램으로 들어옵니다.

단계

  1. /root/graf/provisioning/datasources/prometheus.yml 에 데이터소스를 선언하고, Grafana 를 http://127.0.0.1:3000 으로 띄웁니다. 데이터소스의 uid 는 labprom, 주소는 http://127.0.0.1:9090 입니다.
  2. 이 대시보드가 답할 질문을 /root/graf/02-question.md 에 한 문장으로, 그 질문에 답하는 PromQL 을 /root/graf/02-question.promql 에 한 줄로 적습니다.
  3. 그 쿼리를 담은 패널 하나짜리 대시보드를 만들어 올립니다. uid 는 shop-api 입니다.
  4. p95 응답시간과 현재 요청률 패널을 더합니다. 타입은 질문의 모양을 따릅니다.
  5. handler 템플릿 변수를 넣고 모든 패널이 그 변수로 좁혀지게 합니다.
  6. /root/graf/runbook.md 에 런북을 씁니다 — 무엇이 깨졌나 / 먼저 볼 것 / 되돌리는 법.
  7. /root/graf/provisioning/alerting/shop-api.yml 에 알림 규칙을 만들고 런북을 가리키게 합니다.
  8. 대시보드를 JSON 으로 내보내 /root/graf/dashboards/shop-api.json 에 두고, 제공자 파일로 Grafana 가 그 디렉터리를 보게 합니다.

참고

Grafana 를 띄우고 데이터소스를 파일로 붙인다

/root/graf/provisioning/datasources/prometheus.yml 에 uid 가 labprom 인 prometheus 데이터소스를 선언하고, 그 디렉터리를 프로비저닝 경로로 지정해 Grafana 를 http://127.0.0.1:3000 으로 띄우세요.

데이터소스를 UI 에서 클릭으로 붙이면 Grafana 의 DB 에만 남습니다. 채점기는 API 응답의 readOnlytrue 인지를 봅니다 — 그게 '파일에서 왔다' 는 표시입니다.

mkdir -p /root/graf/provisioning/datasources /root/graf/provisioning/dashboards \
         /root/graf/provisioning/alerting /root/graf/dashboards \
         /tmp/gf/data /tmp/gf/logs /tmp/gf/plugins

GF_PATHS_DATA=/tmp/gf/data GF_PATHS_LOGS=/tmp/gf/logs GF_PATHS_PLUGINS=/tmp/gf/plugins \
GF_PATHS_PROVISIONING=/root/graf/provisioning \
GF_SERVER_HTTP_PORT=3000 \
GF_AUTH_ANONYMOUS_ENABLED=true GF_AUTH_ANONYMOUS_ORG_ROLE=Admin \
GF_PLUGINS_PREINSTALL_DISABLED=true \
  setsid nohup grafana server --homepath /opt/grafana >/var/log/grafana.log 2>&1 </dev/null &

curl -s http://127.0.0.1:3000/api/health          # "database": "ok" 가 나올 때까지 20~40초
curl -s http://127.0.0.1:3000/api/datasources/uid/labprom/health

환경변수 셋의 이유는 이렇습니다. 데이터·로그·플러그인 경로를 /tmp 로 돌리는 것은 이 파드에 capability 가 없어 기본 경로에 못 쓸 수 있어서, 익명 접근을 켜는 것은 웹 미리보기로 열었을 때 로그인 화면부터 만나지 않으려고, 플러그인 사전 설치를 끄는 것은 Grafana 11.4 가 기동할 때마다 grafana-lokiexplore-app 을 인터넷에서 받으려 하는데 이 파드는 바깥으로 못 나가기 때문입니다.

이 대시보드가 답할 질문을 먼저 적는다

/root/graf/02-question.md 에 이 대시보드가 답할 질문을 물음표로 끝나는 한 문장으로 적고, /root/graf/02-question.promql 에 그 질문에 답하는 PromQL 을 적으세요. 질문은 'shop-api 의 응답 중 5xx 는 몇 %인가' 입니다.

개수가 아니라 비율입니다. 트래픽이 두 배가 되면 5xx 개수도 두 배가 되지만 사용자가 실패를 겪을 확률은 그대로입니다.

5xx 요청의 초당 건수를 전체 요청의 초당 건수로 나눕니다. status 라벨과 rate() 를 씁니다. 창은 [5m] 로 두세요.

promq 'sum(rate(http_requests_total[5m]))'
promq 'sum by (status) (rate(http_requests_total[5m]))'
promq "$(cat /root/graf/02-question.promql)"

채점기는 여러분이 쓴 쿼리를 Grafana 데이터소스를 거쳐 실제로 실행해서 나온 값을 봅니다. 쓰는 방법이 달라도 숫자가 맞으면 통과합니다.

질문에 답하는 패널을 만든다

uid 가 shop-api 인 대시보드를 만들고, 2단계의 쿼리를 그리는 timeseries 패널을 하나 넣으세요. 대시보드 제목은 답하는 질문을 알 수 있게 짓습니다.

웹 미리보기로 Grafana 를 열어 새 대시보드를 만들고 패널을 추가해도 되고(그때 대시보드 설정에서 uid 를 shop-api 로 지정하세요), 아래처럼 API 로 올려도 됩니다.

curl -s -XPOST -H 'Content-Type: application/json' \
  -d @/tmp/dash.json http://127.0.0.1:3000/api/dashboards/db

/tmp/dash.json{"overwrite": true, "dashboard": { ... }} 모양입니다.

타입은 timeseries 여야 합니다. 인시던트에서 필요한 답은 '지금 몇 %' 가 아니라 '언제부터 올랐나' 이고, 시간축이 없는 타입으로는 그걸 볼 수 없습니다.

올린 결과는 이렇게 확인합니다.

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api | jq '.dashboard.panels'

패널 타입을 질문의 모양에 맞춘다

같은 대시보드에 p95 응답시간 패널과 현재 요청률 패널을 더해 패널을 셋으로 만드세요. p95 는 timeseries, 현재 요청률은 stat 입니다.

p95 는 히스토그램에서 구합니다. le 는 버킷 경계라서 그것만 남기고 나머지를 합쳐야 합니다.

promq 'histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))'
promq 'sum(rate(http_requests_total[5m]))'

현재 요청률에 gauge 를 쓰지 마세요. 게이지는 0과 최대치 사이 어디쯤인지를 보여 주는 도구인데 초당 요청 수에는 최대치가 없습니다. 지금 이 순간의 숫자 하나는 stat 입니다.

반대로 p95 를 stat 이나 gauge 로 두면 '언제부터 느려졌나' 를 못 봅니다. 분위수는 시간에 따라 어떻게 움직였는지가 전부입니다.

대시보드 하나가 핸들러 넷을 보게 한다

handler 라는 이름의 query 타입 템플릿 변수를 넣고, 세 패널의 쿼리가 모두 그 변수로 좁혀지게 하세요.

값을 손으로 나열하는 custom 변수는 핸들러가 하나 늘어나는 날 낡습니다. 데이터에서 읽어 오세요.

label_values(http_requests_total, handler)

그리고 변수는 선언만으로는 아무 일도 하지 않습니다. 패널 쿼리가 이렇게 좁혀져야 합니다.

sum(rate(http_requests_total{handler="$handler"}[5m]))

채점기는 변수를 실제 값으로 바꿔 쿼리를 던져 봅니다. 라벨 이름이나 따옴표가 틀리면 빈 결과가 나오고 그 자리에서 떨어집니다.

알림보다 런북을 먼저 쓴다

/root/graf/runbook.md 에 런북을 쓰세요. ## 무엇이 깨졌나, ## 먼저 볼 것, ## 되돌리는 법 세 절이 있어야 하고, '먼저 볼 것' 에는 실제로 던질 수 있는 http_requests_total 쿼리가 들어가야 합니다.

순서가 이렇게 되는 이유가 있습니다. 알림을 먼저 만들면 '울리면 그때 생각하자' 가 되고 그 문서는 끝내 안 쓰입니다. 런북을 먼저 쓰면 "이게 울렸을 때 사람이 지금 무엇을 하나" 에 답할 수 없는 알림이 애초에 안 만들어집니다.

'먼저 볼 것' 은 문장이 아니라 명령이어야 합니다. '상태를 확인한다' 는 새벽 3시에 아무 도움이 되지 않습니다. 어느 핸들러가 실패하고 있는지 세는 쿼리를 그대로 적어 두세요.

알림을 만들고 런북을 가리키게 한다

/root/graf/provisioning/alerting/shop-api.yml 에 5xx 비율 알림 규칙을 만드세요. 임계값 조건이 있어야 하고, for 는 5분 이상, annotations.runbook_url 은 6단계의 런북을 가리켜야 합니다. 파일을 쓴 뒤 Grafana 를 다시 띄웁니다.

for 가 이 단계의 핵심입니다. 5xx 는 요청 하나만 실패해도 잠깐 튀는데, 그때마다 사람을 깨우면 그 사람은 다음부터 알림을 무시합니다. 알림은 꺼져서 죽는 게 아니라 무시당해서 죽습니다.

알림 쿼리에는 $handler 같은 대시보드 변수를 쓸 수 없습니다. 알림은 화면 없이 평가되므로 변수를 풀어 줄 드롭다운이 없습니다.

프로비저닝 설정은 기동할 때 읽습니다. 파일만 써 두면 아무 일도 일어나지 않습니다.

pkill -x grafana; sleep 3
# (1단계와 같은 환경변수로 다시 띄운다)
curl -s http://127.0.0.1:3000/api/v1/provisioning/alert-rules | jq '.[].title'

관리자 계정으로 다시 읽히는 방법도 있습니다(익명 접근으로는 403 입니다).

curl -XPOST -u admin:admin http://127.0.0.1:3000/api/admin/provisioning/alerting/reload

대시보드를 파일로 굳힌다

지금 화면에 뜬 대시보드를 JSON 으로 내보내 /root/graf/dashboards/shop-api.json 에 저장하고, /root/graf/provisioning/dashboards/lab.yml 로 Grafana 가 그 디렉터리를 보게 한 뒤 다시 띄우세요. 채점기는 meta.provisionedtrue 인지를 봅니다.

대시보드 JSON 과 제공자(provider) 파일은 다른 것입니다. 제공자 파일에는 대시보드가 아니라 어느 디렉터리를 보라는 지시가 들어갑니다.

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api \
  | jq '.dashboard' > /root/graf/dashboards/shop-api.json

pkill -x grafana; sleep 3
# (1단계와 같은 환경변수로 다시 띄운다)

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api | jq '.meta.provisioned'

meta.provisionedtrue 면 지금 화면에 뜬 것이 파일에서 온 것입니다. 그때부터는 API 로 덮어쓰려 해도 Grafana 가 Cannot save provisioned dashboard 로 거절합니다 — 화면과 파일이 어긋날 길을 막는 것입니다.