DestinationRule subset × port 교차 생성 — 원리와 포트 스코핑 전략
이 글의 원문은 port×subset 곱 다이어그램, 동일-selector 두 Service 공유 구조도, 전략 선택 결정 트리 SVG 3종을 포함한 HTML 레퍼런스다.
→ 전문 보기 (istio-dr-subset-port-scoping.html)
이 페이지는 같은 내용의 markdown 정리본. 전편: Vol.1 — VS ↔ DR 매핑 구조와 디버깅 플레이북. 출발점이 된 상황: 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를 생성한다:
host의 port마다: 기본 cluster 1개 + subset마다 1개. cluster 이름 = outbound|<port>|<subset>|<FQDN>
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은 그 사실의 표현 수단이다.
판별은 실측 하나로 끝난다:
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로 표현한다:
trafficPolicy:
portLevelSettings:
- port: { number: 3128 }
tls: { mode: DISABLE } # CONNECT 평문 터널
- port: { number: 443 }
tls: { mode: ISTIO_MUTUAL }
무장애 최우선이면 변경 최소인 3안으로 시작 → SE·경로 수 증가 시점에 1안으로 정리. 판단 기준: “잘못된 (port, subset) 조합 라우팅을 리뷰로 차단 가능한가”. VS를 ArgoCD 한벌 단위로 관리하면 통제 가능한 위험이다.
보조 수단 — Sidecar 리소스의 egress[](host+port 단위 import 제한)로 소비자 proxy에 밀리는 cluster 수를 줄일 수 있다. 단 이는 config push 절감 수단이지 subset-포트 매핑 수단이 아니며 관리 복잡도가 오르므로 1·3안으로 해결 안 될 때만 검토한다.
5. 완전한 예제 — 1안 (상황 2: 동일 selector)
# 같은 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 }
검증:
# 1. host별로 자기 포트 cluster만 존재하는지
istioctl proxy-config cluster deploy/<caller> -n <ns> --fqdn squid-path.egress.svc.cluster.local
# 예상: PORT=3128 행만 존재. 443 행 없음
istioctl proxy-config cluster deploy/<caller> -n <ns> --port 443
# 예상: tls-path만 표시, squid-path 미표시
# 2. 두 host의 endpoint가 같은 Pod IP를 가리키는지 (Pod 공유 확인)
istioctl proxy-config endpoint deploy/<caller> -n <ns> \
--cluster "outbound|3128||squid-path.egress.svc.cluster.local"
istioctl proxy-config endpoint deploy/<caller> -n <ns> \
--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하지 않음 |
kubectl logs deploy/<caller> -c istio-proxy | grep ' 503 '
# "... 503 UF ..." — UpstreamConnectionFailure
# cluster·endpoint가 모두 정상으로 보이면 listen 포트를 실측함
kubectl exec <pod> -n egress -c <app> -- ss -tlnp
# 예상: 443(8443)만 LISTEN, 3128 없음 → 조합 오류 확정
Vol.1의 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 — 매핑 구조와 디버깅 플레이북 — 전편(Vol.1). 매핑 키·xDS 변환·NC/UH 진단의 기반
- Response flag 실전 판독 — 식별 팁·계층별 카탈로그·조합 해석 — UF를 포함한 flag 판독 워크플로(Vol.3 격)
- Outbound 집약 설계서 — Istio Egress Gateway로 Squid 경로 일원화 — 이 논의의 출발점인 egress 설계 (3128 평문 터널 · 443 mTLS 경로)
- DestinationRule 기초→심화 — DR 필드 전반의 정본
- Cluster 해부 — cluster 이름 규칙·EDS·outlier detection의 정본