Envoy 를 통한 Opentelemetry header 기반 라우팅

1. OTLP Router 구성 문서
OTLP Router는 OpenTelemetry Collector 앞단에 위치하여 수집되는 Telemetry 데이터를 서비스, 테넌트, 환경별로 라우팅(경로 지정) 해주는 역할을 수행합니다.
우리 시스템에서는 여러 테넌트에서 들어오는 OTLP gRPC/HTTP 요청을 자동으로 라우팅 하며, Trace/Metric/Log 데이터가 지정된 Collector로 전달될 수 있도록 구성되어 있습니다.
Router는 다음 역할을 수행합니다.
- 다중 Collector 환경에서 수평 확장된 OTLP 수집 구조 확립
- 요청 헤더, 엔드포인트 규칙, 메타데이터 기반 라우팅 조건 처리
- 잘못된 요청, 미등록 테넌트 요청 등에 대한 Failover / Drop 처리
- Collector 구성 변경 없이 테넌트 추가만으로 손쉬운 확장 제공
참고 문서
- Envoy Proxy 공식 문서: https://www.envoyproxy.io/docs
- OpenTelemetry OTLP 규격: https://opentelemetry.io/docs/specs/otlp/
OTLP Router 주요 역할
OTLP Router는 다음과 같은 입력 기반으로 Collector 목적지를 결정합니다.
| 구분 | 방식 | 설명 |
|---|---|---|
| Host 기반 Routing | :authority 헤더 | 테넌트별 Collector를 도메인 기반으로 구분 |
| Header 기반 Mapping | x-tenant-id | Gateway 및 Agent에서 삽입되는 테넌트 식별값 |
| Path 기반 Routing | /v1/traces, /v1/metrics | OTLP 타입별 엔드포인트 라우팅 |
라우터는 서비스 단계에서 Collector를 직접 호출하는 대신, 다음과 같은 통일된 엔드포인트를 제공하여 Collector 변경 시 서비스 영향 없이 운영이 가능하도록 설계되어 있습니다.
otel-router.<namespace>.svc.cluster.local:4317
otel-router.<namespace>.svc.cluster.local:43181-1. Router 동작 방식
Router는 Envoy 기반으로 동작하며, 다음 3단계로 요청을 처리합니다.
수신(Listener Stage)
- gRPC(4317), HTTP(4318) 기반의 OTLP 요청 수신
- TLS 여부, 인증 정책 등의 필터 적용
매칭(Route Matching Stage)
x-tenant-id,:authority, OTLP 데이터 타입 기반 라우팅- 매칭 실패 시 fallback 규칙 적용 (예: default Collector 전송 또는 Drop)
전송(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 | 상세 경로 규칙 설정 |
cluster | Collector Endpoint 정의 |
http_filters | Metadata 기반 라우팅 |
retry_policy | Collector 응답 실패 시 재시도 제어 |
라우팅 예시 시나리오
| 요청 형태 | 처리 결과 |
|---|---|
tenant=k6-54980, /v1/traces | → otelcol-k6-54980:4317 Collector로 전달 |
tenant=effie_50690, /v1/metrics | → otelcol-effie-50690:4317 Collector로 전달 |
| 테넌트 미정의 상태 요청 | → 기본 Collector 혹은 Drop 처리 |
3. 운영 및 유지 관리
주기적인 점검 항목
- 신규 테넌트 Collector 서비스 등록 여부
- 라우팅 매핑 규칙과 실제 Collector 상태 일치 여부 확인
- Envoy 라우터 리소스 점유율(CPU/MEM), QPS, Retry Count 확인
- gRPC 오류율(UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL) 모니터링
장애 발생 시 확인 절차
- Router 로그 확인 (
envoy.access.log,envoy.error.log) - Drop/Retry Rate 증가 여부 확인
- Collector readiness/liveness 상태 점검
- 라우팅 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: 43176. 설정 파일 영역별 설명
아래 내용은 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를 함께 변경해야 합니다.
- 네임스페이스를 변경하면 Ingress, Service, NetworkPolicy 등 모든 리소스의
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입니다.
- 주요 역할:
listeners0.0.0.0:4317에서 OTLP HTTP 요청 수신/v1/*경로만 허용, 나머지는 404 반환
http_filters- Lua 필터:
x-tenant-id검증 및x-otlp-upstream-host헤더 생성 - Dynamic Forward Proxy 필터: 생성된 호스트명을 기반으로 DNS 조회 후 업스트림 연결
- Router 필터: 최종 업스트림 전송
- Lua 필터:
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-envoyEnvoy 라우터의 실제 실행 Pod를 관리하는 리소스입니다.
주요 포인트:
replicas: 2- Envoy 인스턴스를 2개 실행하여 고가용성 구성
image: envoyproxy/envoy:v1.31.0- 사용 중인 Envoy 버전
volumeMounts/volumes- 앞에서 정의한
otel-router-envoyConfigMap을/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>만 맞게 넣어주면, 나머지는 라우터가 처리
- Endpoint:
운영 시 변경 포인트:
- 도메인 변경 시 →
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 → 영역별 의미” 흐름으로 인수인계 문서 구조가 자연스럽게 이어집니다.