Kubernetes NGINX 트레이싱 구성 (OpenTelemetry + Grafana Tempo)

Kubernetes 환경에서 마이크로서비스 간의 요청 흐름을 추적하고 분석하는 것은 장애 대응 및 성능 최적화에 매우 중요합니다. 본 문서에서는 NGINX의 OpenTelemetry 모듈을 활용하여 트레이싱 데이터를 수집하고, OpenTelemetry CollectorGrafana Tempo와 연동하여 분산 추적 시스템을 구축하는 과정을 소개합니다. NGINX OSS와 NGINX Plus를 모두 지원하는 구성입니다.

목차

1. 환경 구성
2. NGINX Otel Image
3. OpenTelemetry란?
4. Grafana Tempo란?
5. OpenTelemetry & Grafana Tempo 배포
6. NGINX Kubernetes 배포
7. 결론

1. 환경 구성

  • kubernetes : v1.32.2
  • NGINX Plus : R34(OSS 1.27.4 base)
  • Grafana Tempo : 2.7
  • OpenTelemetry Collector : v0.126.0

2. NGINX Otel Image

NGINX에 Otel Module이 설치되어있는 Image를 사용해야합니다.

NGINX OSS의 경우 NGINX 공식 Docker 이미지에 otel 태그를 이용하여 사용할 수 있습니다. (NGINX 공식 Docker 이미지)

NGINX Plus의 경우 이미지를 빌드하여 사용해야합니다.

ARG RELEASE=bookworm
FROM debian:${RELEASE}-slim

LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"

RUN --mount=type=secret,id=nginx-crt,dst=nginx-repo.crt \
    --mount=type=secret,id=nginx-key,dst=nginx-repo.key \
    set -x \
    && groupadd --system --gid 101 nginx \
    && useradd --system --gid nginx --no-create-home --home /nonexistent --comment "nginx user" --shell /bin/false --uid 101 nginx \
    && apt-get update \
    && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg1 lsb-release \
    && \
    NGINX_GPGKEYS="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 8540A6F18833A80E9C1653A42FD21310B49F6B46 9E9BE90EACBCDE69FE9B204CBCDCD8A38D88A2B3"; \
    NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg; \
    export GNUPGHOME="$(mktemp -d)"; \
    found=''; \
    for NGINX_GPGKEY in $NGINX_GPGKEYS; do \
        for server in \
            hkp://keyserver.ubuntu.com:80 \
            pgp.mit.edu \
        ; do \
            echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
            gpg1 --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
        done; \
        test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
    done; \
    gpg1 --export "$NGINX_GPGKEYS" > "$NGINX_GPGKEY_PATH" ; \
    rm -rf "$GNUPGHOME"; \
    apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \
    && nginxPackages=" \
        nginx-plus \
        # nginx-plus=${NGINX_VERSION}-${NGINX_PKG_RELEASE} \
        # nginx-plus-module-geoip \
        # nginx-plus-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
        # nginx-plus-module-image-filter \
        # nginx-plus-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
        # nginx-plus-module-njs \
        # nginx-plus-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${NJS_PKG_RELEASE} \
        nginx-plus-module-otel \ # NGINX Otel Module 설치
        # nginx-plus-module-otel=${NGINX_VERSION}+${OTEL_VERSION}-${OTEL_PKG_RELEASE} \
        # nginx-plus-module-perl \
        # nginx-plus-module-perl=${NGINX_VERSION}-${PKG_RELEASE} \
        # nginx-plus-module-xslt \
        # nginx-plus-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
    " \ 
    && echo "Acquire::https::pkgs.nginx.com::Verify-Peer \"true\";" > /etc/apt/apt.conf.d/90nginx \
    && echo "Acquire::https::pkgs.nginx.com::Verify-Host \"true\";" >> /etc/apt/apt.conf.d/90nginx \
    && echo "Acquire::https::pkgs.nginx.com::SslCert     \"/etc/ssl/nginx/nginx-repo.crt\";" >> /etc/apt/apt.conf.d/90nginx \
    && echo "Acquire::https::pkgs.nginx.com::SslKey      \"/etc/ssl/nginx/nginx-repo.key\";" >> /etc/apt/apt.conf.d/90nginx \
    && echo "deb [signed-by=$NGINX_GPGKEY_PATH] https://pkgs.nginx.com/plus/debian `lsb_release -cs` nginx-plus\n" > /etc/apt/sources.list.d/nginx-plus.list \
    && mkdir -p /etc/ssl/nginx \
    && cat nginx-repo.crt > /etc/ssl/nginx/nginx-repo.crt \
    && cat nginx-repo.key > /etc/ssl/nginx/nginx-repo.key \
    && apt-get update \
    && apt-get install --no-install-recommends --no-install-suggests -y $nginxPackages curl gettext-base \
    && apt-get remove --purge -y lsb-release \
    && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx-plus.list \
    && rm -rf /etc/apt/apt.conf.d/90nginx /etc/ssl/nginx \
# Forward request logs to Docker log collector
    && ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log

EXPOSE 80

STOPSIGNAL SIGQUIT

CMD ["nginx", "-g", "daemon off;"]

NGINX Plus의 라이선스 키를 이용하여 Dockerfile을 Build 합니다.
(NGINX Plus는 상업용 버전입니다. 구독 버전을 체험해 보고 싶으시다면 NGINX STORE를 통해 문의해 무료로 NGINX One trial을 1달간 사용하실 수 있습니다.

docker build  --no-cache --secret id=nginx-key,src=nginx-repo.key --secret id=nginx-crt,src=nginx-repo.crt -t nginxplus .

3. OpenTelemetry란?

OpenTelemetry는 분산 시스템에서 트레이스(요청 흐름), 메트릭(성능 지표), 로그(이벤트 정보)와 같은 관측 가능성 데이터를 표준 방식으로 수집·처리하여 시스템의 상태와 성능을 효과적으로 모니터링할 수 있게 해주는 오픈소스 프레임워크입니다.

4. Grafana Tempo란?

Grafana Tempo는 트레이스(요청 흐름) 데이터를 표준 방식으로 저장하고, Grafana 대시보드와 연동하여 분산 시스템의 요청 흐름을 시각화하고 분석할 수 있게 해주는 오픈소스 트레이싱 백엔드입니다.

5. OpenTelemetry & Grafana Tempo 배포

Grafana Tempo와 OpenTelemetry를 이용하여 Tracing Data를 수집하고 저장할 수 있습니다.

먼저 Grafana Tempo를 배포합니다.(OpenTelemetry 배포시 Tempo URL 필요)

# tempo.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: tempo-conf
data:
  config.yml: |
    server:
      http_listen_port: 3100 # Tempo의 WebUI에서 Tracing Data를 확인할 수 있도록 HTTP Listen Port를 지정합니다.
    distributor:
      receivers:
        otlp: # OpenTelemetry의 Tracing 데이터를 받기 위해 명시합니다.
          protocols:
            grpc:
              endpoint: 0.0.0.0:4317 # OpenTelemetry의 가공된 데이터를 받을 수 있도록 gRPC 프로토콜로 4317포트를 허용합니다. 
    storage:  # Tracing Data가 저장될 스토리지는 Pod의 로컬 스토리지를 사용하였습니다.
      trace:  # Tracing 데이터를 저장합니다.
        backend: local 
        wal:
          path: /tmp/tempo/wal
        local:
          path: /tmp/tempo/blocks
---
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: tempo
  labels: 
    app: tempo 
spec: 
  selector: 
    matchLabels: 
      app: tempo 
  template: 
    metadata: 
      labels: 
        app: tempo
    spec: 
      containers: 
        - name: tempo
          image: grafana/tempo
          ports: 
            - containerPort: 3100 # Grafana Tempo의 WebUI Port를 지정합니다.
              name: tempo-web
            - containerPort: 4317 # Tracing 데이터를 받을 gRPC 프로토콜 포트를 지정합니다.
              name: grpc-receive
          args:
            - --config.file=/etc/tempo/config.yml # 구성 파일의 위치를 지정합니다.
          volumeMounts: 
            - name: tempo-config # Config을 /etc/tempo/config.yml에 할당 할 수 있도록 Volume을 Mount합니다.
              mountPath: /etc/tempo/config.yml
              subPath: config.yml
      volumes: # 해당 볼륨에 Configmap을 적용합니다.
        - name: tempo-config
          configMap:
            name: tempo-conf
---
apiVersion: v1  
kind: Service 
metadata: 
  name: tempo-svc 
  labels: 
    app: tempo
spec: 
  selector: 
    app: tempo
  ports: 
    - name: tempo-web # Tracing Data를 확인하기 위한 Tempo WebUI입니다.
      port: 3100
      targetPort: 3100
    - name: grpc-tempo-otlp # gRPC Tracing Data 수집용 Port입니다.
      port: 4317
      protocol: TCP 
      targetPort: 4317
  type: NodePort # NodePort를 이용하여 Tempo Web로 접속할 수 있도록 합니다.
$ kubectl apply -f tempo.yaml

Grafana Tempo 리소스를 배포합니다.

tempo service 3100번의 NodePort로 접속하여 /api/search 엔드포인트에서 Tracing Data를 확인할 수 있습니다.

현재는 OpenTelemetry와 NGINX가 연동이 연동되어있지 않기 때문에 아무 Tracing 데이터가 없는 것을 확인할 수 있습니다.

OpenTelemetry Collector를 배포하여 Tracing Data를 가져올 수 있도록 합니다.

# otel.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-conf
  labels:
    app: opentelemetry
    component: otel-collector-conf
data:
  otel-collector-config.yaml: |
    processors:
      batch: # Tracing Data를 한번에 묶어서 전송하는 방식입니다.
        timeout: 5 # 데이터를 batch로 전송하기 전 기다리는 시간입니다.
        send_batch_size: 1024 # 한번에 전송할 수 있는 데이터의 batch 최대 크기입니다.
    receivers: # Tracing 데이터를 받을 프로토콜과 포트를 지정합니다.
      otlp:
        protocols:
          grpc: # NGINX otel 모듈은 gRPC 프로토콜을 지원하기 때문에 gRPC 프로토콜을 명시합니다.
            endpoint: 0.0.0.0:4317
    exporters:
      otlp/tempo:
        endpoint: "tempo-svc.tracing.svc.cluster.local:4317" # Grafana Tempo Service의 kubedns url을 작성하여 Tracing Data를 전달합니다.
        tls:
          insecure: true
    service:
      pipelines: # trace(추적) 데이터를 OTLP(수신) → batch 처리 → Tempo(내보내기) 순으로 처리합니다.
        traces:
          receivers: [otlp]
          processors: [batch]
          exporters: [otlp/tempo]
---
apiVersion: v1
kind: Service
metadata:
  name: otel-collector
  labels:
    app: opentelemetry
    component: otel-collector
spec:
  ports:
  - name: otlp-grpc # NGINX로 부터 Tracing Data를 전달 받을 포트를 지정합니다.
    port: 4317
    protocol: TCP
    targetPort: 4317
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-collector
  labels:
    app: opentelemetry
spec:
  selector:
    matchLabels:
      app: opentelemetry
  replicas: 1
  template:
    metadata:
      labels:
        app: opentelemetry
        component: otel-collector
    spec:
      containers:
      - command:
          - "/otelcol"
          - "--config=/conf/otel-collector-config.yaml" # 구성의 위치를 지정합니다.
        image: otel/opentelemetry-collector:latest
        name: otel-collector
        ports:
        - containerPort: 4317 # otel 구성의 gRPC 포트를 명시합니다. 
        volumeMounts: # 위의 구성의 configmap을 /conf path로 전달합니다.
        - name: otel-collector-config
          mountPath: /conf
      volumes: # 지정 한 volume에 configmap을 할당합니다.
      - name: otel-collector-config
        configMap:
          name: otel-collector-conf
$ kubectl apply -f otel.yaml

OpenTelemetry를 배포합니다.

NGINX를 배포하여 Tracing Data가 전달될 수 있도록 합니다.

6. NGINX Kubernetes 배포

OpenTelemetry Collector와 Grafana Tempo를 배포하기 전 NGINX를 배포합니다.

NGINX Otel 모듈의 자세한 내용은 NGINX Opentelemetry 모듈 가이드 에서 확인할 수 있습니다.

해당 포스트에서는 NGINX Plus를 사용하였습니다.

# nginx.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginxplus-opentelemetry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-otel
  template:
    metadata:
      labels:
        app: nginx-otel
    spec:
      containers:
        - name: nginx-plus
          image: [NGINX Plus Image | NGINX OSS]
          ports:
            - containerPort: 80
          volumeMounts:
            - name: config-volume
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf
            - name: nginx-plus-license # NGINX Plus License 검증용 Configmap입니다. NGINX OSS경우 해당 구성은 필요하지 않습니다.
              mountPath: /etc/nginx/license.jwt
              subPath: license.jwt
      imagePullSecrets:
        - name: regcred
      volumes:
        - name: config-volume
          configMap:
            name: nginx-conf
        - name: nginx-plus-license # 해당 구성은 NGINX Plus 전용 구성입니다.
          secret:
            secretName: jwt-secret
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  nginx.conf: |
    load_module modules/ngx_otel_module.so; # 

    user    nginx;
    worker_processes    auto;

    error_log    /var/log/nginx/error.log notice;
    pid          /var/run/nginx.pid;

    events {
        worker_connections    1024;
    }

    http {
        include       /etc/nginx/mime.types;
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';
        access_log    /var/log/nginx/access.log  main;

        otel_exporter { # Tracing Data를 받을 Otel-collector를 지정합니다.
            endpoint otel-collector.apm.svc.cluster.local:4317;
        }

        otel_service_name nginx;
        server {
            listen 80;
            otel_trace on;
            otel_trace_context propagate;
            location / {
               root /usr/share/nginx/html;
               index index.html index.htm;
            }
        }
    }

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-opentelemetry-svc
spec:
  selector:
    app: nginx-otel
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: NodePort # NGINX Service를 접속할 수 있도록 합니다.

NGINX를 배포합니다.

최종 배포는 위와 같습니다.

NGINX에 요청을 여러번 보낸 후 Tempo를 확인하여 Tracing Data를 확인할 수 있습니다.

http://[Tempo IP]:[Tempo Port]/api/search

traceID를 이용하여 Tracing Data에 대한 더 자세한 정보를 API로 알 수 있습니다.

http://[Tempo IP]:[Tempo Port]/api/traces/[Trace ID]
{
  "batches": [
    {
      "resource": {
        "attributes": [
          {
            "key": "service.name", # Trace Service Name
            "value": {
              "stringValue": "nginx"
            }
          }
        ]
      },
      "scopeSpans": [
        {
          "scope": {
            "name": "nginx", # NGINX
            "version": "1.27.4" # NGINX 버전
          },
          "spans": [
            {
              "traceId": "pEKm/wjjsukH+h/IrCR24w==", # Trace ID
              "spanId": "aOHdscrFQm4=", # Span ID
              "name": "/", # Span의 이름입니다. (NGINX에서는 경로로  Span Name을 지정합니다.)
              "kind": "SPAN_KIND_SERVER", # 외부에서 서버로 들어온 요청을 처리한 Span입니다.
              "startTimeUnixNano": "1747621636180000000", # 요청 시작 시간
              "endTimeUnixNano": "1747621636180000000", # 요청 종료 시간  
              "attributes": [
                {
                  "key": "http.target", # 요청 URI 경로
                  "value": {
                    "stringValue": "/"
                  }
                },
                {
                  "key": "http.route", # 라우트 경로
                  "value": {
                    "stringValue": "/"
                  }
                },
                {
                  "key": "http.scheme", # 요청 프로토콜 (HTTP/HTTPS)
                  "value": {
                    "stringValue": "http"
                  }
                },
                {
                  "key": "http.flavor", # HTTP 버전
                  "value": {
                    "stringValue": "1.1"
                  }
                },
                {
                  "key": "http.user_agent", # 클라이언트의 User Agent
                  "value": {
                    "stringValue": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
                  }
                },
                {
                  "key": "http.request_content_length", # 요청 Body 크기
                  "value": {
                    "intValue": "0"
                  }
                },
                {
                  "key": "http.response_content_length", # 응답 Body 크기
                  "value": {
                    "intValue": "615"
                  }
                },
                {
                  "key": "net.host.name", # 서버(호스트) 또는 도메인
                  "value": {
                    "stringValue": "192.168.201.141"
                  }
                },
                {
                  "key": "net.sock.peer.addr", # 클라이언트 IP 주소
                  "value": {
                    "stringValue": "10.0.133.0"
                  }
                },
                {
                  "key": "net.sock.peer.port", # 클라이언트 포트
                  "value": {
                    "intValue": "57483"
                  }
                },
                {
                  "key": "http.method", # HTTP Method
                  "value": {
                    "stringValue": "GET"
                  }
                },
                {
                  "key": "http.status_code", # 응답 HTTP Status Code
                  "value": {
                    "intValue": "200"
                  }
                }
              ],
              "status": {

              }
            }
          ]
        }
      ]
    }
  ]
}

NGINX에서 보내는 Tracing Data는 기본적으로 아래와 같습니다.

  • http.method – HTTP Method
  • http.target – 요청 URI 경로
  • http.route – Route 경로
  • http.scheme – 요청 프로토콜 (HTTP/HTTPS)
  • http.flavor – HTTP 버전
  • http.user_agent – 요청을 보낸 클라이언트의 User Agent
  • http.request_content_length – 요청 Body 크기
  • http.response_content_length – 응답 Body 크기
  • http.status_code – 응답 HTTP Status Code
  • net.host.name – 서버(호스트) IP 또는 도메인
  • net.host.port – 서버의 포트
  • net.sock.peer.addr – 클라이언트 IP 주소
  • net.sock.peer.port – 클라이언트 포트

7. 결론

NGINX, OpenTelemetry Collector, Grafana Tempo를 연동하여 구축한 트레이싱 시스템을 통해 마이크로서비스 아키텍처 내의 요청 흐름을 명확히 파악할 수 있습니다. 본 가이드를 통해 NGINX에서 발생한 요청을 수집하고, 이를 시각화하여 병목 지점이나 지연 원인을 분석하는 기반을 마련할 수 있습니다.

Kubernetes에 대한 더 자세한 정보를 알고싶으시다면 NGINX STORE Kubernetes 카테고리를 방문해주세요.

NGINX STORE를 통한 솔루션 도입 및 기술지원 무료 상담 신청

* indicates required