배경 EKS 클러스터에서 서비스를 운영하다 보면 로그, 트레이스, 메트릭을 한 곳에서 볼 수 있는 모니터링 환경이 필요해진다. Datadog이나 New Relic 같은 SaaS를 쓰면 편하지만, 호스트와 로그량 기반 과금이 서비스가 커질수록 부담이 된다. LGTM 스택(Loki, Grafana, Tempo, Mimir)은 Grafana Labs의 오픈소스 조합으로, EKS에 Helm으로 배포하면 인프라 비용만으로 운영할 수 있다. OTel(OpenTelemetry) 표준 기반이라 벤더 종속 없이 수집 파이프라인을 구성할 수 있고, 나중에 백엔드를 바꿔도 애플리케이션 코드를 수정할 필요가 없다. 이 글에서는 LGTM 스택을 EKS에 배포하고, Spring Boot 앱에 OTel Collector 사이드카를 붙여 로그·트레이스·메트릭을 수집하는 전체 과정을 단계별로 다룬다. 최종 구성: 사전 준비 EKS 클러스터 (1.28+) kubectl, Helm 3 설치 S3 버킷 (Loki/Tempo/Mimir 저장소용) IRSA 설정 가능한 IAM 권한 이 글에서는 observability라는 namespace에 LGTM 스택을 배포한다. 실제 환경에서는 전용 노드풀로 격리하는 것을 추천한다. 1단계: IRSA - S3 접근 권한 설정 Loki, Tempo, Mimir 모두 S3에 데이터를 저장한다. IRSA(IAM Roles for Service Accounts)로 Pod에 S3 접근 권한을 부여하면 Access Key를 하드코딩할 필요가 없다. IAM Policy 생성 ServiceAccount 생성 모든 LGTM 컴포넌트가 이 ServiceAccount를 공유한다. Helm values에서 serviceAccount.create: false, serviceAccount.name: lgtm으로 설정하면 된다. 2단계: Loki 배포 - 로그 저장소 Helm repo 추가: Loki values 작성 배포 핵심 설정 설명 | 설정 | 값 | 이유 | |||| | deploymentMode | SimpleScalable | backend/read/write 분리로 역할별 스케일링 가능 | | auth_enabled | false | 멀티테넌시 불필요 시 Grafana 연결이 간소화됨 | | store | tsdb | Loki 3.x 권장 인덱스 형식 | | schema | v13 | 최신 스키마, structured metadata 지원 | | replication_factor | 1 | 단일 레플리카 환경. HA 필요 시 3으로 변경 | 주의: auth_enabled: true로 두면 Grafana 데이터소스 설정에서 X-Scope-OrgID 헤더를 직접 넣어야 한다. 단일 테넌트에서는 false가 편하다. 3단계: Tempo 배포 - 분산 트레이싱 Tempo values 작성 배포 핵심 설정 설명 | 설정 | 값 | 이유 | |||| | traces.otlp.grpc/http | true | OTel Collector에서 OTLP로 트레이스 수신 | | distributor.replicas | 2 | 트레이스 수신 가용성 확보 | | ingester.persistence | 20Gi | WAL(Write Ahead Log) 저장. 데이터 유실 방지 | | metricsGenerator | enabled | 트레이스에서 서비스 그래프/span 메트릭 자동 생성 | metricsGenerator를 켜면 Grafana에서 서비스 간 호출 관계를 시각적으로 볼 수 있다. 트레이스를 수동 분석하지 않아도 서비스 토폴로지가 자동으로 그려진다. 4단계: Mimir 배포 - 메트릭 장기 저장 Mimir values 작성 배포 핵심 설정 설명 | 설정 | 값 | 이유 | |||| | ingest_storage.enabled | false | Kafka 없는 classic 아키텍처 | | wal_compression_enabled | true | ingester 디스크 사용량 절약 | | ship_interval | 2m | 2분마다 S3로 블록 업로드. 데이터 유실 최소화 | | retention_period | 2160h | 90일 보관. 필요에 따라 조정 | | zoneAwareReplication | false | 단일 레플리카에서는 비활성 필수 | 주의: Mimir 6.x (3.0)부터 기본이 Kafka 기반 ingest로 바뀌었다. classic 모드로 쓰려면 ingest_storage.enabled: false를 명시해야 한다. 5단계: Grafana 배포 - 대시보드 Grafana values 작성 배포 데이터소스 연결 Grafana에 접속한 뒤, 세 가지 데이터소스를 추가한다: | 데이터소스 | Type | URL | |||| | Loki | Loki | http://loki-gateway.observability.svc.cluster.local:80 | | Tempo | Tempo | http://tempo-query-frontend.observability.svc.cluster.local:3100 | | Mimir | Prometheus | http://mimir-gateway.observability.svc.cluster.local:80/prometheus | Mimir는 Prometheus 호환 API를 제공하므로 데이터소스 타입을 Prometheus로 선택한다. Tip: Tempo 데이터소스 설정에서 "Trace to logs" → Loki 데이터소스를 연결하면, 트레이스에서 원클릭으로 해당 시점의 로그를 볼 수 있다. "Trace to metrics" → Mimir를 연결하면 메트릭까지 연동된다. 6단계: OTel Collector 사이드카 - 데이터 수집 RBAC 설정 OTel Collector가 Prometheus Kubernetes SD로 Pod을 발견하려면 ClusterRole이 필요하다: OTel Collector ConfigMap Deployment에 사이드카 추가 핵심 포인트: OTEL_EXPORTER_OTLP_ENDPOINT: http://localhost:4317 - 사이드카는 같은 Pod이라 localhost로 통신한다 OTEL_SERVICE_NAME - Tempo에서 서비스를 구분하는 이름. 반드시 설정해야 한다 Prometheus 어노테이션으로 메트릭 스크래핑을 활성화한다 7단계: OTel Java Agent 적용 Dockerfile CI에서 에이전트 다운로드 Spring Boot 설정 OTel Java Agent는 바이트코드 계측 방식이라 코드 수정 없이 HTTP, JDBC, Redis, gRPC 등을 자동 추적한다. opentelemetry-api 의존성은 수동으로 span을 만들거나 attribute를 추가할 때만 필요하다. 8단계: MDC로 사용자 컨텍스트 연결 로그에 사용자 ID를 넣으면 "이 에러가 어떤 사용자의 요청에서 발생했는지" 바로 추적할 수 있다. MDCFilter MDC.put으로 Logback 로그에, span.setAttribute로 Tempo 트레이스에 동시에 사용자 ID를 넣는다. Grafana에서 사용자 ID로 로그와 트레이스를 모두 검색할 수 있다. 비동기 MDC 전파 @Async 메서드는 별도 스레드에서 실행되므로 MDC가 자동 전파되지 않는다: Logback 포맷 출력 예시: 노이즈 필터링 가이드 OTel Java Agent는 모든 것을 계측한다. 그래서 불필요한 span을 Collector에서 드롭하는 것이 중요하다. 필터링이 필요한 대표적인 노이즈 | 종류 | 예시 | 이유 | |||| | Redis 명령어 | PING, SET, DEL, HGET, TTL | 초당 수백 건, 비즈니스 의미 없음 | | 헬스체크 | /health, /actuator/health | ALB/k8s probe가 주기적 호출 | | 메트릭 엔드포인트 | /actuator/prometheus | Prometheus 스크래핑 | | CORS preflight | OPTIONS /api/* | 브라우저 자동 요청 | filter 프로세서 작성법 노이즈 필터링을 적용하면 Tempo에 저장되는 스팬 수가 절반 이하로 줄어든다. 네트워크 전송, 스토리지 비용 모두 절약된다. 보안: 민감 정보 제거 OTel Agent가 HTTP 헤더를 span attribute로 기록하므로, Authorization 헤더 같은 민감 정보가 Tempo에 저장될 수 있다: 검증 Loki (로그) Tempo (트레이스) Mimir (메트릭) Grafana 트러블슈팅 Loki 데이터소스 연결 안 됨 auth_enabled: true인데 Grafana에서 X-Scope-OrgID 헤더를 안 넣은 경우. 단일 테넌트면 auth_enabled: false로 변경하거나, Grafana 데이터소스 설정에서 Custom HTTP Header에 X-Scope-OrgID: 1을 추가한다. OTel Collector에서 메트릭 수집 안 됨 Prometheus receiver의 Kubernetes SD가 Pod을 못 찾는 경우. ClusterRole/ClusterRoleBinding이 적용됐는지 확인한다: Mimir "zone-aware replication" 에러 단일 레플리카인데 zoneAwareReplication이 활성화된 경우. ingester, store_gateway, alertmanager 모두 zoneAwareReplication.enabled: false로 설정한다. Tempo ingester "too many unhealthy instances in the ring" replication_factor가 레플리카 수보다 큰 경우. 단일 레플리카면 replication_factor: 1로 설정한다. 전체 구성 요약 | 단계 | 작업 | Helm Chart | |||| | 1 | IRSA + ServiceAccount | - | | 2 | Loki 배포 | grafana/loki | | 3 | Tempo 배포 | grafana/temp