K8s 실전 #5 모니터링,알람 — Prometheus / CloudWatch / Alertmanager

9 분 소요

K8s 실전 시리즈의 다섯 번째 글입니다. #4까지 거쳐 myshop-api는 새 버전이 들어오는 흐름까지 자동화됐지만, 운영 단계의 절반은 그 동작을 관측하는 일입니다. CPU,메모리,요청 latency,에러율이 어디서 어떻게 변하는지가 보이지 않으면, 카나리 자동 promote도 불가능하고 사고 대응도 늦습니다. 이번 글은 옵저버빌리티 스택을 EKS 위에 얹는 흐름입니다. 고급 #5에서 다룬 표준 스택(Prometheus + Grafana + Loki + Alertmanager)을 EKS 환경에 맞춰 구체화하고, AWS 매니지드 옵션인 CloudWatch Container Insights와의 결합도 함께 살펴보겠습니다.

이번 시리즈는 K8s 실전 6편입니다.

두 축의 결합 — in-cluster Prometheus + 매니지드 CloudWatch #

EKS 환경의 옵저버빌리티는 보통 두 축의 결합으로 이뤄집니다.

책임
In-cluster (Prometheus + Grafana + Loki)워크로드 메트릭, 비즈니스 메트릭, 알람, 대시보드
CloudWatch (Container Insights + Logs)AWS 관리형 메트릭, 로그 장기 보관, AWS 콘솔 통합

둘 중 하나만 쓰는 방식도 가능하지만, 운영 클러스터의 표준은 둘의 결합입니다. Prometheus가 운영 메트릭과 알람의 기준 소스이고, CloudWatch가 장기 보관과 AWS 자체 자원(RDS, ALB, EBS) 메트릭의 통합 지점입니다. AWS의 매니지드 Prometheus(AMP)와 매니지드 Grafana(AMG)가 in-cluster 운영 부담을 줄이는 옵션으로 자리 잡고 있지만, 이번 글에서는 가장 흔한 in-cluster 모델을 중심으로 살펴보겠습니다.

kube-prometheus-stack — 한 번에 깔리는 표준 묶음 #

고급 #5에서 짚었던 표준 Helm 차트입니다. 한 명령에 Prometheus + Grafana + Alertmanager + kube-state-metrics + node-exporter + Prometheus Operator의 CRD가 모두 들어옵니다.

설치 #

kube-prometheus-stack 설치
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack \
  -n monitoring --create-namespace \
  --values prometheus-values.yaml
prometheus-values.yaml — 핵심 부분
prometheus:
  prometheusSpec:
    retention: 30d
    retentionSize: "50GB"

    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 100Gi

    serviceMonitorSelectorNilUsesHelmValues: false
    podMonitorSelectorNilUsesHelmValues: false
    ruleSelectorNilUsesHelmValues: false

    additionalScrapeConfigs:
      - job_name: ec2-spot-instance
        ec2_sd_configs:
          - region: ap-northeast-2

    remoteWrite:
      - url: https://aps-workspaces.ap-northeast-2.amazonaws.com/workspaces/ws-xxx/api/v1/remote_write
        sigv4:
          region: ap-northeast-2

grafana:
  adminPassword: ""
  ingress:
    enabled: true
    ingressClassName: alb
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
      alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
    hosts:
      - grafana.myshop.example.com
  persistence:
    enabled: true
    storageClassName: gp3
    size: 10Gi

alertmanager:
  alertmanagerSpec:
    storage:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          resources:
            requests:
              storage: 10Gi
  config:
    route:
      receiver: default
    receivers:
      - name: default

핵심 설정 몇 가지를 짚겠습니다.

  • retention: 30d + storageSpec — 메트릭 30일 보관 + EBS 100GB. 30일 이상 보관하려면 remoteWrite로 AMP나 Thanos에 같이 보냄.
  • serviceMonitorSelectorNilUsesHelmValues: false — 모든 네임스페이스의 ServiceMonitor를 자동 인식. myshop 네임스페이스의 ServiceMonitor가 monitoring 네임스페이스 없이도 작동.
  • remoteWrite — AWS Managed Prometheus(AMP)에 메트릭 long-term 저장. 30일 이상 분석이 필요한 경우.

설치 직후 점검 #

기본 헬스 체크
kubectl get pods -n monitoring
kubectl get servicemonitors -A
kubectl get prometheusrules -A
기대 출력
NAME                                                 READY   STATUS    RESTARTS
prometheus-grafana-xxx                               3/3     Running   0
prometheus-kube-prometheus-operator-xxx              1/1     Running   0
prometheus-kube-state-metrics-xxx                    1/1     Running   0
prometheus-prometheus-kube-prometheus-prometheus-0   2/2     Running   0
prometheus-prometheus-node-exporter-xxx              1/1     Running   0
alertmanager-prometheus-kube-prometheus-alertmanager-0  2/2  Running   0

기본 PrometheusRule이 약 100여 개 자동으로 들어옵니다. 노드 다운, etcd 장애, kubelet 문제 같은 K8s 자체의 알람이 그 안에 미리 정의되어 있어서, 클러스터 사고는 별도 작업 없이 바로 알람이 됩니다.

myshop-api에 메트릭 노출 추가 #

표준 스택이 설치된 클러스터에 myshop-api의 메트릭을 노출하는 한 사이클입니다.

1. 애플리케이션이 /metrics를 노출 #

Prometheus 클라이언트 라이브러리가 거의 모든 언어에 있습니다. Python(FastAPI)이라면 다음 한 줄로 시작합니다.

myshop-api/main.py — Prometheus 메트릭 노출
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator

app = FastAPI()
Instrumentator().instrument(app).expose(app)

이 한 줄이 다음 메트릭을 자동으로 노출합니다.

  • http_requests_total{handler, method, status} — 요청 카운터
  • http_request_duration_seconds_bucket{handler, method} — latency 히스토그램
  • http_request_size_bytes / http_response_size_bytes — 페이로드 크기
  • 표준 Python runtime 메트릭(GC, threads, memory)

도메인 메트릭(예: 주문 생성 카운터, 결제 성공률)은 그 위에 추가합니다.

도메인 메트릭 추가
from prometheus_client import Counter, Histogram

orders_created = Counter(
    "myshop_orders_created_total",
    "Total orders created",
    ["status"]
)

checkout_duration = Histogram(
    "myshop_checkout_duration_seconds",
    "Checkout flow duration"
)

2. ServiceMonitor 매니페스트 #

Prometheus Operator가 watch할 ServiceMonitor를 만듭니다.

charts/myshop-api/templates/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: {{ include "myshop-api.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    app.kubernetes.io/name: myshop-api
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: myshop-api
  endpoints:
    - port: http
      interval: 30s
      path: /metrics

이 매니페스트가 적용된 순간부터 Prometheus가 30초마다 myshop-api의 모든 Pod의 /metrics를 긁어 오기 시작합니다. Grafana의 Explore에서 http_requests_total{namespace="myshop"}로 데이터가 보이면 메트릭 수집이 정상입니다.

4 golden signals — 알람 룰셋의 골격 #

고급 #5에서 다룬 4 golden signals(Latency / Traffic / Errors / Saturation)를 myshop-api의 PrometheusRule로 적습니다.

charts/myshop-api/templates/prometheusrule.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: {{ include "myshop-api.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    release: prometheus
spec:
  groups:
    - name: myshop-api.golden-signals
      interval: 30s
      rules:
        # Errors — 5xx 비율
        - alert: MyshopApiHighErrorRate
          expr: |
            sum(rate(http_requests_total{app="myshop-api",status=~"5.."}[5m]))
              / sum(rate(http_requests_total{app="myshop-api"}[5m])) > 0.05
          for: 5m
          labels:
            severity: critical
            team: backend
          annotations:
            summary: "myshop-api 5xx rate > 5% ({{ "{{ $value | humanizePercentage }}" }})"
            description: "5xx 비율이 5% 넘은 상태가 5분 이상 유지됨."
            runbook_url: "https://runbooks.myshop.example.com/myshop-api-5xx"

        # Latency — P95
        - alert: MyshopApiHighLatencyP95
          expr: |
            histogram_quantile(0.95,
              sum by (le) (rate(http_request_duration_seconds_bucket{app="myshop-api"}[5m]))
            ) > 1.0
          for: 10m
          labels:
            severity: warning
            team: backend
          annotations:
            summary: "myshop-api P95 latency > 1s ({{ "{{ $value | printf \"%.2f\" }}" }}s)"

        # Traffic — 트래픽 급감 (downstream 장애 신호)
        - alert: MyshopApiTrafficDrop
          expr: |
            sum(rate(http_requests_total{app="myshop-api"}[5m]))
              < 0.3 * sum(rate(http_requests_total{app="myshop-api"}[5m] offset 1h))
          for: 10m
          labels:
            severity: warning
            team: backend
          annotations:
            summary: "myshop-api traffic 급감 (지난 1시간 대비 30% 미만)"

        # Saturation — Pod 메모리 사용률
        - alert: MyshopApiPodMemoryHigh
          expr: |
            sum by (pod) (
              container_memory_working_set_bytes{namespace="myshop",pod=~"myshop-api-.*"}
            ) / sum by (pod) (
              kube_pod_container_resource_limits{namespace="myshop",pod=~"myshop-api-.*",resource="memory"}
            ) > 0.85
          for: 10m
          labels:
            severity: warning
            team: backend
          annotations:
            summary: "myshop-api Pod memory > 85% of limit"

각 룰의 핵심 패턴 셋을 짚겠습니다.

  • for 기간 — 짧은 스파이크에 알람이 울리지 않도록 5~10분의 지속 시간 요구.
  • severity 라벨critical은 즉시 호출, warning은 다음 영업일에 검토. Alertmanager 라우팅의 키.
  • runbook_url — 알람을 받은 사람이 즉시 따라갈 수 있는 대응 절차 문서입니다. 알람 한 건 = 명확한 대응 한 가지라는 원칙을 따릅니다.

Alertmanager 라우팅 — Slack과 PagerDuty의 분기 #

알람의 흐름은 Prometheus → Alertmanager → 채널입니다. Alertmanager가 라벨을 보고 라우팅을 결정합니다.

alertmanager.yaml — 운영 라우팅
route:
  receiver: default
  group_by: ['alertname', 'team', 'namespace']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

  routes:
    - matchers:
        - severity = "critical"
      receiver: pagerduty-backend
      continue: true
      routes:
        - matchers:
            - severity = "critical"
            - team = "backend"
          receiver: pagerduty-backend
        - matchers:
            - severity = "critical"
            - team = "platform"
          receiver: pagerduty-platform

    - matchers:
        - severity = "warning"
      receiver: slack-warnings
      group_wait: 1m
      repeat_interval: 12h

receivers:
  - name: default
    slack_configs:
      - api_url: ${SLACK_WEBHOOK}
        channel: '#alerts'

  - name: slack-warnings
    slack_configs:
      - api_url: ${SLACK_WEBHOOK}
        channel: '#alerts-warning'
        title: '⚠️  {{ "{{ .GroupLabels.alertname }}" }}'

  - name: pagerduty-backend
    pagerduty_configs:
      - service_key: ${PAGERDUTY_BACKEND_KEY}

  - name: pagerduty-platform
    pagerduty_configs:
      - service_key: ${PAGERDUTY_PLATFORM_KEY}

inhibit_rules:
  - source_matchers: [severity = "critical"]
    target_matchers: [severity = "warning"]
    equal: [alertname, namespace]

핵심 패턴 셋입니다.

  • severity별 분기 — critical은 PagerDuty로 호출, warning은 Slack으로 통지.
  • team별 분기 — 같은 critical도 backend / platform 팀 따로 호출.
  • inhibit_rules — 같은 alertname의 critical이 울리는 동안 같은 namespace의 warning은 묶어서 무음 처리. 알람 폭주 방지.

비밀(SLACK_WEBHOOK, PAGERDUTY_*)은 #3에서 다룬 External Secrets로 주입합니다.

Loki — 로그 스택 추가 #

메트릭 외에 로그도 함께 확보하는 것이 표준입니다. 고급 #5에서 본 Loki 스택을 그대로 적용하겠습니다.

Loki + Promtail Helm 설치
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
  -n monitoring \
  --set promtail.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.storageClassName=gp3 \
  --set loki.persistence.size=100Gi

설치 후 Grafana에 Loki 데이터 소스가 자동으로 추가되고, Explore에서 LogQL 쿼리가 가능해집니다.

myshop-api의 ERROR 로그
{namespace="myshop", app="myshop-api"} |= "ERROR"
에러율 메트릭으로 환산 (Loki → 메트릭처럼)
sum(rate({namespace="myshop", app="myshop-api"} |= "ERROR" [5m]))

장기 보관을 S3에 두려면 Loki의 storage backend를 S3로 설정합니다. 표준 운영 셋업입니다.

CloudWatch Container Insights — 두 번째 축 #

같은 EKS 클러스터에 CloudWatch Container Insights를 함께 설치하면 AWS 콘솔에서 클러스터,노드,Pod,컨테이너 메트릭을 즉시 확인할 수 있습니다. 운영 팀이 AWS 콘솔에 익숙하다면 일상 점검 부담이 줄어듭니다.

CloudWatch Container Insights — Helm
helm repo add aws-observability https://aws-observability.github.io/helm-charts
helm install amazon-cloudwatch-observability \
  aws-observability/amazon-cloudwatch-observability \
  -n amazon-cloudwatch --create-namespace \
  --set clusterName=myshop-prod \
  --set region=ap-northeast-2

이 차트가 Fluent Bit을 DaemonSet으로 띄워 각 노드의 stdout/stderr를 CloudWatch Logs로 보내고, CloudWatch Agent로 메트릭도 함께 수집합니다.

Fluent Bit의 역할 #

Fluent Bit이 노드의 /var/log/containers/를 읽어 다음 두 곳에 라우팅하는 게 표준입니다.

Fluent Bit의 두 출구
컨테이너 로그
   ├─→ Loki (in-cluster, 단기 검색)
   └─→ CloudWatch Logs (S3 export, 장기 보관)

같은 로그를 두 곳에 보내는 이유는 책임이 다르기 때문입니다 — Loki는 일상 디버깅, CloudWatch는 컴플라이언스,감사,장기 분석. 비용 측면에서는 Loki만 쓰는 방식이 더 가볍지만, 규제 환경에서는 CloudWatch가 보통 함께 들어갑니다.

Grafana 대시보드 표준 #

운영 클러스터의 Grafana에 들어가는 표준 대시보드 셋을 정리하겠습니다.

대시보드source
Kubernetes / Compute Resources / Clusterkube-prometheus-stack 기본 (ID 7249)
Kubernetes / Compute Resources / Namespace (Workloads)기본 (ID 7250)
Kubernetes / Compute Resources / Pod기본 (ID 7251)
Kubernetes / Networking / Cluster기본 (ID 7253)
Node Exporter / Nodes기본 (ID 1860)
myshop-api 운영 대시보드자체 작성 — golden signals + 비즈니스 메트릭

기본 대시보드 5개는 kube-prometheus-stack이 자동으로 등록해 주므로 별도 작업 없이 바로 보입니다. 자체 대시보드 한 개만 도메인에 맞춰 만들면 일상 점검의 시야가 거의 완성됩니다.

자체 대시보드의 표준 패널 셋 #

myshop-api 대시보드 — 9개 패널
Row 1: Latency P50 / P95 / P99
Row 2: Request rate (도메인별, status별)
Row 3: Error rate (4xx / 5xx)
Row 4: Pod CPU / 메모리 사용률
Row 5: HPA 현재 replicas
Row 6: PgBouncer 활성 연결 / 대기 큐
Row 7: 비즈니스 메트릭 (orders/min, checkout success rate)
Row 8: 상위 ERROR 로그 (Loki)
Row 9: 최근 배포 (annotations)

마지막 패널의 “최근 배포 annotations"는 Grafana에 ArgoCD 또는 GitHub Actions 이벤트를 annotation으로 주입한 것입니다. 메트릭 그래프 위에 배포 시점이 세로선으로 표시되어, “이 latency 스파이크는 어느 배포 직후에 발생했는가"를 한눈에 볼 수 있습니다.

on-call 흐름 — runbook과 함께 #

알람이 울리는 것 자체가 끝이 아닙니다. 받은 사람이 5분 안에 어디를 봐야 하는지 명확해야 합니다. 운영 표준 흐름입니다.

on-call 알람 받은 직후의 표준 5분
1. PagerDuty에서 알람 본문 확인 (alertname, team, severity)
2. annotation의 runbook_url 클릭
3. Runbook의 "1차 점검" 섹션 따라가기 — 관련 Grafana 대시보드 / 로그 쿼리 / kubectl 명령 제시됨
4. 1차 대응 (스케일 업, 재시작, 트래픽 차단 등)
5. Slack 사고 채널에 상태 공유

Runbook은 별도 git repo의 마크다운으로 관리하는 게 표준입니다. 알람 룰의 runbook_url이 그 repo의 한 페이지를 가리키고, 새 알람을 추가할 때 Runbook도 같이 PR로 들어옵니다.

첫 운영 한 사이클 후 점검 #

스택을 설치하고 며칠 굴려 본 시점에서 점검할 항목들입니다.

Prometheus의 시계열 수 (카디널리티 점검)
kubectl exec -n monitoring prometheus-prometheus-kube-prometheus-prometheus-0 -c prometheus -- \
  promtool tsdb analyze /prometheus/wal | head -50
알람의 발생 빈도 (한 번도 안 운 룰 / 너무 자주 우는 룰)
kubectl exec -n monitoring alertmanager-prometheus-kube-prometheus-alertmanager-0 -c alertmanager -- \
  amtool alert query --alertmanager.url=http://localhost:9093
Loki의 디스크 사용량
kubectl exec -n monitoring loki-0 -- df -h /data

운영 한 달이 지나면 카디널리티 폭증, 알람 SNR 저하, 로그 디스크 압박이 보통 한 번씩 옵니다. 이 점검을 정기 점검으로 두는 게 표준입니다.

마무리 #

myshop-api 위에 옵저버빌리티 스택을 얹는 한 사이클을 따라갔습니다. kube-prometheus-stack으로 Prometheus + Grafana + Alertmanager를 한 번에 설치하고, ServiceMonitor + PrometheusRule로 myshop-api의 4 golden signals 알람을 표준화하고, Alertmanager 라우팅으로 severity,team 별 Slack / PagerDuty 분기까지 잡았습니다. Loki와 CloudWatch의 두 축을 같이 두고, runbook_url로 알람과 대응 절차를 묶는 운영 패턴까지 짚었습니다. 이 시점에서 myshop-api는 코드부터 배포,운영,관측까지 한 사이클이 모두 자동화된 상태입니다. 다음 글이자 시리즈의 마지막 글에서는 이 클러스터를 한 달, 한 분기, 한 해 단위로 안전하게 굴리는 정기 운영 사이클을 다루겠습니다 — EKS 업그레이드, RDS 백업,복구, 비용 점검, 보안 점검의 체크리스트와 K8s 실전 6편의 회고까지가 마지막 글의 범위입니다.

X