DevOpsDevOps 학습 로드맵 · 7/13

Prometheus·Loki 관측 스택 구축과 장애 주입 실험 설계 절차

관측성(메트릭/로그/트레이스) 개념부터 kube-prometheus-stack 설치, Discord 알림 라우팅, Loki 로그 보존 정책, 노드 다운·OOMKill·디스크풀 장애 주입 실험의 실제 결과, 선택 확장인 OpenTelemetry 분산 트레이싱까지 - ServiceMonitor 셀렉터 함정, null 리시버 사고, Grafana 크래시루프 등 실전에서 겪은 버그 포함.

2026-07-1828 min read
#Prometheus#Grafana#Alertmanager#Loki#장애 주입#OpenTelemetry

목표

메트릭(Prometheus)·시각화(Grafana)·알림(Alertmanager)·로그(Loki)를 클러스터에 설치하고, 의도적으로 장애를 주입해 "알림이 실제로 오는지, 얼마나 걸리는지"를 검증한다. (선택 확장으로 분산 트레이싱까지 붙인다.)

개념: 관측성(Observability)이란 무엇인가

시스템이 커질수록 "지금 뭐가 잘못됐는지"를 눈으로 추측하는 게 불가능해진다. 관측성은 시스템이 내보내는 신호로 내부 상태를 외부에서 알아내는 능력이고, 보통 세 종류의 신호로 나뉜다 - 각각이 답하는 질문이 다르다.

신호답하는 질문이 시리즈의 도구
메트릭(Metrics)"지금 수치가 얼마인가" (에러율, 응답시간, CPU 등 시계열 숫자)Prometheus (수집) + Grafana (시각화)
로그(Logs)"그때 정확히 무슨 일이 있었나" (텍스트 기록)Loki
알림(Alerts)"지금 사람이 봐야 하는 상황인가" (메트릭이 임계치를 넘었을 때 통지)Alertmanager
(확장) 트레이스(Traces)"이 요청이 여러 서비스를 거치며 어디서 느려졌나"OpenTelemetry + Tempo (아래 확장 절 참고)

메트릭만으로는 "에러율이 올랐다"는 건 알아도 "왜"는 알 수 없고, 로그만 뒤지면 "왜"는 나오지만 "언제부터 이상했는지"를 미리 알 방법이 없다. 세 신호를 같이 둬야 서로의 빈틈을 메운다.

실제 구축 절차

1. kube-prometheus-stack 설치

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace \
  --set grafana.adminPassword=<시크릿으로 관리>

이 차트 하나로 Prometheus, Grafana, Alertmanager, node-exporter, kube-state-metrics가 함께 설치된다.

Prometheus는 왜 pull 방식인가: 애플리케이션이 메트릭을 어딘가로 쏘는 push 방식과 반대로, Prometheus는 등록된 타겟의 /metrics 엔드포인트를 자기가 주기적으로 찾아가서 긁어온다(scrape). 이 방식 덕분에 "타겟이 지금 응답 가능한가"라는 정보 자체가 공짜로 딸려온다 - 스크랩이 실패하면 그 타겟은 즉시 up=0으로 나타난다(뒤에서 다룰 두 버그 모두 이 up 값을 보고 잡아냈다). 어떤 엔드포인트를 스크랩 대상으로 등록할지는 ServiceMonitor라는 CRD로 선언하고, Prometheus Operator가 이를 실제 Prometheus 설정(scrape config)으로 변환해준다 - api-server/inference-worker/metrics를 이미 노출하고 있다면(시리즈 2), 아래처럼 등록한다.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api-server
  labels: {release: monitoring}     # kube-prometheus-stack의 셀렉터와 맞춰야 수집됨
spec:
  selector:
    matchLabels: {app: api-server}
  endpoints:
    - port: http
      path: /metrics

Grafana Kubernetes/Networking/Namespace(Pods) 대시보드 - jobs 네임스페이스의 api-server·inference-worker·rabbitmq·postgres 파드가 실제로 수집되고 있다

2. Alertmanager - 알림 라우팅

Alertmanager가 하는 일 - 라우팅과 그룹핑: Prometheus 자체는 PrometheusRule의 조건식이 참이 되면 그 알림을 "발화(firing)" 상태로 표시만 한다. 그 알림을 누구에게(어떤 채널로), 어떻게 묶어서, 얼마나 자주 보낼지를 결정하는 게 Alertmanager다. 알림은 route 트리를 위에서부터 타고 내려가며 matchers 조건에 맞는 첫 리시버로 배정되고(맨 위 route가 아무 것도 안 걸리는 알림을 받는 기본 리시버 역할을 한다), 같은 라벨로 묶인 알림들은 group_wait 동안 대기했다가 한 번에 발송된다.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: api-server-alerts
  labels: {release: monitoring}
spec:
  groups:
    - name: api-server
      rules:
        - alert: HighErrorRate
          expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
          for: 2m
          labels: {severity: warning}
          annotations:
            summary: "api-server 5xx 비율이 5% 초과"

Slack/이메일도 되지만, 실제로는 Discord 웹훅으로 붙였다 - 웹훅 URL 끝에 /slack만 붙이면 Alertmanager의 기본 slack_configs 리시버를 그대로 재사용할 수 있다(Discord가 Slack 호환 포맷을 지원한다). 웹훅 URL은 k8s Secret으로 저장해 alertmanagerSpec.secrets로 마운트하고 api_url_file로 참조하면 원본 URL이 git에 올라가지 않는다.

# alertmanager config
route:
  receiver: "null"          # kube-prometheus-stack 기본 Watchdog(헬스체크)용 - 반드시 유지
  routes:
    - receiver: discord
      matchers: [severity =~ "warning|critical"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 3h
receivers:
  - name: "null"
  - name: discord
    slack_configs:
      - api_url_file: /etc/alertmanager/secrets/discord-webhook/url
        channel: "#alerts"

group_wait/group_interval이 "장애 알림이 늦게 오는" 원인이 되는 경우가 많다 - 값이 크면 초기 알림이 그만큼 지연된다. 실험 단계에서 이 값을 조정하며 지연 시간을 직접 재본다.

이렇게 붙이면 실제로 디스코드 채널에 FIRING/RESOLVED 알림이 그대로 온다 - 아래는 실제 수신 화면이다(KubePodNotReady, PodOOMKilled 등 실제 발화 건 포함).

Discord 채널에 도착한 Alertmanager 알림 - FIRING/RESOLVED 쌍으로 오며, KubePodNotReady·PodOOMKilled(tempo-0) 등 실제 발화 건이 보인다

3. Loki - 로그 수집 + 보존 정책

Loki는 로그를 왜 다르게 저장하나: Elasticsearch 같은 전통적인 로그 시스템은 로그 본문 전체를 색인(풀텍스트 인덱스)해서 빠르게 검색하는 대신, 그 인덱스 자체가 로그량에 비례해 커진다. Loki는 반대로 로그 본문은 색인하지 않고, namespace/pod/app 같은 라벨만 인덱싱한다 - 실제 검색은 라벨로 좁힌 스트림 안에서 grep하듯 순차적으로 훑는다. 인덱스가 훨씬 작아지는 대신, 라벨 종류를 신중하게 골라야 한다(요청 ID처럼 값이 무한히 다양한 걸 라벨로 쓰면 스트림 수가 폭발한다 - Prometheus의 카디널리티 문제와 같은 함정). Promtail이 각 노드의 로그를 라벨과 함께 Loki로 밀어 넣고(push - Prometheus의 pull과 반대 방향이다), Grafana가 LogQL로 조회하는 구조다.

helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
  --namespace monitoring \
  --set promtail.enabled=true

보존 기간은 limits_config에서 설정한다 - 이 값 자체가 나중에 ISMS-P 2.9(운영관리) 증적 자료가 된다.

limits_config:
  retention_period: 720h   # 30일
compactor:
  retention_enabled: true
  retention_delete_delay: 2h

Grafana Explore의 Loki 쿼리 - jobs 네임스페이스 로그, RabbitMQ AMQP 연결·헬스체크 요청이 실시간으로 보인다

4. 장애 주입 실험 설계 (3종 이상 택)

카오스 엔지니어링이란: 장애가 언젠가 난다는 걸 전제로, 실제로 터지기 전에 통제된 환경에서 미리 장애를 일으켜보고 시스템과 사람(알림·대응 절차)이 제대로 반응하는지 검증하는 방법론이다. 목적은 "장애를 일으키는 것"이 아니라 **"장애가 났을 때 탐지까지 얼마나 걸리는지를 미리 재보는 것"**이다 - 이 시리즈의 모든 실험은 그 시간을 재는 게 진짜 목적이다.

각 실험은 주입 → 관측 → 기록의 순서로 진행하고, 시작 시각을 먼저 기록해야 나중에 "알림까지 걸린 시간"을 정확히 잴 수 있다.

노드 다운

# 실험 대상 워커 노드에서
sudo systemctl stop kubelet
# 또는 VM 자체를 강제 종료

# 관측할 것
# - kubectl get nodes 에서 NotReady로 바뀌는 시점
# - 해당 노드의 파드가 다른 노드로 재스케줄되기까지 걸리는 시간 (기본 pod-eviction-timeout 5분)
# - Alertmanager 알림이 Discord에 도착하는 시점

OOMKill

리눅스 커널은 메모리가 부족해지면 임의로 프로세스를 골라 강제 종료해서 시스템 전체가 멎는 걸 막는다 - 이 메커니즘을 OOM(Out Of Memory) Killer라 부른다. 컨테이너에 메모리 limits를 걸어두면, 그 컨테이너가 한도를 넘었을 때 커널이 호스트 전체 대신 그 컨테이너만 골라 종료한다(OOMKilled).

# 의도적으로 낮은 메모리 제한을 건 테스트 파드
resources:
  limits: {memory: "64Mi"}
kubectl run oom-test --image=polinux/stress --limits=memory=64Mi -- \
  stress --vm 1 --vm-bytes 256M
# kubectl describe pod oom-test 에서 OOMKilled 상태 확인
# Prometheus의 container_oom_events_total 메트릭에 반영되는 시점 확인

디스크 풀

# 워커 노드에서 디스크를 의도적으로 채움
fallocate -l 10G /tmp/fillfile
# kubelet의 disk pressure 감지 → 노드가 파드 축출(evict)을 시작하는지 확인
# node-exporter의 node_filesystem_avail_bytes 알림이 임계치를 넘는 시점 확인

RabbitMQ 장애

kubectl delete pod rabbitmq-0 --grace-period=0 --force
# 시리즈 2에서 만든 ack/nack 파이프라인이 이 상황에서 실제로 메시지를 보존하는지 재검증

실제로 실행한 결과

#실험탐지 시간비고
1RabbitMQ 파드 강제삭제알림 없음 (46초 내 자기치유)worker 재연결 로직이 임계치 도달 전에 정상 동작 - 이것도 유효한 결과다
2OOMKillkubectl 기준 9초, 메트릭 알림은 못 잡음container_oom_events_total이 재시작 없이 죽는 파드는 못 잡는다는 한계 발견 → kube_pod_container_status_last_terminated_reason 기반 규칙으로 교체
3노드 다운(kubelet 중지)약 2분(NotReady)IP 착각으로 의도와 다른 노드에 주입했지만, 조기 발견 후 3분 만에 복구(축출 타임아웃 전이라 서비스 영향 없음)
4디스크 풀13초(DiskPressure)kubelet의 자동 image GC가 축출 전에 스스로 ~5GB를 회수하는 것까지 실측 확인

4개 전부 디스코드로 알림이 도착했다(1번은 애초에 임계치를 안 넘어서 알림이 없는 게 맞는 동작이다). 2번에서 알림 규칙이 실제 장애 유형을 못 잡는 걸 발견하고 교체한 게 이 실험의 핵심 소득이다 - "장애를 일으켜봤다"가 아니라 "탐지 공백을 찾아 메꿨다"가 진짜 목적이라는 걸 보여주는 사례다.

5. 포스트모템 문서 템플릿

실험마다 아래 형식으로 기록한다.

# 포스트모템: <실험명> (YYYY-MM-DD)

## 타임라인
- 00:00 장애 주입 (구체적 명령/조작)
- 00:XX 첫 이상 신호 감지 (메트릭/로그상 변화)
- 00:XX 알림 도착 (Discord 등)
- 00:XX 자동/수동 복구 완료

## 원인
(장애가 실제로 무엇 때문에 발생했는지)

## 탐지 공백
(장애 발생부터 알림 도착까지 얼마나 걸렸는지, 왜 그만큼 걸렸는지)

## 재발 방지
(알림 규칙 조정, 임계치 변경, 자동 복구 추가 등 - 실행한 조치)

검증 방법

각 실험에서 반드시 남겨야 하는 숫자 3가지: 장애 발생 시각, 첫 메트릭/로그 이상 시각, 알림 도착 시각. 이 세 시각의 차이가 곧 "탐지 공백"이고, 다음 단계(알림 규칙 튜닝)의 기준값이 된다.

(선택 확장) OpenTelemetry로 분산 트레이싱 붙이기

분산 트레이싱이란: 요청 하나가 api-server → RabbitMQ → inference-worker → PostgreSQL을 거치는 이 프로젝트에서, 메트릭과 로그만으로는 "이 요청이 정확히 어느 구간에서 느려졌는지"를 알기 어렵다 - 각 서비스의 로그를 시간으로 짐작해서 이어 붙여야 한다. 트레이스는 요청 하나에 고유 ID(trace ID)를 붙여서, 그 요청이 거친 모든 서비스의 처리 구간(span)을 하나로 이어 보여준다.

트레이스는 하나의 트리 구조다 - 요청이 들어온 지점(root span) 아래로, 그 요청이 호출한 하위 작업마다 자식 span이 매달린다. 각 span은 시작·종료 시각과 소속 서비스 이름을 갖고 있어서, 트레이스 하나를 펼쳐보면 "전체 요청 중 어느 span이 가장 오래 걸렸는지"가 막대그래프처럼 한눈에 보인다. 문제는 이 span들이 서로 다른 프로세스(api-server, inference-worker)에서, 그것도 RabbitMQ라는 비동기 경계를 넘어 만들어진다는 점이다 - 그래서 trace ID와 현재 span ID를 표준 포맷(W3C Trace Contexttraceparent 헤더)으로 실어 날라야, 받는 쪽이 "새 트레이스를 시작하는 게 아니라 기존 트레이스에 이어붙인다"는 걸 알 수 있다.

**OpenTelemetry(OTel)**는 이 계측(span 생성, traceparent 주입·추출)을 위한 표준 규격이고, Tempo는 그 트레이스 데이터를 저장·조회하는 백엔드다.

helm repo add grafana https://grafana.github.io/helm-charts
helm install tempo grafana/tempo --namespace monitoring
# api-server - OTel 계측 최소 예시
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="tempo.monitoring:4317")))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

@app.post("/jobs")
def create_job(payload: JobCreate):
    with tracer.start_as_current_span("create_job"):
        job_id = save_job_as_pending(payload)
        publish_to_queue("jobs.upload", {"job_id": job_id, "trace_id": trace.get_current_span().get_span_context().trace_id})
        return {"job_id": job_id, "status": "PENDING"}

메시지를 큐에 넣을 때 trace_id를 함께 실어 보내면, inference-worker가 그 trace_id로 이어서 span을 기록할 수 있다 - 큐를 거치는 비동기 구간까지 하나의 트레이스로 연결하는 게 핵심이다.

검증: 실제 job 하나를 제출해 Tempo에서 trace를 조회해보면, api-server(5 span)와 inference-worker(1 span)가 같은 트레이스로 묶여 있는 걸 확인할 수 있다 - POST /jobs(루트) → create_job(api-server) → process_job(inference-worker) 순서로 타임스탬프를 보면, 발행부터 워커가 픽업하기까지 약 215ms 간격까지 한 화면에서 보인다. 로그를 시간으로 짐작해 잇던 것보다 훨씬 빠르고 정확하게 "어디서 지연이 생기는지"를 확인할 수 있었다.

Tempo Explore의 api-server 트레이스 테이블 - 실제로는 이 캡처 시점에 Tempo가 메모리 부족으로 OOMKilled돼 있어 우측 상단에 "Failed to connect to Tempo" 에러가 함께 떠 있다(Alertmanager 절의 PodOOMKilled 사건과 동일 건)

Tempo 자체도 관측 대상이라는 걸 그대로 보여주는 장면이다 - 과거 트레이스는 남아있어 테이블 조회는 되지만, 실시간 조회는 막혀있다. 이게 앞서 Alertmanager 절에서 다룬 PodOOMKilled(pod=tempo-0) 알림의 실제 원인이고, 메모리 한도를 512Mi→1Gi로 올리면서 해결됐다.

(선택 확장, 보류) Exemplar 연결·Tempo metrics-generator: Prometheus 메트릭의 특정 지점(예: http_request_duration_seconds의 한 버킷)을 클릭하면 그 지연에 실제로 기여한 트레이스로 바로 넘어가는 exemplar 연결이나, span 데이터에서 RED 메트릭(rate/error/duration)을 별도 계측 없이 자동으로 뽑아주는 Tempo metrics-generator는 지금 단계에서는 하지 않는다. 메트릭도 있고 트레이스도 있는 지금 상태에서 이건 "그 사이 클릭 한 번을 아끼는" 편의 기능에 가깝고, 이 랩 규모에서는 지표·트레이스를 따로 찾아봐도 큰 불편이 없다. 반면 도입하면 Prometheus exemplar storage 활성화, Tempo metrics-generator 설정, remote_write 배선까지 손댈 게 늘어난다 - 편의 대비 설정 비용이 아직은 맞지 않아 보류한다.

실전에서 겪은 버그 모음

여기까지 절차대로 따라가면 되는 부분이었고, 아래는 실제로 이 스택을 구축·운영하면서 순서대로 마주친 버그와 사건들이다.

설치 직후 실제로 겪은 버그 2가지 (kube-prometheus-stack)

1) ServiceMonitor를 만들었는데 타겟을 못 찾음: api-server Service에 포트 이름(http)도 붙였고 셀렉터도 맞는 것 같은데 up{job="api-server"}가 계속 빈 결과였다. /metrics 자체는 정상 응답하고 있어서 "네트워크는 멀쩡한데 왜 안 잡히지"로 한참 헤맸는데, 원인은 ServiceMonitorselector가 Service의 spec.selector(파드를 고르는 필드)가 아니라 metadata.labels로 매칭한다는 점이었다 - 완전히 다른 필드다. Service manifest엔 metadata.labels가 아예 없이 spec.selector만 있었던 게 원인. metadata.labels: {app: api-server}를 추가하니 바로 잡혔다. Prometheus Operator의 config(/api/v1/status/config)를 직접 까봐야 relabel 단계에서 정확히 걸러지는 지점을 확인할 수 있었다.

2) 컨트롤플레인(etcd·controller-manager·scheduler)이 전부 up=0: up{job=~"kube-etcd|kube-controller-manager|kube-scheduler"}를 조회하면 세 컴포넌트가 전부 0으로 나오는 경우가 있다 - etcdInsufficientMembers/KubeControllerManagerInstanceUnreachable/KubeSchedulerInstanceUnreachable 알림이 계속 새고 있다면 이게 원인이다.

Prometheus에서 up 쿼리 결과 - kube-etcd·kube-controller-manager·kube-scheduler만 0, kubelet 등 나머지는 전부 1

kubeadm 기본 설정이 이 세 컴포넌트의 메트릭 엔드포인트를 127.0.0.1에만 바인딩해두기 때문에, 파드 네트워크에서 접근하는 Prometheus가 영영 스크랩하지 못하는 흔한 문제다. 정적 파드 매니페스트(/etc/kubernetes/manifests/{kube-controller-manager,kube-scheduler,etcd}.yaml)를 고쳐야 한다 - controller-manager/scheduler는 --bind-address=0.0.0.0으로, etcd는 --listen-metrics-urls에 노드 사설 IP를 추가한다. 수정하면 kubelet이 감지해서 해당 정적 파드만 재시작한다(다른 워크로드는 영향 없음). 다음 클러스터를 새로 구축할 때부터 재발하지 않도록, kubeadm init 직후 이 수정을 적용하는 태스크를 Ansible 플레이북에 idempotent하게 넣어두면 된다.

Alertmanager 운영하며 실제로 있었던 일 3가지

1) 리시버 목록을 통째로 바꿨더니 알림이 조용히 끊김: receivers 목록을 실제 채널(discord) 하나로 통째로 바꿨더니, kube-prometheus-stack 기본 Watchdog(항상 발화하는 헬스체크용 알림)의 서브라우트가 참조하는 "null" 리시버가 사라져서 Alertmanager Operator의 reconcile 자체가 계속 실패했다(undefined receiver "null" used in route). 겉으로는 아무 에러도 안 보이고 그냥 예전 설정이 계속 살아있어서, 테스트 알림을 몇 번을 보내봐도 왜 안 오는지 한참 헤맸다 - Operator 로그를 직접 까본 뒤에야 원인을 확인했다. "null" 리시버는 지우면 안 되고 실제 채널과 같이 유지해야 한다.

Alertmanager 리시버 목록 - discord(실제 알림)와 null(Watchdog 전용)이 나란히 존재

2) KubeProxyDown 오탐: 이 클러스터는 Cilium이 kube-proxy를 완전히 대체해서 kube-proxy 자체가 애초에 존재하지 않는데, kube-prometheus-stack 기본 규칙이 그걸 모르고 계속 "다운"이라고 오탐했다. kubeProxy.enabled: false + defaultRules.rules.kubeProxy: false로 해당 스크랩·알림 자체를 꺼서 해결했다.

3) PodOOMKilled(tempo-0) 실제 장애: 테스트가 아니라 진짜 장애였다 - Tempo가 메모리 한도 512Mi를 넘어 이미 2번 OOMKilled된 상태였다. 한도를 1Gi로 올리자 재시작 없이 안정화됐다(위 Tempo 절의 사건과 동일 건).

Alertmanager Alerts 화면 - KubeProxyDown(오탐), Watchdog(null), PodOOMKilled(pod=tempo-0, 실제 장애)가 함께 보인다

Loki 붙이면서 실제로 겪은 버그 2가지

1) Grafana가 211번 재시작 크래시루프: "Explore에 Loki가 안 보인다"는 보고를 쫓다가, Grafana 파드가 17시간째 CrashLoopBackOff(211회 재시작) 상태였던 걸 발견했다. 로그를 보니 Only one datasource per organization can be marked as default - loki-stack 차트가 기본값(grafana.sidecar.datasources.enabled=true)으로 자체 Loki 데이터소스를 또 만들면서 isDefault: true를 하드코딩했는데, kube-prometheus-stack이 이미 Prometheus를 isDefault: true로 등록해둔 것과 충돌해 Grafana 자체가 못 뜨고 있었다. grafana.sidecar.datasources.enabled: false로 중복 생성을 끄고 충돌 ConfigMap을 지우니 정상화됐다.

2) 로그 볼륨 패널이 계속 파싱 에러: Explore의 "Logs volume" 히스토그램이 bad_data: ... unexpected IDENTIFIER를 냈다. 쿼리의 빈 라인필터(|= "")가 원인인가 싶었지만 필터를 지워도 재현됐다. 실제 원인은 설치돼있던 Loki(loki-stack 차트 고정 버전 2.6.1)와 Grafana(13.1.1)의 버전 차이 - Grafana의 최신 "Logs volume" 기능이 2.6.1이 모르는 LogQL 문법으로 쿼리를 보내서 항상 파싱에 실패한 것이었다. 로그 검색 자체(Explore에서 로그 직접 보기)는 문제없이 동작하고 있었다. deprecated된 loki-stack 대신 최신 grafana/loki(3.6.8) + 별도 grafana/promtail 차트로 교체 설치해 근본적으로 해결했다(보존기간이 30일이라 며칠 치 로그 유실은 감수).

OTel 계측을 붙이면서 실제로 만난 버그 2가지

1) pkg_resources가 없어서 크래시루프: opentelemetry-instrumentation-fastapi가 내부적으로 pkg_resources(setuptools)를 임포트하는데, python:3.12-slim 최신 이미지의 pip는 setuptools를 기본 포함하지 않아 ModuleNotFoundError로 그대로 크래시했다. requirements.txtsetuptools를 명시적으로 추가해서 해결.

2) 의존성을 추가할 때마다 새 CVE로 Harbor push가 막힘: OTel 패키지를 추가하니 CVE-2026-0994가, setuptools를 추가하니 CVE-2025-47273이 새로 검출돼 매번 Harbor의 prevent_vul 정책(시리즈 2)에 걸렸다. 새 의존성을 추가할 때마다 이 절차(allowlist 갱신)가 반복된다는 걸 전제로 잡아야 한다.

체크리스트

  • kube-prometheus-stack 설치, 서비스 메트릭 수집 (ServiceMonitor는 metadata.labels 매칭 확인)
  • 컨트롤플레인(etcd/controller-manager/scheduler) 스크랩 바인딩 확인 (--bind-address=0.0.0.0)
  • Alertmanager 알림 라우팅 (Discord/Slack 등), 기본 "null" 리시버 유지
  • kube-proxy를 안 쓰는 클러스터(Cilium 등)라면 관련 스크랩·알림 규칙 비활성화
  • Loki 로그 수집 + 보존 기간 정책 설정, Grafana 데이터소스 중복 생성(sidecar) 여부 확인
  • 장애 주입 실험 3종 이상 실행 (노드 다운/OOMKill/디스크풀/RabbitMQ 중 택3)
  • 실험마다 포스트모템 문서 작성, 알림 규칙이 실제로 그 장애 유형을 잡는지 검증
  • (선택 확장) OpenTelemetry + Tempo로 분산 트레이싱 연결, 장애 실험 때 활용도 비교