Dev / App Infra

Kubernetes 환경에 Trace 구성하기

Kubernetes

15 min read
Kubernetes 환경에 Trace 구성하기

안녕하세요. 

오늘은 사내에서 진행한 PoC 사례를 바탕으로, OpenTelemetry(Otel)를 활용한 Observability 구성 중 .NET Core 애플리케이션에서 Trace 데이터를 수집하는 방법에 대해 정리하고자 합니다. 

Kubernetes와 MSA(Microservices Architecture)의 등장으로 인해, 애플리케이션 운영의 가시성 확보는 점점 더 중요한 과제가 되고 있습니다. 이에 따라 시스템 상태를 다각도로 파악할 수 있는 Observability 개념이 주목받고 있으며, 이를 구성하는 핵심 요소로는 Metrics, Logs, Traces가 있습니다. 

이 중 Trace(분산 추적)는, 하나의 요청이 여러 서비스나 컴포넌트를 거치며 처리되는 과정을 시각적으로 보여줍니다. 

이로 인해 요청 흐름에서 어디서 지연이 발생했는지, 어떤 서비스 간 호출에서 문제가 있었는지 등을 빠르게 파악할 수 있어 운영 및 문제 해결에 매우 유용합니다. 

이번 PoC에서는 .NET Core 애플리케이션에 대해 OpenTelemetry Auto-Instrumentation 기능을 활용하여, 코드 수정 없이 Trace 데이터를 수집하는 방법을 적용해 보았습니다. 

자동 계측(Auto-Instrumentation)을 통해 애플리케이션 실행 시 필요한 라이브러리를 주입하고, 이를 통해 주요 프레임워크(예: ASP.NET Core, HttpClient 등)의 호출 흐름을 자동으로 추적할 수 있습니다. 

이번 포스팅에서는 해당 방식을 적용하는 과정과, 이를 통해 얻을 수 있는 인사이트에 대해 간략히 소개 하고자 합니다. 

아키텍처

구현 목표

이번 포스팅은 아래와 같은 흐름으로 진행됩니다.

1. kind를 통해 간단한 kubernetes 클러스터를 생성합니다.
2. Trace Metrics을 확인하기 위한 Grafana를 설치합니다.
3. Trace Metrics의 수집, 저장을 위한 Tempo를 설치합니다.
4. Open Telemetry Collector를 설치하고, Trace Metrics 자동 수집을 위한 CRD를 구성합니다.
5. .Net Application을 배포합니다. 이 때 Trace Metrics을 자동 수집해주기 위한 Annotation을 설정합니다.
6. Grafana에서 Tempo 데이터를 추가하고, 조회합니다.

쿠버네티스 환경에서 .Net Application의 별도 코드 수정 없이, O-Tel 구성을 통해 Trace Metrics을 자동 수집하는 것을 목표로 합니다. 참고로 .Net Application에 O-Tel SDK를 구성하여 수집할 메트릭을 직접 정의할 수도 있습니다.

구현하기

Step 1. Kind로 간단한 Kubernetes 클러스터 생성하기

아래 사진처럼 kind로 간단한 Kubernetes 클러스터를 생성합니다. kind는 로컬 환경에서 소규모로 kubernetes 클러스터를 간단히 생성할 수 있게 도와주며, 처음 접해 보신다면 공식 문서[1]를 참고하여 설정해 보시기 바랍니다.

공식문서 [1] https://kind.sigs.k8s.io/docs/user/quick-start/

저는 brew 패키지 관리자를 사용하기 때문에, `brew install kind` 명령을 통해 kind를 설치하고, `kind create cluster` 명령을 사용하여 로컬 환경에 간단한 쿠버네티스 환경을 구성 했습니다.

Step 2. Grafana 설치하기

Trace Metrics을 시각화 하여 볼 수 있도록 공식 문서를 참고하여 Helm을 통해 Grafana를 설치합니다. 먼저 Helm 설치가 안되어 있다면, 공식 문서[2]를 참고해서 Helm을 설치합니다. 이번에도 저는 `brew install helm`을 통해 helm을 설치 했습니다.

공식문서[2] https://helm.sh/docs/intro/install/

다음으로 아래 명령을 통해 Grafana를 설치합니다.

# Grafana Repo 등록
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Grafana 설치
kubectl create namespace monitoring
helm install my-grafana grafana/grafana --namespace monitoring

설치가 정상적으로 완료되었는지 확인합니다.

관리자 비밀번호를 확인하기 위해 secret 데이터를 조회합니다.

kubectl get secret --namespace monitoring my-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

새로운 터미널을 띄우고 kubectl port-forward 명령을 사용하여 Grafana 대시보드에 접근할 수 있도록 합니다. 호스트 포트에 접근하여 Grafana에 접근합니다. 이 때 로그인 ID는 admin 이고, 비밀번호는 위에서 확인한 비밀번호를 입력합니다.

kubectl port-forward -n monitoring services/my-grafana 30080:80

Step 3. Tempo 설치하기

Trace 메트릭을 받고, 저장, 관리할 수 있도록 Tempo를 설치합니다. Tempo는 Grafana와 같은 Helm repo를 사용하기 때문에 윗 단계에서 repo add를 진행하였다면 바로 설치할 수 있습니다.

먼저 `vi values.yaml`을 통해 Tempo 설치에 사용될 Helm value 파일을 생성합니다.

traces:
  otlp:
    http:
      enabled: true
      receiverConfig: {}
    grpc:
      enabled: true
      receiverConfig: {}
global_overrides:
  metrics_generator_processors:
  - service-graphs

아래 명령어로 Tempo를 설치하기 위한 네임스페이스를 생성 및 설치를 진행합니다.

kubectl create namespace tempo
helm -n tempo install tempo grafana/tempo-distributed -f values.yaml

설치가 정상적으로 완료되었는지 확인합니다.

Step 4. Opentelemetry Collector 설치 및 CRD(Auto-Instrument) 구성하기

Otel Operator 설치 및 Otel Collector 설치하기

Open Telemetry Collector의 경우 Helm 설치와 Operator를 통한 설치가 가능합니다. Helm을 통한 구성이 무조건 안되는 것인지는 모르겠으나 저의 경우 Helm을 통해 구성시 실패하였고, Operator를 통해 구성 시 성공하였기에 Operator를 통한 구성으로 내용을 정리하겠습니다.

먼저 Otel-Opertaor 설치 시 Cert-manager가 선제적으로 설치되어 있어야 하기에 cert-manager를 설치해 줍니다.

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.1/cert-manager.yaml

다음으로, Otel-Operator를 설치합니다.

kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml

설치가 성공적으로 완료되면 Otel CRD도 설치됩니다. 따라서 아래와 같이 `kind: OpenTelemetryCollector`인 리소스도 쿠버네티스 환경에 설치할 수 있게 됩니다. 아래 파일을 생성하고 `kubectl apply -f <filename.yaml>`을 통해 Opentelemetry Collector를 설치합니다. 저는 otel.yaml 이라는 이름으로 생성하였습니다.

코드 보기

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: collector
spec:
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    processors:
      memory_limiter:
        check_interval: 1s
        limit_percentage: 75
        spike_limit_percentage: 15
      batch:
        send_batch_size: 10000
        timeout: 10s
exporters:
  otlphttp/tempo:
    endpoint: http://tempo-distributor.tempo.svc.cluster.local:4318
  # NOTE: Prior to v0.86.0 use `logging` instead of `debug`.
  debug: {}

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/tempo, debug]

위 Otel Collector Config를 해석 해보겠습니다. pipeline쪽을 보시면 traces 메트릭에 대해 recivers가 otlp로 구성되어 있습니다.

상단의 config.receivers.otlp.protocols 부분에 4317, 4318 포트로 OTLP를 리슨하는 것을 의미합니다. 아래 단계에서 .Net Application 구성 중 Trace 메트릭을 Otel Collector의 4317 혹은 4318 포트로 전송하면 될 것입니다.

다음으로 processors가 있습니다. 수집된 데이터를 가공하는 단계입니다. 마지막으로 exporters가 있습니다. 수집, 가공한 데이터를 tempo로 송신하게 됩니다.

이로써 유추할 수 있는 Trace 데이터의 흐름은 .Net App -> Otel-Collector -> Tempo -> Grafana를 통한 Trace 데이터 시각화 일 것입니다.

위 YAML 파일을 작성하였다면 Otel Collector 설치를 위한 네임스페이스 생성 및 Collector 설치를 진행합니다.

kubectl create namespace otel-trace
kubectl apply -f otel.yaml

설치가 잘 되었는지 확인합니다.

Auto-Instrument 구성하기

Auto-Instrument를 사용하면, 기존 Application의 코드 수정 없이 Traces와 관련된 메트릭을 수집할 수 있습니다.(정확히는 metrics, logs, traces 모든 메트릭에 대해 수집 가능합니다.)

기존 코드 수정없이 메트릭을 수집할 수 있기에 관리의 복잡성이 개선되고, 쿠버네티스 환경에서의 Opentelemetry는 Instrument라는 리소스를 통해 메트릭을 수집하게 됩니다.

이전 단계에서 Opentelemetry Operator를 설치 하였는데요, 해당 Operator 덕에 Instrument 리소스를 배포할 수 있습니다. 그럼 아래의 YAML파일을 생성하고, Instrument 리소스 배포를 진행합니다.

코드 보기

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: dotnet-instrumentation
spec:
  exporter:
    endpoint: http://collector-collector.otel-trace.svc.cluster.local:4318
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: '1'
  dotnet:
    env:
    - name: OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED
      value: "true"
    - name: OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED
      value: "false"
    - name: OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED
      value: "false"
 

위 config를 살펴 보겠습니다.

그럼 Instrument를 배포하고, 리소스가 잘 배포되었는지 확인합니다.

# 위 instrument yaml을 생성
kubectl apply -f instrument.yaml
# 리소스 배포 확인
kubectl get instrumentations.opentelemetry.io

Step 5. .Net Core App 배포하기

이번 포스팅의 핵심(?)인 .Net Core Application을 배포합니다. 샘플 앱은 총 4개를 사용합니다.

위 4가지 app은 docker hub에 올라가 있고, app.yaml 이라는 파일을 만들어 아래의 값을 붙여놓은 다음 배포합니다.

코드 보기
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ip-service
  namespace: app
  labels:
    app: ip-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ip-service
  template:
    metadata:
      labels:
        app: ip-service
      annotations:
        instrumentation.opentelemetry.io/inject-dotnet: "default/auto-instrumentation"
    spec:
      containers:
      - name: ip-service
        image: kimhj4270/ip:0.3
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: ip-service
  namespace: app
spec:
  type: ClusterIP
  selector:
    app: ip-service
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gw
  namespace: app
  labels:
    app: api-gw
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api-gw
  template:
    metadata:
      labels:
        app: api-gw
      annotations:
        instrumentation.opentelemetry.io/inject-dotnet: "default/auto-instrumentation"
    spec:
      containers:
      - name: api-gw
        image: kimhj4270/api-gw:0.5
        ports:
        - containerPort: 80
        env:
        - name: IP_SERVICE_HOST
          value: ip-service.app.svc.cluster.local
        - name: COUNTRY_SERVICE_HOST
          value: country-service.app.svc.cluster.local
        - name: TIME_SERVICE_HOST
          value: time-service.app.svc.cluster.local
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: api-gw
  namespace: app
spec:
type: ClusterIP
  selector:
    app: api-gw
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: country-service
  namespace: app
  labels:
    app: country-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: country-service
  template:
    metadata:
      labels:
        app: country-service
      annotations:
        instrumentation.opentelemetry.io/inject-dotnet: "default/auto-instrumentation"
    spec:
      containers:
      - name: country-service
        image: kimhj4270/country:0.2
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: country-service
  namespace: app
spec:
  type: ClusterIP
  selector:
    app: country-service
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: time-service
  namespace: app
  labels:
    app: time-service
    spec:
  replicas: 1
  selector:
    matchLabels:
      app: time-service
  template:
    metadata:
      labels:
        app: time-service
      annotations:
        instrumentation.opentelemetry.io/inject-dotnet: "default/auto-instrumentation"
    spec:
      containers:
      - name: time-service
        image: kimhj4270/time:0.2
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: time-service
  namespace: app
spec:
  type: ClusterIP
  selector:
    app: time-service
  ports:
    - port: 80
      targetPort: 80
  

App 네임스페이스를 생성하고, 위 yaml을 배포한 다음 리소스가 잘 생성 되었는지 확인합니다.

kubectl create namespace app 
kubectl apply -f app.yaml                                                      

Step 6. Grafana에 Tempo 데이터 연결 및 Trace 데이터 조회하기

Tempo Datasource 추가하기

먼저 Grafana의  Connections > Data sources > Add new Data source에 접근합니다. Tempo를 검색하고 클릭한 다음 우측 상단의 [Add new data source]를 클릭합니다.

Connection URL에 http://tempo-query-frontend-discovery.tempo.svc.cluster.local:3200/를 입력합니다.

하단의 [Save & Test]를 클릭합니다. 성공적으로 데이터가 연결됐는지 확인합니다.

Trace 데이터 조회하기

Trace 데이터를 조회하려면, Trace 데이터를 생성해야 합니다. 따라서 아래의 절차에 따라 Application에 요청을 보내 Trace 메트릭을 발생 시킵니다.

먼저 새로운 터미널에서 kubectl port-forward 명령으로 로컬호스트에서 App 파드로 요청할 수 있는 환경을 만들어 줍니다.

kubectl port-forward -n app services/api-gw 8080:80

다음으로 curl localhost:8080 명령으로 App 파드에 http 요청을 보냅니다. 사진과 같은 json값을 반환 받아야 합니다.

Trace 메트릭을 발생시켰으니 Grafana에서 확인해야 합니다. Explore 메뉴에 접근하고, Data Source를 Tempo로 지정합니다. Query type을 Search로 변경하면 하위에 Trace ID를 확인할 수 있습니다. Trace ID를 클릭합니다.

Trace 메트릭을 아래 사진처럼 확인할 수 있습니다.

마무리하며

이번 포스팅에서는 OpenTelemetry(Otel)와 Grafana Tempo를 활용하여 .NET Core 애플리케이션의 Trace 데이터를 수집하는 과정을 살펴보았습니다. 

 Trace 수집은 마이크로서비스 환경에서 각 서비스 간의 호출 흐름, 응답 시간, 실패 여부 등을 요청 단위로 시각화할 수 있어, 문제 원인 분석에 매우 강력한 도구가 됩니다. (아래는 이번 PoC에서 구성한 샘플 대시보드입니다.) 

하지만 단일 요청 흐름만으로는 전체 시스템의 상태나 성능을 온전히 파악하기 어렵기 때문에, Metrics와 Logs까지 함께 수집하고 이를 대시보드화하는 것이 운영 환경에서는 필수적입니다. 

예를 들어, Metrics를 통해 초당 요청 수, 평균 응답 시간, 에러율 등 시스템 전반의 성능 지표를 확인할 수 있고, Logs는 특정 TraceID를 기준으로 연관된 에러 메시지나 예외 스택을 추적할 수 있어, Trace에서 포착된 이슈에 대한 원인 분석을 보완해 줍니다. 

여러분도 이 과정을 참고하여 Metrics과 Logs를 수집하고 대시보드화 하는데 도움이 되었으면 합니다. 

감사합니다. 

Share This Post

Check out these related posts

개발팀의 애자일 도입 이야기2

개발팀의 애자일 도입 이야기 1

Lambda@Edge 고급 로깅 제어 기능