Files
project-infra/docs/examples/infra/observability-health.md
T

14 KiB
Raw Blame History

observability / health 예시


좋은 예시 1: ServiceMonitor (kube-prometheus-stack 표준)

---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: auth-server
  namespace: auth-prod
  labels:
    app.kubernetes.io/name: auth-server
    app.kubernetes.io/instance: auth-server-prod
    app.kubernetes.io/part-of: identity-platform
    release: kube-prometheus-stack
spec:
  namespaceSelector:
    matchNames:
      - auth-prod
  selector:
    matchLabels:
      app.kubernetes.io/name: auth-server
      app.kubernetes.io/instance: auth-server-prod
  endpoints:
    - port: metrics            # named port (required)
      path: /actuator/prometheus
      scheme: http
      interval: 30s
      scrapeTimeout: 10s
      honorLabels: false
      relabelings:
        - sourceLabels: [__meta_kubernetes_pod_name]
          targetLabel: pod
        - sourceLabels: [__meta_kubernetes_namespace]
          targetLabel: namespace
        - sourceLabels: [__meta_kubernetes_pod_label_app_kubernetes_io_version]
          targetLabel: version
        - action: labeldrop
          regex: "pod_template_hash|controller_revision_hash"
      metricRelabelings:
        - sourceLabels: [__name__]
          regex: "jvm_gc_pause_seconds_.*"
          action: keep
        - sourceLabels: [__name__]
          regex: "debug_.*"
          action: drop

왜 좋은가:

  • namespaceSelector 명시로 암묵적 전체 허용 방지.
  • port: metrics 는 Service/Deployment의 named port를 참조 → 포트 번호 변경에 내성.
  • interval / scrapeTimeout 관계 유지 (timeout < interval).
  • relabelings 로 pod / namespace / version label 정리, noise label drop.
  • metricRelabelings 로 불필요 metric drop (cardinality / storage 절감).
  • release: kube-prometheus-stack label 로 Operator가 선택.

좋은 예시 2: ServiceMonitor with bearer token (Vault telemetry)

---
apiVersion: v1
kind: Secret
metadata:
  name: vault-metrics-token
  namespace: vault
type: Opaque
stringData:
  token: "hvs.xxxx.prometheus-readonly"
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: vault
  namespace: vault
  labels:
    app.kubernetes.io/name: vault
    app.kubernetes.io/instance: vault-prod
    release: kube-prometheus-stack
spec:
  namespaceSelector:
    matchNames:
      - vault
  selector:
    matchLabels:
      app.kubernetes.io/name: vault
      app.kubernetes.io/instance: vault-prod
  endpoints:
    - port: https
      path: /v1/sys/metrics
      params:
        format: ["prometheus"]
      scheme: https
      interval: 30s
      scrapeTimeout: 10s
      bearerTokenSecret:
        name: vault-metrics-token
        key: token
      tlsConfig:
        insecureSkipVerify: false
        ca:
          secret:
            name: vault-ca
            key: ca.crt
        serverName: vault.vault.svc
      relabelings:
        - sourceLabels: [__meta_kubernetes_pod_name]
          targetLabel: pod

왜 좋은가:

  • Vault /sys/metrics 는 read token 필수. bearerTokenSecret 참조로 Operator가 주입.
  • TLS CA pinning + serverName 으로 MitM 방지.
  • params 로 Prometheus format 요청.

좋은 예시 3: PodMonitor (Service 없는 워크로드)

---
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: batch-worker
  namespace: batch
  labels:
    release: kube-prometheus-stack
spec:
  namespaceSelector:
    matchNames:
      - batch
  selector:
    matchLabels:
      app.kubernetes.io/name: batch-worker
  podMetricsEndpoints:
    - port: metrics
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s
      relabelings:
        - sourceLabels: [__meta_kubernetes_pod_name]
          targetLabel: pod

왜 좋은가:

  • Job / headless workload처럼 Service 뒤에 없는 경우 PodMonitor로 직접 pod 매칭.

좋은 예시 4: Annotation-based fallback (Operator 없는 환경 only)

---
apiVersion: v1
kind: Service
metadata:
  name: legacy-app
  namespace: legacy
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8081"
    prometheus.io/path: "/metrics"
    prometheus.io/scheme: "http"
spec:
  selector:
    app.kubernetes.io/name: legacy-app
  ports:
    - name: http
      port: 80
      targetPort: 8080
    - name: metrics
      port: 8081
      targetPort: 8081

왜 좋은가 (조건부):

  • kube-prometheus-stack이 없는 legacy 환경에서만 유효.
  • Operator가 있으면 ServiceMonitor로 전환.

좋은 예시 5: NetworkPolicy — prometheus namespace만 metrics scrape 허용

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: auth-server-default-deny
  namespace: auth-prod
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: auth-server
  policyTypes: ["Ingress", "Egress"]
  ingress: []
  egress: []
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: auth-server-allow-metrics
  namespace: auth-prod
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: auth-server
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
          podSelector:
            matchLabels:
              app.kubernetes.io/name: prometheus
      ports:
        - port: metrics
          protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: auth-server-allow-http-from-ingress
  namespace: auth-prod
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: auth-server
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
      ports:
        - port: http
          protocol: TCP

왜 좋은가:

  • default-deny → allow-list 패턴.
  • metrics port는 monitoring namespace의 prometheus pod만.
  • http port는 ingress controller namespace만.

좋은 예시 6: JSON structured log (Spring Boot logback)

<!-- logback-spring.xml -->
<configuration>
  <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <includeMdcKeyName>trace_id</includeMdcKeyName>
      <includeMdcKeyName>span_id</includeMdcKeyName>
      <includeMdcKeyName>request_id</includeMdcKeyName>
      <customFields>{"service":"auth-server"}</customFields>
    </encoder>
  </appender>
  <root level="INFO">
    <appender-ref ref="JSON"/>
  </root>
</configuration>

Actual output:

{"timestamp":"2026-04-16T09:31:42.017Z","level":"INFO","service":"auth-server","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","logger":"c.e.auth.LoginController","thread":"http-nio-8080-exec-3","message":"login success","user_id_hash":"ab12..."}

왜 좋은가:

  • ISO 8601 UTC timestamp.
  • trace_id / span_id 가 MDC에서 자동 주입 → Tempo / Jaeger와 correlate.
  • service label이 customFields로 고정.
  • user_id는 hashed → cardinality/PII 안전.

좋은 예시 7: OpenTelemetry Collector (DaemonSet agent + Deployment gateway)

---
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel-agent
  namespace: observability
spec:
  mode: daemonset
  image: otel/opentelemetry-collector-contrib:0.101.0
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    processors:
      batch:
        send_batch_size: 1024
        timeout: 5s
      k8sattributes:
        passthrough: false
        extract:
          metadata:
            - k8s.pod.name
            - k8s.namespace.name
            - k8s.node.name
    exporters:
      otlp/gateway:
        endpoint: otel-gateway.observability.svc:4317
        tls:
          insecure: true
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [k8sattributes, batch]
          exporters: [otlp/gateway]
        metrics:
          receivers: [otlp]
          processors: [k8sattributes, batch]
          exporters: [otlp/gateway]
---
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel-gateway
  namespace: observability
spec:
  mode: deployment
  replicas: 3
  image: otel/opentelemetry-collector-contrib:0.101.0
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
    processors:
      batch:
        send_batch_size: 2048
        timeout: 5s
      tail_sampling:
        decision_wait: 10s
        policies:
          - name: errors-keep
            type: status_code
            status_code: { status_codes: [ERROR] }
          - name: slow-keep
            type: latency
            latency: { threshold_ms: 500 }
          - name: default-10pct
            type: probabilistic
            probabilistic: { sampling_percentage: 10 }
      attributes/redact:
        actions:
          - key: http.request.header.authorization
            action: delete
          - key: user.email
            action: hash
    exporters:
      otlp/tempo:
        endpoint: tempo.observability.svc:4317
        tls:
          insecure: true
      prometheusremotewrite:
        endpoint: http://prometheus.monitoring.svc:9090/api/v1/write
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [attributes/redact, tail_sampling, batch]
          exporters: [otlp/tempo]
        metrics:
          receivers: [otlp]
          processors: [batch]
          exporters: [prometheusremotewrite]

왜 좋은가:

  • agent (DaemonSet) → gateway (Deployment) 2단 구조.
  • gateway에서 tail-based sampling (error + slow + 10% 나머지).
  • PII redaction을 gateway에서 중앙 처리.
  • agent가 node-local이라 app은 localhost endpoint만 알면 됨.

좋은 예시 8: Loki + Grafana Alloy DaemonSet (log shipping)

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: alloy-config
  namespace: observability
data:
  config.alloy: |
    discovery.kubernetes "pods" {
      role = "pod"
    }
    discovery.relabel "pods" {
      targets = discovery.kubernetes.pods.targets
      rule {
        source_labels = ["__meta_kubernetes_namespace"]
        target_label  = "namespace"
      }
      rule {
        source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
        target_label  = "service"
      }
    }
    loki.source.kubernetes "pods" {
      targets    = discovery.relabel.pods.output
      forward_to = [loki.write.default.receiver]
    }
    loki.write "default" {
      endpoint {
        url = "http://loki.observability.svc:3100/loki/api/v1/push"
      }
    }

왜 좋은가:

  • Alloy DaemonSet이 node-level log tail.
  • label은 namespace / service 두 개로 제한 (cardinality 안전).

좋은 예시 9: kubectl events (1.27+ stable)

# cluster-wide live watch, warnings only
kubectl events -A --types=Warning --watch

# specific pod
kubectl events -n auth-prod --for pod/auth-server-abc123

# last hour
kubectl events -n auth-prod --since=1h

왜 좋은가:

  • --for 로 특정 오브젝트 event 만 필터링.
  • --watchget events -w 보다 안정적.
  • timestamp sort 기본 제공.

나쁜 예시 1: /metrics 를 Ingress로 외부 공개

apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
  rules:
    - host: auth.example.com
      http:
        paths:
          - path: /metrics         # BAD
            pathType: Prefix
            backend:
              service:
                name: auth-server
                port:
                  number: 8081

문제:

  • Prometheus metric으로 내부 구조 / error rate / version 노출.
  • DoS vector (scrape 비용).
  • audit / compliance 위반.

Fix: metrics port는 외부 비공개, NetworkPolicy로 monitoring namespace만 허용.


나쁜 예시 2: high-cardinality label

# app code
http_requests_total{user_id="12345", path="/users/12345/orders/98765", request_id="a1b2c3..."}

문제:

  • user_id × path × request_id = 수백만 time series → Prometheus OOM.
  • query 성능 붕괴.

Fix:

http_requests_total{route="/users/:id/orders/:id", method="GET", status_class="2xx"}
  • route template 화, status는 bucket (2xx/4xx/5xx).
  • user_id 는 logging에만, metric label 금지.

나쁜 예시 3: ServiceMonitor에 namespaceSelector 없음

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
spec:
  selector:
    matchLabels:
      app: my-app
  # namespaceSelector 없음 → Operator 설정에 따라 전체 cluster scan
  endpoints:
    - port: metrics

문제:

  • 암묵적으로 너무 넓은 범위 (Operator 설정에 따라 다름).
  • 동일 label 를 다른 namespace에서 쓰면 의도치 않은 scrape.

Fix: namespaceSelector.matchNames 명시.


나쁜 예시 4: probe가 /metrics 사용

readinessProbe:
  httpGet:
    path: /metrics     # BAD
    port: 8081
  periodSeconds: 5

문제:

  • /metrics 는 비용이 큰 endpoint (모든 registry dump).
  • periodSeconds 5초 × N pod = unnecessary load.
  • readiness 의미와 무관.

Fix: /actuator/health/readiness 같은 전용 shallow endpoint.


나쁜 예시 5: 로그에 access token 그대로

2026-04-16T09:32:11.002 INFO  Exchanging code for token: access_token=eyJhbGciOi...

문제:

  • token이 log index에 그대로 저장 → 유출 리스크.
  • 중앙 로그 시스템 (OpenSearch / Loki) 에 영구 보관.

Fix:

  • 애플리케이션에서 token 값 로깅 금지.
  • 중앙 파이프라인에 regex redaction (access_token=[^ ]+access_token=***).
  • debug 로그에서도 masking.

나쁜 예시 6: 로그를 PVC / file로 적재

volumeMounts:
  - name: app-logs
    mountPath: /var/log/app      # BAD
volumes:
  - name: app-logs
    persistentVolumeClaim:
      claimName: app-logs-pvc

문제:

  • 컨테이너 표준 (stdout/stderr) 위반.
  • Pod 삭제 시 로그 손실 또는 orphan PVC.
  • kubectl logs 로 안 보임.
  • node log agent가 수집 못 함.

Fix: stdout/stderr로 출력, DaemonSet agent가 수집.