OpenTelemetry + Istio mTLS 트러블슈팅
문제 상황#
EKS Staging 환경에서 Java 서비스들이 CrashLoopBackOff 상태로 시작 실패. 로그 분석 결과 OTEL (OpenTelemetry) agent가 OTEL Collector에 연결 실패하는 문제 발견.
환경#
- Kubernetes: EKS 1.34
- Istio: mTLS STRICT 모드 활성화
- OTEL Agent: opentelemetry-javaagent v2.11.0
- OTEL Collector: opentelemetry-collector (Deployment)
- Namespace: staging-webs (앱), monitoring (OTEL Collector)
증상#
Pod 상태#
staging-webs auth-guard-xxx 0/1 CrashLoopBackOff 17
staging-webs order-core-xxx 0/1 CrashLoopBackOff 21
staging-webs queue-xxx 0/1 CrashLoopBackOff 22
staging-webs seat-xxx 0/1 CrashLoopBackOff 21
에러 로그#
[otel.javaagent] ERROR io.opentelemetry.exporter.internal.grpc.GrpcExporter -
Failed to export metrics. Server is UNAVAILABLE.
Make sure your collector is running and reachable from this network.
Full error message: upstream connect error or disconnect/reset before headers.
retried and the latest reset reason: remote connection failure,
transport failure reason: TLS_error:|268435703:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER:TLS_error_end
원인 분석#
1. 정상적인 Istio mTLS 트래픽 흐름#
일반적인 서비스 간 통신:
┌─────────────────────────────────────────────────────────────────┐
│ 정상적인 mTLS 통신 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Pod A] [Pod B] │
│ ┌────────────┐ ┌────────────┐ │
│ │ App A │ │ App B │ │
│ │ │ │ │ ↑ │ │
│ │ ↓ │ │ │ │ │
│ │ Envoy │ ════ mTLS ════════> │ Envoy │ │
│ │ Sidecar │ (자동 암호화) │ Sidecar │ │
│ └────────────┘ └────────────┘ │
│ │
│ * App은 plain HTTP로 요청 │
│ * Envoy sidecar가 자동으로 mTLS 처리 │
│ * 양쪽 모두 Istio sidecar가 있어서 문제없음 │
│ │
└─────────────────────────────────────────────────────────────────┘
2. OTEL Agent의 특수한 동작 방식#
OTEL Java agent는 JVM 내부에서 직접 네트워크 연결:
┌─────────────────────────────────────────────────────────────────┐
│ OTEL Agent 통신 문제 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Java Pod] [OTEL Collector Pod] │
│ ┌────────────────────┐ ┌────────────┐ │
│ │ ┌──────────────┐ │ │ Collector │ │
│ │ │ Spring App │ │ │ ↑ │ │
│ │ │ + │──┼─ HTTP ──────>│ │ │ ❌ 실패 │
│ │ │ OTEL Agent │ │ (직접) │ Envoy │ │
│ │ └──────────────┘ │ │ Sidecar │ │
│ │ │ │ │ (mTLS │ │
│ │ ↓ │ │ STRICT) │ │
│ │ Envoy Sidecar │ └────────────┘ │
│ │ (우회됨!) │ │
│ └────────────────────┘ │
│ │
│ 문제점: │
│ 1. OTEL agent가 JVM 내부에서 직접 gRPC/HTTP 연결 │
│ 2. Istio sidecar를 통하지 않음 (또는 불완전하게 통함) │
│ 3. OTEL Collector의 sidecar는 mTLS 기대 │
│ 4. 결과: TLS 핸드셰이크 실패 │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 에러 메시지 해석#
TLS_error:|268435703:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER
| 코드 | 의미 |
|---|---|
WRONG_VERSION_NUMBER | TLS 프로토콜 버전 불일치 |
| 원인 1 | 클라이언트가 plain HTTP 보냈는데 서버가 TLS 기대 |
| 원인 2 | 클라이언트가 TLS 보냈는데 서버가 plain HTTP 기대 |
| 원인 3 | TLS 버전 불일치 (TLS 1.2 vs 1.3 등) |
이 경우: OTEL agent가 plain HTTP(gRPC)를 보냈는데, Istio mTLS가 중간에 개입하면서 꼬임.
해결#
핵심 아이디어#
OTEL Collector만 mTLS 예외 처리 (PERMISSIVE 모드)
┌─────────────────────────────────────────────────────────────────┐
│ 해결 후 통신 흐름 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Java Pod] [OTEL Collector Pod] │
│ ┌────────────────────┐ ┌────────────┐ │
│ │ ┌──────────────┐ │ │ Collector │ │
│ │ │ Spring App │ │ │ ↑ │ │
│ │ │ + │──┼─ HTTP ──────>│ │ │ ✅ 성공 │
│ │ │ OTEL Agent │ │ │ Envoy │ │
│ │ └──────────────┘ │ │ Sidecar │ │
│ │ │ │ (PERMISSIVE│ │
│ │ │ │ = HTTP OK)│ │
│ └────────────────────┘ └────────────┘ │
│ │
│ PeerAuthentication: PERMISSIVE │
│ → mTLS도 허용, plain HTTP도 허용 │
│ │
└─────────────────────────────────────────────────────────────────┘
1. DestinationRule 추가#
클라이언트(앱) → OTEL Collector 연결 시 mTLS 비활성화
파일: common-charts/infra/monitoring/otel-collector/templates/destinationrule.yaml
# mTLS 예외 처리 - OTEL agent가 직접 연결하므로 DISABLE 필요
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: otel-collector-mtls-disable
namespace: {{ .Release.Namespace }}
spec:
host: otel-collector.{{ .Release.Namespace }}.svc.cluster.local
trafficPolicy:
tls:
mode: DISABLE
역할:
- 클라이언트 Envoy sidecar에게 “OTEL Collector로 갈 때는 mTLS 쓰지마” 지시
- 트래픽이 암호화 없이 전송됨
2. PeerAuthentication 추가#
OTEL Collector가 plain HTTP 연결도 수락하도록 설정
파일: common-charts/infra/monitoring/otel-collector/templates/peerauthentication.yaml
# OTEL Collector - mTLS PERMISSIVE 설정
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: otel-collector
namespace: {{ .Release.Namespace }}
spec:
selector:
matchLabels:
app.kubernetes.io/name: opentelemetry-collector
mtls:
mode: PERMISSIVE
portLevelMtls:
# gRPC 포트 - OTEL agent가 연결하는 포트
4317:
mode: PERMISSIVE
# HTTP 포트
4318:
mode: PERMISSIVE
역할:
- OTEL Collector의 Envoy sidecar에게 “mTLS와 plain HTTP 둘 다 받아” 지시
- 포트별 세부 설정 가능 (4317: gRPC, 4318: HTTP)
Istio mTLS 모드 비교#
| 모드 | 설명 | 사용 케이스 |
|---|---|---|
STRICT | mTLS만 허용 | 일반 서비스 간 통신 (기본값) |
PERMISSIVE | mTLS + plain HTTP 둘 다 허용 | 마이그레이션, 특수 클라이언트 |
DISABLE | mTLS 비활성화 | 외부 서비스, 레거시 |
DestinationRule vs PeerAuthentication#
| 리소스 | 적용 대상 | 역할 |
|---|---|---|
DestinationRule | 클라이언트 (발신 측) | “이 서비스로 갈 때 어떻게 연결할지” |
PeerAuthentication | 서버 (수신 측) | “어떤 연결을 받아들일지” |
둘 다 필요한 이유:
- DestinationRule만 있으면: 클라이언트는 plain HTTP 보내지만, 서버가 거부
- PeerAuthentication만 있으면: 서버는 준비됐지만, 클라이언트가 여전히 mTLS 시도
적용 후 트래픽 정책#
| 트래픽 경로 | mTLS 설정 |
|---|---|
| 앱 ↔ 앱 (staging-webs 내) | STRICT (유지) |
| 앱 → OTEL Collector | DISABLE/PERMISSIVE (예외) |
| Prometheus → 앱 metrics | STRICT (유지) |
| 외부 → Istio Gateway | TLS (인증서) |
배포 명령어#
1. 변경사항 커밋 & 푸시#
cd /Users/wonny/Documents/GitHub/303-goormgb-k8s-helm
git add -A
git commit -m "fix(otel): add mTLS exception for otel-collector"
git push
2. ArgoCD Sync (Bastion)#
# OTEL Collector 동기화 (mTLS 예외 적용)
argocd app sync staging-otel-collector --force
# 확인
kubectl get destinationrule -n monitoring
kubectl get peerauthentication -n monitoring
3. 앱 서비스 재시작#
# 모든 deployment 재시작
kubectl rollout restart deployment -n staging-webs
# 상태 확인
kubectl get pods -n staging-webs -w
검증 방법#
1. Pod 상태 확인#
kubectl get pods -n staging-webs
# 모든 Pod가 Running, READY 2/2 여야 함
2. OTEL Collector 연결 확인#
# Pod 로그에서 OTEL 에러 없어야 함
kubectl logs -n staging-webs -l app.kubernetes.io/name=auth-guard --tail=50 | grep -i otel
3. Trace 수집 확인#
Grafana → Tempo → 서비스 trace 확인
대안적 해결 방법#
방법 1: OTEL Collector sidecar 제거#
# OTEL Collector deployment에 annotation 추가
metadata:
annotations:
sidecar.istio.io/inject: "false"
장점: 간단함 단점: OTEL Collector가 mTLS 보호 안 됨
방법 2: OTEL Agent가 TLS 사용#
# 앱 values에서 OTEL 엔드포인트 변경
otel:
collectorEndpoint: "https://otel-collector.monitoring.svc.cluster.local:4317"
# + TLS 인증서 설정 필요
장점: 보안 유지 단점: 인증서 관리 복잡
방법 3: 현재 선택한 방법 (PERMISSIVE)#
장점:
- 기존 설정 최소 변경
- OTEL Collector만 예외 처리
- 다른 서비스는 STRICT 유지
단점:
- OTEL Collector로의 트래픽은 암호화 안 됨
- (내부 네트워크라 큰 문제 아님)
관련 파일#
| 파일 | 역할 |
|---|---|
staging/values/values-istio-security.yaml | mTLS 전역 설정 |
common-charts/infra/monitoring/otel-collector/templates/destinationrule.yaml | OTEL용 DestinationRule |
common-charts/infra/monitoring/otel-collector/templates/peerauthentication.yaml | OTEL용 PeerAuthentication |
common-charts/apps/java-service/templates/deployment.yaml | OTEL agent 설정 |
common-charts/apps/java-service/values.yaml | OTEL 엔드포인트 기본값 |
교훈#
- Istio mTLS는 sidecar 기반: sidecar를 통하지 않는 트래픽은 문제 발생 가능
- OTEL agent는 특수 케이스: JVM 내부에서 직접 연결하므로 sidecar 우회
- PERMISSIVE는 마이그레이션용: 일반적으로 STRICT 권장, 특수 케이스만 예외
- DestinationRule + PeerAuthentication: 클라이언트와 서버 양쪽 설정 필요