--- title: DestinationRule subset × port 교차 생성 — 원리와 포트 스코핑 전략 date: 2026-08-01 type: guide domain: istio tags: [istio, destinationrule, subset, eds, envoy, egress, diagnosis] --- > [!note] 원본은 스타일링된 HTML 레퍼런스 (Vol.2) > 이 글의 원문은 port×subset 곱 다이어그램, 동일-selector 두 Service 공유 구조도, 전략 선택 결정 트리 SVG 3종을 포함한 HTML 레퍼런스다. > > **[→ 전문 보기 (istio-dr-subset-port-scoping.html)](files/istio-dr-subset-port-scoping.html)** > > 이 페이지는 같은 내용의 markdown 정리본. 전편: [Vol.1 — VS ↔ DR 매핑 구조와 디버깅 플레이북](/docs/istio/egress/vs-dr-mapping-playbook/). 출발점이 된 상황: egress 설계에서 "3128용 subset·443용 subset"으로 나눠 등록했는데 `istioctl pc cluster`에 모든 포트 × 모든 subset 조합이 보였다. ## 0. 핵심 요약 - **원리** — subset은 **label 축**, port는 **Service 축**. subset 스키마에 port 필드가 없어 istiod는 **host의 port × subset 곱**으로 cluster를 생성한다. 관찰된 현상은 버그가 아닌 설계상 정상. - **EDS** — endpoint는 **label로만** 필터된다. Pod가 해당 포트를 실제 listen하는지 검사하지 않으며, HEALTHY는 "readiness 통과 + ejection 아님"의 의미다. - **정석** — 포트 스코핑은 Service의 책임: **포트당 Service(host) 분리**로 곱 자체를 제거한다. **Pod는 나눌 필요 없다** (같은 Deployment를 두 Service가 공유 가능). - **위험** — 잘못된 (port, subset) 조합으로 라우팅하면 cluster·endpoint 모두 정상으로 보이지만 **503 UF**로 실패한다 — NC/UH 진단 축에 안 잡히는 구간. ## 1. 왜 교차 생성되는가 DR subset의 스키마는 `name` · `labels` · `trafficPolicy` 세 필드가 전부다. **"이 subset은 3128 전용"이라고 선언할 자리가 없다.** 포트별 subset이라는 의도는 리소스에 표현되지 않고, istiod는 다음 규칙으로 CDS를 생성한다: > [!key] CDS 생성 규칙 > host의 **port마다**: 기본 cluster 1개 + **subset마다** 1개. cluster 이름 = `outbound|||` ``` Service axis (ports) DR axis (subsets, label only) proxy-svc: [3128, 443] x squid {app: squid} tls-gw {app: tls-gw} | v cross product (istiod) CDS output: 2 ports x (1 default + 2 subsets) = 6 clusters outbound|3128||proxy-svc... outbound|3128|squid|proxy-svc... outbound|3128|tls-gw|proxy-svc... <- unintended outbound|443||proxy-svc... outbound|443|squid|proxy-svc... <- unintended outbound|443|tls-gw|proxy-svc... ``` `pc cluster`에 443용으로 만든 subset이 3128 포트 행으로도 보이는 것은 정확히 이 규칙의 결과다. "의도 밖" 조합도 문법상 유효한 cluster로 생성된다 — istiod는 의도를 모른다. 문제는 존재 자체가 아니라, 이 중 일부가 "설정상 존재하지만 실제로는 동작 불가능한 조합"이라는 점이다 (→ §6). ## 2. EDS는 label만 본다 — HEALTHY의 실제 의미 `pc endpoint`에서 의도 밖 조합의 endpoint까지 HEALTHY로 보이는 이유도 같은 축 분리에서 나온다. | 단계 | 동작 | 검사하는 것 | 검사하지 않는 것 | |---|---|---|---| | EDS endpoint 선정 | Service endpoint 중 subset **labels 일치** Pod 선택 | label 일치 여부 | **해당 포트 listen 여부** | | endpoint port 결정 | 그 Service port의 `targetPort`를 그대로 사용 | Service 선언 | Pod의 실제 open port | | HEALTHY 판정 | readiness probe 통과 + outlier ejection 아님 | probe 결과 | 이 cluster의 포트로 통신 가능한지 | - `outbound|3128|tls-gw|...`의 endpoint = "tls-gw label Pod들 + 3128의 targetPort". 그 Pod가 443만 listen해도 목록에 들어간다. - readiness probe가 앱 포트(예: 443)만 체크하면, 3128 조합 cluster에서도 같은 Pod가 HEALTHY로 표시된다. **"HEALTHY ≠ 이 cluster로 통신 가능"**이 핵심 구분이다. ## 3. Service selector vs subset labels — Pod를 나눠야 하는가 둘 다 "Pod를 고르는 조건"일 뿐 **Pod 배치를 강제하지 않는다.** 한 Pod가 여러 Service에 잡히는 것도, 여러 subset에 잡히는 것도 정상이다. 따라서 "label이 다르니 Pod를 나눠야 한다"는 순서가 반대다 — **Pod가 실제로 다른 워크로드인지가 먼저**이고, label은 그 사실의 표현 수단이다. 판별은 실측 하나로 끝난다: ```bash kubectl get pods -n egress -l app=squid --show-labels kubectl get pods -n egress -l app=tls-gw --show-labels # 같은 Pod가 나옴 → 상황 2 (단일 워크로드, 포트 구분 목적의 label이었음) # 다른 Pod가 나옴 → 상황 1 (이미 분리된 워크로드) ``` | 상황 | 실체 | 조치 | Pod 변경 | |---|---|---|---| | **상황 1** | labels가 서로 다른 Pod 그룹을 가리킴 | Service 2개, **selector 각각** (그룹별) | 없음 — 이미 나뉘어 있음 | | **상황 2** | labels가 결국 같은 Pod들에 매칭됨 | Service 2개, **selector 동일**, 포트만 다름 | 없음 — subset이 잘못된 도구였을 뿐 | ``` 상황 2 — 같은 Deployment를 두 Service가 공유 (Pod 분리 불필요) +---------------------------+ +---------------------------+ | Service: squid-path | | Service: tls-path | | selector: {app: egress-gw}| | selector: {app: egress-gw}| <- 동일 | ports: [3128] | | ports: [443] | +------------+--------------+ +------------+--------------+ | | v v +--------------------------------------+ | Deployment: egress-gw (한 벌) | | labels: {app: egress-gw} | | listens: 3128, 443 | +--------------------------------------+ ``` Service는 논리 객체다 — selector가 같아도 host가 다르면 cluster 곱의 축이 분리된다. 각 host에 자기 port cluster만 생성된다: `outbound|3128||squid-path...` / `outbound|443||tls-path...` — subset 불필요. ## 4. 해결 전략 3안 | 안 | 방법 | cross product | Pod 변경 | 적합 조건 | |---|---|---|---|---| | **1안** | **Service만 분리** (Pod 공유) | 제거됨 | 없음 | 대부분의 경우 — 정석 | | **2안** | **Pod까지 분리** (워크로드 격리) | 제거됨 | Deployment 분리 | 격리가 별도로 필요할 때만 | | **3안** | **수용** + VS `destination.port`로 정밀 라우팅 | 존재 (무해화) | 없음 | 규모 작음 · 무장애 우선 초기 단계 | **1안 — Service(host) 분리, Pod 공유.** 포트 스코핑은 Service의 책임이므로 "포트당 하나의 host"로 모델링한다. cluster는 host 단위로 생성되므로 host가 갈라지는 순간 각 host엔 자기 포트만 존재하고, 그룹 구분이 host로 표현되므로 **subset 자체가 불필요해진다**. 상황 1이면 selector를 그룹별로, 상황 2면 동일 selector로 작성한다 (→ §3). 리소스 추가는 Service 오브젝트뿐이라 무장애 전환과도 충돌 없다. **2안 — Pod까지 분리.** cross product 해소만이 목적이면 과잉이다. 선택 기준은 별개의 운영 요구 — 리소스 경합 분리, 장애 반경 축소, HPA 개별 스케일링, 노드 배치(egress 노드 집약) 차이 — 가 있는지다. Squid 경유 TCP 터널 트래픽과 TLS 트래픽의 부하 특성이 크게 다르면 고려한다. **3안 — 수용 + 라우팅 통제.** 조합 cluster는 트래픽에 무해하다 — 라우팅되지 않는 cluster는 config로만 존재한다. VS에서 `destination.port.number`를 항상 명시해 유효한 (port, subset) 조합만 참조하면 된다. 비용은 두 가지: ① proxy 메모리·xDS push 크기 증가(Service·subset 수에 곱으로 비례), ② **사람 실수로 잘못된 조합을 라우팅할 위험** — 이 위험을 설정 리뷰(GitOps PR)로 막을 수 있는지가 채택 기준이다. 포트별 정책 차이는 subset이 아니라 `trafficPolicy.portLevelSettings`로 표현한다: ```yaml trafficPolicy: portLevelSettings: - port: { number: 3128 } tls: { mode: DISABLE } # CONNECT 평문 터널 - port: { number: 443 } tls: { mode: ISTIO_MUTUAL } ``` > [!tip] 운영 경로 제안 > 무장애 최우선이면 변경 최소인 3안으로 시작 → SE·경로 수 증가 시점에 1안으로 정리. 판단 기준: "잘못된 (port, subset) 조합 라우팅을 리뷰로 차단 가능한가". VS를 ArgoCD 한벌 단위로 관리하면 통제 가능한 위험이다. 보조 수단 — `Sidecar` 리소스의 `egress[]`(host+port 단위 import 제한)로 소비자 proxy에 밀리는 cluster 수를 줄일 수 있다. 단 이는 config push 절감 수단이지 subset-포트 매핑 수단이 아니며 관리 복잡도가 오르므로 1·3안으로 해결 안 될 때만 검토한다. ## 5. 완전한 예제 — 1안 (상황 2: 동일 selector) ```yaml # 같은 Deployment(egress-gw, 3128·443 모두 listen)를 두 Service가 공유 apiVersion: v1 kind: Service metadata: name: squid-path namespace: egress spec: selector: { app: egress-gw } # 동일 selector ports: - name: tcp-proxy port: 3128 targetPort: 3128 --- apiVersion: v1 kind: Service metadata: name: tls-path namespace: egress spec: selector: { app: egress-gw } # 동일 selector ports: - name: tls port: 443 targetPort: 8443 --- # DR은 host별로 — subset 없이 정책만 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: squid-path namespace: egress spec: host: squid-path.egress.svc.cluster.local trafficPolicy: tls: { mode: DISABLE } --- apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: tls-path namespace: egress spec: host: tls-path.egress.svc.cluster.local trafficPolicy: tls: { mode: ISTIO_MUTUAL } ``` 검증: ```bash # 1. host별로 자기 포트 cluster만 존재하는지 istioctl proxy-config cluster deploy/ -n --fqdn squid-path.egress.svc.cluster.local # 예상: PORT=3128 행만 존재. 443 행 없음 istioctl proxy-config cluster deploy/ -n --port 443 # 예상: tls-path만 표시, squid-path 미표시 # 2. 두 host의 endpoint가 같은 Pod IP를 가리키는지 (Pod 공유 확인) istioctl proxy-config endpoint deploy/ -n \ --cluster "outbound|3128||squid-path.egress.svc.cluster.local" istioctl proxy-config endpoint deploy/ -n \ --cluster "outbound|443||tls-path.egress.svc.cluster.local" # 예상: 동일한 Pod IP 집합, 포트만 3128 / 8443으로 다름 # 3. 정적 검증 istioctl analyze -n egress # 예상: No validation issues found ``` ## 6. 실패 케이스 — 의도 밖 조합으로 라우팅 시 503 UF VS에서 실수로 `port: 3128` + 443만 listen하는 Pod의 subset으로 라우팅한 경우의 관측 시그니처: | 진단 단계 | 결과 | 함정 | |---|---|---| | `pc cluster` | cluster 존재 — 정상으로 보임 | NC 아님 — DR 계층 무혐의로 판단하기 쉬움 | | `pc endpoint` | endpoint HEALTHY — 정상으로 보임 | UH 아님 — label 필터는 통과했기 때문 | | 실제 요청 | connection refused → **503 UF** | Pod가 그 포트를 listen하지 않음 | ```bash kubectl logs deploy/ -c istio-proxy | grep ' 503 ' # "... 503 UF ..." — UpstreamConnectionFailure # cluster·endpoint가 모두 정상으로 보이면 listen 포트를 실측함 kubectl exec -n egress -c -- ss -tlnp # 예상: 443(8443)만 LISTEN, 3128 없음 → 조합 오류 확정 ``` > [!warning] 진단 축 확장 > [Vol.1](/docs/istio/egress/vs-dr-mapping-playbook/)의 NR(라우트 부재)/NC(cluster 부재)/UH(endpoint 부재)에 더해, **UF는 "설정은 전부 정합한데 실체와 불일치"**하는 구간이다. 설정 계층 순회가 전부 통과하면 마지막으로 워크로드 실체(listen 포트, NetworkPolicy, mTLS 정합)를 실측해야 한다. ## 7. 판단 플로우 ``` Q1. 두 subset의 labels가 같은 Pod를 가리키는가? (kubectl get pods -l ... 로 실측) | v Q2. 워크로드 격리(리소스·장애 반경·HPA·노드 배치)가 별도로 필요한가? | YES | NO 무장애 우선·변경 최소가 우선이면 +---+---+ | v v v [2안] [1안 — 정석] [3안 — 전환 초기] Pod까지 Service만 분리 수용 + VS destination.port 필수 분리 포트당 host, Pod 공유 GitOps 리뷰로 오조합 차단 가능 시 상황1: selector 각각 규모 증가 시 1안으로 이행 상황2: selector 동일 ``` 어느 경로든 Pod를 새로 띄우는 것은 "cross product 해소" 목적이 아니라 "워크로드 격리" 목적일 때만 정당화된다. ## 관련 문서 - [Istio VirtualService ↔ DestinationRule — 매핑 구조와 디버깅 플레이북](/docs/istio/egress/vs-dr-mapping-playbook/) — 전편(Vol.1). 매핑 키·xDS 변환·NC/UH 진단의 기반 - [Response flag 실전 판독 — 식별 팁·계층별 카탈로그·조합 해석](/docs/istio/xds-envoy/response-flags-triage/) — UF를 포함한 flag 판독 워크플로(Vol.3 격) - [Outbound 집약 설계서 — Istio Egress Gateway로 Squid 경로 일원화](/docs/istio/egress/squid-consolidation-guide/) — 이 논의의 출발점인 egress 설계 (3128 평문 터널 · 443 mTLS 경로) - [DestinationRule 기초→심화](/docs/istio/egress/destinationrule-fundamentals/) — DR 필드 전반의 정본 - [Cluster 해부](/docs/istio/xds-envoy/cluster-anatomy/) — cluster 이름 규칙·EDS·outlier detection의 정본