Pod가 Running이어도 사용자는 서비스 오류를 겪을 수 있습니다. 프로세스가 살아 있는 동안에도 요청의 절반이 500을 반환하거나, 평균 지연은 정상인데 일부 요청만 10초를 넘길 수 있기 때문입니다. CPU가 높다는 알림에 따라 Pod를 늘려도 원인이 하류 데이터베이스의 잠금이라면 문제는 해결되지 않을 수 있습니다.
Kubernetes를 운영할 때는 “현재 상태가 무엇인가”를 확인한 뒤 “왜 이런 상태가 되었는가”까지 설명할 수 있어야 합니다. 모니터링은 미리 정의한 지표와 조건을 지속적으로 확인하는 과정입니다. 관측성(Observability)은 시스템이 내보낸 신호를 이용해 사전에 목록화하지 못한 내부 상태까지 추론하는 능력을 뜻합니다. 두 개념은 함께 사용하며, 원인을 추적할 수 있는 관측성을 갖추려면 신뢰할 수 있는 모니터링과 알림이 먼저 필요합니다.
이 글에서는 운영 중 답해야 할 질문을 정리하고, 그 답을 얻기 위해 어떤 데이터가 필요한지 살펴봅니다. 마지막에는 로그와 Prometheus 형식 메트릭을 함께 내보내는 작은 애플리케이션을 Kubernetes에 배포해 신호를 확인합니다. 다음 글에서는 이 앱을 kube-prometheus-stack과 Loki·Fluent Bit에 연결합니다.
이 글에서 다루는 것
- 모니터링과 관측성의 관계
- Kubernetes 인프라 신호와 애플리케이션 신호의 차이
- 메트릭, 로그, 트레이스, 프로파일이 각각 답하는 질문
- 수집, 처리, 저장, 질의, 시각화, 알림의 계층 구조
- Prometheus pull과 OTLP push를 선택하는 기준
- Prometheus, Loki, Elasticsearch/OpenSearch, Tempo, Jaeger의 적용 경계
- OpenTelemetry Collector가 담당하는 것과 담당하지 않는 것
- 카디널리티, 보존 기간, 개인정보가 비용과 보안에 미치는 영향
- 구조화 로그와
/metrics를 제공하는 테스트 앱 구축 및 검증
실습 환경과 치환값
이 글의 실습은 외부 도메인이나 계정을 사용하지 않습니다. 다음 값만 사용합니다.
| 항목 | 값 | 의미 |
|---|---|---|
| Namespace | observability-lab |
관측성 실습 격리 공간 |
| Deployment | telemetry-demo |
로그와 메트릭을 생성하는 앱 |
| Service | telemetry-demo |
다음 글의 ServiceMonitor가 찾을 대상 |
| 애플리케이션 포트 | 8080 |
/, /fail, /healthz, /metrics 제공 |
| 외부 노출 | 없음 | kubectl port-forward로만 접속 |
필요한 도구는 kubectl, Bash, curl, python3입니다. 클러스터에 대한 쓰기 권한이 없다면 개념 부분까지만 읽고 실습은 허용된 Namespace에서 진행합니다.
먼저 질문을 신호로 번역한다
관측 도구를 고르려면 먼저 어떤 운영 질문에 답할지 정해야 합니다. 아래 표처럼 질문과 신호를 연결하면 수집할 데이터의 범위를 판단할 수 있습니다.
| 운영 질문 | 우선 신호 | 예시 |
|---|---|---|
| 요청이 얼마나 들어오는가 | 메트릭 | 초당 요청 수, 큐 길이 |
| 실패율이 평소보다 높은가 | 메트릭 | 5xx 비율, Job 실패 수 |
| 실패한 요청에서 무슨 일이 있었나 | 로그 | 오류 코드, 예외, 구조화된 이벤트 |
| 여러 서비스 중 어디서 느려졌나 | 트레이스 | span별 지연, 호출 관계 |
| 어느 코드 경로가 CPU를 쓰나 | 프로파일 | 함수별 CPU·메모리 사용 |
| Deployment가 원하는 replica를 유지하는가 | 상태 메트릭 | desired와 available replica 차이 |
| Pod가 왜 시작하지 못했나 | 이벤트와 로그 | 스케줄 실패, 이미지 pull 실패, 컨테이너 오류 |
신호마다 확인하기 좋은 정보가 다릅니다. CPU 사용률의 변화를 보려면 로그를 매초 집계하는 것보다 메트릭이 적합하고, 예외가 발생한 문맥을 알아보려면 숫자 하나보다 로그가 유용합니다. 하나의 신호로 모든 질문을 처리하려 하면 비용이 늘고 정확성도 떨어질 수 있습니다.
두 층을 분리한다: 클러스터와 애플리케이션
클러스터·인프라 관측
Kubernetes 자체가 워크로드를 원하는 상태로 유지하는지 봅니다.
- 노드 CPU, 메모리, 디스크, 네트워크
- kubelet과 컨테이너의 자원 사용량
- Deployment, StatefulSet, DaemonSet, Job, PVC의 상태
- API server, scheduler, controller-manager, etcd, CoreDNS
- 스케줄링 실패, OOMKilled, CrashLoopBackOff, 이미지 pull 오류
대표적인 소스로 node-exporter, kube-state-metrics, kubelet/cAdvisor, 각 컨트롤 플레인 컴포넌트의 /metrics, Kubernetes Events가 있습니다.
애플리케이션 관측
사용자가 실제로 경험하는 서비스 동작을 봅니다.
- 요청률(Rate), 오류율(Errors), 지연(Latency): RED
- 사용량(Utilization), 포화도(Saturation), 오류(Errors): USE
- 주문 완료율, 학습 Job 성공률 같은 비즈니스 지표
- 애플리케이션의 구조화 로그
- 서비스 간 trace와 span
클러스터 지표만 수집하면 “Pod는 정상인데 결제 실패율이 높다”는 상황을 놓칠 수 있습니다. 애플리케이션 지표만으로는 “노드 디스크 압박 때문에 새 Pod가 배치되지 않는다”는 원인을 찾기 어렵습니다. 서비스의 증상과 클러스터의 상태를 함께 비교할 수 있도록 같은 타임라인과 공통 레이블로 연결해야 합니다.
네 가지 신호의 역할
메트릭: 빠른 탐지와 추세
메트릭은 시간에 따른 숫자입니다. 작은 저장 공간으로 긴 기간의 경향을 비교하고, 집계와 알림을 만들기 좋습니다.
http_requests_total{service="api",status="500"} 42
request_duration_seconds_bucket{service="api",le="0.5"} 1530Prometheus의 시계열은 메트릭 이름과 레이블 집합으로 식별됩니다. service, namespace, status처럼 값의 종류가 제한적인 레이블은 유용합니다. 반면 user_id, request_id, 원문 URL처럼 값이 계속 늘어나는 필드를 레이블로 만들면 시계열 수가 폭발합니다.
메트릭은 “5분 동안 오류율이 5%를 넘었다”를 빠르게 알려주지만 개별 실패의 전체 문맥은 담지 않습니다.
로그: 사건의 문맥
로그는 특정 시점의 사건 기록입니다. Kubernetes에서 가장 이식성 높은 애플리케이션 로깅 방식은 컨테이너의 stdout과 stderr에 기록하는 것입니다. 컨테이너 런타임은 이를 CRI 로그 형식으로 노드에 저장하고, kubectl logs와 노드 로그 에이전트가 읽습니다.
운영 로그에 JSON 같은 구조화 형식을 사용하면 상태나 요청 식별자 같은 필드를 구분해 읽을 수 있습니다. 문장만 기록하는 방식보다 이후 검색과 분석에 활용하기 좋습니다.
{
"timestamp": "2026-07-14T09:00:00Z",
"level": "error",
"service": "api",
"status": 500,
"request_id": "e4a1...",
"message": "upstream request failed"
}request_id는 검색할 때 유용하지만 Loki의 인덱스 레이블로 쓰면 안 됩니다. 로그 본문 또는 structured metadata로 보관합니다.
트레이스: 분산 요청의 경로
트레이스는 하나의 요청이 API Gateway, 인증 서비스, 업무 서비스, 데이터베이스를 거치는 경로를 span으로 연결합니다. 각 span에는 시작·종료 시간, 상태, 서비스 이름, 속성이 들어갑니다.
gateway는 auth와 order-service를 호출합니다. 이 그림은 호출 관계와 각 구간의 예시 시간을 보여줍니다.
gateway → auth인증 호출
gateway → order-service주문 호출
구성 관계를 차례로 강조합니다. 단계는 실제 실행 순서가 아닙니다.
전체 단계 한눈에 읽기
- 호출 관계부터 보기
gateway는 auth와 order-service를 호출합니다. 이 그림은 호출 관계와 각 구간의 예시 시간을 보여줍니다.
- gateway → auth: 인증 호출
- gateway → order-service: 주문 호출
- 인증 구간 확인
auth의 예시 시간은 18 ms입니다. 다른 호출 구간과 비교할 때도 원본의 시간을 그대로 봅니다.
- gateway → auth: 인증 호출
- 주문 처리와 DB 구간 비교
order-service는 840 ms, 연결된 database는 790 ms입니다. 중첩될 수 있는 구간이므로 숫자를 모두 더해 전체 지연시간으로 삼지 않습니다.
- gateway → order-service: 주문 호출
- order-service → database: DB 호출
도식 원문
flowchart LR
G["gateway<br/>12 ms"] --> A["auth<br/>18 ms"]
G --> S["order-service<br/>840 ms"]
S --> D["database<br/>790 ms"]평균 지연 메트릭만으로는 어느 호출이 느린지 알 수 없습니다. 트레이스는 병목 위치를 보여줍니다. 다만 모든 요청을 무제한 저장하면 비용이 커지므로 샘플링 정책과 오류 우선 보존을 설계합니다.
프로파일: 코드 수준 자원 사용
연속 프로파일은 함수와 코드 경로별 CPU, 메모리 할당, lock 대기 같은 정보를 모읍니다. “이 서비스가 느리다”를 넘어 “어느 함수가 CPU를 소비하는가”에 답합니다. Pyroscope 같은 백엔드가 이 영역을 담당합니다.
메트릭·로그·트레이스로 문제가 있는 서비스와 요청을 좁혔다면, 프로파일을 통해 코드 수준의 원인을 더 살펴볼 수 있습니다. 각 신호를 함께 사용해 조사 범위를 단계적으로 좁히는 방식입니다.
관측성은 파이프라인이다
노드·Kubernetes 상태와 애플리케이션 메트릭을 수집해 저장합니다. 이동 표시는 수집된 데이터 방향이며 scrape 요청 방향은 아닙니다.
노드·Kubernetes → Prometheus scrape메트릭 수집
애플리케이션 → Prometheus scrape메트릭 수집
Prometheus scrape → 메트릭 저장소메트릭 저장
단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.
전체 단계 한눈에 읽기
- Prometheus의 메트릭 수집
노드·Kubernetes 상태와 애플리케이션 메트릭을 수집해 저장합니다. 이동 표시는 수집된 데이터 방향이며 scrape 요청 방향은 아닙니다.
- 노드·Kubernetes → Prometheus scrape: 메트릭 수집
- 애플리케이션 → Prometheus scrape: 메트릭 수집
- Prometheus scrape → 메트릭 저장소: 메트릭 저장
- Fluent Bit의 로그 경로
애플리케이션 로그는 Fluent Bit을 거쳐 Loki 또는 검색 엔진에 저장됩니다.
- 애플리케이션 → Fluent Bit: 로그 수집
- Fluent Bit → 로그 저장소: 로그 저장
- OpenTelemetry 수집
애플리케이션의 관측 신호를 OpenTelemetry Collector가 수집·처리합니다.
- 애플리케이션 → OTel Collector: 관측 신호
- 신호 종류별 저장소
Collector는 메트릭·로그·트레이스를 각각의 저장소로 보낼 수 있습니다. 세 저장소는 서로 이어지는 직렬 단계가 아닙니다.
- OTel Collector → 메트릭 저장소: 메트릭
- OTel Collector → 로그 저장소: 로그
- OTel Collector → 트레이스 저장소: 트레이스
- Grafana에서 함께 조회
Grafana는 각 저장소를 조회합니다. 이 연결선은 조회 관계를 나타냅니다.
- 메트릭 저장소 → Grafana: 메트릭 조회
- 로그 저장소 → Grafana: 로그 조회
- 트레이스 저장소 → Grafana: 트레이스 조회
- 조건을 평가해 경보 전달
메트릭에 대한 규칙 평가 결과는 Alertmanager로 이어집니다. 화면 조회와는 별개의 경로입니다.
- 메트릭 저장소 → 규칙 평가: 조건 평가
- 규칙 평가 → Alertmanager: 경보 전달
도식 원문
flowchart LR
subgraph S["신호 소스"]
N["노드·Kubernetes 상태"]
A["애플리케이션"]
end
subgraph C["수집·처리"]
P["Prometheus scrape"]
F["Fluent Bit"]
O["OpenTelemetry Collector"]
end
subgraph B["저장·질의"]
M["Prometheus / 장기 메트릭 저장소"]
L["Loki 또는 검색 엔진"]
T["Tempo / Jaeger"]
end
subgraph V["사용"]
G["Grafana"]
R["규칙 평가"]
AM["Alertmanager"]
end
N --> P --> M
A --> P
A --> F --> L
A --> O
O --> M
O --> L
O --> T
M --> G
L --> G
T --> G
M --> R --> AM이 경로에서는 각 구성요소의 상태와 함께 데이터가 끝까지 전달되는지도 확인해야 합니다. 수집기가 정상이어도 저장소가 데이터를 거부할 수 있고, 데이터가 저장되어도 서비스·환경 레이블이 일관되지 않으면 서로 연결해 분석하기 어렵습니다. 대시보드를 만든 뒤에도 실제 사용자 목표와 연결된 알림을 구성해야 장애를 제때 발견할 수 있습니다.
Pull과 Push는 대상에 따라 고른다
Prometheus는 일반적으로 Service Discovery로 대상을 찾고 /metrics를 주기적으로 pull합니다. 타깃이 사라지거나 스크랩에 실패한 사실 자체를 up 메트릭으로 알 수 있고, Kubernetes의 동적 Pod와 잘 맞습니다.
OTLP 기반 계측은 애플리케이션 SDK나 에이전트가 Collector로 신호를 push하는 방식이 일반적입니다. 트레이스처럼 이벤트가 발생할 때 전송하는 신호, 수명이 짧은 작업에 자연스럽습니다.
| 상황 | 적합한 출발점 |
|---|---|
장시간 실행되는 Kubernetes 서비스의 /metrics |
Prometheus pull |
| 분산 trace span | OTLP push |
| 컨테이너 stdout 로그 | 노드 DaemonSet이 파일 tail 후 push |
| 아주 짧은 batch Job 메트릭 | Job 자체 성공 상태, Pushgateway를 제한적으로 검토, 또는 OTLP |
Pushgateway를 일반 서비스의 기본 메트릭 경로로 쓰면 사라진 인스턴스의 시계열을 별도로 정리해야 합니다. Prometheus 공식 지침처럼 서비스 수준 batch job 등 제한된 경우에만 사용합니다.
구성 요소별 역할과 선택 기준
Prometheus와 확장 저장소
Prometheus는 Kubernetes 메트릭의 출발점입니다. PromQL, 서비스 디스커버리, 규칙 평가, exporter 생태계가 강점입니다. 단일 Prometheus의 로컬 TSDB는 무한 장기 저장이나 전역 멀티클러스터 질의를 위한 시스템은 아닙니다.
장기 보존과 중앙 질의가 필요하면 Thanos, Mimir, VictoriaMetrics 같은 호환 시스템을 검토할 수 있습니다. 다만 “규모가 커 보인다”는 이유만으로 모든 컴포넌트를 도입하기보다는, 보존 기간·초당 샘플 수·고가용성·멀티테넌시 요구를 수치로 정한 뒤 필요한 구성을 선택합니다.
Loki와 Elasticsearch/OpenSearch
두 계열은 어떤 정보를 인덱스로 저장하고, 질의를 어디서 시작하는지에 차이가 있습니다. 이 차이를 기준으로 필요한 검색 방식과 운영 비용을 비교할 수 있습니다.
| 관점 | Loki | Elasticsearch/OpenSearch 계열 |
|---|---|---|
| 기본 인덱스 | 스트림 레이블 | 로그 문서의 필드와 텍스트 |
| 질의 시작 | 레이블로 범위를 좁힌 뒤 본문 필터 | 필드·전문 검색과 집계 |
| 강점 | 운영 로그 비용 효율, Grafana 상관 분석 | 복잡한 검색, 분석, 감사·보안 활용 |
| 위험 | 레이블 카디널리티 폭증 | shard·index·heap과 저장 비용 |
Kubernetes 운영 로그와 Prometheus 메트릭을 한 화면에서 연결하려는 경우에는 Loki에서 시작할 수 있습니다. 자유로운 전문 검색, 장기 감사, 보안 분석이 중심이라면 Elasticsearch 또는 OpenSearch 계열이 더 적합할 수 있습니다. “어느 제품이 항상 우수한가”를 정하기보다 필요한 검색 모델과 감당할 비용에 맞춰 선택합니다.
Tempo와 Jaeger
둘 다 OpenTelemetry의 OTLP를 받을 수 있는 트레이스 백엔드로 사용할 수 있습니다. Tempo는 오브젝트 스토리지와 Grafana 통합을 중심으로 비용 효율적인 trace 보관에 초점을 두고, Jaeger는 성숙한 분산 추적 기능과 자체 질의 경험을 제공합니다. 애플리케이션을 OTLP로 계측하면 백엔드 선택을 나중에 바꾸기 쉬워집니다.
Grafana와 Alertmanager
Grafana는 여러 데이터소스를 질의하고 대시보드와 탐색 화면을 제공합니다. 데이터를 수집하거나 Prometheus 알림을 라우팅하는 저장소는 아닙니다. Alertmanager는 Prometheus가 발생시킨 알림을 그룹화, 중복 제거, 억제하고 수신자에게 라우팅합니다.
대시보드는 사람이 데이터를 탐색하고 상황을 판단하는 데 사용합니다. 알림은 담당자가 바로 행동해야 할 때 전달되므로, 즉시 대응이 필요하고 담당자가 명확한 조건만 포함해야 합니다.
OpenTelemetry의 위치
OpenTelemetry는 메트릭·로그·트레이스를 생성하고 전송하기 위한 벤더 중립 API, SDK, 규약, Collector 생태계입니다. Collector는 다음 컴포넌트의 파이프라인으로 동작합니다.
receiver → processor → exporter- receiver: OTLP, Prometheus 등에서 신호를 받습니다.
- processor: batch, 필터, 속성 변환, 민감정보 제거, 샘플링을 수행합니다.
- exporter: Tempo, Prometheus 호환 백엔드, 상용 APM 등으로 보냅니다.
Collector를 두면 애플리케이션이 특정 백엔드에 의존하는 부분을 줄이고, 신호를 한곳에서 변환할 수 있습니다. 다만 신호를 저장할 백엔드는 별도로 필요하며, 애플리케이션 계측이 잘못된 경우에도 해당 계측을 직접 수정해야 합니다. Collector 컴포넌트마다 안정성 수준이 다르므로 사용할 receiver·processor·exporter의 상태를 공식 registry에서 확인합니다.
권장 원칙은 다음과 같습니다.
애플리케이션 계측: OpenTelemetry API/SDK와 OTLP
Kubernetes 인프라 메트릭: Prometheus 생태계
컨테이너 파일 로그: 검증된 노드 수집기
저장 백엔드: 비용·검색·보존 요구에 따라 선택eBPF 관측의 역할
eBPF 기반 도구는 커널 수준의 네트워크와 시스템 이벤트를 이용해 코드 변경 없이 서비스 관계와 일부 메트릭·트레이스를 얻을 수 있습니다. Cilium Hubble은 네트워크 흐름 가시성을 제공하고, Grafana Beyla 같은 도구는 지원되는 프로토콜과 런타임에서 자동 계측을 제공합니다.
코드를 수정하지 않고 얻을 수 있는 신호에도 범위가 있습니다. 비즈니스 의미, 사용자 ID를 제거한 도메인 속성, 모델 버전, 학습 데이터셋 ID 같은 정보는 애플리케이션이 가장 잘 알고 있으므로 수동 계측으로 보완할 수 있습니다. eBPF는 이런 수동 계측과 함께 사용하면서 레거시 워크로드와 네트워크 관측에서 부족한 정보를 보완하는 데 활용할 수 있습니다.
실습: 메트릭과 구조화 로그를 내보내는 앱
이 실습 앱은 다음 엔드포인트를 제공합니다.
/: HTTP 200과 JSON 로그 생성/fail: HTTP 500과 JSON 로그 생성/healthz: Kubernetes probe용 HTTP 200/metrics: Prometheus text exposition 형식 메트릭
1. 앱과 Kubernetes 리소스 배포
다음 내용을 telemetry-demo.yaml로 저장합니다.
apiVersion: v1
kind: Namespace
metadata:
name: observability-lab
---
apiVersion: v1
kind: ConfigMap
metadata:
name: telemetry-demo-code
namespace: observability-lab
data:
server.py: |
import json
import time
import uuid
from collections import Counter
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
REQUESTS = Counter()
DURATION_SUM = 0.0
DURATION_COUNT = 0
class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
return
def send_body(self, status, content_type, body):
encoded = body.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def do_GET(self):
global DURATION_SUM, DURATION_COUNT
if self.path == "/metrics":
lines = [
"# HELP demo_http_requests_total Requests handled by status.",
"# TYPE demo_http_requests_total counter",
f'demo_http_requests_total{{status="200"}} {REQUESTS["200"]}',
f'demo_http_requests_total{{status="500"}} {REQUESTS["500"]}',
"# HELP demo_http_request_duration_seconds Request duration summary.",
"# TYPE demo_http_request_duration_seconds summary",
f"demo_http_request_duration_seconds_sum {DURATION_SUM}",
f"demo_http_request_duration_seconds_count {DURATION_COUNT}",
]
self.send_body(200, "text/plain; version=0.0.4", "\n".join(lines) + "\n")
return
if self.path == "/healthz":
self.send_body(200, "text/plain", "ok\n")
return
started = time.perf_counter()
status = 500 if self.path == "/fail" else 200
message = "simulated failure" if status == 500 else "request completed"
body = json.dumps({"status": status, "message": message}) + "\n"
self.send_body(status, "application/json", body)
duration = time.perf_counter() - started
REQUESTS[str(status)] += 1
DURATION_SUM += duration
DURATION_COUNT += 1
record = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": "error" if status == 500 else "info",
"service": "telemetry-demo",
"path": self.path,
"status": status,
"duration_seconds": round(duration, 6),
"request_id": str(uuid.uuid4()),
"message": message,
}
print(json.dumps(record), flush=True)
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: telemetry-demo
namespace: observability-lab
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: telemetry-demo
template:
metadata:
labels:
app.kubernetes.io/name: telemetry-demo
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: python:3.13-slim-bookworm
command: ["python", "/app/server.py"]
env:
- name: PYTHONDONTWRITEBYTECODE
value: "1"
- name: PYTHONUNBUFFERED
value: "1"
ports:
- name: metrics
containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: metrics
livenessProbe:
httpGet:
path: /healthz
port: metrics
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 250m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: code
mountPath: /app
readOnly: true
volumes:
- name: code
configMap:
name: telemetry-demo-code
---
apiVersion: v1
kind: Service
metadata:
name: telemetry-demo
namespace: observability-lab
labels:
app.kubernetes.io/name: telemetry-demo
spec:
selector:
app.kubernetes.io/name: telemetry-demo
ports:
- name: metrics
port: 8080
targetPort: metricskubectl apply -f telemetry-demo.yaml
kubectl -n observability-lab rollout status deployment/telemetry-demo --timeout=5m
kubectl -n observability-lab get pod,service운영 이미지라면 python:3.13-slim-bookworm 같은 가변 태그 대신 검증한 digest를 사용합니다. 여기서는 관측 신호를 만드는 데 집중하기 위한 실습용 이미지입니다.
2. 정상과 실패 요청 생성
터미널 하나에서 port-forward를 실행합니다.
kubectl -n observability-lab port-forward service/telemetry-demo 8080:8080다른 터미널에서 요청을 만듭니다.
for _ in $(seq 1 20); do
curl --silent --output /dev/null http://127.0.0.1:8080/
done
for _ in $(seq 1 3); do
curl --silent --output /dev/null http://127.0.0.1:8080/fail
done
curl --fail --silent http://127.0.0.1:8080/metrics예상 메트릭의 핵심은 다음과 같습니다. /healthz는 비즈니스 요청 카운터에서 제외했으므로, 다른 요청이 없었다면 200 카운터는 20입니다.
demo_http_requests_total{status="200"} ...
demo_http_requests_total{status="500"} 3
demo_http_request_duration_seconds_count ...3. 로그와 Kubernetes 상태 확인
kubectl -n observability-lab logs deployment/telemetry-demo --tail=10
kubectl -n observability-lab logs deployment/telemetry-demo --tail=1 \
| python3 -m json.tool
kubectl -n observability-lab get deployment telemetry-demo \
-o jsonpath='{.status.readyReplicas}{" ready / "}{.status.replicas}{" desired\n"}'
kubectl -n observability-lab get events \
--sort-by='.lastTimestamp'로그에는 status, path, duration_seconds, request_id가 포함됩니다. 이 중 request_id는 개별 요청을 찾는 데 유용하지만 값이 매번 바뀌므로 메트릭 레이블이나 Loki 인덱스 레이블로 만들지 않습니다.
4. 지금 답할 수 없는 질문 찾기
현재 상태에서 답할 수 있는 질문은 다음과 같습니다.
kubectl logs로 어떤 요청이 실패했는가?/metrics로 누적 500 요청이 몇 개인가?- Deployment가 원하는 replica를 유지하는가?
하지만 다음 질문에는 아직 답할 수 없습니다.
- 시간에 따른 5분 오류율은 어떻게 변했는가?
- Pod가 삭제된 뒤에도 과거 로그를 검색할 수 있는가?
- 여러 서비스로 호출이 이어졌다면 어느 서비스가 느렸는가?
- 오류율이 임계치를 넘었을 때 누가 알림을 받는가?
이 “답할 수 없는 질문 목록”을 기준으로 다음에 추가할 구성을 정할 수 있습니다. 다음 글에서는 Prometheus가 메트릭을 시계열로 수집하고, Fluent Bit과 Loki가 Pod 수명과 분리해 로그를 보관하도록 구성합니다. Grafana에서는 두 신호를 한 화면에서 연결합니다. 트레이스에는 별도 계측과 백엔드가 필요하므로 다음 실습의 범위에는 포함하지 않습니다.
검증 체크리스트
telemetry-demoPod가 Ready 상태입니다./요청은 200,/fail은 500을 반환합니다./metrics가 Prometheus text 형식과 두 상태별 counter를 반환합니다.- 컨테이너 로그가 한 줄 JSON으로 출력됩니다.
- 로그의
request_id는 요청마다 다르고 메트릭 레이블에는 없습니다. - Service port 이름이
metrics입니다. 다음 글의 ServiceMonitor는 숫자가 아니라 이 이름을 참조합니다. - 현재 구성에 중앙 저장소와 trace가 없다는 한계를 설명할 수 있습니다.
문제 해결
| 증상 | 원인 후보 | 확인 방법 |
|---|---|---|
Pod가 ImagePullBackOff |
외부 Registry 접근 또는 이미지 정책 | kubectl describe pod의 Events 확인, 내부 미러가 있으면 이미지 주소 교체 |
Pod가 CrashLoopBackOff |
ConfigMap 코드 또는 보안 컨텍스트 오류 | kubectl logs --previous, ConfigMap mount와 Python 오류 확인 |
| port-forward 연결 실패 | Pod 미준비, 포트 충돌 | EndpointSlice와 로컬 8080 사용 프로세스 확인 |
/metrics는 보이지만 값이 늘지 않음 |
다른 Pod/포트로 요청, 경로 오류 | 같은 Service port-forward인지 확인하고 /, /fail 재호출 |
| JSON 로그가 여러 줄로 깨짐 | 앱이 stack trace나 multiline 출력 | 수집기 multiline parser와 앱 로깅 형식 설계 필요 |
kubectl logs에 과거 로그가 없음 |
Pod 교체·노드 로그 회전 | Kubernetes 로컬 로그의 수명 한계이며 중앙 로깅이 필요 |
kubectl top이 동작하지 않음 |
Metrics Server 미설치 | Metrics Server는 kubectl top/HPA용 리소스 API이며 Prometheus와 목적이 다름 |
운영 설계 원칙
먼저 SLI와 보존 목적을 정한다
“모든 것을 수집”한다는 목표만으로는 필요한 데이터와 보존 범위를 정하기 어렵습니다. 사용자 관점의 가용성, 지연, 품질을 SLI로 정하고 목표(SLO)와 오류 예산을 연결합니다. 원인 분석용 로그와 감사 기록은 보존 기간을 각각 정하고, 실시간 알림에 허용할 지연도 별도로 구분합니다.
카디널리티 예산을 둔다
메트릭 레이블과 Loki stream label의 가능한 조합 수를 관리합니다. namespace, cluster, 안정적인 service, 제한된 status는 보통 적합합니다. request_id, trace_id, user_id, 전체 URL, timestamp는 인덱스 레이블로 쓰지 않습니다. 고유값은 로그 본문, trace 속성, structured metadata에 둡니다.
민감정보는 수집 전에 줄인다
로그와 trace에는 인증 토큰, 쿠키, 비밀번호, 주민 식별정보, 원문 요청 본문을 남기지 않습니다. 애플리케이션 로깅 정책과 Collector/Fluent Bit processor에서 필터링하되, 사후 마스킹만 믿지 않습니다. 민감정보를 애초에 생성하지 않는 것이 가장 안전합니다.
관측 파이프라인도 관측한다
알림이 없는 상태에서도 데이터 수집이 계속되는지 확인해야 합니다. 이를 위해 수집기의 buffer 사용량, dropped records, exporter 실패, Prometheus scrape 실패, Loki ingest 오류를 모니터링합니다. 이렇게 해야 서비스가 정상인 경우와 수집이 끊긴 경우를 구분할 수 있습니다.
원시 신호와 파생 알림을 분리한다
대시보드 패널을 늘리는 것과 함께, 수집한 정보를 어떤 판단과 대응에 사용할지도 정해야 합니다. 기록 규칙으로 반복 집계를 표준화하고 알림에는 영향·담당자·즉시 행동·runbook 링크를 넣습니다. CPU가 높다는 신호를 받았을 때도 실제 사용자 오류율과 포화 상태를 함께 보고 즉시 대응이 필요한지 판단합니다.
요약
- 메트릭은 탐지와 추세, 로그는 사건 문맥, 트레이스는 분산 경로, 프로파일은 코드 수준 자원 원인을 보여줍니다.
- Kubernetes 관측은 노드와 오브젝트 상태뿐 아니라 애플리케이션의 RED·비즈니스 신호를 함께 봐야 합니다.
- 관측성은 수집 → 처리 → 저장 → 질의 → 시각화·알림으로 이어지는 파이프라인입니다.
- Prometheus pull, 로그 에이전트, OTLP push는 서로 대체하는 단일 방식이 아니라 신호와 워크로드 수명에 따라 조합합니다.
- OpenTelemetry는 계측과 전송을 표준화하지만 저장소와 올바른 도메인 계측을 대신하지 않습니다.
- 카디널리티와 민감정보를 수집 전에 통제해야 비용과 보안을 동시에 지킬 수 있습니다.
- 실습 앱은 중앙 수집 전에도 로그와
/metrics를 올바르게 내보내야 합니다. 다음 단계는 이 신호의 수명을 Pod 밖으로 확장하는 것입니다.
← 이전 글: Keycloak에서 Argo CD 권한까지: OIDC 그룹과 프로젝트별 RBAC
→ 다음 글: Prometheus·Grafana와 Loki·Fluent Bit로 관측성 스택 구축하기