Envoy 요청 라우팅은 listener→route→cluster→endpoint 체인을 따르며 istioctl proxy-config로 각 단계를 짚어 오설정을 찾는다
Envoy가 요청 하나를 처리하는 경로는 항상 Listener → Route → Cluster → Endpoint (+ mTLS면 Secret) 라는 고정된 체인이다. 그래서 장애 진단은 자유로운 추리가 아니라 "이 고정 체인의 어느 단계에서 끊겼는가" 하나만 묻는 결정적 절차가 된다. 그 답을 단계별로 dump해 주는 도구가 istioctl proxy-config {listener,route,cluster,endpoint,secret}, 시작점을 바로 가리켜 주는 단서가 access log의 response flag다. 이 note는 그 체인 멘탈모델과 단계별 디버깅 절차에 집중하고, flag 전체 표·xDS 계층 내부·cluster 필드 매핑 같은 깊은 detail은 각 src 문서로 위임한다.
01. 배경 — 왜 "체인"으로 생각해야 하나
Istio 메시에서 "v2가 안 떠요", "503이 나요" 같은 증상은 원인 후보가 너무 많다. VirtualService 오타일 수도, DestinationRule 누락일 수도, Pod label 불일치일 수도, mTLS mode 충돌일 수도 있다. 증상에서 곧장 원인을 찍으려 하면 추측이 되고, 추측은 틀린 곳을 고치게 만든다.
빠져나갈 길은 Envoy 자체의 구조에 있다. Envoy는 요청을 받으면 매번 똑같은 순서로 "이 요청을 어떻게 처리할지"를 resolve한다. 이 순서는 설정마다 달라지지 않는 불변(invariant)이다. 즉 어떤 라우팅 장애든, 이 고정된 결정 파이프라인 위의 정확히 한 지점에서 답이 안 나와 끊긴 것이다. 그러면 진단은 "원인이 무엇일까"라는 열린 질문에서, "이 파이프라인을 위에서 아래로 내려가며 처음 답이 빈 단계를 찾아라" 라는 닫힌 절차로 바뀐다. 이것이 체인 멘탈모델의 전부이고, 이 문서의 나머지는 그 체인을 짚는 법이다.
이 멘탈모델을 쓰려면 두 가지 선행 개념이 필요하다.
- xDS = Envoy의 동적 설정 프로토콜. istiod가 각 Envoy(sidecar/gateway)에게 listener(LDS)·route(RDS)·cluster(CDS)·endpoint(EDS)·secret(SDS)을 push한다. 체인의 각 단계는 그대로 하나의 xDS 종류에 대응한다. xDS 계층 자체의 내부 동작은 → xDS 계층 개념.
- cluster ≠ endpoint. Envoy의 cluster는 "어디로 보낼지"라는 논리적 목적지(upstream pool)이고, 그 안에 실제로 누가 있는지(Pod IP)는 별도 채널(EDS)로 내려온다. 이 분리가 진단의 핵심 함정을 만든다(§02 anchor, §03).
02. 핵심 — 체인의 각 단계가 답하는 질문
Anchor: 머릿속에 그릴 그림은 이 한 줄이다 — 요청은 Listener → Route → Cluster → Endpoint (+Secret) 를 따라 흐르고, 각 단계는 직전 단계의 결정을 입력으로 받아 "예/아니오"를 하나씩 답한다. 어느 단계가 처음 "아니오"를 내는지가 곧 근본 원인의 위치다.
각 단계를 "필드"가 아니라 "그게 답하는 질문"으로 보면 체인이 직관적으로 잡힌다.
| 단계 | xDS | 결정하는 것 | 한 줄 질문 | Istio 리소스(주된 것) |
|---|---|---|---|---|
| Listener | LDS | 어느 포트/주소에서 받고 어떤 filter chain으로 처리할지 | "트래픽을 받긴 했나" | Gateway, Sidecar, PeerAuthentication, Service port |
| Route | RDS | 이 요청(host/path/header)을 어느 cluster로 보낼지 | "보낼 곳을 정했나" | VirtualService, Gateway, HTTPRoute |
| Cluster | CDS | 그 목적지(upstream pool)가 존재하는지 + LB/TLS/CB 정책 | "목적지가 정의됐나" | Service, ServiceEntry, DestinationRule |
| Endpoint | EDS | 그 cluster 안의 실제 Pod IP가 healthy하게 있는지 | "보낼 실체가 있나" | EndpointSlice, readiness, selector, WorkloadEntry |
| Secret | SDS | mTLS handshake에 쓸 cert/key/CA | "신원 증명을 할 수 있나" | istiod CA, PeerAuthentication, DestinationRule TLS |
왜 cluster와 endpoint가 분리됐나 — 가장 흔한 함정의 뿌리
cluster(CDS)는 "reviews v1으로 보내라"는 논리적 목적지이고, 그 안에 실제로 누가 있는지(EDS)는 따로 내려온다. 이렇게 쪼갠 이유는 운영상 명확하다 — Pod는 scale·재시작·장애로 IP가 수시로 바뀌지만 "reviews v1으로 보낸다"는 라우팅 의도는 그대로다. endpoint만 자주 갱신하고 cluster/route는 안정적으로 두기 위해 두 채널을 분리했다. 그 대가로 "cluster는 있는데 endpoint가 0개"라는 상태가 정상적으로 존재하고, 이것이 진단을 헷갈리게 하는 1순위 함정이다. "cluster가 보인다 = 트래픽이 간다"는 거짓이다.
cluster 이름 — 진단이 곧 문자열 대조인 이유
cluster 이름은 direction|port|subset|fqdn 규칙을 따른다(예: outbound|9080|v1|reviews.default.svc.cluster.local). subset이 없으면 가운데가 비어 outbound|9080||reviews... 가 된다. route 단계는 이 cluster 이름을 문자열 그대로 참조한다. 그래서 진단의 핵심 동작은 거창한 추론이 아니라, "route가 가리키는 cluster 문자열"과 "실제 존재하는 cluster 문자열"을 글자 그대로 대조하는 것이다. 한 글자라도 어긋나면(특히 subset 칸) 요청은 그 자리에서 끊긴다. cluster 필드의 상세 매핑은 → Cluster 해부.
subset cluster — DestinationRule이 만드는 단계, 두 실패 모드
가장 자주 만나는 라우팅 실패는 route는 subset cluster를 가리키는데 그 cluster가 존재하지 않는 경우다. 원인은 거의 항상 DestinationRule이다.
subset은 Kubernetes Service 개념도, Envoy 순수 개념도 아니다. DestinationRule이 정의하는, 같은 Service 뒤의 endpoint를 label로 나눈 named 그룹이다. istiod는 DestinationRule의 subsets를 보고서야 outbound|9080|v1|... 같은 subset cluster를 만든다. 따라서 VirtualService가 subset: v1로 보내는데 DestinationRule이 없거나 그 subset을 정의하지 않으면, route는 존재하지 않는 cluster를 가리키게 된다.
여기서 결정적인 건 두 실패 모드를 단계로 구분하는 것이다. 둘 다 "v2가 안 된다"로 보이지만 체인에서 끊긴 위치가 다르고, 따라서 고치는 곳도 다르다.
- cluster 부재 →
NC(NoClusterFound): subset 자체가 DestinationRule에 정의 안 됨. route가 가리키는 cluster 문자열이proxy-config cluster목록에 아예 없다. 고친다 = DestinationRule에 subset 추가. - endpoint 부재 →
503 UH(NoHealthyUpstream): subset cluster는 만들어졌지만, subset의 label selector와 실제 Pod label이 어긋나 endpoint가 0개. cluster는 있는데proxy-config endpoint가 비었다. 고친다 = subset label ↔ Pod label 일치.
# DestinationRule이 있어야 subset cluster가 생성된다
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews.default.svc.cluster.local
subsets:
- name: v1
labels: { version: v1 } # 이 label이 Pod에 실제로 있어야 endpoint가 채워짐
- name: v2
labels: { version: v2 }
subset cluster는 route가 명시적으로 그 subset으로 보낼 때만 트래픽이 흐른다. subset이 정의돼 cluster가 만들어져 있어도, VirtualService에서 subset: v2로 라우팅하지 않으면 그 cluster는 그냥 idle하게 존재만 한다. 다시, "cluster가 보인다 = 트래픽이 간다"가 아니다.
또 하나 흔한 케이스는 warming이다. DestinationRule을 막 apply한 직후엔 istiod가 CDS→EDS→RDS 순으로 push하는데(ADS bottom-up), endpoint가 다 채워지기 전 짧은 시간 동안 NC/UH가 잠깐 보일 수 있다. 지속되면 설정 문제, 수 초 내 사라지면 warming이다. 각 xDS 계층이 정확히 무엇을 내려주고 ADS가 적용 순서를 어떻게 보장하는지는 → xDS 5계층과 진단.
response flag — 체인의 어느 단계인지 Envoy가 직접 말해 준다
체인을 1단계부터 다 훑지 않아도 된다. access log의 response flag가 "어느 단계에서 끊겼는지"를 직접 가리키므로, flag → 단계로 시작점을 점프할 수 있다. flag는 방향(U=upstream/D=downstream/L=local) + 사건(F/H/O/T/R)의 약어 조합이라 외우지 않아도 추론된다 — UF는 U(upstream 쪽) + F(connection Failure), UH는 U + H(no Healthy upstream)로 읽힌다. 방향이 끊긴 위치(보내는 쪽 vs upstream)를, 사건이 §03의 어느 검증 단계인지를 알려 준다. 체인 멘탈모델과 flag가 같은 단계 구조를 공유하기 때문에 둘은 1:1로 맞물린다.
| flag | long | 끊긴 단계 | 먼저 볼 proxy-config |
|---|---|---|---|
NR |
NoRouteFound | Route | route — VS hosts/gateways/port, SNI |
NC |
NoClusterFound | Cluster | cluster — DestinationRule subset, ServiceEntry |
UH |
NoHealthyUpstream | Endpoint | endpoint — selector/readiness/outlier |
UF |
UpstreamConnectionFailure | Secret/transport | secret — mTLS mode mismatch, port/firewall |
UO |
UpstreamOverflow | Cluster 정책 | cluster — DestinationRule connectionPool/CB |
UT |
UpstreamRequestTimeout | upstream/app | VS timeout, app latency |
즉 NR이면 route부터, NC면 cluster, UH면 endpoint, UF면 secret/mTLS부터 보면 된다. flag 28종 전체 표·short↔long·response_code_details/upstream_transport_failure_reason까지 access log에 노출하는 법은 → Response Flags 레퍼런스.
03. 예시 — istioctl proxy-config로 체인을 단계별로 검증
진단은 체인을 위에서 아래로 내려가며 "끊긴 곳"을 찾는 절차다. 각 명령은 그 proxy의 Envoy가 실제로 받은 설정을 dump한다(istiod가 의도한 설정이 아님 — 이 차이는 §What you might be missing). 시작 단계는 response flag로 점프하거나, 모르면 route부터 내려가면 된다.
# 0. 진단 대상 Pod 확정 (보내는 쪽 sidecar 기준으로 본다)
POD=$(kubectl get pod -n default -l app=sleep -o jsonpath='{.items[0].metadata.name}')
# 1. route가 원하는 cluster를 가리키는지
istioctl proxy-config route "$POD" -n default
# 2. 그 cluster가 존재하는지 (route의 cluster 문자열과 글자 그대로 대조)
istioctl proxy-config cluster "$POD" -n default
# 3. 그 cluster에 healthy endpoint가 있는지
istioctl proxy-config endpoint "$POD" -n default
# 4. mTLS면 secret이 있는지
istioctl proxy-config secret "$POD" -n default
1단계 — route가 가리키는 cluster 문자열 뽑기 (특정 host로 좁혀서):
istioctl proxy-config route "$POD" -n default --name 9080 -o json \
| jq '.[].virtualHosts[] | select(.name|test("reviews")) | .routes[].route.cluster'
"outbound|9080|v1|reviews.default.svc.cluster.local"
2단계 — 그 cluster가 실재하는지 대조:
istioctl proxy-config cluster "$POD" -n default --fqdn reviews.default.svc.cluster.local
SERVICE FQDN PORT SUBSET DIRECTION TYPE DESTINATION RULE
reviews.default.svc.cluster.local 9080 - outbound EDS reviews.default
reviews.default.svc.cluster.local 9080 v1 outbound EDS reviews.default
reviews.default.svc.cluster.local 9080 v2 outbound EDS reviews.default
- route가
v2를 가리키는데 위 목록에v2줄이 없다 → cluster 부재(NC). DestinationRule subset 누락이다. v2줄은 있는데 endpoint가 비었다 →UH. 다음 단계로:
3단계 — 그 cluster에 healthy endpoint가 있는지:
istioctl proxy-config endpoint "$POD" -n default \
--cluster "outbound|9080|v2|reviews.default.svc.cluster.local"
ENDPOINT STATUS OUTLIER CHECK CLUSTER
10.244.1.12:9080 HEALTHY OK outbound|9080|v2|reviews...
endpoint 목록이 0줄이면 selector mismatch 또는 readiness 미충족. STATUS가 UNHEALTHY거나 outlier로 eject됐으면 그쪽을 본다.
이 decision tree가 §02 anchor를 절차로 옮긴 것이다 — 각 분기가 체인의 한 단계, 각 leaf가 하나의 response flag다:
flag로 시작점 점프 — 체인을 1단계부터 훑기 전에, 끊긴 요청의 flag를 보면 어느 단계부터 볼지 바로 정해진다:
# 끊긴 요청의 flag를 보는 가장 빠른 길 — 보내는 쪽 sidecar 로그
kubectl logs -n default "$POD" -c istio-proxy --tail=20
[2026-06-07T...] "GET /api/v1/reviews HTTP/1.1" 503 UF ... outbound|9080|v2|reviews...
^^^^ → secret/mTLS 단계부터 본다
503 -(flag가 -)는 "Envoy 레벨 에러 없음"일 뿐 "장애 없음"이 아니다. app이 직접 503 body를 돌려준 경우다. 503 UF(Envoy가 app에 닿지도 못함)와 대응 부서가 다르다.
체인을 보강하는 두 도구 — proxy-config가 "받은 설정"만 보여주므로 양옆을 막아 준다:
istioctl analyze -A— 적용 전 의도 수준의 오류(host mismatch, subset 미정의, injection 누락)를 잡는다. 경고 0이 시나리오 합격선.istioctl proxy-status— 그 proxy가 istiod와 sync(ACK) 됐는지.proxy-config출력이 최신인지 보증한다(→ xDS 진단 §05).
Envoy admin API(
config_dump/clusters/stats) 직접 접근으로 더 raw하게 보는 법은 → Envoy Admin API 진단.
핵심 정리
1. 요청 path는 고정 체인이다.
Listener → Route → Cluster → Endpoint (+ mTLS면 Secret).
진단 = "이 체인의 어느 단계에서 끊겼나"를 묻는 닫힌 절차.
2. cluster와 endpoint는 분리돼 있다 (Pod IP가 자주 바뀌니까).
cluster 있음 ≠ 정상. cluster는 CDS, endpoint(Pod IP)는 EDS로 따로 온다.
"cluster 있는데 endpoint 0" = 503 UH가 가장 흔한 함정.
3. subset cluster는 DestinationRule이 만든다.
subset 미정의 → NC(cluster 부재) / subset label↔Pod label 불일치 → UH(endpoint 부재).
4. proxy-config로 단계별로 글자 그대로 대조한다.
route(가리키는 cluster) ↔ cluster(존재 여부) ↔ endpoint(healthy 여부).
5. response flag로 시작 단계를 점프한다.
NR→route, NC→cluster, UH→endpoint, UF→secret/mTLS.
What you might be missing
proxy-config는 "그 proxy가 받은 설정"이지 "istiod가 의도한 설정"이 아니다. push가 안 갔거나 ACK가 안 됐으면(proxy-status가STALE이거나 proxy가 목록에서 누락)proxy-config출력이 옛 설정일 수 있다. 그래서analyze(의도 검증)와proxy-status(전달 검증)를 체인 진단과 같이 봐야 한다. 라우팅이 "설정대로면 맞는데 안 된다"면 전달 단계부터 의심하라.- 체인은 "보내는 쪽" sidecar에서 본다. outbound 라우팅 실패는 client Pod의 sidecar(15001 outbound)에서 결정된다. 그래서
$POD는 호출하는 쪽(sleep/curl) Pod여야 한다. 서버 쪽 sidecar(15006 inbound)를 봐서는 route/cluster 누락이 안 보인다. ingress gateway 경유면 gateway Pod가 진단 대상이다. NR이 라우팅이 아니라 "포트가 HTTP로 인식 안 됨"일 수 있다. Service port name이http/http2/grpcprefix가 아니면 Istio가 L7 route를 아예 안 만들어, VirtualService가 멀쩡해도 route가 없어NR이 난다. 이때proxy-config route에 host 자체가 안 보이므로 "VS 오타"로 오진하기 쉽다. Service port name부터 확인하라.UF인데upstream_transport_failure_reason이 비었으면 mTLS 문제가 아닐 가능성이 높다. 반대로 이 필드에 TLS 에러가 찍히면 port/firewall보다 mTLS mode mismatch(DestinationRule TLS mode vs PeerAuthentication STRICT)부터 의심하라. 이 한 필드가 secret 단계 triage 시간을 크게 줄인다(→ Response Flags).- subset 정책은 route가 그 subset으로 보낼 때만 발동한다. canary로 DestinationRule subset과 traffic policy를 다 정의해도, VirtualService route가
subset:을 지정하지 않으면 전체 pool cluster로 가서 subset별 정책(별도 connectionPool/outlier 등)이 통째로 무시된다. "정책을 썼는데 안 먹는다"의 단골 원인이다.