Observability는 선택이 아니라 필수다. 하지만 SaaS 모니터링 비용이 인프라 비용보다 높아지는 순간이 온다. 그때가 자체 구축을 고민할 타이밍이다.
Datadog을 메인으로 사용하고 있었지만, 모든 환경에 적용하기엔 비용 부담이 있었다. Observability가 아예 없는 환경이 생기는 것보다는, 오픈소스로라도 갖추는 게 낫다고 판단했다. LGTM 스택(Loki, Grafana, Tempo, Mimir)을 EKS에 구축한 과정을 공유한다.
Datadog을 메인으로 사용하고 있지만, 모든 환경에 적용하기엔 비용 부담이 있었다. 특히 작은 규모의 계정들은 Observability가 아예 없는 상태로 운영되고 있었다.
관측성(Observability)은 앱의 관심사가 아니다. 인프라가 책임져야 한다.
OpenTelemetry 커뮤니티와 Grafana Labs가 공통적으로 강조하는 원칙이다. 앱 개발자는 비즈니스 로직에 집중하고, 로그/트레이스/메트릭 수집은 플랫폼이 알아서 처리해야 한다. 하지만 현실은 그렇지 않은 경우가 많다. Dockerfile에 Agent를 넣고, Init Container를 설정하고, 환경변수를 10개 넘게 직접 입력하는 작업을 앱 개발자가 하고 있다면, 관심사 분리가 안 되고 있는 것이다.
이 글에서는 Init Container / 이미지 내장 방식에서 OTel Operator 방식으로 전환해야 하는 이유와 동작 원리를 정리한다.
JVM의 Cold Start는 순간 트래픽 상황에서 치명적이다. 스케일 아웃이 되어도 새 Pod가 준비되기까지 시간이 걸리면 이미 늦는다. GraalVM Native Image는 이 문제를 해결한다.
광고 푸시 후 순간 트래픽이 몰리는 서비스에 GraalVM Native 이미지를 적용했다. 개발자가 애플리케이션을 만들고, 나는 CI/CD 파이프라인 구축과 배포, 부하 테스트를 담당했다. 빌드 시간과 이미지 크기를 줄이면서 보안 규격까지 맞춘 과정을 공유한다.
이 서비스는 광고성 메시지를 대량으로 발송한 직후 트래픽이 급증한다. 사용자들이 링크를 클릭하면 반드시 이 서버를 거쳐야 하는 구조다.
Observability의 최종 목표는 MTTD(Mean Time To Detect)를 줄이는 것이다. 대시보드가 아무리 훌륭해도 24시간 지켜볼 수는 없다. Push 기반 알림이 있어야 문제를 빠르게 인지할 수 있다.
LGTM 스택에서 Mimir Ruler와 Alertmanager를 활용해 알림 시스템을 구축했다. 알림 룰을 GitOps로 관리하고, 알림 폭탄을 방지하기 위한 설계 포인트를 정리한다.
flowchart LR
subgraph GitOps[GitOps 관리]
Git[Git Repository]
CM[ConfigMap]
end
subgraph Mimir[Mimir]
Ruler[Ruler<br/>PromQL 평가]
Storage[(Metrics<br/>Storage)]
AM[Alertmanager<br/>라우팅/그룹핑]
end
subgraph Grafana[Grafana]
AlertUI[Alerting UI<br/>알림 현황 조회]
end
subgraph Slack[Slack]
Ch1[#alerts-service]
Ch2[#alerts-infra]
end
Git -->|Helm Deploy| CM
CM -->|Alert Rules| Ruler
Storage -->|메트릭 쿼리| Ruler
Ruler -->|알림 발생| AM
AM -->|namespace: app| Ch1
AM -->|namespace: observability| Ch2
AM -->|알림 상태| AlertUI
Ruler가 주기적으로 메트릭을 쿼리하고, 조건이 충족되면 Alertmanager로 알림을 보낸다. Alertmanager는 알림을 그룹핑하고, 라우팅 규칙에 따라 적절한 Slack 채널로 전송한다. Grafana에서는 현재 발생 중인 알림을 조회하고 히스토리를 확인할 수 있다.
Kubernetes에서 JVM 애플리케이션은 시작이 느리다. Pod가 뜨고 컨테이너가 Running 상태가 되어도, JVM이 준비되기까지는 시간이 더 걸린다. 이 간극을 무시하면 초기 요청이 실패한다.
Karpenter 노드 정리로 Pod가 재스케줄링될 때마다 초기 API 요청이 타임아웃으로 실패하는 문제가 있었다. ReadinessProbe만으로는 부족했다. StartupProbe와 전용 엔드포인트를 도입해서 해결한 과정을 공유한다.
새벽에 Karpenter가 비용 최적화를 위해 유휴 노드를 정리하면서 Pod가 다른 노드로 이동했다. Pod 재시작 자체는 정상이었지만, 재시작 직후 들어온 요청들이 실패했다.
Kubernetes Probe 3종류가 항상 헷갈린다. StartupProbe, ReadinessProbe, LivenessProbe. 왜 3개로 나눠져 있고, 언제 어떤 걸 써야 할까?
| Probe |
질문 |
실패 시 |
| StartupProbe |
“앱 시작 끝났어?” |
계속 대기 (다른 Probe 차단) |
| ReadinessProbe |
“트래픽 받을 수 있어?” |
Service에서 제외 (트래픽 차단) |
| LivenessProbe |
“죽은 거 아니야?” |
Pod 재시작 |
flowchart LR
subgraph Probes["Probe 역할"]
Startup[StartupProbe<br/>시작 완료?]
Readiness[ReadinessProbe<br/>트래픽 OK?]
Liveness[LivenessProbe<br/>살아있어?]
end
subgraph Actions["실패 시 동작"]
Wait[대기]
Remove[Service에서 제외]
Restart[Pod 재시작]
end
Startup -->|실패| Wait
Readiness -->|실패| Remove
Liveness -->|실패| Restart
style Startup fill:#74c0fc,color:#000
style Readiness fill:#69db7c,color:#000
style Liveness fill:#ef4444,color:#000
StartupProbe → 시작할 때만 (1회성)
ReadinessProbe → 트래픽 On/Off (반복)
LivenessProbe → 죽으면 재시작 (반복)
JVM 애플리케이션은 시작이 느리다. Spring Boot + DB 연결까지 1~2분 걸리기도 한다.
인프라에서 가장 위험한 컴포넌트는 “잘 돌아가고 있어서 아무도 신경 쓰지 않는 것"이다. ingress-nginx가 정확히 그랬다.
2025년 11월, Kubernetes 공식 블로그에서 ingress-nginx의 EOL을 발표했다. 유지보수자 1~2명(자원봉사)이 감당할 수 없는 기술 부채가 쌓였고, 후속 프로젝트(InGate)도 무산됐다. 2026년 3월부터 보안 패치, 버그 수정, 신규 K8s 버전 지원이 모두 중단된다. 클러스터에는 88개의 Ingress 리소스가 이 위에 올라가 있었다. 이 글은 “그래서 뭘로 바꿀 것인가"를 조사한 기록이다.
| 시점 |
상태 |
의미 |
| 2025-11 |
EOL 발표 |
Kubernetes 공식 블로그에서 retirement 공지 |
| 2026-02 |
Best-effort 기간 |
보안 패치 없음, 유지보수자 자발적 대응만 |
| 2026-03 |
Full EOL |
모든 지원 중단 — 보안 패치, 버그 수정, 신규 K8s 버전 지원 없음 |
| 이후 |
Repo Archived |
kubernetes-retired/로 이관 |
EOL 이후 기존 배포가 즉시 멈추지는 않는다. 하지만 보안 취약점이 발견되어도 패치가 나오지 않는다. 시한부 운영인 셈이다.
Liveness Probe는 죽은 Pod를 살리기 위한 것이다. 그런데 잘못 설정하면 멀쩡한 Pod를 죽이는 도구가 된다.
이론적으로는 알고 있었다. Kubernetes Probe 3종류에서도 “LivenessProbe는 단순하게"라고 정리했다. 그런데 실제로 겪고 나니 체감이 다르다. Karpenter 노드 교체와 무거운 Liveness Probe가 만나면서 연쇄 재시작이 발생했다. Datadog 메트릭으로 타임라인을 복원하고 원인을 분석한 과정을 공유한다.
아침에 5xx 알람이 터졌다. Spring Boot API 서버에서 약 15분간 간헐적 에러가 발생했다.
EKS 1.33이 릴리스됐다. 보통 마이너 버전 업그레이드는 “Control Plane 올리고, Addon 올리고, Node 올리면 끝"이라고 생각하기 쉽다. 나도 그렇게 생각했다.
dev 환경에서 먼저 작업하면서 예상치 못한 문제들을 만났다. VPC CNI 설정 불일치로 Pod IP 할당이 안 되고, Karpenter CRD 버전 문제로 노드 프로비저닝이 막히고, Datadog DD_HOSTNAME 누락으로 모니터링이 깨졌다.
dev에서 삽질한 덕분에 prod는 40분 만에 깔끔하게 끝냈다. 그 과정을 공유한다.
| 환경 |
노드 구성 |
특이사항 |
| dev |
MNG + Karpenter |
Self-managed Addons |
| prod |
MNG + Karpenter |
Self-managed Addons |
두 환경 모두 Self-managed Addon 방식이다. EKS managed addon이 아니라 직접 이미지 버전을 관리한다. 이게 업그레이드 시 추가 작업이 필요한 이유다.
Karpenter consolidation의 진짜 어려움은 “켜느냐 끄느냐"가 아니다. 클러스터 안의 워크로드가 모두 같은 수준의 내구성을 갖고 있지 않다는 점이다. 어떤 서비스는 재배치되어도 문제없지만, 어떤 서비스는 한 번의 재시작이 장애로 이어진다.
Prod 환경에서 유휴 노드 3대가 CPU 3~4%로 방치되고 있었지만, consolidation을 켤 수 없었다. 과거에 consolidation으로 민감한 서비스들이 동시에 재시작되면서 장애가 발생한 이력이 있었기 때문이다. do-not-disrupt 어노테이션으로 민감한 서비스만 선택적으로 보호하고, consolidation 시간대를 새벽으로 제한하여 안정성과 비용 절감을 양립한 과정을 공유한다.
이전 글에서는 Beta 환경에서 Spot + Consolidation + Drift가 복합적으로 작용하는 상황을 다뤘다. 이번 글은 Prod 환경에서 On-Demand + Stable NodePool이라는 다른 맥락이다.