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

602 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# observability / health 예시
---
## 좋은 예시 1: ServiceMonitor (kube-prometheus-stack 표준)
```yaml
---
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)
```yaml
---
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 없는 워크로드)
```yaml
---
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)
```yaml
---
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 허용
```yaml
---
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)
```xml
<!-- 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:
```json
{"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)
```yaml
---
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)
```yaml
---
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)
```bash
# 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 만 필터링.
- `--watch``get events -w` 보다 안정적.
- timestamp sort 기본 제공.
---
## 나쁜 예시 1: `/metrics` 를 Ingress로 외부 공개
```yaml
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
```yaml
# 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 없음
```yaml
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` 사용
```yaml
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로 적재
```yaml
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가 수집.