Istio Traffic Management Reference · Vol.2

DestinationRule subset × port 교차 생성
원리와 포트 스코핑 전략

"3128용 subset·443용 subset"으로 나눠 등록했는데 istioctl pc cluster에 모든 포트 × 모든 subset 조합이 보이는 이유, EDS·HEALTHY의 실제 의미, 그리고 의도한 포트만 남기는 세 가지 전략과 선택 기준을 정리함.

00 / SUMMARY

핵심 요약

  • 원리 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 진단 축에 안 잡히는 구간임.
01 / CROSS PRODUCT

왜 교차 생성되는가

DR subset의 스키마는 name · labels · trafficPolicy 세 필드가 전부임. "이 subset은 3128 전용"이라고 선언할 자리가 없음. 포트별 subset이라는 의도는 리소스에 표현되지 않고, istiod는 다음 규칙으로 CDS를 생성함:

CDS 생성 규칙 — host의 port마다: 기본 cluster 1개 + subset마다 1개. cluster 이름 = outbound|<port>|<subset>|<FQDN>
Service axis — ports proxy-svc.egress.svc.cluster.local ports: [3128, 443] DR axis — subsets (label only) - name: squid labels: {app: squid} - name: tls-gw labels: {app: tls-gw} × cross product (istiod) CDS output — 2 ports × (1 default + 2 subsets) = 6 clusters outbound|3128||proxy-svc... outbound|443||proxy-svc... outbound|3128|squid|proxy-svc... outbound|443|squid|proxy-svc... ← 의도 밖 outbound|3128|tls-gw|proxy-svc... ← 의도 밖 outbound|443|tls-gw|proxy-svc... "의도 밖" 조합도 문법상 유효한 cluster로 생성됨 — istiod는 의도를 모름 subset을 늘리거나 port를 늘리면 곱으로 증가함. host(Service)를 분리하면 곱의 한 축이 사라짐 (→ 04)
fig 1. port 축 × subset 축 = cluster 곱. "포트별 subset"이라는 의도는 어디에도 기록되지 않음

따라서 pc cluster에 443용으로 만든 subset이 3128 포트 행으로도 보이는 것은 정확히 이 규칙의 결과임. 문제는 존재 자체가 아니라, 이 중 일부가 "설정상 존재하지만 실제로는 동작 불가능한 조합"이라는 점임 (→ 06).

02 / EDS SEMANTICS

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의 포트로 통신 가능한지
03 / SELECTOR VS SUBSET

Service selector vs subset labels — Pod를 나눠야 하는가

둘 다 "Pod를 고르는 조건"일 뿐 Pod 배치를 강제하지 않음. 한 Pod가 여러 Service에 잡히는 것도, 여러 subset에 잡히는 것도 정상임. 따라서 "label이 다르니 Pod를 나눠야 한다"는 순서가 반대임 — Pod가 실제로 다른 워크로드인지가 먼저이고, label은 그 사실의 표현 수단임.

판별: 두 subset의 labels가 실제로 누구를 가리키는가

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 변경
상황 1labels가 서로 다른 Pod 그룹을 가리킴Service 2개, selector 각각 (그룹별)없음 — 이미 나뉘어 있음
상황 2labels가 결국 같은 Pod들에 매칭됨Service 2개, selector 동일, 포트만 다름없음 — subset이 잘못된 도구였을 뿐
상황 2 — 같은 Deployment를 두 Service가 공유 (Pod 분리 불필요) Service: squid-path selector: {app: egress-gw} ports: [3128] Service: tls-path selector: {app: egress-gw} ← 동일 ports: [443] Deployment: egress-gw (한 벌) labels: {app: egress-gw} listens: 3128, 443 각 host에 자기 port cluster만 생성됨: outbound|3128||squid-path... / outbound|443||tls-path... — subset 불필요
fig 2. Service는 논리 객체 — selector가 같아도 host가 다르면 cluster 곱의 축이 분리됨
04 / STRATEGIES

해결 전략 3안

표는 색인이며, 각 안의 메커니즘과 트레이드오프는 아래 해설 참조.

방법cross productPod 변경적합 조건
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로 작성함 (→ 03). 리소스 추가는 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안으로 해결 안 될 때만 검토함.

05 / EXAMPLE

완전한 예제 — 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
06 / FAILURE CASE

실패 케이스 — 의도 밖 조합으로 라우팅 시 503 UF

VS에서 실수로 port: 3128 + 443만 listen하는 Pod의 subset으로 라우팅한 경우의 관측 시그니처:

진단 단계결과함정
pc clustercluster 존재 정상으로 보임NC 아님 — DR 계층 무혐의로 판단하기 쉬움
pc endpointendpoint HEALTHY 정상으로 보임UH 아님 — label 필터는 통과했기 때문
실제 요청connection refused → 503 UFPod가 그 포트를 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 정합)를 실측해야 함.
07 / DECISION FLOW

판단 플로우

두 subset의 labels가 같은 Pod를 가리키는가? kubectl get pods -l ... 로 실측 워크로드 격리(리소스·장애 반경· HPA·노드 배치)가 별도로 필요한가? YES NO 2안 — Pod까지 분리 Deployment 분리 + Service 각각 격리 요구가 명확할 때만 1안 — Service만 분리 (정석) 포트당 host · Pod 공유 상황 1: selector 각각 / 상황 2: 동일 3안 — 수용 (전환 초기) VS destination.port 필수 명시 GitOps 리뷰로 오조합 차단 가능 시 규모 증가 시 1안으로 이행 무장애 우선 · 변경 최소가 우선이면 어느 경로든 Pod를 새로 띄우는 것은 "cross product 해소" 목적이 아니라 "워크로드 격리" 목적일 때만 정당화됨
fig 3. 판별(labels 실측) → 격리 필요성 → 전략 선택. 3안은 전환기 임시 경로로 유효함