목차

Envoy 를 통한 Opentelemetry header 기반 라우팅

Monitoring/Resources/4991336bdb1279e909c8dc6913267359_MD5.jpeg

1. OTLP Router 구성 문서

OTLP Router는 OpenTelemetry Collector 앞단에 위치하여 수집되는 Telemetry 데이터를 서비스, 테넌트, 환경별로 라우팅(경로 지정) 해주는 역할을 수행합니다.
우리 시스템에서는 여러 테넌트에서 들어오는 OTLP gRPC/HTTP 요청을 자동으로 라우팅 하며, Trace/Metric/Log 데이터가 지정된 Collector로 전달될 수 있도록 구성되어 있습니다.

Router는 다음 역할을 수행합니다.

  • 다중 Collector 환경에서 수평 확장된 OTLP 수집 구조 확립
  • 요청 헤더, 엔드포인트 규칙, 메타데이터 기반 라우팅 조건 처리
  • 잘못된 요청, 미등록 테넌트 요청 등에 대한 Failover / Drop 처리
  • Collector 구성 변경 없이 테넌트 추가만으로 손쉬운 확장 제공

참고 문서

OTLP Router 주요 역할

OTLP Router는 다음과 같은 입력 기반으로 Collector 목적지를 결정합니다.

구분방식설명
Host 기반 Routing:authority 헤더테넌트별 Collector를 도메인 기반으로 구분
Header 기반 Mappingx-tenant-idGateway 및 Agent에서 삽입되는 테넌트 식별값
Path 기반 Routing/v1/traces, /v1/metricsOTLP 타입별 엔드포인트 라우팅

라우터는 서비스 단계에서 Collector를 직접 호출하는 대신, 다음과 같은 통일된 엔드포인트를 제공하여 Collector 변경 시 서비스 영향 없이 운영이 가능하도록 설계되어 있습니다.

otel-router.<namespace>.svc.cluster.local:4317
otel-router.<namespace>.svc.cluster.local:4318

1-1. Router 동작 방식

Router는 Envoy 기반으로 동작하며, 다음 3단계로 요청을 처리합니다.

  1. 수신(Listener Stage)

    • gRPC(4317), HTTP(4318) 기반의 OTLP 요청 수신
    • TLS 여부, 인증 정책 등의 필터 적용
  2. 매칭(Route Matching Stage)

    • x-tenant-id, :authority, OTLP 데이터 타입 기반 라우팅
    • 매칭 실패 시 fallback 규칙 적용 (예: default Collector 전송 또는 Drop)
  3. 전송(Upstream Forwarding Stage)

    • 대상 Collector의 서비스 엔드포인트로 전달
    • 전달 실패 시 Retry/Backoff/Timeout 처리

주요 수집 대상

OTLP Router는 다음과 같은 환경에서 발생하는 Telemetry 데이터를 수집할 수 있는 확장성을 제공합니다.

  • Kubernetes 기반 서비스(OpenTelemetry Agent, Sidecar, Gateway)
  • Kafka 기반 이벤트 처리 서비스
  • Bare-metal 또는 VM 기반 Agent 적용 환경
  • IoT 또는 Edge 시스템에서 발생하는 원격 Trace/Metric Log 데이터

시스템 적용 목적

Router를 도입함으로써 다음과 같은 이점이 있습니다.

  • Collector가 다중 테넌트 환경에서 직접 외부에 노출되지 않음
  • Collector 개별 설정 변경 없이 테넌트 구조 확장 가능
  • OTLP 데이터 처리 파이프라인의 안정적 운영 및 장애 도메인 분리
  • Trace/Metric/Log 분리 수집 및 저장 구조 자동화

2. 라우팅 규칙 구조 설명

아래는 Router 구성에서 사용되는 라우팅 규칙 요소입니다.

구성 요소역할
virtual_hosts도메인 기반 라우팅
routes상세 경로 규칙 설정
clusterCollector Endpoint 정의
http_filtersMetadata 기반 라우팅
retry_policyCollector 응답 실패 시 재시도 제어

라우팅 예시 시나리오

요청 형태처리 결과
tenant=k6-54980, /v1/tracesotelcol-k6-54980:4317 Collector로 전달
tenant=effie_50690, /v1/metricsotelcol-effie-50690:4317 Collector로 전달
테넌트 미정의 상태 요청→ 기본 Collector 혹은 Drop 처리

3. 운영 및 유지 관리

주기적인 점검 항목

  • 신규 테넌트 Collector 서비스 등록 여부
  • 라우팅 매핑 규칙과 실제 Collector 상태 일치 여부 확인
  • Envoy 라우터 리소스 점유율(CPU/MEM), QPS, Retry Count 확인
  • gRPC 오류율(UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL) 모니터링

장애 발생 시 확인 절차

  1. Router 로그 확인 (envoy.access.log, envoy.error.log)
  2. Drop/Retry Rate 증가 여부 확인
  3. Collector readiness/liveness 상태 점검
  4. 라우팅 Key (x-tenant-id, :authority, path`) 정상 여부 확인

4. 구성 파일

Router의 실제 구성은 아래 파일에 정의되어 있습니다.

otel-router.yaml

해당 파일은 테넌트 기반 Collector 구조가 확장됨에 따라 지속 업데이트됩니다.

5. 설정 파일

# ==============================================================
# Namespace
# ==============================================================
apiVersion: v1
kind: Namespace
metadata:
  name: monitoring

# ===============================================================
# Envoy Config (HTTP/Protobuf on 4317, x-tenant-id 기반 동적 라우팅)
# - Downstream: HTTP/1.x 수신(4317)
# - Lua: x-tenant-id 검증 + x-otlp-upstream-host 헤더 구성
# - DFP: x-otlp-upstream-host 값을 :authority 로 사용해 동적 DNS/업스트림 연결
# - Upstream: HTTP/1.x
# - Envoy 1.31.0 호환 (use_tcp_for_dns_lookups 제거)
# ===============================================================
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-router-envoy
  namespace: monitoring
data:
  envoy.yaml: |
    static_resources:
      listeners:
      - name: otlp_http_listener_4317
        address:
          socket_address:
            address: 0.0.0.0
            port_value: 4317
        filter_chains:
        - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: hcm_http_4317
              codec_type: AUTO
              route_config:
                name: local_route
                virtual_hosts:
                - name: any
                  domains: ["*"]
                  routes:
                  - match:
                      prefix: "/v1/"
                    route:
                      cluster: dynamic_forward_proxy_cluster
                      timeout: 15s
                    # Per-Route: DFP가 사용할 :authority 헤더 지정
                    typed_per_filter_config:
                      envoy.filters.http.dynamic_forward_proxy:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.PerRouteConfig
                        host_rewrite_header: "x-otlp-upstream-host"

              http_filters:
              - name: envoy.filters.http.lua
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                  inline_code: |
                    -- /v1/* 만 허용하고, x-tenant-id 검증 + x-otlp-upstream-host 설정
                    function envoy_on_request(handle)
                      local path = handle:headers():get(":path") or ""
                      if not string.match(path, "^/v1/") then
                        handle:respond({[":status"]="404"}, "not found")
                        return
                      end

                      local tenant = handle:headers():get("x-tenant-id")
                      if tenant  nil then
                        handle:respond({[":status"]="400"}, "missing x-tenant-id")
                        return
                      end
                      if not string.match(tenant, "^[a-zA-Z0-9_-]+$") then
                        handle:respond({[":status"]="400"}, "invalid tenant id")
                        return
                      end

                      -- 업스트림 FQDN(:authority로 사용). 포트 포함.
                      local upstream = string.format("otelcol-%s.%s.svc.cluster.local:4317", tenant, tenant)
                      handle:headers():add("x-otlp-upstream-host", upstream)
                    end

              - name: envoy.filters.http.dynamic_forward_proxy
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig
                  dns_cache_config:
                    name: otlp_http_dns_cache_4317
                    dns_lookup_family: V4_ONLY
                    dns_refresh_rate: 5s
                    host_ttl: 300s
                    max_hosts: 1024
                    # CoreDNS 리졸버 고정 (ClusterIP 198.19.128.3)
                    dns_resolution_config:
                      resolvers:
                      - socket_address:
                          address: 198.19.128.3
                          port_value: 53

              - name: envoy.filters.http.router
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

              access_log:
              - name: envoy.access_loggers.stdout
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                  log_format:
                    text_format: "[%START_TIME%] %REQ(:method)% %REQ(:path)% x-tenant-id=%REQ(x-tenant-id)% x-otlp-upstream-host=%REQ(x-otlp-upstream-host)% upstream=%UPSTREAM_HOST% status=%RESPONSE_CODE% flags=%RESPONSE_FLAGS%\n"

      clusters:
      - name: dynamic_forward_proxy_cluster
        connect_timeout: 5s
        lb_policy: CLUSTER_PROVIDED
        cluster_type:
          name: envoy.clusters.dynamic_forward_proxy
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig
            # 아래 dns_cache_config 는 위(http 필터)의 설정과 바이트 단위로 동일해야 함
            dns_cache_config:
              name: otlp_http_dns_cache_4317
              dns_lookup_family: V4_ONLY
              dns_refresh_rate: 5s
              host_ttl: 300s
              max_hosts: 1024
              dns_resolution_config:
                resolvers:
                - socket_address:
                    address: 198.19.128.3
                    port_value: 53
            allow_insecure_cluster_options: true  # 평문 HTTP 업스트림 허용
        typed_extension_protocol_options:
          envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
            "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
            explicit_http_config:
              http_protocol_options: {}

# ===============================================================
# Envoy Deployment
# ===============================================================
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-router
  namespace: monitoring
spec:
  replicas: 2
  selector:
    matchLabels:
      app: otel-router
  template:
    metadata:
      labels:
        app: otel-router
    spec:
      containers:
      - name: envoy
        image: envoyproxy/envoy:v1.31.0
        args: ["-c", "/etc/envoy/envoy.yaml", "--log-level", "info"]
        ports:
        - containerPort: 4317
          name: http-otlp
        volumeMounts:
        - name: envoy-config
          mountPath: /etc/envoy
        readinessProbe:
          tcpSocket:
            port: 4317
          initialDelaySeconds: 3
          periodSeconds: 5
        livenessProbe:
          tcpSocket:
            port: 4317
          initialDelaySeconds: 10
          periodSeconds: 10
        startupProbe:
          tcpSocket:
            port: 4317
          failureThreshold: 30
          periodSeconds: 2
      volumes:
      - name: envoy-config
        configMap:
          name: otel-router-envoy

# ===============================================================
# Envoy Service (ClusterIP) - Ingress가 이 서비스로 전달
# ===============================================================
apiVersion: v1
kind: Service
metadata:
  name: otel-router
  namespace: monitoring
spec:
  selector:
    app: otel-router
  ports:
  - name: http-otlp
    port: 4317
    targetPort: 4317
    protocol: TCP
  type: ClusterIP

# ===============================================================
# NetworkPolicy
#  - DNS (UDP/TCP 53): CoreDNS ClusterIP ipBlock 화이트리스트(198.19.128.3/32)
#  - 업스트림(Service 포트) 4317 허용 (monitoring 네임스페이스)
#  - Ingress-NGINX 로부터 4317 수신 허용
# ===============================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-otel-router-4317
  namespace: monitoring
spec:
  podSelector:
    matchLabels:
      app: otel-router
  policyTypes: ["Ingress","Egress"]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          ingress-allow: "true"                   # 환경에 맞게 조정
      podSelector:
        matchLabels:
          app.kubernetes.io/name: ingress-nginx   # 환경 라벨에 맞게 조정
    ports:
    - protocol: TCP
      port: 4317
  egress:
  - to:
    - ipBlock:
        cidr: 198.19.128.3/32                     # CoreDNS ClusterIP 고정
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
    ports:
    - protocol: TCP
      port: 4317

# ===============================================================
# Ingress (nginx 클래스, 80/443에서 /v1/* → otel-router:4317로 전달)
# ===============================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: otlp-http-external
  namespace: monitoring
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-body-size: "32m"
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
  ingressClassName: nginx
  rules:
  - host: otel.monitoring.mesimsaas.com
    http:
      paths:
      - path: /v1/
        pathType: Prefix
        backend:
          service:
            name: otel-router
            port:
              number: 4317

# =
# tenant01 Collector Service (selector 기반, 자동 Endpoints)
# - Collector가 HTTP=4317 리스닝이면 targetPort=4317 유지
# - Collector가 HTTP=4318이면 targetPort=4318 로 변경 필요
# =
apiVersion: v1
kind: Service
metadata:
  name: otelcol-tenant01
  namespace: monitoring
spec:
  type: ClusterIP
  selector:
    app: otelcol-master
  ports:
  - name: http-otlp
    port: 4317
    targetPort: 4317

6. 설정 파일 영역별 설명

아래 내용은 otel-router.yaml 내 주요 리소스를 영역별로 나누어 설명한 것입니다. 인수인계 시 “어디를 수정하면 어떤 동작에 영향을 주는지”를 빠르게 파악할 수 있도록 구성했습니다.

6-1. Namespace 정의

apiVersion: v1
kind: Namespace
metadata:
  name: monitoring
  • OTLP Router 및 Collector 등 모니터링 관련 리소스를 모아두는 전용 네임스페이스입니다.
  • 이 네임스페이스 기준으로:
    • Envoy 라우터(Deployment/Service/ConfigMap/NetworkPolicy)
    • 예시 Collector 서비스(otelcol-tenant01) 등이 함께 배치됩니다.
  • 커스텀 가능 부분:
    • 네임스페이스를 변경하면 Ingress, Service, NetworkPolicy 등 모든 리소스의 namespace를 함께 변경해야 합니다.

6-2. Envoy ConfigMap (envoy.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-router-envoy
  namespace: monitoring
data:
  envoy.yaml: |
    static_resources:
      listeners:
      - name: otlp_http_listener_4317
        ...
      clusters:
      - name: dynamic_forward_proxy_cluster
        ...
  • Envoy 프로세스가 사용할 **실제 설정 파일(envoy.yaml)**을 담고 있는 ConfigMap입니다.
  • 주요 역할:
    • listeners
      • 0.0.0.0:4317에서 OTLP HTTP 요청 수신
      • /v1/* 경로만 허용, 나머지는 404 반환
    • http_filters
      • Lua 필터: x-tenant-id 검증 및 x-otlp-upstream-host 헤더 생성
      • Dynamic Forward Proxy 필터: 생성된 호스트명을 기반으로 DNS 조회 후 업스트림 연결
      • Router 필터: 최종 업스트림 전송
    • clusters.dynamic_forward_proxy_cluster
      • DFP용 클러스터 정의
      • CoreDNS(198.19.128.3)로 DNS 질의
      • 업스트림 Collector와 HTTP(평문)로 통신 커스텀 가능 부분:
  • 테넌트 FQDN 포맷을 바꾸고 싶을 때 → Lua 코드의 string.format("otelcol-%s.%s.svc.cluster.local:4317", tenant, tenant) 수정
  • OTLP 포트를 Collector에서 4318로 바꿀 때 → 위 포맷 문자열의 포트를 :4318로 변경해야 함
  • DNS 서버가 바뀔 경우 → dns_resolution_config.resolvers[0].socket_address.address 값 수정

6-3. Envoy Deployment (otel-router)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-router
  namespace: monitoring
spec:
  replicas: 2
  selector:
    matchLabels:
      app: otel-router
  template:
    metadata:
      labels:
        app: otel-router
    spec:
      containers:
      - name: envoy
        image: envoyproxy/envoy:v1.31.0
        args: ["-c", "/etc/envoy/envoy.yaml", "--log-level", "info"]
        ports:
        - containerPort: 4317
        volumeMounts:
        - name: envoy-config
          mountPath: /etc/envoy
        readinessProbe: ...
        livenessProbe: ...
        startupProbe: ...
      volumes:
      - name: envoy-config
        configMap:
          name: otel-router-envoy
  • Envoy 라우터의 실제 실행 Pod를 관리하는 리소스입니다.

  • 주요 포인트:

    • replicas: 2

      • Envoy 인스턴스를 2개 실행하여 고가용성 구성
    • image: envoyproxy/envoy:v1.31.0

      • 사용 중인 Envoy 버전
    • volumeMounts / volumes

      • 앞에서 정의한 otel-router-envoy ConfigMap을 /etc/envoy/envoy.yaml로 마운트
    • readinessProbe / livenessProbe / startupProbe

      • 4317 포트 TCP 체크로 Envoy 상태를 확인해, 비정상 상태시 재시작/트래픽 차단

운영 시 변경 포인트:

  • 라우터 수평 확장 필요 시 → replicas 값 조정
  • Envoy 버전 업그레이드 시 → image 태그 변경
  • 로그 레벨 조정 필요 시 → args--log-level 값을 debug 등으로 변경

6-4. Envoy Service (otel-router)

apiVersion: v1
kind: Service
metadata:
  name: otel-router
  namespace: monitoring
spec:
  selector:
    app: otel-router
  ports:
  - name: http-otlp
    port: 4317
    targetPort: 4317
    protocol: TCP
  type: ClusterIP
  • 네임스페이스 내부에서 otel-router:4317 이름으로 Envoy에 접근할 수 있게 해주는 Service입니다.

  • Ingress 및 다른 내부 Pod들은 이 Service를 통해 라우터와 통신합니다.

  • 일반적으로 변경할 부분은 거의 없지만:

    • 포트를 변경할 경우 Ingress, Probe 설정 등 연관 리소스 포트도 함께 변경해야 합니다.

6-5. NetworkPolicy (allow-otel-router-4317)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-otel-router-4317
  namespace: monitoring
spec:
  podSelector:
    matchLabels:
      app: otel-router
  policyTypes: ["Ingress","Egress"]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          ingress-allow: "true"
      podSelector:
        matchLabels:
          app.kubernetes.io/name: ingress-nginx
    ports:
    - protocol: TCP
      port: 4317
  egress:
  - to:
    - ipBlock:
        cidr: 198.19.128.3/32
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
    ports:
    - protocol: TCP
      port: 4317
  • 라우터 Pod(app: otel-router)에 대한 네트워크 입·출력을 제한/허용하는 정책입니다.

  • Ingress:

    • ingress-allow: "true" 라벨이 붙은 네임스페이스의
    • app.kubernetes.io/name: ingress-nginx 라벨을 가진 Pod에서 오는
    • TCP 4317 트래픽만 허용
    • 즉, Ingress NGINX → Envoy 경로만 받도록 강제
  • Egress:

    • CoreDNS(198.19.128.3/32)로의 DNS 질의(UDP/TCP 53) 허용
    • monitoring 네임스페이스로의 TCP 4317 트래픽 허용 → Collector 서비스들로 OTLP HTTP가 나갈 수 있도록 허용

커스텀 가능 부분:

  • Ingress Controller 라벨이 다른 경우 → namespaceSelector / podSelector 라벨 값 환경에 맞게 수정 필요
  • DNS ClusterIP가 변경될 경우 → cidr: 198.19.128.3/32 수정
  • Collector가 다른 네임스페이스에 존재하도록 설계할 경우 → Egress namespaceSelector 조건 조정 필요

6-6. Ingress (otlp-http-external)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: otlp-http-external
  namespace: monitoring
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-body-size: "32m"
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
  ingressClassName: nginx
  rules:
  - host: otel.monitoring.mesimsaas.com
    http:
      paths:
      - path: /v1/
        pathType: Prefix
        backend:
          service:
            name: otel-router
            port:
              number: 4317
  • 외부 클라이언트(애플리케이션/에이전트 등)가 HTTP OTLP를 보내는 진입점입니다.
  • 구성:
    • 도메인 otel.monitoring.mesimsaas.com + 경로 /v1/*로 들어온 요청을
    • otel-router:4317 서비스로 전달
  • 애플리케이션 입장에서는:
    • Endpoint: http://otel.monitoring.mesimsaas.com/v1/traces
    • Header: x-tenant-id: <tenant> 만 맞게 넣어주면, 나머지는 라우터가 처리

운영 시 변경 포인트:

  • 도메인 변경 시 → host 수정
  • IngressClass가 다를 경우 → ingressClassName 변경
  • OTLP 엔드포인트 경로 정책을 바꾸고 싶을 경우 → path 및 Lua 코드의 /v1/ 검증 로직을 같이 수정

6-7. Collector Service 예시 (otelcol-tenant01)

apiVersion: v1
kind: Service
metadata:
  name: otelcol-tenant01
  namespace: monitoring
spec:
  type: ClusterIP
  selector:
    app: otelcol-master
  ports:
  - name: http-otlp
    port: 4317
    targetPort: 4317
  • 예시용 Collector 서비스 정의입니다.

  • 실제 Lua에서는 FQDN을 otelcol-<tenant>.<tenant>.svc.cluster.local:4317 형태로 만들기 때문에,

    • 실 운영에서는 각 테넌트 네임스페이스에
    • otelcol-<tenant> 서비스가 동일한 포트로 존재해야 합니다.
  • 이 예시는:

    • Collector가 4317 포트에서 HTTP OTLP를 수신하는 경우의 Service 템플릿으로 활용할 수 있습니다.

커스텀 가능 부분:

  • Collector가 4318에서 HTTP를 받을 경우 → port / targetPort 값을 4318로 변경 → 동시에 Lua의 포트 포맷도 :4318로 맞춰야 함
  • Collector Pod 라벨이 다를 경우 → selector.app 값을 실제 Collector Deployment 라벨에 맞게 수정

이 6번 섹션을 5번 “설정 파일” 바로 아래에 붙이면, “설정 전체 → 실제 YAML → 영역별 의미” 흐름으로 인수인계 문서 구조가 자연스럽게 이어집니다.