NCP와 AWS를 잇는 멀티클러스터 Observability 구축기
현재 아이지넷(보닥)은 AWS와 NCP, 두 개의 클라우드 플랫폼에서 Kubernetes 클러스터를 운영하고 있습니다.
기존에는 각 환경마다 Prometheus, Grafana, Loki 등이 개별적으로 구축되어 있었습니다. 그래서 장애를 확인하거나 로그를 추적할 때마다 여러 Grafana 대시보드를 오가야 했고, 특정 서비스의 문제가 어느 클러스터에서 발생했는지 먼저 찾아야 했습니다.
처음에는 이 방식도 큰 문제가 없어 보였습니다. 하지만 AI 챗봇 서비스가 등장하고 새로운 기능이 추가될수록 서비스는 자연스럽게 분리되었고, 각 서비스가 서로를 호출하는 관계도 생기기 시작했습니다.
물론 이를 완전한 MSA라고 부르기는 어렵습니다. 하지만 운영 관점에서는 이미 단일 애플리케이션처럼 보기 어려운 구조가 되어가고 있었습니다. 하나의 요청이 여러 서비스를 거치고, 그 과정에서 AWS와 NCP에 걸친 리소스를 함께 확인해야 하는 상황이 생겼습니다.
이때부터 단순히 "각 클러스터에 모니터링이 있다"는 것만으로는 부족해졌습니다.
우리는 다음과 같은 문제를 해결해야 했습니다.
- 여러 Grafana 대시보드를 오가며 로그를 확인해야 하는 문제
- 클러스터가 달라지면 로그와 트레이스의 흐름이 끊기는 문제
- 서비스 간 호출 관계를 한눈에 파악하기 어려운 문제
- 장애가 발생했을 때 원인 분석의 시작점이 매번 달라지는 문제
그래서 APM을 도입하고, AWS와 NCP의 로그와 트레이스를 하나의 Grafana에서 볼 수 있는 멀티클러스터 Observability 환경을 구축하기로 했습니다.
이 글은 그 과정에서 왜 이런 구조를 선택했는지, 어떤 문제를 만났는지, 그리고 지금의 멀티클러스터 Observability 구조가 어떻게 만들어졌는지를 정리한 기록입니다.
전체 아키텍처

메트릭 수집 구조
먼저 메트릭 수집 구조를 다시 정리했습니다.
기존에는 각 클러스터마다 Prometheus Stack을 Operator 방식으로 운영하고 있었습니다. Prometheus Operator를 사용하면 ServiceMonitor, PodMonitor 같은 CRD로 수집 대상을 선언적으로 관리할 수 있습니다. 애플리케이션이 늘어나도 scrape 설정을 직접 수정하기보다, Kubernetes 리소스로 수집 대상을 관리할 수 있다는 점에서 운영하기 편했습니다.
하지만 각 클러스터에 Prometheus와 Grafana가 따로 있으면, 단순히 메트릭을 "수집할 수 있다"는 점에서는 문제가 없습니다. 문제는 장애를 분석하는 순간부터 시작됩니다. 어느 클러스터의 Grafana를 봐야 하는지 먼저 판단해야 하고, 클러스터를 넘나드는 서비스 호출이 생기면 메트릭과 로그, 트레이스를 한 흐름으로 보기 어려워집니다.
물론 이런 문제를 해결하기 위해 Mimir나 Thanos 같은 구성을 도입할 수도 있습니다. 여러 Prometheus의 데이터를 장기간 저장하고, 전역 쿼리를 제공하는 데 적합한 도구들입니다. 하지만 저희 서비스의 현재 트래픽 규모와 운영 복잡도를 고려했을 때, 이 단계에서 Mimir나 Thanos까지 도입하는 것은 조금 과하다고 판단했습니다.
그래서 먼저 단순한 방향을 선택했습니다. 지금 필요한 것은 거대한 메트릭 플랫폼이 아니라, 여러 클러스터의 상태를 한곳에서 볼 수 있는 운영 기준점이었습니다.
각 클러스터에서 모든 모니터링 스택을 운영하는 대신, AWS 클러스터에 단일 Prometheus를 두고 각 클러스터의 메트릭을 그곳으로 모으기로 했습니다. 이미 로그 수집을 위해 각 클러스터에 Alloy를 사용하고 있었기 때문에, 메트릭 수집도 Alloy가 담당하도록 역할을 확장했습니다.
각 클러스터의 Alloy가 메트릭을 수집하고, 수집한 데이터를 중앙 Prometheus의 remote_write endpoint로 전달합니다. 중앙 Prometheus는 Kubernetes 전체를 직접 scrape하지 않고, 대부분의 수집 책임을 Alloy에 맡깁니다.
애플리케이션과 서비스 메트릭은 기존 Prometheus Operator 생태계의 PodMonitor, ServiceMonitor를 그대로 활용했습니다. 기존에 사용하던 선언적 수집 방식은 유지하면서, 실제 scrape와 전달은 Alloy가 담당하도록 한 것입니다.
노드 메트릭은 별도의 Node Exporter DaemonSet을 배포하지 않았습니다. 대신 Alloy의 unix exporter 기능으로 노드 CPU, 메모리, 디스크, 네트워크 메트릭을 수집했습니다. kubelet과 cAdvisor 메트릭도 Alloy가 직접 scrape하도록 구성했습니다.
Kubernetes 리소스 상태 메트릭은 kube-state-metrics를 배포하고, ServiceMonitor를 통해 수집했습니다. Deployment, Pod, HPA, PVC 같은 Kubernetes 객체의 상태는 kube-state-metrics가 제공하고, Alloy가 이를 수집해 중앙 Prometheus로 전달하는 흐름입니다.
이렇게 구성하면 메트릭은 하나의 Prometheus에 모이지만, 모든 데이터가 섞여버리면 의미가 없습니다. 그래서 클러스터를 구분하기 위해 cluster 라벨을 부여했습니다. 예를 들어 AWS 개발 클러스터에서 수집된 메트릭에는 cluster=eks-dev와 같은 라벨이 붙습니다.
로그 수집 구조
로그도 비슷한 방향으로 정리했습니다.
기존에도 각 클러스터의 로그는 Alloy를 통해 수집하고 있었습니다. Kubernetes Pod에서 발생하는 stdout 로그를 Alloy가 읽고, 이를 Loki로 전달하는 방식입니다. 이 구조 자체는 유지하되, Loki를 클러스터마다 따로 보는 것이 아니라 AWS 클러스터의 중앙 Loki로 모으는 방향으로 바꿨습니다.
각 클러스터의 Alloy는 /var/log/pods 경로를 읽어 Kubernetes 로그를 수집합니다. 수집한 로그에는 cluster 같은 라벨을 붙여 어느 클러스터에서 발생한 로그인지 구분할 수 있도록 했습니다. 이렇게 하면 하나의 Loki 안에서도 클러스터별 로그를 나눠서 조회할 수 있습니다.
이렇게 구성하면 로그 저장소도 클러스터마다 따로 볼 필요가 없습니다. Grafana에서 Loki datasource 하나를 기준으로 조회하고, 필요한 경우 cluster 라벨로 범위를 좁혀 확인할 수 있습니다.
Trace 수집 구조
Trace도 같은 원칙으로 구성했습니다. 메트릭과 로그가 "무슨 일이 일어났는지"를 보여준다면, Trace는 "요청이 어떤 경로로 흘렀는지"를 보여줍니다.
서비스 간 호출 관계를 보기 위해서는 로그만으로는 부족했습니다. 어떤 요청이 어느 서비스를 거쳐 갔는지, 중간에 어느 구간에서 시간이 오래 걸렸는지 확인하려면 Trace가 필요했습니다. 그래서 Tempo를 도입하고, OpenTelemetry 기반으로 애플리케이션 Trace를 수집하기로 했습니다.
Node.js나 Java로 구성된 서비스에는 OpenTelemetry 계측을 적용했습니다. 이때 OpenTelemetry Operator의 Instrumentation 리소스를 사용해 애플리케이션에 Java Agent나 Node.js auto-instrumentation 설정을 주입했습니다.
다만 OpenTelemetry Operator가 데이터를 직접 Tempo로 보내도록 구성하지는 않았습니다. Operator는 어디까지나 애플리케이션에 OpenTelemetry 계측을 주입하는 역할로만 사용했습니다.
Trace 데이터의 수집과 전달은 Alloy가 담당하도록 했습니다.
애플리케이션에서 생성된 Trace는 OTLP 형식으로 Alloy의 OTLP receiver로 전달됩니다. Alloy는 이 데이터를 받아 필요한 라벨과 리소스 속성을 정리한 뒤, 중앙 Tempo로 전달합니다.
이렇게 구성한 이유는 단순합니다. 메트릭, 로그, Trace의 수집 경로를 각각 다른 컴포넌트로 흩어놓기보다, 각 클러스터에서는 Alloy를 중심으로 수집을 관리하고 싶었습니다. Prometheus 메트릭은 Alloy가 remote_write로 전달하고, 로그는 Loki로 전달하고, Trace는 Tempo로 전달하는 구조입니다.
Tempo 자체는 Trace 저장소 역할을 담당합니다. 그리고 Tempo의 metrics-generator를 통해 Trace 기반의 span metrics나 service graph metrics를 생성할 수 있습니다. 이 메트릭은 Prometheus로 remote_write되어 Grafana에서 Trace 기반 지표로 함께 조회할 수 있습니다.
정리하면 OpenTelemetry Operator는 계측 주입을 담당하고, Alloy는 수집과 전달을 담당하며, Tempo는 Trace 저장과 분석을 담당하도록 역할을 분리했습니다.