Table of contents
- 관측 없는 운영은 도박이다
- 로그 — 쿠버네티스에서의 흐름
- 메트릭 — Prometheus와 Grafana
- 분산 추적 — Jaeger와 OpenTelemetry
- 네 번째 축 — 이벤트
- kubectl 디버깅 실전
- 세 축을 엮어서 보기
- 무엇부터 도입할까
관측 없는 운영은 도박이다
쿠버네티스 클러스터가 복잡해질수록 “지금 뭐가 어떻게 돌아가는지”를 파악하기가 어려워진다. Pod가 죽어도 새로 뜨니까 겉으로는 멀쩡해 보이고, 트래픽이 폭주해도 오토스케일링이 붙으면 티가 안 난다. 문제가 수면 위로 올라왔을 때는 이미 사용자가 피해를 입은 뒤다.
관측성(Observability)은 이 불투명함을 깨는 도구다. 시스템이 내보내는 세 가지 신호 — 로그, 메트릭, 트레이스 — 를 수집하고 연결해서, “왜 지금 이 상태인가”를 설명할 수 있게 만드는 것.
flowchart LR
A[로그<br/>Logs] -->|"무슨 일이<br/>있었나"| D[관측성 플랫폼]
B[메트릭<br/>Metrics] -->|"지금 얼마나<br/>쓰고 있나"| D
C[트레이스<br/>Traces] -->|"요청이 어떻게<br/>흘러갔나"| D
D --> E[대시보드]
D --> F[알람]
D --> G[디버깅]
각 신호가 답할 수 있는 질문이 다르다. 이 셋을 엮어서 봐야 “방금 장애가 왜 터졌는지”를 알 수 있다.
로그 — 쿠버네티스에서의 흐름
컨테이너 로그는 표준 출력(stdout/stderr)으로 나가는 것이 원칙이다. 옛날처럼 파일에 쓰면 그 파일은 컨테이너 파일시스템에 갇힌다. Pod가 죽으면 로그도 같이 사라진다.
쿠버네티스는 컨테이너가 stdout으로 쏟아내는 로그를 노드의 /var/log/containers/ 디렉토리에 파일로 저장한다. kubectl logs는 그 파일을 읽어서 보여주는 래퍼다.
# 특정 Pod 로그
kubectl logs my-pod
# 여러 컨테이너가 있는 Pod에서 특정 컨테이너 로그
kubectl logs my-pod -c sidecar
# 실시간 follow
kubectl logs -f my-pod
# 라벨로 묶어서 여러 Pod 로그 동시에 (-l + --tail=N)
kubectl logs -l app=web --tail=100 -f
# 이전 컨테이너 로그 (크래시 후)
kubectl logs my-pod --previous
kubectl logs는 디버깅용으로 편하지만, 운영에서는 부족하다. 노드가 로그 파일을 순환(rotate)시키면 오래된 로그는 사라지고, 여러 Pod의 로그를 시간순으로 섞어서 보기도 어렵고, 검색도 안 된다.
로그 수집 파이프라인
실제 운영에서는 각 노드에 로그 수집 에이전트를 DaemonSet으로 띄워서, 노드의 로그 파일들을 중앙 저장소로 보낸다.
flowchart LR
A[App Pod<br/>stdout] --> B[노드 파일시스템<br/>/var/log/containers/]
B --> C[Fluent Bit<br/>DaemonSet]
C --> D[Elasticsearch]
C --> E[Loki]
C --> F[CloudWatch / GCP]
D --> G[Kibana]
E --> H[Grafana]
많이 쓰이는 조합이 몇 가지 있다.
- EFK 스택: Elasticsearch + Fluentd/Fluent Bit + Kibana. 전통적인 ELK 기반
- Loki 스택: Loki + Promtail + Grafana. 로그를 Prometheus 스타일 라벨로 인덱싱해서 가볍게 운영
- 클라우드 매니지드: CloudWatch Logs, GCP Logging, Azure Monitor. 설정이 간단하지만 비용이 있음
처음 쿠버네티스를 세팅한다면 Loki가 가벼워서 실습용으로 좋다. 인덱싱을 라벨에만 걸기 때문에 저장 비용이 낮고, Grafana와 기본 연동되어서 메트릭과 로그를 한 화면에서 본다.
간단한 Fluent Bit 설정은 이런 식이다.
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
namespace: logging
data:
fluent-bit.conf: |
[SERVICE]
Parsers_File parsers.conf
[INPUT]
Name tail
Path /var/log/containers/*.log
Parser docker
Tag kube.*
Refresh_Interval 5
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
Keep_Log Off
[OUTPUT]
Name loki
Match *
Host loki.logging.svc.cluster.local
Port 3100
Labels job=fluentbit, namespace=$kubernetes['namespace_name']
핵심은 Kubernetes 필터가 Pod 라벨과 네임스페이스 같은 메타데이터를 로그에 붙여준다는 점이다. 나중에 “backend 네임스페이스의 api Pod 로그만”으로 쉽게 필터링할 수 있다.
구조화된 로그
로그를 제대로 활용하려면 구조화된 포맷(JSON)으로 찍어야 한다. 단순 텍스트는 사람 눈에는 편해도 검색과 집계가 어렵다.
{
"timestamp": "2026-04-20T10:15:32Z",
"level": "ERROR",
"service": "payment-api",
"trace_id": "abc123def456",
"user_id": "user-9284",
"message": "Payment failed",
"amount": 15000,
"error": "Card declined"
}
trace_id가 들어 있는 게 포인트다. 나중에 분산 추적과 연결할 때 이 ID 하나로 로그와 트레이스를 오갈 수 있다.
메트릭 — Prometheus와 Grafana
출처: Prometheus 공식 리포 — Apache 2.0 License
메트릭은 시간에 따라 변하는 수치 데이터다. “1분마다의 요청 수”, “현재 메모리 사용률”, “에러 카운트” 같은 것들. 쿠버네티스 생태계에서 메트릭의 표준은 사실상 Prometheus다.
Prometheus는 풀(pull) 방식으로 동작한다. 각 Pod가 HTTP 엔드포인트(/metrics)에 메트릭을 노출하면, Prometheus가 주기적으로 긁어간다.
flowchart LR
A[앱 A<br/>/metrics] -->|scrape| D[Prometheus]
B[앱 B<br/>/metrics] -->|scrape| D
C[kube-state-metrics<br/>/metrics] -->|scrape| D
E[node-exporter<br/>/metrics] -->|scrape| D
D --> F[Alertmanager]
D --> G[Grafana]
설치 — kube-prometheus-stack
가장 빠른 경로는 공식 Helm 차트다. Prometheus Operator가 함께 들어 있어서, CRD로 스크래핑 설정을 관리할 수 있다.
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--set grafana.adminPassword='admin123'
설치 후 포트 포워딩으로 확인한다.
# Prometheus
kubectl port-forward -n monitoring svc/monitoring-kube-prometheus-prometheus 9090:9090
# Grafana
kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80
이 차트 하나로 Prometheus + Alertmanager + Grafana + node-exporter + kube-state-metrics가 한꺼번에 설치된다. 노드 리소스, Pod 상태, 컨테이너 메트릭, kubelet 메트릭이 기본으로 수집되고, 쿠버네티스 대시보드 수십 개가 Grafana에 미리 깔려 있다.
ServiceMonitor — 앱 메트릭 등록
Prometheus Operator를 쓰면 앱의 /metrics를 긁어가도록 설정하는 게 ServiceMonitor CRD 하나로 끝난다.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: myapp
namespace: backend
labels:
release: monitoring # kube-prometheus-stack이 찾는 라벨
spec:
selector:
matchLabels:
app: myapp
endpoints:
- port: http
path: /metrics
interval: 15s
이걸 적용하면 app=myapp 라벨이 붙은 Service의 포트로 Prometheus가 스크래핑을 시작한다. 앱 쪽은 Prometheus 클라이언트 라이브러리(Spring Boot Actuator, prom-client, prometheus_client 등)를 붙여서 /metrics를 노출하면 된다.
PromQL — 메트릭 쿼리 언어
Prometheus 메트릭은 PromQL로 쿼리한다. 처음엔 낯설지만 몇 가지 패턴만 익히면 대부분의 질문에 답할 수 있다.
# 현재 각 노드의 CPU 사용률
sum by (instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m]))
# 네임스페이스별 총 메모리 사용량
sum by (namespace) (container_memory_working_set_bytes{container!=""})
# 5분 평균 HTTP 요청률 (Pod별)
sum by (pod) (rate(http_requests_total[5m]))
# 에러율 (5xx 비율)
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
# 지난 1시간 동안 CrashLoopBackOff 상태가 된 Pod
kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff"} == 1
rate()는 카운터 메트릭을 초당 증가율로 바꿔주고, sum by는 라벨별로 집계한다. 알람을 만들 때도 이 표현식을 그대로 쓴다.
알람 — PrometheusRule
임계치를 넘으면 알람을 보내도록 PrometheusRule을 정의한다.
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: app-alerts
namespace: backend
labels:
release: monitoring
spec:
groups:
- name: app.rules
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
> 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.service }} 에러율이 5%를 넘음"
description: "최근 5분간 5xx 비율이 {{ $value | humanizePercentage }}"
- alert: PodCrashLooping
expr: |
rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 10m
labels:
severity: critical
annotations:
summary: "{{ $labels.pod }} 재시작 반복 중"
for: 5m은 “조건이 5분 동안 계속 참이어야 발화”라는 뜻이다. 순간적인 스파이크로 시끄럽게 울리는 걸 막는다. Alertmanager가 이 알람을 Slack, PagerDuty, 이메일로 라우팅한다.
분산 추적 — Jaeger와 OpenTelemetry
마이크로서비스가 늘어나면 한 요청이 여러 서비스를 거친다. 로그만으로는 “어디서 500ms가 걸렸는지” 알기 어렵다. 이 문제를 푸는 게 분산 추적(distributed tracing)이다.
각 서비스가 요청을 처리할 때 trace_id와 span_id를 붙여서 다음 서비스로 넘기고, 모든 서비스의 span을 수집해서 재조립한다.
flowchart TD
A[요청 in: trace_id=abc] --> B[Gateway<br/>span 1]
B --> C[Auth Service<br/>span 2]
C --> D[Order Service<br/>span 3]
D --> E[Payment Service<br/>span 4]
D --> F[Inventory Service<br/>span 5]
E --> G[DB<br/>span 6]
한 요청에 대한 trace를 보면 서비스 간 호출 그래프와 각 단계의 소요 시간이 한눈에 보인다. 어느 서비스가 느린지, 불필요한 호출이 있는지 바로 드러난다.
OpenTelemetry
예전엔 Jaeger/Zipkin 각각의 SDK가 따로 있었다. 지금은 OpenTelemetry(OTel)가 표준이다. OTel SDK로 코드에 계측을 넣으면, 데이터 포맷은 OTel 프로토콜(OTLP)로 통일된다. 수집 백엔드는 Jaeger, Tempo, Honeycomb 뭐든 골라 쓴다.
간단한 구조는 이렇다.
flowchart LR
A[앱 A<br/>OTel SDK] --> C[OTel Collector<br/>DaemonSet 또는 Deployment]
B[앱 B<br/>OTel SDK] --> C
C --> D[Tempo / Jaeger]
C --> E[Prometheus]
C --> F[Loki]
D --> G[Grafana]
OTel Collector는 에이전트 역할이다. 앱이 Collector로 데이터를 보내면, Collector가 가공/필터링 후 백엔드로 라우팅한다. 앱은 백엔드가 뭐든 상관하지 않고, Collector만 바라보면 된다.
Jaeger 설치
Jaeger Operator가 가장 간단하다.
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger
namespace: tracing
spec:
strategy: production
storage:
type: elasticsearch
options:
es:
server-urls: http://elasticsearch.logging:9200
이렇게 하면 Jaeger의 컴포넌트(Collector, Query, Agent)가 한꺼번에 배포된다. 앱에서 OTLP로 데이터를 보내도록 설정하고, Jaeger UI에서 trace를 검색하면 된다.
네 번째 축 — 이벤트
로그/메트릭/트레이스 외에 쿠버네티스만의 신호가 하나 더 있다. 이벤트(Event)다.
# 네임스페이스의 모든 이벤트
kubectl get events -n backend --sort-by='.lastTimestamp'
# 특정 Pod와 관련된 이벤트
kubectl describe pod my-pod | tail -20
# 실시간 이벤트 모니터링
kubectl get events --watch
Pod 스케줄링 실패, 이미지 풀 에러, OOMKilled, 헬스체크 실패 같은 쿠버네티스 내부 신호가 여기 찍힌다. 장애 트러블슈팅의 첫 번째 단서다. 이벤트도 메트릭처럼 수집해서 보관하는 게 좋은데, 기본적으로 1시간 이후 삭제되기 때문이다. kube-state-metrics나 Event Exporter 같은 도구로 장기 보관한다.
kubectl 디버깅 실전
실제 장애 상황에서 자주 쓰는 명령어를 정리해보자. 이 목록을 외워두면 웬만한 문제는 5분 안에 방향을 잡을 수 있다.
Pod 상태 점검
# 문제가 있는 Pod 한눈에 찾기
kubectl get pods -A | grep -vE "Running|Completed"
# Pod 상세 상태와 이벤트 (가장 먼저 보는 명령)
kubectl describe pod my-pod -n backend
# 컨테이너가 재시작한 이유
kubectl get pod my-pod -o jsonpath='{.status.containerStatuses[*].lastState}'
# JSON으로 전체 스펙 확인
kubectl get pod my-pod -o json | jq '.status'
describe의 하단 이벤트 섹션이 특히 유용하다. 스케줄링 실패인지, 이미지 풀 실패인지, 헬스체크 실패인지 바로 보인다.
로그 추적
# 라벨로 여러 Pod의 로그를 시간순으로
kubectl logs -l app=web --tail=100 --prefix=true
# 특정 시각 이후 로그
kubectl logs my-pod --since=10m
kubectl logs my-pod --since-time="2026-04-20T10:00:00Z"
# 사이드카가 있는 경우 모든 컨테이너 로그
kubectl logs my-pod --all-containers=true
# 전에 죽은 컨테이너 로그 (CrashLoopBackOff 때)
kubectl logs my-pod --previous
컨테이너 안으로 들어가기
# 실행 중인 컨테이너에 쉘
kubectl exec -it my-pod -- /bin/bash
kubectl exec -it my-pod -c sidecar -- sh
# 한 번만 명령 실행
kubectl exec my-pod -- env
kubectl exec my-pod -- cat /etc/config/app.yaml
이미지에 쉘이 없다면? kubectl debug
distroless나 scratch 이미지에는 쉘이 없다. 그럴 때 kubectl debug로 디버깅용 사이드카를 붙일 수 있다.
# 디버깅 컨테이너를 임시로 같은 Pod에 추가
kubectl debug -it my-pod --image=busybox --target=my-container
# 새 Pod를 기존 Pod의 복사본으로 띄우되, 이미지를 바꿈
kubectl debug my-pod --copy-to=my-pod-debug --image=busybox -- sleep 3600
네트워크 디버깅도 가능하다.
# 노드에 임시 Pod 띄워서 네트워크 확인
kubectl debug node/node-1 -it --image=nicolaka/netshoot
netshoot 이미지에는 dig, curl, nslookup, tcpdump 같은 네트워크 툴이 다 들어 있어서, 클러스터 내부 네트워크 이슈를 파헤칠 때 유용하다.
리소스 사용량 확인
# 노드 리소스
kubectl top nodes
# Pod별 리소스 (metrics-server 필요)
kubectl top pods -A --sort-by=cpu
kubectl top pods -A --sort-by=memory
# 특정 Pod의 컨테이너별
kubectl top pod my-pod --containers
포트 포워딩과 프록시
# 로컬에서 Pod 포트 직접 접근
kubectl port-forward pod/my-pod 8080:80
# Service 포트 포워딩
kubectl port-forward svc/my-service 8080:80
# API 서버 프록시 (kubectl proxy 후 http://localhost:8001/api/...)
kubectl proxy
포트 포워딩은 개발 환경에서 DB에 직접 붙거나, Prometheus/Grafana 같은 대시보드를 띄울 때 일상적으로 쓴다.
세 축을 엮어서 보기
관측성의 진짜 힘은 세 축을 연결하는 데서 나온다. Grafana 한 화면에서 이런 흐름이 가능하다.
- 알람이 울린다 — “payment-api 에러율 5% 초과”
- Prometheus 대시보드에서 해당 서비스의 그래프를 본다 — CPU/메모리/요청률
- 같은 시간대의 로그를 Loki로 찾는다 — 어떤 에러 메시지가 찍혔나
- 에러 로그에 있는
trace_id로 Tempo에서 트레이스를 연다 — 어느 다운스트림이 실패의 원인인가 - 원인 서비스의 메트릭으로 다시 돌아간다
이 흐름이 한 툴에서 돌아가면 MTTR(Mean Time To Recover)이 극적으로 줄어든다. 따로따로 구축하면 각 시스템을 오가며 “아 그때가 언제였지” 하고 검색하게 되는데, 연결되어 있으면 클릭 몇 번이면 된다.
Prometheus + Loki + Tempo + Grafana 조합이 이 연결에 강한 이유다. 공통 라벨과 trace_id 링크로 자연스럽게 엮인다.
무엇부터 도입할까
처음부터 전부 세팅하려고 들면 질리기 쉽다. 이런 순서를 추천한다.
- Prometheus + Grafana: 기본 클러스터 메트릭과 대시보드. kube-prometheus-stack이면 30분
- 기본 알람: Pod crash, 노드 디스크, CPU/메모리 임계치. 공식 차트가 기본 알람도 깔아준다
- 로그 수집: Loki + Promtail. 처음엔 7일 보관 정도로 가볍게 시작
- 앱 메트릭: 비즈니스에 중요한 서비스부터
/metrics노출, 알람 정의 - 분산 추적: 마이크로서비스가 3개 이상 되면 고려. OTel SDK로 시작
처음부터 완벽하려 들 필요 없다. 구멍이 생길 때마다 하나씩 채우는 편이 오래 간다.
이번 시리즈의 마지막 편은 이 모든 걸 다시 묶어낸다. 수십 개의 YAML을 매번 손으로 관리하는 대신, Helm으로 애플리케이션을 패키지화하는 방법을 살펴본다.




Loading comments...