--- title: Istio VirtualService ↔ DestinationRule — 매핑 구조와 디버깅 플레이북 date: 2026-08-01 type: guide domain: istio tags: [istio, virtualservice, destinationrule, subset, xds, istioctl, diagnosis, troubleshooting] --- > [!note] 원본은 스타일링된 HTML 레퍼런스 (Vol.1) > 이 글의 원문은 매핑 와이어 다이어그램·xDS 파이프라인 SVG 2종과 8단계 플레이북 카드를 포함한 HTML 레퍼런스다. > > **[→ 전문 보기 (istio-vs-dr-mapping.html)](files/istio-vs-dr-mapping.html)** > > 이 페이지는 같은 내용의 markdown 정리본이며, 원문 작성 이후 대화에서 나온 교정 1건(NR flag의 응답 코드 — §7 참조)과 보충(맹점·확인 질문)을 함께 반영했다. 후속편: [Vol.2 — DR subset × port 교차 생성](/docs/istio/egress/dr-subset-port-scoping/). ## 0. 핵심 요약 - **발동 조건** — VS `spec.hosts` = 요청의 **목적지 이름**과 매칭 (HTTP는 `:authority` 헤더, TLS는 SNI, plain TCP는 Service VIP). DR과는 무관하다. - **매핑 키 ①** — VS `route.destination.host` ⟷ DR `spec.host`. FQDN 정규화 후 문자열 비교. - **매핑 키 ②** — VS `destination.subset` ⟷ DR `subsets[].name`. subset labels가 Pod label과 일치해야 endpoint가 채워진다. - **진단 축** — response flag로 원인 계층을 가른다: `NR`=route 부재(VS/hosts, **404**), `NC`=cluster 부재(DR/host, 503), `UH`=endpoint 부재(label/Pod, 503). ## 1. 왜 두 리소스로 쪼개져 있나 — 역할 분담 Istio는 트래픽 제어를 **라우팅 결정**과 **목적지 정책**으로 분리한다. 변경 주기와 소유자가 다른 관심사를 리소스 단위로 나눈 설계다. | 리소스 | 책임 | 답하는 질문 | Envoy 대응 | |---|---|---|---| | **VirtualService** | L7 라우팅 규칙 (match/rewrite/weight/retry/timeout/fault) | 이 요청을 **어디로** 보낼까 | Route (RDS) | | **DestinationRule** | 목적지 정책 (LB, connection pool, outlier detection, mTLS 모드) + **subset 정의** | 그 목적지로 가는 트래픽을 **어떻게** 다룰까 | Cluster (CDS) | | **subset** | 같은 Service 뒤 Pod들을 label로 나눈 논리적 버전 그룹 | 목적지 안에서 **어느 그룹**인가 | 별도 Cluster + endpoint 필터 (EDS) | | **Service / ServiceEntry** | service registry 등록 — VS·DR이 참조 가능한 이름의 원천 | mesh가 아는 이름인가 | Cluster·Endpoint의 기반 | | **Gateway** | edge에서 수신할 port·protocol·host 선언 | 외부 트래픽을 **어디서 받을까** | Listener (LDS) | subset은 라우팅 대상 그룹의 "정의"만 담당하고, 그 그룹을 "선택"하는 것은 VS의 몫이다. 정의(DR)와 선택(VS)이 분리되어 **이름 참조로만** 연결된다 — 이 참조가 끊기는 것이 대부분의 장애 원인이다. ## 2. `spec.hosts`는 무엇과 매칭되는가 `spec.hosts`는 **"요청이 향하는 목적지 이름"**과 매칭되어 이 VS의 발동 여부를 결정한다. DR의 `host`와 비교되는 필드가 아니다 — 여기가 가장 많이 헷갈리는 지점이다. 매칭은 두 단계로 일어난다. **단계 1 — 컴파일 타임 (istiod).** istiod가 `hosts:`를 보고 이 VS의 규칙을 어느 Envoy virtualHost에 부착할지 결정한다. sidecar(mesh) 트래픽의 경우 host는 **service registry에 등록된 이름**(K8s Service 또는 ServiceEntry)이어야 한다. 미등록 이름을 쓰면 규칙이 어디에도 부착되지 않고 **조용히 무시된다** — 에러가 없어 발견이 늦다. **단계 2 — 런타임 (Envoy).** 프로토콜에 따라 매칭 재료가 다르다. L7 정보가 있는 프로토콜만 이름 기반 매칭이 가능하다. | 프로토콜 | 매칭 재료 | 비고 | |---|---|---| | HTTP/1.1, HTTP/2, gRPC | `:authority` (Host) 헤더 | virtualHost domains에 FQDN·short name·Service VIP 변형이 모두 등록됨 | | TLS (passthrough) | SNI | `tls:` 라우트 블록 사용 시 | | Plain TCP | 목적지 IP (Service VIP) | L7 헤더가 없어 이름 매칭 불가 | 부착 위치(`gateways` 필드)에 따른 차이: | gateways 필드 | 적용 범위 | hosts 매칭 대상 | |---|---|---| | 미지정 또는 `mesh` | 모든 sidecar의 outbound | mesh 내부 요청의 목적지 이름 (registry 등록 필수) | | `[my-gateway]` | 해당 ingress gateway | Gateway `servers[].hosts`와의 **교집합** + 외부 인입 Host/SNI. 외부 도메인 사용 가능 | | `[mesh, my-gateway]` | 둘 다 | 각각 위 규칙 적용 | > [!key] 정리 > `spec.hosts` = "요청의 목적지 이름과 매칭해 **이 VS를 발동시킬지**", `route.destination.host` = "발동 후 **실제로 보낼 곳**(= DR 매칭 키)". 단순 케이스에선 둘이 같지만, `hosts: [reviews]`로 받아 `destination.host: reviews-canary`로 보내는 식으로 다를 수 있다. ## 3. 매핑 키 — 무엇이 무엇과 연결되는가 VS와 DR을 잇는 키는 정확히 두 개다. 그 외 필드는 매핑에 관여하지 않는다. ``` Client request (Host: reviews) | v +---------------------------+ | VirtualService | | hosts: [reviews] <------+-- matches request :authority | http.route.destination: | | host: reviews -------+--+ KEY 1: host (FQDN normalized) | subset: v2 -------+--+ KEY 2: subset name +---------------------------+ | v +---------------------------+ | DestinationRule | | host: reviews <------ KEY 1 match | subsets: | | - name: v2 <------ KEY 2 match | labels: | | version: v2 -------+--> endpoint filter +---------------------------+ | v Envoy cluster: outbound|9080|v2|reviews.default.svc.cluster.local | (EDS: Service endpoint among label version=v2 only) v Pod reviews-v2-xxxxx ``` **FQDN 정규화 규칙** — `host: reviews`처럼 short name을 쓰면 **그 리소스가 정의된 namespace** 기준으로 `reviews..svc.cluster.local`로 확장된다. 호출자 namespace 기준이 아니다. VS와 DR이 서로 다른 namespace에 있으면 같은 short name이 다른 FQDN으로 확장되어 매핑이 **조용히 끊어진다**. 운영 환경에서는 FQDN 명시가 원칙이다. ## 4. istiod → Envoy xDS 변환 경로 디버깅에서 "어느 계층에서 끊겼는지"를 판단하려면 각 리소스가 어떤 xDS로 변환되는지 알아야 한다. `istioctl proxy-config`(이하 `pc`)의 서브커맨드가 이 계층과 1:1 대응이다. | Istio API | Envoy xDS | 내용 | 조회 | |---|---|---|---| | Gateway | LDS Listener | 수신 port·protocol | `pc listener` | | VirtualService | RDS Route | virtualHost.domains ← hosts, route → cluster 이름 | `pc route` | | DestinationRule | CDS Cluster | `outbound\|port\|subset\|fqdn` | `pc cluster` | | Service/Endpoint | EDS Endpoint | label 필터된 Pod IP | `pc endpoint` | - **RDS** — VS의 `hosts`가 virtualHost의 `domains`가 되고, route 규칙이 cluster 이름 문자열을 가리킨다. - **CDS** — DR의 subset마다 `outbound|||` 이름의 cluster가 생성된다. subset 없는 기본 cluster는 subset 자리가 빈 문자열이다. - **EDS** — 해당 cluster의 endpoint 목록. Service endpoint 중 subset labels와 일치하는 Pod만 포함된다. - 즉 VS→DR 연결의 실체는 **"route가 참조하는 cluster 이름 문자열"**이며, 이 문자열을 만드는 재료가 매핑 키 두 개다. ## 5. 완전한 예제 — v1/v2 가중치 분배 ```yaml # DestinationRule — subset 정의 + 목적지 정책 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: reviews namespace: default spec: host: reviews.default.svc.cluster.local # KEY 1 trafficPolicy: loadBalancer: simple: LEAST_REQUEST subsets: - name: v1 # KEY 2 labels: version: v1 # Pod label과 일치 필요 - name: v2 labels: version: v2 --- # VirtualService — 라우팅 규칙 apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: reviews namespace: default spec: hosts: - reviews.default.svc.cluster.local # 요청 매칭용 (DR과 무관) http: - route: - destination: host: reviews.default.svc.cluster.local # KEY 1 → DR.host subset: v1 # KEY 2 → subsets[].name weight: 90 - destination: host: reviews.default.svc.cluster.local subset: v2 weight: 10 ``` ## 6. 디버깅 플레이북 — 설정이 꼬였을 때 원칙: **정적 검증 → 동기화 확인 → xDS 계층 순회(Route → Cluster → Endpoint) → 런타임 관측** 순으로 내려간다. 각 단계는 "여기까지는 정상"을 확정하고 다음 계층으로 넘어가는 이분 탐색이다. ```bash # 1. 정적 검증 — 리소스 간 참조 무결성 istioctl analyze -n default # IST0101 referenced DestinationRule subset not found → subset 오타/DR 누락 # IST0132 host not found in registry → hosts에 미등록 이름 # 2. 동기화 확인 — STALE이면 이후 단계는 의미 없음 (istiod부터) istioctl proxy-status # 모든 컬럼(CDS/LDS/EDS/RDS)이 SYNCED 여야 함 # 3. 대상 요약 — 이 Pod에 내 설정이 붙긴 했나 istioctl experimental describe pod reviews-v2-abc -n default # 4. Route 계층 — VS가 라우트로 변환됐는가 istioctl proxy-config route deploy/productpage -n default --name 9080 -o json \ | jq '.[0].virtualHosts[] | select(.name | startswith("reviews"))' # 기대: weightedClusters에 outbound|9080|v1|... =90, |v2|... =10 # 규칙이 안 보임 → hosts가 registry 미등록이거나 exportTo로 가려짐 # 5. Cluster 계층 — DR·subset이 cluster로 변환됐는가 istioctl proxy-config cluster deploy/productpage -n default \ --fqdn reviews.default.svc.cluster.local # 기대: SUBSET 컬럼에 v1, v2 행이 각각 존재 # subset 행 없음 → DR 미적용 (host FQDN 불일치, exportTo, namespace) # 6. Endpoint 계층 — label 필터 결과가 비어있지 않은가 istioctl proxy-config endpoint deploy/productpage -n default \ --cluster "outbound|9080|v2|reviews.default.svc.cluster.local" # 기대: version=v2 Pod IP들이 HEALTHY. 0개면 아래로 교차 확인: kubectl get pods -n default -l app=reviews --show-labels kubectl get endpointslices -n default -l kubernetes.io/service-name=reviews # 7. 런타임 관측 — 호출자 sidecar의 access log response flag kubectl logs deploy/productpage -n default -c istio-proxy --tail=100 | grep -E ' 50[03] | 404 ' # "... 503 NC ..." → cluster 부재 "... 503 UH ..." → endpoint 부재 # "... 404 NR ..." → route 부재 # 8. 최후 수단 — config dump / Envoy 로그 레벨 kubectl exec deploy/productpage -n default -c istio-proxy -- \ curl -s localhost:15000/config_dump > dump.json istioctl proxy-config log deploy/productpage -n default --level router:debug,upstream:debug ``` ### 증상 → 진입 단계 매핑 | 증상 | 진입 단계 | 전형적 원인 | |---|---|---| | 배포 직후 전면 503 | 1 → 7 (flag 확인) | subset 오타(NC), DR host 불일치(NC) | | 특정 namespace에서만 503 | 4, 5 | VS/DR `exportTo` 제한, short name의 namespace 확장 차이 | | 가중치가 반영 안 됨 | 4 | 다른 VS가 같은 host 선점, match 순서 | | 일부 요청만 간헐 503 | 6, 7 | 일부 Pod not-ready(UH 간헐), outlier ejection, rollout 중 label 전환 | | 설정 바꿨는데 반영 안 됨 | 2 | proxy STALE, webhook 오류로 리소스 미반영 | | mTLS 관련 연결 실패 | 7 (UF) | DR tls 모드 ↔ PeerAuthentication 불일치 | ## 7. 진단 축 — 계층별 response flag | Flag | 의미 | 응답 코드 | 의심 계층 | 1차 확인 | |---|---|---|---|---| | `NR` | NoRoute — 매칭 route/filter chain 없음 | **404** (TCP는 코드 없이 연결 종료) | VS / hosts | `pc route`, hosts 철자·registry 등록 | | `NC` | NoCluster — route가 가리키는 cluster 없음 | 503 | DR / host·subset | `pc cluster`, DR host FQDN·exportTo | | `UH` | NoHealthyUpstream — cluster는 있으나 endpoint 0개 | 503 | subset labels / Pod | `pc endpoint`, Pod label·readiness | | `UF` | UpstreamConnectionFailure — 연결 실패 | 503 | mTLS / NetworkPolicy / 포트 | DR tls 모드 ↔ PeerAuthentication 정합 | | `UO` | UpstreamOverflow — circuit breaker 발동 | 503 | DR connectionPool | `pc cluster -o json` thresholds, `upstream_rq_pending_overflow` stat | - **NR vs NC** — NR은 "라우팅 테이블에 규칙 자체가 없음"(VS 계층), NC는 "규칙은 있는데 목적지 cluster가 없음"(DR 계층). 고칠 리소스가 다르다. - **NC vs UH** — NC는 cluster 생성 실패(DR host/subset 참조 문제), UH는 cluster는 정상이나 label 필터 결과가 빈 경우. `pc cluster` → `pc endpoint` 순 확인으로 즉시 구분된다. - **UF** — 설정 매핑이 아니라 연결 자체의 문제. Vol.2의 [port×subset 오조합](/docs/istio/egress/dr-subset-port-scoping/)이 전형이다. > [!warning] 원문 HTML의 교정 사항 — NR은 404다 > 원문 HTML(00 요약)은 NR을 503 계열로 묶었는데, 후속 검토에서 교정됐다. **HTTP에서 NR은 404와 함께 남고**, TCP·filter chain 미매칭은 응답 코드 없이 연결이 끊어지는 형태로 관측된다. 503 계열은 UH/NC/UF/UC/UR/LR/UO다. flag 판독 전반은 [Response flag 실전 판독](/docs/istio/xds-envoy/response-flags-triage/) 참조. ## 8. 실패 시나리오 — 어떻게 관측되는가 - **A. subset 참조 대상 DR 부재 → `503 NC`** — VS가 `subset: v2`를 참조하는데 DR이 없거나 host FQDN 불일치. cluster `outbound|9080|v2|...` 자체가 생성되지 않아 route가 존재하지 않는 cluster 이름을 가리킨다. `istioctl analyze`의 IST0101로 사전 탐지 가능. - **B. subset labels와 일치하는 Pod 없음 → `503 UH`** — DR은 정상 적용됐으나 해당 label Pod가 0개(오타, rollout 중 label 변경, 전부 not-ready). cluster는 존재하지만 EDS 결과가 빈 목록. 정적 검증으로 안 잡히고 런타임에만 드러난다 — `pc endpoint`가 유일한 확정 수단. - **C. short name의 namespace 확장 차이 → 조용한 미적용** — VS는 `team-a`, DR은 `team-b`에 있고 둘 다 `host: reviews`면 서로 다른 FQDN으로 확장되어 KEY ①이 불일치한다. subset까지 참조했다면 NC로 표면화되지만, trafficPolicy만 있는 DR이면 **증상 없이 정책만 사라진다**. - **D. exportTo로 가려진 리소스 → 호출자 위치 의존 503** — DR에 `exportTo: ["."]`가 걸리면 다른 namespace의 sidecar에는 subset cluster가 생성되지 않는다. "같은 namespace에서는 되는데 다른 데선 NC"가 시그니처. > [!key] 공통 원칙 > proxy-config 조회는 항상 **호출자(client) Pod 기준**으로 수행한다. 라우팅 결정은 호출자 sidecar의 outbound에서 일어나며, exportTo·Sidecar 리소스에 의해 Pod마다 보이는 설정이 다를 수 있다. ## What you might be missing - **DR의 exportTo와 namespace 경계** — DR은 기본적으로 전 mesh에 export되지만(`meshConfig.defaultDestinationRuleExportTo`에 따라 다름), `exportTo: ["."]`로 제한하면 다른 namespace의 sidecar에는 subset cluster가 아예 안 생긴다. "내 namespace에선 되는데 다른 데선 503 NC"의 흔한 원인. - **같은 host에 DR 여러 개** — 같은 host를 겨냥한 DR이 여러 namespace에 있으면 병합 규칙이 직관적이지 않고(호출자 namespace의 DR 우선 등), trafficPolicy가 예상과 다르게 적용될 수 있다. **host당 DR 하나**를 원칙으로 하는 게 안전하다. - **subset 없이도 DR은 의미가 있다** — mTLS 모드, connection pool, outlier detection은 host 레벨에서만 걸어도 동작한다. 반대로 subset 레벨 trafficPolicy는 host 레벨을 상속이 아니라 **통째로 override**하므로, subset에 정책을 하나라도 쓰면 host 레벨 정책을 다시 명시해야 한다. > [!question]- 확인 질문 — v2 subset의 labels를 `version: v2`에서 `version: v3`로 바꾸면 (Pod는 그대로) 무슨 일이 생기나 > cluster `outbound|9080|v2|...`는 그대로 존재한다 — subset **이름**(v2)은 안 바뀌었으므로 VS route 참조도 유효하고 NC가 아니다. 대신 그 cluster의 EDS label 필터가 `version: v3`로 바뀌는데 매칭되는 Pod가 0개이므로 endpoint 목록이 빈다. 결과: v2로 가던 트래픽 전부가 **503 UH**. `istioctl analyze`는 못 잡는다(참조 무결성은 멀쩡한, 문법상 유효한 설정이므로) — `pc endpoint`가 유일한 확정 수단이다. ## 관련 문서 - [DR subset × port 교차 생성 — 원리와 포트 스코핑 전략](/docs/istio/egress/dr-subset-port-scoping/) — 이 문서의 후속편(Vol.2). "설정은 전부 정합한데 UF"인 네 번째 진단 축 - [Response flag 실전 판독 — 식별 팁·계층별 카탈로그·조합 해석](/docs/istio/xds-envoy/response-flags-triage/) — §7 진단 축의 확장판(Vol.3 격). details 필드·집계·디코더 - [DestinationRule 기초→심화](/docs/istio/egress/destinationrule-fundamentals/) — DR 필드 전반의 정본 - [Envoy Response Flags 운영 레퍼런스](/docs/istio/xds-envoy/envoy-response-flags/) — 28-flag 정본·파이프라인 멘탈모델 (검증 문서) - [xDS 계층과 진단](/docs/istio/xds-envoy/xds-layers-and-diagnosis/) — §4·§6의 계층 순회를 xDS 관점에서 정리한 정본 - [SE와 Envoy 반영 범위 — sidecar vs gateway·DR](/docs/istio/egress/se-envoy-config-scope/) — 같은 매핑이 ServiceEntry·gateway에서 어떻게 달라지는가