--- type: guide tags: [istio, destinationrule, connection-pool, subset, tls, loadbalancer, outlier-detection, egress] created: 2026-07-03 --- # DestinationRule 만들기 — 기초부터 심화까지 > [!abstract] > DR은 "어디로 보낼지"(VS의 일)가 아니라 **"도착이 결정된 목적지와 어떻게 통신할지"**를 정하는 > 리소스다. host 문자열이 서비스 레지스트리와 매칭되어 Envoy cluster에 컴파일되는 전 과정, > trafficPolicy 필드가 cluster의 어느 칸으로 흩어지는지, 레벨 3층(top/subset/port)의 병합 > 규칙("통째 교체"), 같은 host 다중 DR의 조용한 병합, 그리고 만들었으면 반드시 도는 검증 3단계까지. > 모든 예시는 홈랩 실측(2026-07-03, mesh-test namespace)이다. **선행 문서**: [Egress 4-CRD 직관](mm__guide-crd-mental-model.html) — DR이 4-CRD 중 어느 질문을 담당하는지. --- ## 01. DR의 자리 — 세 리소스의 분업 host라는 단어가 세 리소스에 다 나오지만 역할이 전부 다르다: ``` Service / ServiceEntry hosts "이 목적지가 존재한다" -> istiod가 cluster 생성 DestinationRule host "그 cluster에 정책 부착" -> pool/LB/TLS가 cluster에 컴파일 VirtualService hosts+dest "트래픽을 그 cluster로" -> 어느 cluster를 탈지 결정 ``` istiod가 만드는 cluster의 이름이 이 구조를 그대로 담는다: ``` outbound | 443 | httpbin | istio-egressgateway.istio-system.svc.cluster.local (방향) (포트) (subset) (레지스트리에 등록된 host — DR.host의 매칭 대상) ``` **DR은 VS 없이도 동작한다** — 레지스트리 host와 매칭만 되면 cluster에 정책이 박힌다. 반대로 DR 없이도 트래픽은 흐른다 — 기본 cluster(무제한 풀, keepalive off, plaintext 그대로)로. DR은 "흐르게 하는" 리소스가 아니라 "흐르는 방식을 통제하는" 리소스다. --- ## 02. 기초 — 최소 DR과 host 매칭 규칙 ### 최소 형태 ```yaml apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: httpbin-ext, namespace: mesh-test } spec: host: httpbin.org # <- 이 문자열이 전부를 결정한다 trafficPolicy: connectionPool: tcp: { maxConnections: 100 } ``` ### host 매칭 규칙 — "명확해야 한다"의 정확한 의미 DR의 `host`는 **서비스 레지스트리(k8s Service / ServiceEntry)에 등록된 host 문자열과 매칭**된다. VS와 매칭되는 것이 아니다. | 대상 | 레지스트리 등록 형태 | DR host에 적을 것 | |---|---|---| | k8s Service | `..svc.cluster.local` (자동) | FQDN 전체 권장 | | ServiceEntry | `spec.hosts`의 문자열 그대로 | 그 문자열과 정확 일치 (또는 `*.example.com` 접두 wildcard) | > [!warning] > **매칭 실패는 조용하다.** DR은 선언적 정책이라 대상 host가 "나중에 생길 수도" 있으므로, > istiod는 매칭 안 되는 DR을 에러로 취급하지 않는다. 오타 하나로 정책 전체가 소리 없이 > 무시되고, 증상은 "설정했는데 적용 안 됨"으로만 나타난다. 그래서 §06의 검증이 필수다. **short-name 확장 함정**: `host: istio-egressgateway`처럼 짧게 적으면 **DR이 있는 namespace 기준**으로 확장된다. DR이 `mesh-test`에 있으면 `istio-egressgateway.mesh-test.svc.cluster.local`로 풀리는데, 실제 gateway는 `istio-system`에 있으므로 존재하지 않는 host가 되어 조용히 무시된다. **항상 FQDN 전체를 적는 것이 규칙이다.** ### 만들었으면 바로 확인 — 결부(attribution) 검증 ```bash istioctl proxy-config cluster deploy/sleep -n mesh-test --fqdn httpbin.org -o json | \ jq '.[] | {name, dr: .metadata.filterMetadata.istio.config}' # "dr": ".../namespaces/mesh-test/destination-rule/httpbin-ext" <- 결부 성공 # "dr": null <- host 매칭 실패 ``` --- ## 03. trafficPolicy 해부 — 필드 4+1과 컴파일 위치 trafficPolicy의 최상위 필드는 사실상 4+1개이고, 각각 cluster JSON의 **다른 칸**으로 컴파일된다. 이 매핑을 모르면 검증에서 "한 칸만 보고 통과" 판정을 내리는 사고가 난다. | 필드 | 제어 대상 | cluster JSON 위치 | |---|---|---| | `connectionPool` | 연결 풀 한도·타임아웃·keepalive | 한도 4종→`circuitBreakers.thresholds`, keepalive→`upstreamConnectionOptions`, connectTimeout→`connectTimeout`, HTTP 공통→`typedExtensionProtocolOptions[...].commonHttpProtocolOptions` | | `loadBalancer` | 엔드포인트 선택 알고리즘 | `lbPolicy` (+ 관련 config) | | `outlierDetection` | 이상 엔드포인트 자동 격리 | `outlierDetection` | | `tls` | 이 목적지로 나갈 때의 TLS 모드/SNI | `transportSocket` | | `portLevelSettings` | 위 4개를 포트 단위로 override | 해당 포트 cluster에만 | 각 필드의 깊은 내용은 전담 문서로: connectionPool 값 도출은 [Egress TCP 처방전](cfg__guide-egress-tcp-tuning.html), tcpKeepalive 3필드는 [keepalive 필드 노트](ref__note-tcp-keepalive-fields.html), 컴파일 위치 전체는 [cluster 해부 정본](/public/istio/xds__src-cluster-anatomy.html). 한 가지만 여기서: **outlierDetection은 단일 IP 외부 목적지에 켜면 ejection = 전체 차단**이다. DNS가 다중 IP를 줄 때만 검토한다. --- ## 04. subsets — 이름·labels·라우팅의 3연결 subset은 "한 host의 엔드포인트를 나눠서 각각 다른 정책을 주는" 장치다: ```yaml spec: host: istio-egressgateway.istio-system.svc.cluster.local subsets: - name: cnn # ① VS destination.subset이 참조하는 이름 # labels: { version: v2 } # ② 있으면 그 label의 pod만, 없으면 전체 엔드포인트 trafficPolicy: { ... } # ③ 이 subset cluster에만 적용되는 정책 ``` - **labels가 없는 subset도 유효하다** — 엔드포인트는 전체 그대로 두고 **정책·SNI 앵커로만** 쓰는 패턴. egress gateway의 채널별 subset(cnn/httpbin)이 정확히 이것이다: pod는 같은 gateway지만 subset마다 다른 SNI·풀 한도를 준다. - subset이 정의되면 istiod는 **VS가 참조하든 안 하든** subset cluster를 만든다 (`outbound|443|cnn|...`). 존재는 결부의 증거일 뿐, 사용의 증거가 아니다 — 사용 여부는 VS의 `destination.subset`과 stats로 확인(§06). - canary/버전 라우팅에서는 labels가 엔드포인트를 실제로 가른다: `subset: v1` + `labels: {version: v1}`. --- ## 05. 병합 규칙 (심화 핵심) — 레벨 3층, "최상위 필드 단위 통째 교체" DR 정책은 세 레벨에 적을 수 있고, 병합은 **deep-merge가 아니다**: ``` top-level trafficPolicy | subset에 같은 "최상위 필드"가 있으면 -> 그 필드는 통째로 subset 것으로 교체 v subset trafficPolicy | portLevelSettings에 그 포트 엔트리가 있으면 -> 또 한 번 통째 교체 v portLevelSettings (그 포트의 cluster에 최종 적용) ``` 교체 단위는 `connectionPool`·`loadBalancer`·`outlierDetection`·`tls` **각각**이다. subset에 `connectionPool.tcp.maxConnections` 하나만 적어도 상위의 `tcpKeepalive`는 그 subset cluster에서 사라진다. > [!danger] > **실측으로 확인된 2단 함정 (2026-07-03)**: subset에 connectionPool을 넣고 같은 subset의 > `portLevelSettings`에 `tls`만 있는 엔트리(port 15443)를 두면 — **그 포트의 cluster에서는 > subset-level connectionPool까지 무시된다.** 실측: `outbound|443|cnn|...`엔 max=100이 박혔는데 > 정작 VS가 라우팅하는 `outbound|15443|cnn|...`만 4294967295(기본값)로 남았다. > **처방: 트래픽이 실제 타는 포트의 portLevelSettings 엔트리에 필요한 필드를 전부 재기재.** ```yaml subsets: - name: cnn trafficPolicy: connectionPool: { tcp: { maxConnections: 100, tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } } } portLevelSettings: - port: { number: 15443 } tls: { mode: ISTIO_MUTUAL, sni: edition.cnn.com } connectionPool: # <- 이 재기재가 없으면 15443 cluster는 기본값 tcp: { maxConnections: 100, tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } } ``` **meshConfig는 이 병합의 바깥에 있다** — `meshConfig.tcpKeepalive` 같은 전역 기본값은 istiod가 DR 병합과 별개 단계에서 cluster에 입히므로, subset 통째-교체에 휩쓸리지 않는다. 전역 기본 + 채널별 DR override의 3층 분업은 [keepalive 노트 §05](ref__note-tcp-keepalive-fields.html) 참조. --- ## 06. 같은 host에 DR 여러 벌 — 조용한 병합 한 host에 DR을 여러 벌 만들면 에러가 아니라 **병합**된다 (실측: `egressgateway-cnn` + `egressgateway-httpbin`, 같은 host): - **subsets는 합집합** — 두 DR의 subset이 모두 cluster로 생성된다. - **top-level trafficPolicy와 결부 귀속은 최고참(생성시각 가장 오래된 DR)이 승자** — ①의 `dr` 필드가 모든 cluster에서 오래된 DR만 가리키고, 나중 DR의 top-level 정책은 조용히 무시된다. - 같은 이름의 subset이 겹치면 최고참 것만 남는다. 당장은 동작해도, 나중에 어느 한쪽 top-level에 정책을 넣는 순간 "누가 이기는지"가 생성시각이라는 보이지 않는 축으로 결정된다. **host당 DR 1벌 + 채널당 subset 1개**로 통합하는 것이 원칙이다 (egress 레이어 2 설계가 이 형태다 — [4-CRD 직관](mm__guide-crd-mental-model.html)). **스코프 규칙 요약** (client 관점에서 어느 DR이 선택되나): 클라이언트 namespace의 DR > 서비스(대상) namespace의 DR > mesh root namespace(istio-system)의 DR. `exportTo`로 가시성을 좁힐 수 있고, 클라이언트 namespace에 `Sidecar` 리소스가 있으면 host가 그 egress 스코프 안에 있어야 cluster 자체가 존재한다 ([Sidecar scope](mm__note-sidecar-scope.html)). --- ## 07. tls 블록 — 나가는 연결의 4가지 모드 DR의 `tls`는 **이 클라이언트가 그 목적지로 나갈 때** 어떤 TLS를 입힐지다 (수신 측 설정 아님): | mode | 의미 | 대표 용도 | |---|---|---| | `DISABLE` | 평문 | 내부 평문 통신 | | `SIMPLE` | 표준 TLS origination | 앱은 HTTP로 보내고 프록시가 HTTPS로 승격 | | `MUTUAL` | 사용자 인증서 mTLS | 외부 파트너가 client cert를 요구할 때 | | `ISTIO_MUTUAL` | mesh 인증서 mTLS + SPIFFE 신원 | sidecar→gateway 구간 (레이어 2 subset) | `ISTIO_MUTUAL` + `sni`가 egress 레이어 2의 심장이다: SNI가 gateway의 filter chain 매칭 키가 되어 "어느 채널인가"를 식별한다. 종단 시 SNI가 소비되는 제약은 [HTTPS over mTLS 해부](id__src-https-over-mtls.html). --- ## 08. 검증 — 만들었으면 반드시 이 순서로 "적용 안 됨"의 원인 우선순위와 검증 절차의 정본은 [keepalive 노트 §04](ref__note-tcp-keepalive-fields.html)이고, 여기선 순서만: ```bash # 0) 값이 클러스터에 저장은 됐나 (apply 누락·다른 컨텍스트가 0순위 원인) kubectl config current-context && kubectl get dr -n -o yaml # 1) cluster 결부 + 값 (default와 subset을 나란히) istioctl proxy-config cluster -n --fqdn -o json | \ jq '.[] | {name, dr: .metadata.filterMetadata.istio.config, max: .circuitBreakers.thresholds[0].maxConnections, ka: .upstreamConnectionOptions.tcpKeepalive}' # 1b) 트래픽이 그 cluster를 타나 — sidecar 기본 stats가 트리밍된 환경에선 /clusters 사용 kubectl exec -c istio-proxy -- pilot-agent request GET clusters | \ grep "outbound|443||" | grep -E "cx_total|cx_active" # 2) (keepalive라면) 소켓 레벨 kubectl exec -c istio-proxy -- ss -tno state established # -> timer:(keepalive,4min55sec,0) # time=300s 카운트다운 (실측값) ``` > [!tip] > **stats 트리밍 주의 (실측)**: Istio 기본 `proxyStatsMatcher`는 cluster별 Envoy stats를 > 수집하지 않는 경우가 있다(`pilot-agent request GET stats`에 xds-grpc만 보임). 이때 > `GET clusters` 엔드포인트는 트리밍과 무관하게 cluster·엔드포인트별 `cx_total/cx_active`를 > 항상 보여준다 — ①-b의 견고한 대안. --- ## 09. 함정 모음 — 이 아카이브에서 실제로 밟은 것들 | 함정 | 증상 | 처방 | |---|---|---| | apply 누락 / 다른 kubeconfig 컨텍스트 | 모든 값 기본(4294967295), `dr`은 정상일 수도 | 검증 0단계부터 | | host 오타·short-name 확장 | `dr: null`, 조용한 무시 | FQDN 전체 기재 | | subset 통째 교체 | subset cluster만 일부 필드 소실 | subset에 전부 재기재 | | portLevelSettings 2단 교체 | **트래픽이 타는 포트만** 기본값 | 그 포트 엔트리에 재기재 | | 같은 host DR 다중 | 나중 DR의 top-level이 무시 | host당 1벌로 통합 | | 단일 IP에 outlierDetection | ejection = 전체 차단 | 다중 IP일 때만 | | 기존 연결 비소급 | 일부 소켓만 옛 설정 | 수명 상한으로 자연 교체 or rollout restart | --- ## 핵심 정리 | 항목 | 내용 | |---|---| | DR의 정체 | 라우팅이 아니라 "목적지와의 통신 방식"(풀·LB·격리·TLS)을 cluster에 컴파일하는 리소스 | | host 규칙 | 레지스트리(Service/SE) 문자열과 매칭. 실패는 조용함 → FQDN 전체 + 결부 검증 필수 | | 병합 규칙 | top→subset→port 3층, 최상위 필드 단위 **통째 교체**. 재기재가 원칙, meshConfig만 병합 밖 | | 다중 DR | subsets 합집합, top-level은 최고참 승 → host당 1벌 | | 검증 | 0) 저장 확인 → 1) `dr`+값 → 1b) `/clusters` 사용 확인 → 2) 소켓(ss) | --- ## 참조 **아카이브 내부** - [Egress 4-CRD 직관](mm__guide-crd-mental-model.html) — DR이 담당하는 질문의 위치 - [tcpKeepalive 필드 노트](ref__note-tcp-keepalive-fields.html) — §04 검증 정본, §05 meshConfig vs sysctl - [Egress TCP 문제별 처방전](cfg__guide-egress-tcp-tuning.html) — connectionPool 값 도출식 - [TCP 병목 정본](ref__src-tcp-bottlenecks.html) — 한도 값의 "왜" - [cluster 해부 정본](/public/istio/xds__src-cluster-anatomy.html) — 필드→cluster 컴파일 전체 매핑 **외부** - [Istio DestinationRule reference](https://istio.io/latest/docs/reference/config/networking/destination-rule/) - [Envoy cluster circuit breaking](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/circuit_breaking) **작업 파일 (다운로드)** - [/files/istio-egress/tcp-keepalive/](/files/istio-egress/tcp-keepalive/) — 검증 스크립트