xDS 5계층과 istioctl 진단 — LDS/RDS/CDS/EDS/SDS (출처: Istio 1.30 공식 문서 + 홈랩 검증)
istiod는 컴파일러다. K8s 상태 + Istio CRD를 입력으로 받아 Envoy 설정(Listener/Route/Cluster/Endpoint/Secret)으로 컴파일하고, xDS 채널로 각 프록시에 push한다. 그래서 모든 트래픽 장애는 “이 5계층 중 어디서 끊겼나"로 환원되고, 진단은 Listener→Route→Cluster→Endpoint(+Secret) 순으로 한 칸씩 내려가며 빈 칸을 찾는 일이 된다. 이 문서는 그 컴파일 산출물 5계층을 운영·진단 관점(무엇을 내려주나 / 확인 명령 / 영향 주는 리소스 / 고장 증상)으로 정리하고, ADS 적용 순서·장애 분석 순서·proxy-status·x describe·EnvoyFilter·CRD↔Envoy 번역표·매일 쓰는 명령 세트로 닫는다.
대상환경 Istio 1.30 / Envoy sidecar mode · 대상독자 xDS를 “YAML이 아니라 Envoy 설정"으로 사고하려는 DevOps/SRE · 범위 진단·운영(개념 원론은 아래 링크) · 선행개념 Envoy listener/cluster, K8s Service/EndpointSlice 개념 원론은 xDS API 계층, sync 상태값은 데이터플레인 sync 상태. 본 문서는 그 둘의 운영·진단 상세판이다.
⚠️ 버전 skew 주의: 이 환경은 istiod 1.30.0 ↔ 로컬 istioctl 1.27.0이다.
proxy-status의 VERSION 컬럼이나proxy-config일부 필드가 어긋나 보일 수 있으나 이는 client 표시 한계이며 메시 동작 이상이 아니다. 정밀 진단은 버전을 맞춘 1.30 istioctl 사용 권장.
01. 배경 — 왜 xDS인가: Envoy는 “빈 상자"이고 istiod가 채운다
Envoy는 그 자체로는 아무 라우팅도 모르는 고성능 L4/L7 프록시 엔진입니다. “reviews로 가는 요청은 v1/v2로 나눠라” 같은 지식이 0이고, 그 지식은 전부 **설정(configuration)**으로 외부에서 주입돼야 합니다. 문제는 메시 환경이 정적이지 않다는 데 있습니다 — Pod는 뜨고 죽고, Service endpoint는 분 단위로 바뀌고, VirtualService 한 줄 고치면 수백 개 프록시가 동시에 새 route를 알아야 합니다. 설정 파일을 디스크에 깔고 프로세스를 재시작하는 방식으로는 이걸 못 따라갑니다.
그래서 Envoy는 설정을 런타임에 원격으로 받아오는 메커니즘을 가집니다. 이것이 xDS입니다. xDS = "무언가 Discovery Service"들의 묶음이고, x 자리에 Listener, Route, Cluster, Endpoint, Secret이 들어갑니다. Envoy는 이 동적 설정들을 xDS management server로부터 streaming gRPC로 받고, Istio에서는 그 management server가 바로 istiod입니다.
여기서 멘탈모델을 한 단계 끌어올려야 합니다. Istio를 “서비스 메시"라는 흐릿한 말로 두지 말고, **“K8s 상태 + Istio CRD를 Envoy 설정으로 컴파일해 모든 프록시에 배포하는 제어 시스템”**으로 보십시오. istiod = 컴파일러, K8s/CRD = 소스 코드, Envoy 설정 5계층 = 컴파일된 바이너리, xDS = 그 바이너리를 프록시 메모리에 적재하는 로더. 이 관점이 서면 진단이 단순해집니다 — 트래픽이 깨졌다는 건 소스(CRD)가 잘못됐거나, 컴파일이 안 됐거나, 로드가 안 됐거나 셋 중 하나이고, 그걸 가르는 게 아래 도구들입니다.
xDS = istiod가 Kubernetes/Istio 상태를 Envoy 설정(Listener/Route/Cluster/Endpoint/Secret)으로 컴파일해서 각 프록시에 push하는 5개 API. 진단 = 이 5계층을 순서대로 따라가며 빈 칸을 찾는 일.
02. 아키텍처 — 5계층이 “요청 처리 경로” 그대로인 이유
핵심 통찰은 이것입니다. 5개 계층은 임의로 나눈 게 아니라, 요청 하나가 Envoy 내부에서 resolve되는 경로 그 자체입니다. 요청이 들어오면 Envoy는 항상 같은 질문을 순서대로 던지고, 각 질문에 답하는 설정 조각이 곧 xDS 한 계층입니다.
LDS "이 트래픽을 어디서 받나?" → Listener (소켓 + filter chain)
RDS "그래서 어느 cluster로 보내나?" → Route (host/path/header → cluster 이름)
CDS "그 cluster는 어떤 pool인가?" → Cluster (LB/timeout/circuit breaker/TLS)
EDS "그 pool의 실제 Pod IP는?" → Endpoint (IP:port 목록)
SDS "mTLS 인증서는?" (가로지름) → Secret (cert/key/CA)
flowchart TD L["Listener (LDS)<br/>어디서 받나"] R["Route (RDS)<br/>어느 cluster로"] C["Cluster (CDS)<br/>upstream pool"] E["Endpoint (EDS)<br/>실제 Pod IP"] S["Secret (SDS)<br/>mTLS cert/key/CA"] L --> R R --> C C --> E C -.mTLS.-> S
이 그림 한 장이 이 문서의 앵커입니다. 진단도, 적용 순서도, 응답 플래그도 전부 이 사슬에서 파생됩니다. 사슬의 어느 고리가 끊겼는지가 곧 장애의 위치입니다.
02.1 다섯 고리를 하나씩 — 무엇을 내려주고, 어떻게 보고, 왜 깨지나
LDS — Listener Discovery Service. Listener는 “Envoy가 어디에서 트래픽을 받을 것인가"입니다. sidecar에는 두 종류가 있습니다 — capture listener와 port별 listener.
0.0.0.0:15001 outbound capture listener
0.0.0.0:15006 inbound capture listener
0.0.0.0:9080 outbound HTTP listener (port별, 대부분 0.0.0.0 wildcard)
10.96.0.10:53 service-IP별 listener (조건부 — 아래 주석)
sidecar outbound listener는 대부분
0.0.0.0:PORTwildcard로 capture됩니다.10.96.0.10:53처럼 service IP별 dedicated listener는 protocol 충돌을 피해야 하는 경우(같은 포트에 여러 프로토콜)나 TCP/특정 케이스에서만 생성되며, 일반 HTTP outbound에서는 보통 만들어지지 않습니다.
15001/15006이 capture listener인 이유는 iptables가 모든 outbound/inbound를 이 두 포트로 redirect하기 때문입니다(메커니즘은 → Sidecar 트래픽 캡처). LDS가 반영되면 Envoy 안에 listener, filter chain, protocol inspector, HTTP connection manager, TCP proxy 같은 처리 구조가 생깁니다.
istioctl proxy-config listener <pod> -n <namespace>
- 영향 주는 Istio 리소스:
Gateway(edge listener + server + TLS),Sidecar(config scope),PeerAuthentication(inbound mTLS filter),Serviceport - 고장 증상: listener 자체가 없거나 잘못된 filter chain → 트래픽이 protocol을 잘못 판단(HTTP인데 TCP proxy로 처리), inbound 정책 미적용
RDS — Route Discovery Service. Route는 “이 HTTP 요청을 어느 cluster로 보낼 것인가"입니다. ⚠️ RDS의 D는 흔히 “Direct"로 오해되지만 정확히는 Route Discovery Service 입니다(xDS 전체가 x + Discovery Service 패턴 — 이것만 기억하면 안 헷갈림). route configuration 안에는 virtual host(어떤 host 묶음인가), route entry(path/header match → 어느 cluster), header modification이 들어갑니다.
Host: reviews.default.svc.cluster.local
Path: /api/v1/reviews
Header: x-user = jason
→ cluster: outbound|9080|v2|reviews.default.svc.cluster.local
route의 destination은 outbound|PORT|SUBSET|FQDN 형태의 cluster를 참조합니다. 이 이름 규칙과 subset의 의미는 → Cluster 해부.
istioctl proxy-config route <pod> -n <namespace>
- 영향 주는 Istio 리소스:
VirtualService,Gateway,Service host, Gateway API 사용 시HTTPRoute - 고장 증상: route 없음 → access log에
NR(NoRouteFound). VirtualServicehosts가 실제:authority와 다르거나, port가 HTTP로 인식 안 되거나,gateways필드 때문에 sidecar에는 rule이 적용 안 되는 경우가 대표적
CDS — Cluster Discovery Service. Cluster는 “upstream backend pool”, 즉 Envoy가 요청을 보낼 논리적 목적지입니다(아직 실제 IP는 모름).
outbound|9080||reviews.default.svc.cluster.local (subset 없음 = 전체 pool)
outbound|9080|v1|reviews.default.svc.cluster.local
outbound|9080|v2|reviews.default.svc.cluster.local
CDS에는 cluster 이름, EDS/DNS 사용 여부, connect timeout, circuit breaker, outlier detection, load balancing, upstream TLS 설정이 들어갑니다.
istioctl proxy-config cluster <pod> -n <namespace>
- 영향 주는 Istio 리소스:
Service,ServiceEntry,DestinationRule(subset/LB/TLS/connectionPool/outlier),Sidecaregress scope,PeerAuthentication/DestinationRuleTLS - 고장 증상: cluster 없음 →
NC(NoClusterFound). DestinationRule/ServiceEntry 누락, service host 오타, config scope에서 잘려 나간 경우
EDS — Endpoint Discovery Service. CDS가 “reviews v1으로 보내라"라는 논리라면, EDS는 “reviews v1 Pod IP는 이것들이다"라는 실체입니다. 왜 따로 받나? endpoint는 cluster 정의보다 훨씬 자주 바뀌기 때문입니다 — Pod가 죽고 살 때마다 무거운 cluster 설정(LB/circuit breaker/TLS)을 통째로 다시 보내는 건 낭비라, Istio는 변하지 않는 cluster(CDS)와 자주 변하는 endpoint(EDS)를 분리해서 endpoint만 가볍게 갱신합니다.
Cluster: outbound|9080|v1|reviews.default.svc.cluster.local
Endpoints:
10.244.1.12:9080
10.244.2.18:9080
istioctl proxy-config endpoint <pod> -n <namespace>
- 영향 주는 정보/리소스: Kubernetes
EndpointSlice, Pod readiness, Service selector,WorkloadEntry,ServiceEntryendpoints, multicluster remote endpoint, locality/zone/region metadata - 고장 증상: healthy endpoint 없음 →
UH(NoHealthyUpstream). readiness 실패, outlier detection으로 전부 eject, selector mismatch
SDS — Secret Discovery Service. TLS 인증서, private key, root CA를 내려줍니다. 위 4계층이 “어디로 보내나"의 사슬이라면 SDS는 그 사슬을 가로지르는 보안 축입니다. 핵심 설계는 이것 — Istio sidecar mode에서 workload 인증서는 app container가 직접 들고 있지 않습니다. workload 시작 시 Envoy가 SDS API로 cert/key를 요청하고, Istio agent가 istiod CA에서 받은 cert를 Envoy 메모리로만 전달합니다(디스크 secret 마운트 없음 = blast radius 축소). 이 cert가 mTLS handshake에서 workload identity를 증명합니다.
istioctl proxy-config secret <pod> -n <namespace>
- 영향 주는 리소스: istiod CA,
PeerAuthentication,DestinationRuleTLS mode - 고장 증상: mTLS handshake/설정 불일치(흔히
UC나 일반503으로 나타나며,UF가 반드시 뜨는 것은 아님 — §02.3 참고), AuthorizationPolicy principal match 실패, 인증서 만료/rotation 문제
02.2 ADS와 적용 순서 — “route가 빈 cluster를 가리키는 순간"을 없애는 불변식
Envoy 공식 문서 기준 CDS/EDS/LDS/RDS/SDS 각각이 독립 streaming endpoint를 가지지만, ADS(Aggregated Discovery Service) 는 이 여러 xDS resource를 하나의 bidirectional gRPC stream으로 묶어 전달합니다. Istio가 ADS를 쓰는 이유는 단 하나, 적용 순서 보장입니다.
왜 순서가 생명인가. route를 cluster X에서 cluster Y로 바꿀 때, Envoy가 cluster Y와 그 endpoint를 먼저 알아야 route가 안 깨집니다. 채널이 5개로 따로 놀면 RDS가 CDS보다 먼저 도착해 “존재하지 않는 cluster를 가리키는 route"가 생기는 race가 발생합니다. ADS는 하나의 stream에서 CDS→EDS→LDS→RDS를 순서대로 흘려 이 race를 원천 차단합니다.
flowchart LR K8S[K8s API + Istio CRD] ISTIOD[istiod compiler] STREAM[ADS single gRPC stream] ENVOY[Envoy proxy] K8S --> ISTIOD ISTIOD --> STREAM STREAM --> ENVOY ENVOY --> APPLY["apply order: CDS"] APPLY --> EDS2[EDS] EDS2 --> LDS2[LDS] LDS2 --> RDS2[RDS]
순서에는 비대칭이 있고, 이게 진짜 포인트입니다. add(추가) 는 CDS→EDS→LDS→RDS로 bottom-up(참조 대상부터 만들고 나중에 참조)으로 적용됩니다 — Envoy 공식 xDS protocol 문서(“eventual consistency considerations”)가 명시하는 순서로, 직관적으로는 참조하는 쪽인 LDS가 나중에 올 것 같지만 실제로는 LDS가 RDS보다 먼저 도착해야 합니다. 반대로 remove(삭제)나 warm shutdown은 LDS/RDS 먼저 → 그 다음 CDS/EDS로 top-down(참조하는 쪽을 먼저 떼고 나중에 대상 제거)으로 적용됩니다.
add : CDS → EDS → LDS → RDS (bottom-up, 대상 먼저)
remove : LDS / RDS → CDS / EDS (top-down, 참조자 먼저)
방향은 정반대지만 두 경우 모두 같은 불변식을 지키려는 설계입니다 — “route가 아직 없거나 이미 사라진 cluster를 가리키는 순간이 생기지 않게 한다.” 다만 이 불변식은 동일 리소스 하나가 새로 만들어질 때의 add 순서에 대한 보장이지, 서로 다른 두 CRD(예: DestinationRule과 VirtualService)를 한 번에 동시 전환하는 경우까지 원자적으로 묶어 주지는 않습니다 — istiod가 K8s 오브젝트의 watch 이벤트를 리소스 종류별로 독립적으로 재계산·push하기 때문입니다. 실제로 홈랩에서 DestinationRule(신규 subset 추가)과 VirtualService(그 subset으로 라우팅 전환)를 단일 kubectl apply로 동시에 바꿔보면, RDS가 아직 해당 Envoy에 CDS/EDS로 생성되지 않은 신규 subset cluster를 가리키는 약 23~33ms의 짧은 창에서 NC(cluster_not_found)가 실제로 관측됐습니다(400회 중 3회, T25). 창이 매우 짧고 자동 회복되지만, “불변식이 절대 깨지지 않는다"는 것은 과장이고 크로스 리소스(다른 종류의 CRD) 동시 전환에는 원자성이 없다는 게 더 정확한 서술입니다. Envoy는 update를 받은 뒤 ACK/NACK로 적용 여부를 control plane에 알리고, 이 ACK/NACK가 proxy-status의 SYNCED/STALE로 드러납니다(§04).
02.3 응답 플래그 = 사슬의 끊긴 고리 좌표
위 사슬의 각 고리가 끊기면 Envoy access log에 고유한 response flag가 찍힙니다. 이게 진단의 출발점입니다 — flag만 보면 5계층 중 몇 번째부터 보면 되는지 바로 압니다(flag 전체표는 → Envoy 응답 플래그).
| flag | 끊긴 고리 | 먼저 볼 계층 |
|---|---|---|
NR (NoRoute) |
RDS | route (1단계) |
NC (NoCluster) |
CDS | cluster (2단계) |
UH (NoHealthyUpstream) |
EDS | endpoint (3단계) |
UF (UpstreamFailure) |
특정 계층 전용 아님 — TCP 연결 자체 실패 | endpoint 도달성(3단계) 우선 의심 → 안 풀리면 secret(4단계) |
UF는 예외로 다뤄야 합니다. Envoy 공식 문서상 UF(Upstream connection Failure)는 “TCP 연결 자체의 실패"를 뜻하는 범용 플래그이고, TLS/mTLS/SDS를 특정하지 않습니다. mTLS handshake 실패가 UF로 잡히는 경우도 있지만, 실무에서 UF의 훨씬 흔한 원인은 endpoint 미기동/포트 오류/NetworkPolicy 차단 같은 EDS·네트워크 계층 문제입니다. 그래서 “UF를 보면 곧장 secret(SDS)부터 본다"는 매핑은 과잉단순화이고, UF를 보면 먼저 EDS/네트워크 도달성(엔드포인트가 실제로 열려 있는지)을 의심하고, 그래도 안 풀리면 mTLS/SDS를 보는 순서가 더 정확합니다. 홈랩 실측(T20)에서도 순수 TCP 연결 실패와 mTLS mode mismatch(서버 STRICT + 클라이언트 tls.mode: DISABLE) 재현 모두에서 UF 판별에 쓸 만한 신호(response flag·UPSTREAM_TRANSPORT_FAILURE_REASON)가 동일하게 비어 있거나 겹쳐, 로그만으로 “UF=mTLS 문제"를 단정할 수 없음이 확인됐습니다.
03. 적용 예시 — 끊긴 고리 찾아 내려가기 (worked diagnosis)
이제 멘탈모델을 실제 명령으로 돌립니다. 시나리오: “reviews로 보낸 요청이 503이다.” 분석은 사슬을 위에서 아래로 내려가며 빈 칸을 찾는 일입니다.
03.1 0단계 게이트 — 먼저 “전달은 됐나"부터
layer trace 전에 proxy-status로 설정이 Envoy에 도착했는지부터 확인합니다. NOT SENT/STALE이면 설정이 아직 프록시에 없는 것이라, 그 위에서 layer를 따라가 봐야 의미가 없습니다(sync 상태의 1단계 게이트).
istioctl proxy-status
예상 출력:
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
details-v1-558b8b4b76-qzqsg.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-6cf8d4f9cb-wm7x6 1.30.1
productpage-v1-6987489c74-nc7tj.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-6cf8d4f9cb-wm7x6 1.30.1
istio-ingressgateway-66c994c45c-cmb7x.istio-system Kubernetes SYNCED SYNCED SYNCED NOT SENT NOT SENT istiod-6cf8d4f9cb-wm7x6 1.30.1
reviews-v1-7f99cc4496-rtsqn.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-6cf8d4f9cb-wm7x6 1.30.1
여기서 모두 SYNCED(또는 정상 NOT SENT)이고 reviews proxy가 목록에 보이면 전달은 정상 — 이제 layer를 내려가도 됩니다. 만약 reviews proxy가 목록에서 빠졌다면 그건 istiod 미연결이고, 그 즉시 원인이 좁혀집니다(layer 추적 불필요).
03.2 1~4단계 — 사슬을 따라 내려가기
# 1. route가 원하는 cluster를 가리키는지 (NR이면 여기)
istioctl proxy-config route <pod> -n <ns>
# 2. 그 cluster가 존재하는지 (NC이면 여기)
istioctl proxy-config cluster <pod> -n <ns>
# 3. 그 cluster에 endpoint가 있는지 (UH이면 여기 / UF도 우선 여기부터 의심)
istioctl proxy-config endpoint <pod> -n <ns>
# 4. mTLS secret이 있는지 (endpoint까지 정상인데 UF면 여기)
istioctl proxy-config secret <pod> -n <ns>
# 5. (재동기 확인) proxy가 istiod와 sync됐는지
istioctl proxy-status
이 시나리오에서 1·2단계는 정상인데 3단계에서 endpoint가 비어 있다고 합시다 — access log의 UH와 일치합니다. cluster outbound|9080|v1|reviews...는 존재하지만 endpoint 목록이 0이면 “cluster 있음 ≠ 정상"의 전형입니다. 원인은 readiness 실패 / outlier detection eject / selector mismatch 중 하나로 좁혀지고, kubectl get endpointslice로 K8s 쪽 실체를 대조하면 끝납니다.
response flag로 trace를 단축할 수 있음. NR→1단계, NC→2단계, UH→3단계로 점프하면 빠름. UF는 SDS 전용 신호가 아니라 TCP 연결 실패 일반이므로 3단계(EDS/네트워크 도달성)부터 보고, 그래도 안 풀리면 4단계(mTLS/secret)로 내려가는 순서가 더 정확함.
03.3 한 단계 위 시점 — istioctl x describe pod
proxy-config가 Envoy raw 설정이라면, istioctl x describe pod(정식 istioctl experimental describe pod)는 “이 Pod에 영향을 주는 Istio 설정"을 사람이 읽기 쉽게 요약합니다. 보통 이걸로 큰 그림을 잡고 proxy-config로 내려갑니다.
1.30에서
x describe는 여전히 experimental입니다(deprecated 트랙으로 분류 — 향후 변경/제거 가능). 동작은 하지만 운영 자동화·스크립트의 안정적 기반으로 삼지 말고, 결정적 진단은proxy-config/analyze로 하십시오.
장애 분석의 첫 질문 “이 Pod가 mesh 안에 있나?“를 즉답합니다. sidecar 없는 Pod:
istioctl x describe pod coredns-f9fd979d6-2zsxk -n kube-system
Pod: coredns-f9fd979d6-2zsxk
Pod Ports: 53/UDP (coredns), 53 (coredns), 9153 (coredns)
WARNING: coredns-f9fd979d6-2zsxk is not part of mesh; no Istio sidecar
--------------------
Error: failed to execute command on sidecar ...
→ Envoy sidecar가 없으면 mesh policy, mTLS, AuthorizationPolicy, routing rule 대부분이 동작하지 않음.
mesh 안 Pod(DestinationRule 포함):
istioctl x describe pod ratings-v1-7dc98c7588-8jsbw -n default
Pod: ratings-v1-7dc98c7588-8jsbw
Pod Ports: 9080 (ratings), 15090 (istio-proxy)
--------------------
Service: ratings
Port: http 9080/HTTP targets pod port 9080
DestinationRule: ratings for "ratings"
Matching subsets: v1
(Non-matching subsets v2,v2-mysql,v2-mysql-vm)
Traffic Policy TLS Mode: ISTIO_MUTUAL
→ container port, istio-proxy port(15090), service protocol, DestinationRule, matching subset, TLS mode를 한눈에 요약.
초기 진단 : istioctl x describe pod ← "이 Pod에 적용되는 Istio 설정 요약" (추상화 관점)
깊은 분석 : istioctl proxy-config ... ← "Envoy 내부 실제 설정" (raw)
03.4 proxy-status 상태값을 정확히 읽기
§03.1에서 본 컬럼들의 의미입니다. CLUSTER는 proxy가 속한 mesh cluster — 단일 클러스터에서는 모두 Kubernetes, multicluster일 때만 갈립니다.
| 상태 | 의미 |
|---|---|
SYNCED |
Envoy가 istiod의 마지막 설정을 ACK함 (정상) |
NOT SENT |
istiod가 보낼 것이 없었음 (예: gateway에 적용할 RDS route 없음 — 정상일 수 있음) |
STALE |
istiod가 update를 보냈지만 Envoy ACK를 못 받음 (네트워크 또는 Istio 자체 문제 의심) |
| (proxy 누락) | 그 proxy가 현재 istiod에 연결 안 됨 → 설정을 못 받는 중 |
CDS STALE → cluster 설정 sync 문제 LDS STALE → listener 설정 sync 문제
EDS STALE → endpoint 설정 sync 문제 RDS STALE → route 설정 sync 문제
ECDS → WasmPlugin/EnvoyFilter의 extension config sync. extension 안 쓰면 보통 NOT SENT
proxy 없음 → sidecar가 istiod에 연결 안 됨 (injection 누락, agent crash, 네트워크 차단)
ECDS(Extension Config Discovery Service)는WasmPlugin이나 extension을 inject하는EnvoyFilter의 확장 설정을 별도로 push하는 채널입니다. extension이 없는 일반 proxy는NOT SENT가 정상이고, 사용 중인데STALE이면 해당 WasmPlugin/EnvoyFilter sync 문제로 좁혀집니다.
NOT SENT는 장애가 아닐 수 있음. ingress gateway의 RDS가 NOT SENT인 건 그 gateway에 적용할 HTTP route가 아직 없다는 뜻일 수 있음. 반면 proxy가 목록에서 아예 빠진 것은 거의 항상 문제(istiod 미연결).
03.5 diff 훈련 — “YAML이 아니라 Envoy 설정으로 사고하기”
xDS를 진짜로 이해하는 가장 빠른 길은 리소스 apply 전후로 proxy-config ... -o json을 dump해 diff를 떠 보는 것입니다. CRD 한 줄이 어느 Envoy 설정으로 컴파일되는지가 눈에 보입니다.
istioctl proxy-config route <pod> -n <ns> -o json > before-route.json
kubectl apply -f virtualservice.yaml
istioctl proxy-config route <pod> -n <ns> -o json > after-route.json
diff -u before-route.json after-route.json
§03.2의 layer-trace와 이 diff 워크플로를 한 번에 자동화한 스크립트가 있습니다(listeners/routes/clusters/endpoints/secret 일괄 덤프).
04. CRD ↔ Envoy 번역표 + 매일 쓰는 명령 세트
운영자는 늘 “이 YAML이 어느 Envoy 설정으로 바뀌었나"를 묻습니다. 외우는 게 아니라 매번 proxy-config로 확인하는 용도의 매핑입니다.
| Istio/K8s 리소스 | Envoy 관점 | 장애 시 보는 곳 |
|---|---|---|
Service, EndpointSlice, ServiceEntry |
service registry / endpoint source | proxy-config endpoint, proxy-config cluster |
VirtualService |
HTTP/TCP/TLS route (RDS) | proxy-config route |
DestinationRule |
LB, subset, TLS, connection pool, outlier detection (CDS) | proxy-config cluster |
Gateway |
edge listener + server + TLS (LDS) | proxy-config listener, proxy-config route |
PeerAuthentication |
inbound mTLS requirement (LDS filter + SDS) | proxy-config listener, proxy-config secret |
AuthorizationPolicy |
Envoy RBAC filter | proxy-config listener -o json |
Sidecar |
config scope / import boundary | proxy-config cluster, proxy-config listener |
EnvoyFilter |
raw Envoy patch | proxy-config listener/cluster/route -o json |
매일 쓰는 디버깅 명령 10종:
# 1. 전체 proxy sync 상태
istioctl proxy-status
# 2. 특정 pod가 어떤 istiod에 붙었는지
istioctl proxy-status | grep <pod>
# 3. pod 관점 설명 (mesh 소속 + 적용 설정 요약) — experimental, 향후 변경/제거 가능
istioctl x describe pod <pod> -n <ns>
# 4. Envoy listener 확인 (어디서 받나)
istioctl proxy-config listener <pod> -n <ns>
# 5. HTTP/gRPC route 확인 (어느 cluster로)
istioctl proxy-config route <pod> -n <ns>
# 6. upstream cluster 확인 (backend pool)
istioctl proxy-config cluster <pod> -n <ns>
# 7. endpoint 확인 (실제 Pod IP)
istioctl proxy-config endpoint <pod> -n <ns>
# 8. mTLS secret 확인 (cert/key/CA)
istioctl proxy-config secret <pod> -n <ns>
# 9. bootstrap 확인 (어느 istiod, 어떤 metadata로 시작했는지)
istioctl proxy-config bootstrap <pod> -n <ns>
# 10. 구성 오류 사전 분석 (host mismatch, injection 누락, subset 불일치 등)
istioctl analyze -A
05. EnvoyFilter는 언제 쓰나 — escape hatch의 비용
EnvoyFilter는 istiod가 생성한 Envoy 설정을 직접 patch하는 escape hatch입니다. 특정 field를 바꾸거나, filter를 추가하거나, listener/cluster를 새로 추가할 수 있습니다. 존재 이유는 단순합니다 — Istio API가 Envoy의 모든 기능을 추상화하지는 않기 때문입니다. 그래서 운영 기준으로 일반 application team은 거의 안 쓰는 게 정상이고, platform team도 신중히 제한적으로만 씁니다.
쓰게 되는 경우:
- 특정 Envoy HTTP filter 삽입
- Lua/Wasm/ext_authz 같은 고급 확장
- mesh-wide idle timeout / connection option 세밀 조정
- 특정 vendor agent / observability / security filter 연동
- Istio API로 아직 지원 안 하는 Envoy 기능 임시 사용 / 긴급 workaround
우선순위 원칙은 “위에서부터 가능하면 그걸로, EnvoyFilter는 최후"입니다. EnvoyFilter는 컴파일러의 출력물을 손으로 후패치하는 것이라, 컴파일러(istiod)가 다음 버전에서 출력 형태를 바꾸면 조용히 깨집니다.
VirtualService로 가능하면 → VirtualService
DestinationRule로 가능하면 → DestinationRule
Telemetry API로 가능하면 → Telemetry
WasmPlugin으로 가능하면 → WasmPlugin
마지막에만 → EnvoyFilter
잘못된 EnvoyFilter는 mesh 전체를 불안정하게 만들 수 있음. Istio networking 내부 구현 및 Envoy xDS API에 깊게 묶여 있어 proxy version upgrade마다 재검증 필요. 여러 EnvoyFilter가 충돌하면 동작은 undefined.
성숙도 신호와 가드레일:
[성숙도 신호]
Application namespace에 EnvoyFilter가 많다 → 운영 성숙도 낮음 또는 platform abstraction 부족 가능성
Root namespace의 global EnvoyFilter가 많다 → upgrade risk 큼. owner/test/rollback/version guard 필수
[운영 가드레일]
- EnvoyFilter는 platform team만 merge 가능
- workloadSelector 없는 root namespace EnvoyFilter 금지 또는 강한 승인
- 적용 전후 config_dump diff 필수
- canary namespace에서 먼저 검증
- Istio minor upgrade마다 재검증
핵심 정리
멘탈모델 : istiod=컴파일러. K8s/CRD → Envoy 설정 5계층 → xDS로 push. 장애 = 사슬의 끊긴 고리 찾기.
5계층 : LDS(어디서 받나) → RDS(어느 cluster) → CDS(어떤 pool) → EDS(실제 Pod IP) (+SDS mTLS cert)
ADS 적용 : add=CDS→EDS→LDS→RDS(bottom-up) / remove=top-down — 불변식은 "동일 리소스" add에 한정,
cross-resource(다른 CRD 동시 전환) 시 수십ms대 NC 예외가 실측됨(T25)
분석 순서 : (0) proxy-status 게이트 → route → cluster → endpoint → secret. flag로 시작점 점프
(NR/NC/UH는 그대로 매핑, UF는 SDS 전용 아님 — EDS/네트워크부터 의심)
proxy-status : SYNCED(ACK) / NOT SENT(보낼 것 없음) / STALE(보냈지만 ACK 못 받음) / 누락(미연결) / ECDS=확장설정
x describe : 초기 진단(experimental) | proxy-config : 깊은 분석(Envoy raw) | diff로 CRD→Envoy 번역 체득
EnvoyFilter : 컴파일러 출력 후패치(escape hatch). VS/DR/Telemetry/WasmPlugin 우선, EnvoyFilter 최후
RDS의 D는 Discovery(Direct 아님). 요청은 항상 Listener → Route → Cluster → Endpoint (+Secret) 순으로 resolve됨.
What you might be missing
NOT SENT을 장애로 오해하지 말 것. gateway의 RDS가NOT SENT인 건 적용할 route가 없다는 뜻일 수 있음 — 정상. 진짜 위험 신호는STALE(ACK 실패)과 proxy 목록에서의 누락(istiod 미연결)임.- cluster는 CDS, endpoint는 EDS로 따로 온다(분리는 의도된 설계). cluster가 존재해도 endpoint가 비어 있으면
503 UH가 남. “cluster 있음 = 정상"이 아니라 endpoint까지 봐야 함 — endpoint를 분리한 이유 자체가 “자주 바뀌니까 가볍게 갱신"이라, readiness probe와 outlier detection이 언제든 endpoint를 비울 수 있음. proxy-config는 그 proxy가 실제로 받은 설정이지, istiod가 의도한 설정이 아님. push가 안 갔거나 ACK가 안 됐으면(STALE)proxy-config출력이 옛날 설정일 수 있음. 그래서analyze(의도 검증)와proxy-status(전달 검증)를 같이 봐야 함.- ADS의 순서 보장은 “동일 리소스 add” 범위에 한정된다. DestinationRule과 VirtualService처럼 서로 다른 CRD를 한 번에 동시 전환하면, istiod가 두 오브젝트를 독립적으로 재계산·push하기 때문에 수십ms대의 짧은
NC창이 생길 수 있음(실측 T25). 트래픽이 아주 짧게라도 민감한 전환(예: canary 100% 컷오버)은 이 창을 감안해야 함. UF는 SDS/mTLS 전용 신호가 아니다. Envoy 공식 정의는 “TCP 연결 실패"일 뿐이라 endpoint 미기동·NetworkPolicy 차단 같은 네트워크 문제로도 흔히 뜬다. UF를 보면 EDS/네트워크 도달성부터 의심하고, 그래도 안 풀릴 때 mTLS/SDS로 내려가는 순서가 안전하다(실측 T20).- EnvoyFilter는 Istio/Envoy upgrade의 숨은 지뢰. 컴파일러 출력물에 후패치를 박는 것이라 minor upgrade에서 출력 형태가 바뀌면 조용히 깨짐. root namespace의 workloadSelector 없는 EnvoyFilter는 blast radius가 mesh 전체이므로 owner/rollback/version guard 없으면 upgrade 전 반드시 재검증 대상으로 분류할 것.
관련 파일 · 참조
- 📎 proxy-dump.sh (proxy-config 일괄 덤프) — §03.2 layer-trace와 §03.5 diff 워크플로를 자동화한 listeners/routes/clusters/endpoints/secret 일괄 덤프 스크립트
- xDS API 계층 — 본 진단 상세판의 개념 note
- 데이터플레인 sync 상태 — proxy-status SYNCED/STALE/NOT SENT 상태 개념
- Cluster 해부 · Sidecar 트래픽 캡처 · Envoy 응답 플래그
검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)
검증 방법: Envoy/Istio 공식 문서 대조 + homelab k8s 클러스터 실측(T20, T24, T25, T44)을 병행. 15개 주장 중 2건(C6 ADS 적용 순서, C8 응답 플래그 UF 매핑)이 오류로 확정돼 본문을 교정했고, 그중 C6은 문헌 정정에 더해 실측이 원 correction의 “불변식은 유지된다"는 판단까지 반박해 그 부분도 다시 고쳤다. C4는 문헌만으로는 단일 canonical 근거를 못 찾은 주장이었으나 클러스터 실측으로 확인됐다.
| 주장 | 판정 | 근거 |
|---|---|---|
| C1. istiod는 K8s 상태+CRD를 Envoy 설정(L/R/C/E/S)으로 컴파일해 xDS로 push하는 컴파일러다 | ✅ 실측 확인 | istio.io/…/architecture · T44 실측 |
| C2. xDS 5계층은 LDS/RDS/CDS/EDS/SDS이며 각각 listener/route/cluster/endpoint/secret에 대응 | ✅ 문헌 확인 | envoyproxy.io/…/dynamic_configuration |
| C3. sidecar는 outbound 15001 / inbound 15006 capture listener를 가지며 iptables가 모든 트래픽을 여기로 redirect | ✅ 문헌 확인 | istio.io/…/application-requirements |
| C4. outbound listener는 대부분 wildcard, service-IP별 dedicated listener는 protocol 충돌 회피 등 조건부로만 생성 | ✅ 실측 확인 | github.com/istio/istio#18684 · T24 실측 |
| C5. ADS는 CDS/EDS/LDS/RDS/SDS를 단일 gRPC stream으로 묶고, 존재 이유는 적용 순서 보장 | ✅ 문헌 확인 | envoyproxy.io/…/xds_protocol |
| C6. ADS 적용 순서는 add=CDS→EDS→RDS→LDS이며 불변식이 절대 깨지지 않는다 | 🔬 실측 반증 — 본문 교정 | envoyproxy.io/…/xds_protocol · T25 실측 |
| C7. Envoy는 xDS update에 ACK/NACK로 응답하고, 이것이 proxy-status의 SYNCED/STALE로 드러남 | ✅ 문헌 확인 | istio.io/…/proxy-cmd |
| C8. 응답 플래그 대응표(NR/NC/UH는 RDS/CDS/EDS, UF는 SDS/mTLS 전용) | 🔬 실측 반증 — 본문 교정 | envoyproxy.io/…/access_log/usage · T20 실측 |
| C9. workload 인증서는 Envoy가 SDS로 요청하고, istio agent가 디스크 마운트 없이 메모리로 전달 | ✅ 문헌 확인 | istio.io/v1.3/…/auth-sds |
| C10. proxy-status는 CDS/LDS/EDS/RDS/ECDS/ISTIOD/VERSION 컬럼과 SYNCED/NOT SENT/STALE/누락 상태를 보여줌 | ✅ 문헌 확인 | istio.io/…/proxy-cmd |
| C11. 1.30에서 istioctl x describe pod는 여전히 experimental | ✅ 문헌 확인 | istio.io/…/istioctl-describe |
| C12. CDS/EDS 분리 이유는 endpoint가 cluster보다 훨씬 자주 바뀌기 때문 | ✅ 문헌 확인 | envoyproxy.io/…/dynamic_configuration |
| C13. cluster 이름 규칙은 방향|포트|subset|FQDN | ✅ 문헌 확인 | istio.io/…/proxy-cmd |
| C14. ECDS는 WasmPlugin/EnvoyFilter 확장 설정 채널이며 미사용 시 NOT SENT가 정상 | ✅ 문헌 확인 | envoyproxy.io/…/config_discovery.proto |
| C15. 여러 EnvoyFilter 충돌 시 동작은 undefined이며 proxy 버전 업그레이드마다 재검증 필요 | ✅ 문헌 확인 | istio.io/…/envoy-filter |