--- title: Istio ServiceEntry와 Envoy 설정 반영 범위 — sidecar vs gateway, 그리고 DestinationRule date: 2026-07-23 type: guide domain: istio tags: [istio, envoy, service-mesh, service-entry, egress, gateway, xds, istiod, destinationrule] --- > 읽는 법: 각 절은 "이 절이 답하는 질문" → 표(빠른 참조) → **필드/항목별 상세 해설**(메커니즘) → 한 문장 요약 순서임. 표는 색인이고, 실제 이해는 상세 해설에 있음. ## 0. 한눈에 보기 > **용어**: **listener**(트래픽을 받는 입구 설정) · **route**(HTTP 요청을 어디로 보낼지 규칙) · **cluster**(목적지 그룹과 연결 방식) · **endpoint**(목적지의 실제 IP 목록). LDS·RDS·CDS·EDS는 이 넷을 istiod가 Envoy에 내려주는 API(xDS = x Discovery Service)의 이름임. "CDS 반영" = "cluster 설정에 포함됨". 낯선 용어는 [부록 A. 미니 용어집](#부록-a-미니-용어집) 참조. **한 줄 결론**: ServiceEntry(이하 SE) `ports[].protocol`은 sidecar에선 listener 구조까지 결정하지만, gateway에선 listener에 관여하지 않음(Gateway CR 전담). gateway의 cluster(CDS) 반영조차 **조건부** — export 스코프를 통과해야 하고, `PILOT_FILTER_GATEWAY_CLUSTER_CONFIG=true` 환경에선 해당 gateway에 바인딩된 VirtualService(이하 VS) 라우트가 그 host를 참조해야만 push됨. 그리고 cluster의 완성체는 **SE(등록·발견) + DestinationRule(연결 방식)의 합성**임. **빠른 참조 — SE `ports[].protocol`의 영향 범위** | 대상 proxy | LDS (listener) | RDS (route) | CDS (cluster) | 쉬운 말 | |---|---|---|---|---| | sidecar | O — filter chain 결정 (HCM vs tcp_proxy vs SNI) | O — HTTP 계열이면 RDS route 생성 | O | 받는 입구의 구조까지 SE가 정함 | | gateway | X — Gateway CR `servers[]` 전담 | 간접 — VS 라우트 유형(http/tls/tcp) 유효성에 관여 | **조건부** (아래) | 입구는 Gateway CR이 정하고, SE는 목적지 등록만 | **gateway CDS 반영 조건** (모두 충족 필요): 1. SE `exportTo`(또는 meshConfig `defaultServiceExportTo`)가 gateway 네임스페이스를 포함함 2. `PILOT_FILTER_GATEWAY_CLUSTER_CONFIG=false`(기본값)이거나, true라면 해당 gateway에 바인딩된 VS 라우트가 그 host/port를 목적지로 참조함 > **환경 메모 (2026-07 확인)**: 현 환경 istiod는 `PILOT_FILTER_GATEWAY_CLUSTER_CONFIG=true`로 운영 중임. 따라서 **SE만 생성하면 gateway CDS에도 나타나지 않는 것이 정상 동작**이며, Gateway CR + VS 바인딩 시점에 cluster가 push됨. 검증·디버깅 시 이 전제를 기본으로 둘 것. --- ## 1. 배경지식 ### 1.1 istiod와 xDS — 설정이 Envoy에 도달하는 경로 > 이 절이 답하는 질문: "내가 만든 Istio 리소스는 어떤 경로로 실제 proxy 동작이 되는가?" - istiod(control plane)는 K8s 리소스(Service, Endpoints)와 Istio CRD(ServiceEntry, Gateway, VirtualService, DestinationRule 등)를 읽어 **Envoy 설정으로 변환**하고 xDS API로 각 proxy에 push함 - Envoy는 K8s를 모름 — 오직 istiod가 내려주는 xDS 스냅샷대로 동작함. `istioctl proxy-config`는 바로 이 스냅샷(Envoy가 현재 들고 있는 설정)을 조회하는 도구임 — 즉 "리소스를 만들었다"와 "proxy에 반영됐다"는 다른 사건이고, 후자를 확인하는 수단이 proxy-config임 - 중요: istiod는 모든 proxy에게 같은 설정을 주지 않음. **proxy별로 "필요한 설정"을 계산(scoping)해 push**함 — sidecar는 `Sidecar` CRD·exportTo가, gateway는 exportTo·pilot 플래그가 스코프를 결정함. 이 문서의 핵심 주제가 바로 이 스코핑 차이임 | 계층 | 이름 | 결정하는 것 | 쉬운 말 | |---|---|---|---| | LDS | Listener | 트래픽을 **어떻게 받을지** — 바인딩 포트, filter chain(HTTP 처리기 vs L4 프록시, TLS 종료 여부) | 입구 설정 | | RDS | Route | HTTP 요청을 **어디로 보낼지** — host/path 매칭 → cluster 선택 | 규칙표 | | CDS | Cluster | upstream **목적지 집합** — endpoint 발견 방식(DNS 등), upstream 프로토콜(H1/H2), TLS 설정 | 목적지 설정 | | EDS | Endpoint | cluster의 실제 IP 목록 | IP 목록 | 트래픽 관점의 통과 순서는 **listener(입구) → [HTTP라면 route(규칙표)] → cluster(목적지 설정) → endpoint(IP 선택)**임. TCP 경로는 route 없이 listener의 tcp_proxy가 cluster를 직접 가리킴. 한 문장 요약: **리소스 작성 → istiod가 계산·스코핑 → xDS 4계층으로 push → Envoy 동작. proxy-config는 그 결과를 보는 창.** ### 1.2 같은 Envoy, 두 가지 역할 (SIDECAR vs ROUTER) > 이 절이 답하는 질문: "sidecar와 gateway는 뭐가 다르길래 같은 리소스가 다르게 반영되는가?" sidecar와 gateway는 같은 istio-proxy 이미지지만 부트스트랩 시 역할 메타데이터가 다르고, istiod는 이 역할로 **listener 생성 코드 경로 자체를 분기**함. **SIDECAR — 투명 가로채기의 실제 동작** 앱은 proxy의 존재를 모른 채 원래 목적지로 connect()함. 이것이 Envoy에 도달하는 과정: ``` app: connect(203.0.113.10:443) | v iptables OUTPUT chain (istio-init/CNI가 설치) - UID 1337(= Envoy 자신)의 트래픽은 제외 (무한 루프 방지) - 나머지 outbound를 127.0.0.1:15001로 REDIRECT | (커널 conntrack이 원래 목적지를 기억) v Envoy :15001 "virtualOutbound" listener - original_dst listener filter가 getsockopt(SO_ORIGINAL_DST)로 원래 IP:port 복원 | v 복원된 IP:port를 키로 per-service listener/filter chain 매칭 - 매칭 성공: 해당 서비스의 listener 로직 적용 (2.1) - 매칭 실패: outboundTrafficPolicy에 따라 처리 (1.3) ``` 읽는 법: REDIRECT로 포트가 15001로 바뀌어도 **커널이 원래 목적지를 기억**하고, Envoy가 소켓 옵션으로 그걸 되찾아 "어느 서비스로 가려던 트래픽인지"를 판별함. inbound도 같은 원리로 15006에서 처리됨. 이 구조 때문에 sidecar는 **앱이 접속할 수 있는 모든 목적지의 listener를 미리** 갖고 있어야 함 → istiod가 registry(K8s Service + SE) 전체에 대해 outbound listener를 자동 생성하는 이유. **ROUTER — 명시적 reverse proxy** - ingress/egress gateway pod. iptables 가로채기 장치가 없음 — 클라이언트가 gateway의 주소:포트로 **직접** 접속함 - 따라서 "모든 목적지의 listener를 미리" 만들 필요가 없고, **명시적으로 선언한 포트만** listen함 — 선언 수단이 Gateway CR - registry를 순회하며 listener를 만드는 로직이 ROUTER 경로에는 아예 없음 → SE가 gateway listener에 무관한 근본 원인 - 참고: `Sidecar` CRD는 이름대로 SIDECAR에만 적용됨. ROUTER의 스코프 제어 수단은 exportTo와 pilot 플래그임 한 문장 요약: **sidecar는 "모든 목적지를 미리 아는 투명 프록시"(그래서 listener 자동 생성), gateway는 "선언한 것만 여는 명시적 프록시"(그래서 Gateway CR이 listener의 유일한 원천).** ### 1.3 ServiceEntry란 — 그리고 미등록 목적지의 운명 > 이 절이 답하는 질문: "SE는 왜 필요하고, SE 없이 외부로 나가면 무슨 일이 생기는가?" - SE는 mesh 내부 service registry에 **외부(또는 미등록) 서비스를 등록**하는 CRD. 등록되면 K8s Service와 동급의 registry 시민이 되어, sidecar가 해당 host를 "아는 목적지"로 취급해 라우팅·정책·텔레메트리를 적용함 - 주요 필드: `hosts`, `ports[].{number,name,protocol,targetPort}`, `resolution`, `location`, `exportTo`, `addresses`, `endpoints`, `workloadSelector`, `subjectAltNames` — 각 필드의 반영 위치는 2.2에서 전부 다룸 **미등록 목적지의 운명 — outboundTrafficPolicy** registry 매칭에 실패한 outbound는 meshConfig `outboundTrafficPolicy.mode`가 결정함: | 모드 | 동작 | 쉬운 말 | |---|---|---| | `ALLOW_ANY` (기본) | PassthroughCluster(type: ORIGINAL_DST)로 원래 IP에 그대로 연결 | 모르는 목적지도 일단 통과 | | `REGISTRY_ONLY` | BlackHoleCluster로 폐기 — HTTP는 502, TCP는 연결 거부 | 등록 안 된 곳은 차단 | - REGISTRY_ONLY 환경에서 SE는 사실상 **egress 허용 목록(allowlist)** 역할을 함 — 규제·금융 환경에서 표준적인 조합 - 단, 이 통제는 sidecar를 지나는 트래픽에만 유효함. sidecar를 우회하는 프로세스는 막지 못하므로 완전한 차단은 NetworkPolicy·노드 방화벽 계층이 담당해야 함 - 운영 권장: `protocol`은 항상 명시할 것. 미지정 시 port `name` 접두(`http-`, `tcp-` 등)로 추론을 시도하고, 그마저 없으면 환경 설정(protocol sniffing 여부)에 따라 취급이 달라져 비결정적임 — 의도(HTTP 라우팅 vs L4 vs SNI 매칭)를 선언으로 고정해야 이후 메커니즘이 예측 가능해짐 한 문장 요약: **SE는 "이 목적지를 mesh가 알게 하는" 등록 행위이며, REGISTRY_ONLY와 결합하면 egress allowlist가 됨.** --- ## 2. 핵심 메커니즘 ### 2.1 sidecar — protocol이 listener filter chain을 결정 > 이 절이 답하는 질문: "SE의 protocol 값 하나가 왜 트래픽 처리 구조를 바꾸는가?" 먼저 용어 둘: **HCM**(HTTP Connection Manager — Envoy에서 HTTP를 파싱·라우팅하는 filter), **SNI**(Server Name Indication — TLS 접속 시작 시 클라이언트가 평문으로 밝히는 목적지 서버 이름). ``` [SIDECAR outbound path] app ──connect()──> iptables REDIRECT ──> :15001 (virtualOutbound) | original dst IP/port로 per-service listener 매칭 | protocol: HTTP/HTTP2/GRPC | protocol: TCP / TLS +---------------------------+ | +---------------------------+ | listener 0.0.0.0: | | | listener : | | http_connection_manager | | | (TLS: tls_inspector | | └─ RDS route "" | | | reads SNI, no terminate) | | host header match | | | tcp_proxy | +-------------+-------------+ | +-------------+-------------+ v v cluster: outbound||| cluster: outbound||| ``` 읽는 법: 앱의 연결이 가로채져 들어온 뒤, **선언된 protocol에 따라 왼쪽(HTTP 파싱 경로) 또는 오른쪽(L4 통과 경로)의 listener 구조를 탐**. 두 경로 모두 최종적으로 같은 이름 규칙의 cluster에 도달함. **HTTP/HTTP2/GRPC 경로의 상세** - 포트당 **0.0.0.0:port listener 하나를 공유**함. HTTP는 요청 안에 Host 헤더라는 판별자가 있으므로, 같은 포트를 쓰는 수십 개 서비스가 listener 하나 + RDS의 virtual host 다수로 공존 가능함 - HCM이 요청을 파싱하고 RDS route ""를 조회 → Host 헤더가 어느 virtual host의 domains와 일치하는지로 cluster를 선택 - `istioctl proxy-config listeners`의 MATCH 열(예: `Trans: raw_buffer; App: http/1.1 http2`)은 그 filter chain의 **매칭 조건**(전송층/애플리케이션 프로토콜 판별 결과)을 표시한 것임 - **protocol sniffing**: protocol이 선언되지 않은 포트는 Envoy가 첫 바이트를 들여다봐 HTTP 여부를 감지(sniffing)하는 filter chain이 생김. 편리하지만 판별 시점·오판 가능성이 생기므로, 선언으로 고정하는 것이 예측 가능함 **TCP 경로의 상세** - tcp_proxy는 내용을 해석하지 않는 L4 포워딩이므로 **host를 구분할 판별자가 없음**. 그래서 판별자를 주소로 대신함 — `addresses`(VIP)를 지정하면 그 IP(:port)에 매칭되는 filter chain/listener가 분리 생성됨 - `addresses` 미지정 시 0.0.0.0:port 전체가 해당 cluster로 감 → **같은 포트의 다른 TCP 트래픽까지 흡수**하는 함정. 나아가 같은 포트에 addresses 없는 TCP 정의가 여럿이거나 HTTP 정의와 겹치면 병합 규칙에 따라 한쪽이 무시될 수 있음(istiod 로그에 경고, `istioctl analyze`로 일부 탐지) — 동일 포트 다중 정의는 반드시 addresses로 정밀화할 것 **TLS/HTTPS 경로의 상세** - tcp_proxy 앞에 tls_inspector(listener filter)가 붙음. TLS를 **종료하지 않고** ClientHello에서 SNI만 읽어, filter_chain_match의 `server_names`와 대조해 chain을 선택함 - 즉 TLS에선 판별자가 "SNI"가 됨 — hosts에 적은 이름이 그대로 SNI 매칭 값이 됨(와일드카드 포함). SNI가 어느 chain과도 안 맞으면 해당 정의로는 라우팅되지 않음 - 암호문 자체는 그대로 통과하므로 sidecar는 내용을 볼 수 없음 — L7 정책이 필요하면 별도 설계(TLS 종료 지점 이동)가 필요함 한 문장 요약: **protocol은 "이 포트에서 무엇을 판별자로 쓸 것인가"의 선언 — HTTP는 Host 헤더, TLS는 SNI, TCP는 주소(addresses) — 이고 그 선택이 listener 구조를 결정함.** ### 2.2 SE의 나머지 필드는 어디에 반영되나 > 이 절이 답하는 질문: "protocol이 LDS를 정한다면, hosts·resolution·endpoints 같은 나머지 필드는 어디로 가는가?" ``` ServiceEntry field ──> xDS artifact (sidecar 기준) hosts ────────────┬─> CDS: cluster name "outbound|||" ├─> RDS: virtual host domains match (HTTP) └─> LDS: filter_chain_match SNI (TLS) addresses ────────> LDS: TCP listener bind VIP / filter_chain_match dst IP/CIDR ports[].number ───> LDS port + cluster name + RDS vhost port ports[].protocol ─> LDS filter chain kind (2.1) ports[].targetPort> EDS: actual endpoint port (remap) resolution ───────> CDS: cluster type endpoints ────────> EDS: address / weight / locality / labels workloadSelector ─> EDS: select WorkloadEntry/Pod by label location ─────────> mTLS handling / telemetry class (indirect) subjectAltNames ──> CDS: upstream TLS SAN verification list exportTo ─────────> NOT an xDS artifact — push scoping (2.4) ``` 읽는 법: 필드마다 꽂히는 xDS 계층이 다름. **hosts만 유일하게 세 계층에 모두 관여** — "이 서비스를 무엇으로 식별할지"의 원천이기 때문. | 필드 | 반영 위치 | 동작 | 쉬운 말 | |---|---|---|---| | `hosts` | CDS+RDS+LDS | cluster 이름의 host 자리, HTTP면 RDS domains(Host 헤더 매칭), TLS면 SNI 매칭 값 | 이 서비스를 부르는 이름표 | | `addresses` | LDS | TCP에서 host 판별자가 없을 때의 대체 판별자 — VIP/CIDR 매칭 | 이름을 못 볼 때 쓰는 주소표 | | `resolution` | CDS type | 목적지 IP를 찾는 방식 결정 (아래 4종 상세) | 목적지 IP를 누가·어떻게 찾을지 | | `endpoints` | EDS | inline WorkloadEntry — 주소·가중치·locality·라벨 | IP 목록을 직접 손으로 적기 | | `workloadSelector` | EDS | 라벨로 WorkloadEntry/Pod 선택해 endpoint 구성 | IP 목록을 라벨로 자동 수집 | | `location` | 간접 | 내부/외부 취급 — 자동 mTLS 여부·텔레메트리 분류 | 우리 편인지 외부인지 표시 | | `subjectAltNames` | CDS | TLS/mTLS 시작 시 upstream 인증서 SAN 검증 목록 | 상대 인증서에서 확인할 이름 | | `ports[].name` | 메타데이터 | 텔레메트리 라벨 + protocol 미지정 시 추론 근거 | 라벨 + 비상용 힌트 | **필드별 상세 해설** `hosts` — 판별자의 원천이자 cluster 이름의 구성 요소 - Envoy cluster 이름은 `outbound|||` 4요소 규칙임 (subset 자리는 DR이 채움, 2.5). hosts가 이 이름의 마지막 자리를 차지하므로, **모든 계층에서 이 서비스를 지칭하는 공통 키**가 됨 - HTTP: RDS virtual host의 domains에 `host`, `host:port` 형태로 들어가 Host 헤더와 대조됨 - TLS: filter_chain_match의 `server_names`가 되어 SNI와 대조됨. 와일드카드 host(`*.example.com`)는 SNI 와일드카드 매칭으로 동작함 - 와일드카드 사용 시 범위가 넓어져 의도치 않은 트래픽까지 매칭될 수 있음 — 좁은 정의와 겹치면 더 정확한 매칭이 우선하지만, 설계상 최소 범위로 선언하는 것이 안전함 `addresses` — TCP의 판별자, TLS/HTTP에선 보조 - 단일 IP뿐 아니라 CIDR도 허용됨 — filter_chain_match의 destination IP(prefix) 매칭 조건으로 반영됨 - TCP에선 사실상 필수(2.1의 흡수 함정 방지). HTTP에선 Host 헤더가 있으므로 거의 무의미하고, TLS에선 SNI가 1차 판별자이므로 보조적임 - 외부 시스템의 IP가 유동적(GSLB 등)이면 "고정 VIP 집합 열거"라는 전제 자체가 흔들리므로, addresses 의존 설계는 IP 변동 정책을 먼저 확인하고 결정할 것 `resolution` — cluster type을 결정하는 스위치 (4종) | 값 | Envoy cluster type | 동작 | 적합한 상황 | |---|---|---|---| | `DNS` | STRICT_DNS | Envoy가 주기적으로 resolve해 **반환된 모든 A 레코드**를 endpoint로 씀 | 다중 A를 모두 쓰는 일반 외부 도메인 | | `DNS_ROUND_ROBIN` | LOGICAL_DNS | 연결 시점에 **하나의 주소만** 사용, 재해석은 새 연결 때 | GSLB처럼 매번 하나의 A를 회전 반환하는 도메인 | | `STATIC` | EDS(정적) | `endpoints`에 적은 IP 목록을 그대로 사용 | IP가 고정된 내부/외부 시스템 | | `NONE` | ORIGINAL_DST | 앱이 이미 resolve한 원래 목적지 IP로 그대로 전달 (lb: CLUSTER_PROVIDED) | resolve 주체를 앱에 두고 proxy는 통과만 할 때 | - 주의: GSLB가 "가까운 곳 하나"를 골라주는 체계에서 STRICT_DNS를 쓰면 Envoy가 여러 시점의 A를 모아 **자체 로드밸런싱**해버려 GSLB의 의도를 왜곡할 수 있음 — 이 경우 LOGICAL_DNS가 의미상 맞음 - NONE은 "proxy가 목적지를 몰라도 됨"이라는 뜻이라, DNS를 누가 쥐는가(앱 vs mesh)의 설계 결정과 직결됨 `endpoints` / `workloadSelector` — EDS를 채우는 두 가지 방법 (상호 배타) - `endpoints`: inline WorkloadEntry 목록. 각 항목에 `address`, `ports`(포트 재매핑), `weight`(가중 분배), `locality`(지역 기반 우선순위), `labels`를 줄 수 있음 - `workloadSelector`: 목록을 손으로 적는 대신 라벨로 WorkloadEntry(또는 Pod)를 선택해 endpoint 집합을 동적으로 구성 — VM을 mesh에 편입시키는 흐름에서 나온 메커니즘임 - `ports[].targetPort`: "서비스 포트 443으로 부르지만 실제 endpoint는 8443" 같은 재매핑이 EDS 단계에서 일어남 `location` — 신뢰 경계 표시 - `MESH_INTERNAL`: mesh 정체성(SPIFFE ID 기반 워크로드 인증서)을 가진 대상으로 취급 → 자동 mTLS·PeerAuthentication의 대상이 됨 - `MESH_EXTERNAL`: mesh 밖 시스템으로 취급 → **자동 mTLS가 적용되지 않음**. 이 목적지로 TLS를 걸고 싶으면 DR의 `tls`로 명시해야 함(2.5). 텔레메트리에서 내부/외부 분류에도 쓰임 `subjectAltNames` — 나갈 때의 상대 검증 - proxy가 이 목적지로 TLS/mTLS를 **시작(originate)** 할 때, upstream이 제시한 인증서의 SAN이 이 목록과 일치하는지 검증함. 잘못된 대상에 연결되는 것을 인증서 수준에서 차단하는 장치 한 문장 요약(4분류): **hosts/addresses = 무엇으로 식별할지(판별자), protocol = 어떤 구조로 받을지(LDS), resolution/endpoints = 목적지를 어떻게 찾을지(CDS/EDS), location/SAN = 어떤 신뢰 관계로 나갈지.** ### 2.3 gateway — listener는 Gateway CR 전담 > 이 절이 답하는 질문: "왜 SE를 만들어도 gateway 입구(listener)에는 아무 변화가 없는가? 그리고 VS는 어떻게 gateway에 붙는가?" ``` [ROUTER: ingress/egress gateway] Gateway CR ServiceEntry servers[].port.{number,protocol} hosts / ports / resolution | | v v LDS: listener 0.0.0.0: CDS: cluster outbound|p||host filter chain (protocol, endpoints (DNS/STATIC), tls.mode가 결정) upstream protocol (H1/H2) | ^ | VirtualService | +----------> (gateways: 바인딩) -------------+ RDS/route: 매칭 → cluster 선택 ``` 읽는 법: 왼쪽 기둥(입구)은 Gateway CR만이, 오른쪽 기둥(목적지)은 SE가 만들고, 둘을 잇는 다리가 VS임. **SE에서 왼쪽 기둥으로 가는 화살표가 없다는 것**이 이 문서의 출발 질문에 대한 답. **Gateway CR → LDS 상세** - gateway listener는 **오직 `servers[]`에서 생성**됨. `port.protocol`과 `tls.mode` 조합이 filter chain을 결정함(HTTP → HCM / TLS+PASSTHROUGH → SNI 매칭 tcp_proxy / HTTPS+SIMPLE·MUTUAL → TLS 종료 chain — 상세는 4.3) - `servers[].hosts`는 이 server가 받아줄 host의 범위 선언임 — VS 바인딩의 교집합 조건이자, SNI/Host 매칭의 상한 - registry를 순회해 listener를 만드는 로직이 ROUTER 경로에는 없으므로, SE·K8s Service가 몇 개든 gateway LDS는 불변임 **VS가 gateway에 붙는 규칙 (바인딩 3조건)** 1. VS `gateways:`에 해당 Gateway를 지칭 — 같은 네임스페이스면 이름만(`egress-http`), 다르면 `/` 형식. 예약어 `mesh`는 "모든 sidecar"를 뜻하며, `gateways: [mesh, egress-http]`처럼 쓰면 한 VS로 sidecar 구간과 gateway 구간 라우팅을 함께 정의함 2. VS `hosts`와 Gateway `servers[].hosts`의 **교집합이 존재**해야 함 — 교집합이 없으면 라우트가 그 server에 부착되지 않음(증상: NR) 3. 라우트 섹션 유형(`http:`/`tls:`/`tcp:`)이 server의 protocol과 일치해야 함(4.1) - ingress/egress는 배포 위치·용도 차이일 뿐 config 생성 모델은 동일한 ROUTER임 한 문장 요약: **gateway에서 SE가 하던 "입구 결정" 역할은 Gateway CR이 대신하고, VS는 이름·hosts 교집합·유형 일치라는 3조건으로 그 입구에 부착됨.** ### 2.4 gateway CDS는 조건부 — push 스코핑 파이프라인 > 이 절이 답하는 질문: "SE를 만들었는데 gateway의 cluster 목록에조차 없는 건 왜인가?" 배경: 기본 동작에서 gateway는 **자신에게 보이는 모든 서비스의 outbound cluster를 전부** 받음. 서비스가 수천 개인 mesh에선 gateway Envoy의 CDS 크기·메모리·push 시간이 비대해짐. 이를 줄이는 opt-in 최적화가 `PILOT_FILTER_GATEWAY_CLUSTER_CONFIG`임 (기본 false — "gateway에 바인딩된 VS가 참조하는 cluster만 push"). ``` [istiod -> ROUTER(CDS) push pipeline] mesh registry (K8s Service + ServiceEntry) | v (1) visibility filter: exportTo / defaultServiceExportTo | (gateway가 뜬 namespace 기준 평가) v (2) PILOT_FILTER_GATEWAY_CLUSTER_CONFIG | +── false (default): push ALL visible clusters | +── true: keep ONLY clusters referenced by routes of VirtualServices bound to THIS gateway | v CDS push ──> gateway Envoy ``` 읽는 법: registry의 cluster 후보가 **두 개의 관문**을 거쳐야 gateway에 도달함. (1)은 네임스페이스 가시성, (2)는 플래그 켜짐 여부에 따른 "라우트 참조" 요구. - "라우트가 참조한다"의 의미: 해당 gateway에 바인딩된(2.3의 3조건 충족) VS의 `destination.host`(+subset)가 그 서비스를 가리키는 것. subset을 참조하면 해당 subset cluster(2.5)가 push 대상이 됨 - 함의 1: **리소스 적용 순서가 관측 결과를 바꿈** — SE만 먼저 넣고 CDS를 조회하면 "반영 안 됨"으로 보임. SE + Gateway + VS를 세트로 적용한 뒤 확인해야 함 - 함의 2: 이 스코핑은 CDS에 대한 것이며, LDS가 Gateway CR 전담이라는 사실(2.3)과는 **독립된 별개 층위**임 — "listener에 없음"과 "cluster에 없음"은 원인이 다름 - 운영 관점 상세는 4.4 한 문장 요약: **flag=true 환경에서 "CDS에 없음"은 버그가 아니라 아직 아무 라우트도 그 목적지를 원하지 않는다는 뜻일 수 있음.** ### 2.5 DestinationRule — cluster를 완성하는 두 번째 리소스 > 이 절이 답하는 질문: "연결 개수 제한·로드밸런싱·mTLS 설정은 SE의 어느 필드에 쓰는가? → SE가 아니라 DestinationRule(이하 DR)에 씀." SE가 cluster를 **만들고**(존재·IP 찾는 법·upstream 프로토콜), DR은 같은 host에 매칭되어 그 cluster에 **연결 방식을 덧붙임**. DR은 LDS에 전혀 관여하지 않고, 거의 전부 CDS에 반영됨. ``` cluster "outbound|||" base (ServiceEntry) exists, type(resolution), endpoints, H1/H2 | overlay (DestinationRule, host match) ├─ trafficPolicy.loadBalancer ──> lb_policy ├─ connectionPool ─────────────> circuit_breakers thresholds, │ maxConnectionDuration, timeouts ├─ outlierDetection ───────────> outlier_detection ├─ tls (ISTIO_MUTUAL 등) ──────> transport_socket (upstream TLS) └─ subsets ────────────────────> NEW clusters: "outbound|||" ``` 읽는 법: 위쪽이 SE가 만든 기본 cluster, 아래쪽이 DR이 덧씌우는 정책. **subsets만 유일하게 "덧씌우기"가 아니라 "새 cluster 생성"임** (이름의 세 번째 자리가 subset명). | DR 필드 | cluster에 반영되는 것 | 쉬운 말 | |---|---|---| | `loadBalancer` | lb_policy (ROUND_ROBIN, LEAST_REQUEST, consistent hash 등) | IP가 여러 개일 때 누구에게 보낼지 고르는 방식 | | `connectionPool` | circuit_breakers thresholds, 각종 timeout, maxConnectionDuration | 동시에 몇 개까지 연결·요청을 허용할지 (초과분 차단) | | `outlierDetection` | outlier_detection | 계속 실패하는 IP를 잠시 목록에서 빼기 | | `tls` | transport_socket | 이 목적지로 나갈 때의 암호화 방식 | | `subsets` | 파생 cluster 추가 생성 + EDS 라벨 분할 | 라벨(v1/v2 등)로 목적지 그룹을 쪼개기 | **항목별 상세 해설** `tls` — 실무적으로 가장 중요한 패치 - "upstream으로 나갈 때의 transport_socket"을 정하는 필드로, 대표 패턴이 둘 있음: - **mesh 내부 홉의 mTLS**: `mode: ISTIO_MUTUAL` — Istio가 워크로드 인증서(SPIFFE ID)로 자동 mTLS 수행. sidecar→(egress) gateway 홉에 이걸 걸면 홉 전체가 워크로드 정체성 기반이 되어 AuthorizationPolicy로 "어느 워크로드가 이 gateway를 쓸 수 있나"를 통제할 수 있음 - **외부로의 TLS origination**: `mode: SIMPLE`(+ 필요 시 `caCertificates`, `sni`) — 앱은 평문으로 보내고 proxy(주로 gateway)가 외부 구간에서 TLS를 시작함. 이때 SE `subjectAltNames`가 상대 검증에 쓰임(2.2) - MESH_EXTERNAL 목적지는 자동 mTLS 대상이 아니므로(2.2 location), 암호화가 필요하면 **DR로 명시하는 것이 유일한 경로**임 `connectionPool` / `outlierDetection` — 연결 수명과 건강 관리 - connectionPool의 한도(tcp.maxConnections, http.http1MaxPendingRequests 등)는 Envoy circuit_breakers thresholds로 반영되고, 초과분은 즉시 차단됨(503, response flag UO) — "터지기 전에 끊는" 보호 장치 - `tcp.maxConnectionDuration`은 연결의 최대 수명 상한 — 장수 연결이 오래된 endpoint(예: 이미 물러난 GSLB 대상)에 눌러앉는 것을 방지해, **failover 지연의 상한**으로 쓸 수 있음 - outlierDetection은 연속 오류 기준으로 endpoint를 일시 퇴출(passive health check) — LB가 병든 IP를 계속 고르는 것을 막음 `subsets` — 유일한 "생성" 필드 - `subsets[].labels`로 endpoint를 나누면, subset마다 `outbound|||` cluster가 **추가 생성**되고 EDS도 라벨 기준으로 분할됨 - VS의 `destination.subset`이 이 이름의 세 번째 자리를 지정하는 것 — **VS가 subset을 참조하는데 그 subset을 정의한 DR이 없으면 대상 cluster 자체가 존재하지 않아 503(NC)** - subset에도 개별 trafficPolicy를 줄 수 있어(상위 정책 override), 버전별로 다른 연결 정책이 가능함 적용 규칙 — 우선순위·스코핑·선별 - **DR 단독으론 무효과**: cluster를 생성하는 건 registry(Service/SE)이고, DR은 존재하는 cluster에 붙는 정책임. host가 registry에 없으면 매칭 대상이 없음 - **우선순위(병합 아님, 택1)**: 같은 host에 DR이 여럿이면 ① 클라이언트 proxy의 네임스페이스 → ② 서비스(SE)의 네임스페이스 → ③ root namespace(보통 istio-system) 순으로 **하나가 선택**됨. 서로 다른 네임스페이스의 DR이 합쳐지지 않으므로, "일부 필드만 다른 DR을 추가"하는 식의 운영은 의도대로 되지 않음 - **exportTo 스코핑 동일 적용**: DR도 exportTo 대상이라 4.2의 함정이 그대로 성립함 — SE·VS·DR 세 객체의 export 범위 정합을 함께 볼 것 - **workloadSelector로 proxy 선별 가능**: DR에 `workloadSelector`를 주면 라벨이 일치하는 proxy에만 적용됨 — 예: egress gateway pod에만 특정 연결 정책을 걸고 앱 sidecar에는 적용하지 않는 구성 검증: ```bash istioctl proxy-config clusters --fqdn -o json \ | grep -E 'lbPolicy|circuitBreakers|transportSocket|outlierDetection' -A3 ``` 한 문장 요약: **cluster = SE(등록과 발견) + DR(연결을 다루는 방식)의 합성 — 그리고 mesh 홉의 mTLS든 외부 TLS origination이든 "나갈 때의 암호화"는 전부 DR의 tls가 정함.** ### 2.6 리소스별 책임 분담 총정리 | 리소스 | xDS 계층 | 결정하는 것 | sidecar에서 | gateway에서 | 쉬운 말 | |---|---|---|---|---|---| | ServiceEntry | CDS(+EDS) | 목적지 존재·endpoint 해석·upstream 프로토콜 | listener까지 자동 생성됨 | cluster만 (스코핑 통과 시) | 목적지 등록부 | | DestinationRule | CDS | LB·연결 제한·이상 감지·upstream TLS·subset | 동일 | 동일 | 등록된 목적지와의 연결 방식 | | Gateway CR | LDS | 열 포트·filter chain·TLS 종료 여부 | 해당 없음 | listener 생성 | gateway의 입구 설계도 | | VirtualService | RDS | 매칭 규칙 → cluster 선택 | sidecar 라우팅 | gateway 라우팅 + (flag=true 시) **CDS push 트리거** | 입구와 목적지를 잇는 규칙표 | 방향으로 기억: **SE = upstream(밖으로 어떻게) / DR = 그 연결의 다듬기 / Gateway CR = downstream(어떻게 받을지) / VS = 그 사이 매핑(+ 스코핑 트리거)** --- ## 3. 검증 실습 전제: Istio + egress gateway 설치, 테스트용 sleep pod 존재. 출력은 **예시**이며 버전·환경에 따라 상이함 (직접 실행해 확인 요망). flag = `PILOT_FILTER_GATEWAY_CLUSTER_CONFIG`. ### 3.1 ServiceEntry(HTTP) 적용 ```yaml apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: name: httpbin-ext spec: hosts: ["httpbin.org"] location: MESH_EXTERNAL resolution: DNS ports: - number: 80 name: http protocol: HTTP ``` ### 3.2 sidecar listener 확인 — HCM + RDS 형태 ```bash istioctl proxy-config listeners deploy/sleep --port 80 ``` ``` ADDRESSES PORT MATCH DESTINATION 0.0.0.0 80 Trans: raw_buffer; App: http/1.1 http2 Route: 80 ``` → MATCH 열은 filter chain의 매칭 조건(평문 전송 + HTTP 애플리케이션 프로토콜), DESTINATION의 `Route: 80`은 RDS 규칙표를 조회한다는 뜻 ```bash istioctl proxy-config routes deploy/sleep --name 80 ``` ``` NAME VHOST NAME DOMAINS MATCH VIRTUAL SERVICE 80 httpbin.org:80 httpbin.org /* ``` → hosts가 RDS domains(Host 헤더 매칭 값)로 반영된 모습 (2.2의 hosts 상세) ### 3.3 protocol만 TCP로 변경 → listener 구조 변화 ```yaml ports: - number: 80 name: tcp-httpbin protocol: TCP ``` ``` ADDRESSES PORT MATCH DESTINATION 0.0.0.0 80 ALL Cluster: outbound|80||httpbin.org ``` → HCM/RDS가 사라지고 tcp_proxy가 cluster에 직결됨. **protocol 필드 하나로 listener 구조가 바뀜** → `addresses` 미지정 TCP라 MATCH가 `ALL` — 0.0.0.0:80 전체를 이 cluster가 흡수함 (2.1 함정) 변형: `addresses: ["240.0.0.10"]`을 추가하면 매칭이 해당 VIP로 좁혀져 흡수 문제가 사라짐: ``` ADDRESSES PORT MATCH DESTINATION 240.0.0.10 80 ALL Cluster: outbound|80||httpbin.org ``` ### 3.4 gateway 확인 — listener 불변, cluster는 환경에 따라 분기 listener (공통 — SE protocol을 어떻게 바꿔도 동일): ```bash istioctl proxy-config listeners deploy/istio-egressgateway -n istio-system ``` ``` ADDRESSES PORT MATCH DESTINATION 0.0.0.0 15021 ALL Inline Route: /healthz/ready* 0.0.0.0 15090 ALL Inline Route: /stats/prometheus* ``` → 15021/15090은 헬스체크·메트릭용 관리 포트임. Gateway CR 생성 전엔 서비스 listener가 하나도 없음 — SE가 gateway LDS에 무관함의 직접 증거 clusters (환경 분기): ```bash istioctl proxy-config clusters deploy/istio-egressgateway -n istio-system --fqdn httpbin.org ``` - **flag=false (기본)**: ``` SERVICE FQDN PORT SUBSET DIRECTION TYPE DESTINATION RULE httpbin.org 80 - outbound STRICT_DNS ``` → TYPE 열이 STRICT_DNS — SE의 `resolution: DNS`가 cluster type으로 반영된 모습 (2.2) - **flag=true (현 환경)**: **출력 없음이 정상** — VS가 참조하기 전이므로 push 대상이 아님 - 어느 경우든 exportTo가 gateway 네임스페이스를 제외하면 부재함 (4.2) ### 3.5 Gateway CR + VS 적용 → listener 생성, (flag=true면) cluster도 이 시점 등장 ```yaml apiVersion: networking.istio.io/v1 kind: Gateway metadata: name: egress-http namespace: istio-system spec: selector: istio: egressgateway servers: - port: { number: 80, name: http, protocol: HTTP } hosts: ["httpbin.org"] --- apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: httpbin-egress namespace: istio-system spec: hosts: ["httpbin.org"] gateways: ["egress-http"] http: - match: - port: 80 route: - destination: host: httpbin.org port: number: 80 ``` → 바인딩 3조건(2.3) 확인: gateways 이름 일치(같은 ns라 이름만), hosts 교집합(`httpbin.org`), 유형 일치(HTTP server ↔ `http:` 라우트) 재확인: ```bash istioctl proxy-config listeners deploy/istio-egressgateway -n istio-system ``` ``` ADDRESSES PORT MATCH DESTINATION 0.0.0.0 8080 ALL Route: http.8080 0.0.0.0 15021 ALL Inline Route: /healthz/ready* ... ``` → listener 생성됨. 8080인 이유: gateway pod는 비루트 실행이라 Service port 80 → containerPort 8080 매핑이 기본값임 (배포 방식에 따라 상이) ```bash istioctl proxy-config clusters deploy/istio-egressgateway -n istio-system --fqdn httpbin.org ``` → flag=true 환경에서도 **이제 cluster가 나타남** (VS 라우트가 참조하므로 push 대상이 됨) ### 3.6 DR 오버레이 관찰 — 같은 cluster의 전/후 비교 DR 적용 전 cluster의 원본 JSON 일부를 확인: ```bash istioctl proxy-config clusters deploy/sleep --fqdn httpbin.org -o json | head -40 ``` → `circuitBreakers`는 Istio 기본값(사실상 무제한에 가까운 큰 값), `transportSocket` 없음(평문) DR 적용: ```yaml apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: httpbin-ext spec: host: httpbin.org trafficPolicy: connectionPool: tcp: maxConnections: 2 outlierDetection: consecutive5xxErrors: 3 interval: 10s baseEjectionTime: 30s ``` 적용 후 재조회 (예상되는 변화 부분): ```json "circuitBreakers": { "thresholds": [ { "maxConnections": 2, ... } ] }, "outlierDetection": { "consecutive5xx": 3, "interval": "10s", "baseEjectionTime": "30s", ... } ``` → **cluster 이름은 그대로, 내용만 덧씌워짐** — "SE가 만들고 DR이 패치한다"(2.5)의 직접 관찰. listener/route 출력은 전혀 변하지 않음도 함께 확인할 것 (DR은 LDS/RDS 무관) 부하로 한도 초과를 만들면(동시 연결 3개 이상) access log에 response flag `UO`가 찍힘 — 5장 대응표와 연결됨 ### 3.7 환경 진단 커맨드 — "왜 안 보이지?" 판별 절차 ```bash # (a) istiod 플래그 확인 kubectl -n istio-system get deploy istiod \ -o jsonpath='{.spec.template.spec.containers[0].env}' | tr ',' '\n' | grep -i FILTER_GATEWAY # (b) SE의 네임스페이스·exportTo kubectl get serviceentry -A \ -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,EXPORT:.spec.exportTo # (c) mesh 기본 export 정책 kubectl -n istio-system get cm istio -o jsonpath='{.data.mesh}' | grep -i exportto # (d) 동기화 상태 istioctl proxy-status # (e) 필터 없이 원시 재조회 istioctl proxy-config clusters deploy/istio-egressgateway -n istio-system | grep -i ``` ``` cluster not in gateway CDS? ├─ (b)(c) exportTo에 gateway ns 포함? ─ no ─> exportTo 수정 ├─ (a) flag=true? ─ yes ─> VS가 이 gateway에 바인딩되어 │ host를 참조? ─ no ─> VS 적용 │ (바인딩 3조건: 이름·hosts 교집합·유형) └─ 모두 정상인데 부재 ─> (d) 동기화 / istiod 로그 확인 ``` --- ## 4. 심화 — 놓치기 쉬운 것들 ### 4.1 gateway에서 SE protocol의 실제 역할 (listener 무관 ≠ 무의미) - **upstream 프로토콜 결정**: `http2`/`grpc` 선언 시 cluster에 HTTP/2 옵션 부여 → gateway→외부 구간이 H2로 나감. 미선언 시 H1으로 나가 gRPC upstream 연결이 깨짐. 증상이 "listener는 멀쩡한데 gRPC만 실패"로 나타나므로 원인 추적이 어려움 — 프로토콜 세대는 cluster(나가는 쪽) 문제임을 기억할 것 - **VS 라우트 유형 유효성**: 라우트 섹션(`http:`/`tls:`/`tcp:`)은 아무 포트에나 붙는 게 아니라 대상 port protocol과 맞아야 istiod가 라우트를 생성함. 예컨대 SE port가 `TCP`인데 gateway→외부 구간 라우팅에 `http:` 섹션을 쓰면 그 라우트는 만들어지지 않고, 트래픽은 매칭 실패로 유실됨(NR). `istioctl analyze`가 이런 불일치를 일부 잡아줌 ``` Gateway CR ────> LDS : 어떻게 받을지 (downstream) VirtualService > RDS : 어느 cluster로 (유형 일치 필요) ServiceEntry ──> CDS : 어떻게 내보낼지 (upstream, H1/H2) DestinationRule> CDS : 그 연결을 어떻게 다룰지 (LB/제한/TLS) ``` ### 4.2 exportTo 함정 — gateway pod도 자기 네임스페이스 기준으로 평가됨 `exportTo`는 "이 리소스가 **어느 네임스페이스의 proxy에게 보이는가**"를 제어함(`.` = 자기 네임스페이스만, `*` = 전체). 놓치기 쉬운 점: gateway pod도 그냥 하나의 proxy이고, **자신이 떠 있는 네임스페이스 기준으로 visibility가 계산**됨. ``` [ns: team-a] [ns: istio-system] app pod + sidecar egressgateway (router) | | | SE(ns: team-a, exportTo: ".") | same SE: NOT exported here | -> cluster visible, routing OK | -> cluster ABSENT in CDS | | +----- mTLS ----> gateway listener -----+-> route points to missing cluster -> 503 ``` 읽는 법: 같은 SE인데 왼쪽 proxy에겐 보이고 오른쪽 proxy에겐 안 보임 — 보이는 범위가 **proxy가 뜬 네임스페이스** 기준이기 때문. - 증상: mesh→gateway 구간은 정상. gateway access log response flag는 구성에 따라 **NC**(cluster 부재), **NR**(라우트 미생성), cluster는 있으나 연결 실패면 **UF** - 디버깅 함정: sidecar config만 보면 모든 게 정상으로 보임 → 반드시 **gateway proxy의 clusters/routes를 직접 확인**할 것. "어느 proxy의 시점에서 보는가"를 항상 명시하는 습관이 핵심 - 해결: `exportTo: ["*"]`(기본값) 유지 또는 gateway 네임스페이스 명시 포함 - 확장: **VS·DR도 각각 exportTo 스코핑 대상**임. SE만 열어주고 VS/DR을 좁혀두면 다른 단면에서 같은 부류의 결손이 생김 — 세 객체의 export 범위 정합을 한 세트로 점검할 것. visibility를 좁히는 정책(멀티테넌시·규제 환경)일수록 발생 확률이 높음 ### 4.3 gateway filter chain은 Gateway CR protocol + tls.mode가 결정 sidecar에서 SE protocol이 하던 "filter chain 결정"을 gateway에선 Gateway CR이 대신함. TLS passthrough(TLS를 풀지 않고 통과) 예: ```yaml servers: - port: { number: 443, name: tls, protocol: TLS } tls: mode: PASSTHROUGH hosts: ["api.external.com"] ``` ``` listener 0.0.0.0:8443 ├─ tls_inspector (listener filter) │ └─ read SNI from ClientHello (no TLS terminate) └─ filter_chain_match: server_names = [api.external.com] └─ tcp_proxy ──> cluster (external) ``` 읽는 법: 암호화를 풀지 않고도 SNI(평문으로 노출되는 목적지 이름)만 훔쳐봐서 어느 filter chain으로 보낼지 고른 뒤, 바이트를 그대로 흘려보냄. 이 경우 VS도 `tls:` 섹션 + `sniHosts` 매칭을 써야 함(유형 일치, 4.1). mode별 filter chain 요약: | Gateway 구성 | 생성되는 chain | 클라이언트가 보내야 하는 것 | |---|---|---| | `HTTP` | 평문 HCM | 평문 HTTP | | `TLS` + `PASSTHROUGH` | tls_inspector + SNI 매칭 tcp_proxy (미종료) | TLS (SNI 필수) | | `HTTPS` + `SIMPLE`/`MUTUAL` | TLS **종료** + HCM (서버 인증서 필요) | TLS | | `HTTPS` + `ISTIO_MUTUAL` | Istio 워크로드 인증서로 mTLS 종료 + HCM | mesh mTLS (sidecar 홉) | 잘못된 구성 시 증상: | 잘못된 구성 | 결과 | 쉬운 말 | |---|---|---| | Gateway `protocol: HTTPS` + `mode: SIMPLE/MUTUAL` (종료 의도 없음) | TLS **종료** chain(HCM) 생성. cert 미설정 시 listener 미생성/handshake 실패 | 통과시킬 생각이었는데 잠금을 풀려고 시도함 | | SE에만 `TLS` 선언, Gateway는 `HTTP` | 평문 HCM이 TLS ClientHello 수신 → 400/NR + 깨진 바이트 로그. **SE의 TLS 선언은 gateway listener에 무효과** | 입구는 평문을 기대하는데 암호문이 도착함 | ### 4.4 PILOT_FILTER_GATEWAY_CLUSTER_CONFIG 운영 노트 - **켜는 이유**: gateway CDS 비대 완화 — 전체 cluster push에서 라우트 참조분만으로 축소. 효과는 플래그 전후 `istioctl proxy-config clusters deploy/istio-egressgateway -n istio-system | wc -l` 비교로 체감 가능 - **적용 범위**: istiod 전역 env — mesh의 **모든 gateway에 일괄 적용**됨. 특정 gateway만 선별 불가 - **디버깅 멘탈모델 전환**: 이 환경에선 "gateway CDS에 없음"이 버그가 아니라 스코핑 결과일 수 있음 → cluster 부재를 보면 **VS 바인딩 여부부터** 확인하는 습관 필요 (3.7 절차) - **적용 순서 규율**: 신규 외부 목적지 추가 시 SE·Gateway·VS(+필요시 DR)를 세트로 적용하고 CDS 확인까지를 완료 조건으로 삼을 것 - **엣지 케이스 주의**: VS가 아닌 경로로 cluster를 참조하는 구성(EnvoyFilter 직접 참조, 일부 확장 provider의 cluster)은 필터링 추적에서 빠질 수 있음 — 과거 이 플래그의 기본 활성화 논의에서 ext_authz 등 참조 누락이 blocking 이슈였음. 해당 구성 사용 시 gateway CDS에 대상 cluster 존재를 별도 검증 요망 - **버그 이력**: 1.18에서 "플래그 활성 시 gateway에 서비스 누락" 수정 이력 있음(istio/istio#44439) → **버전 업그레이드 시 gateway CDS 회귀 확인**을 체크리스트에 포함할 것 --- ## 5. 증상 → 원인 빠른 대응표 | 증상 | 유력 원인 | 1차 확인 | 쉬운 말 | |---|---|---|---| | gateway CDS에 SE cluster 없음 | flag=true + VS 미바인딩 / exportTo 미포함 | 3.7 (a)(b)(c) | 목적지가 아직 push 자격을 못 얻음 | | 503, flag `NC` | route는 있으나 cluster 부재 | proxy-config clusters, exportTo | 규칙표는 있는데 목적지 설정이 없음 | | 503, flag `NC` (VS가 subset 참조) | subset을 정의한 DR 부재/라벨 미매칭 | DR subsets, cluster 이름의 subset 자리 확인 | 쪼갠 그룹을 참조했는데 쪼개준 DR이 없음 | | 503, flag `NR` | 라우트 미생성 — protocol·라우트 유형 불일치, VS 바인딩 3조건 실패 | `istioctl analyze`, proxy-config routes | 규칙표에 해당 줄이 아예 없음 | | 503, flag `UO` | connectionPool 한도 초과 (circuit breaker 발동) | DR connectionPool, clusters -o json의 circuitBreakers | 연결 허용량을 넘어 차단됨 | | 503, flag `UF` | cluster 존재, upstream 연결 실패 | resolution/DNS, 방화벽·ACL | 목적지는 아는데 실제 연결이 안 됨 | | 400 + 깨진 바이트 로그 | TLS ClientHello를 평문 HTTP listener로 수신 | Gateway `servers[].protocol`/`tls.mode` | 입구가 기대한 형식과 다른 트래픽 도착 | | 502 (REGISTRY_ONLY 환경) | 미등록 목적지 → BlackHoleCluster 차단 | SE 존재 여부, outboundTrafficPolicy | 등록 안 된 목적지라 차단됨 | | gRPC만 실패 (HTTP는 정상) | SE protocol에 http2/grpc 미선언 → upstream H1 | SE `ports[].protocol` | 나가는 프로토콜 세대가 맞지 않음 | | 무관한 TCP 트래픽이 특정 외부 cluster로 흡수 | `addresses` 없는 TCP SE가 0.0.0.0:port 점유 | SE `addresses` 지정 | 주소표 없는 TCP가 포트 전체를 가로챔 | | GSLB 뒤 목적지에 오래된 IP로 계속 연결 | STRICT_DNS의 자체 LB / 장수 연결 | resolution을 LOGICAL_DNS로, DR maxConnectionDuration | 회전 반환 체계와 다중 A 수집이 충돌 | | 설정 변경이 전반적으로 미반영 | push 미동기화 | `istioctl proxy-status` | istiod와 proxy의 대화가 밀림 | --- ## 6. 핵심 요약 1. **sidecar**: registry(K8s Service + SE) 전체에 outbound listener 자동 생성 — SE protocol이 filter chain(HCM vs tcp_proxy vs SNI)을 결정하고, 그 본질은 "판별자 선택"(Host 헤더 / SNI / 주소)임 2. **gateway**: LDS = Gateway CR, RDS = VS, CDS = SE. 단 CDS는 **exportTo와 pilot 플래그 스코핑을 통과해야** push됨 3. **flag=true 환경(현 환경)**: SE + Gateway + VS 세트 적용 → CDS 확인이 기본 절차. CDS 부재는 버그가 아니라 스코핑일 수 있음 4. **cluster = SE(등록·발견) + DR(연결 방식)의 합성** — 연결 제한·LB·mTLS·subset은 전부 DR에 씀. DR 단독으론 무효과, subsets만 cluster를 추가 생성하며, 같은 host의 DR은 병합되지 않고 우선순위로 하나만 선택됨 --- ## 7. 자가 점검 (체화용) 먼저 스스로 답한 뒤 펼쳐서 확인할 것. > [!question]- Q1. SE의 protocol을 HTTP → TCP로 바꾸면 sidecar listener는 구체적으로 어떻게 바뀌는가? > HCM + RDS route 구조가 사라지고 tcp_proxy가 cluster에 직결됨. 판별자가 Host 헤더에서 "주소"로 바뀌는 것이므로, `addresses` 미지정 시 0.0.0.0:port 전체가 그 cluster로 흡수됨. > [!question]- Q2. flag=true 환경에서 SE만 만들고 gateway CDS를 조회하면 결과는? 이유는? 선행 조건 하나는? > 출력 없음이 정상. gateway에 바인딩된 VS 라우트가 그 host를 참조해야 push 대상이 되기 때문. 선행 조건: exportTo가 gateway 네임스페이스를 포함해야 함(스코핑 관문 1). > [!question]- Q3. 외부 목적지로 mTLS를 걸고 동시 연결 수를 제한하려면 어떤 리소스의 어떤 필드를 쓰고, 어느 xDS 계층에 반영되는가? > DestinationRule의 `trafficPolicy.tls`(외부라면 SIMPLE origination 또는 mesh 홉이라면 ISTIO_MUTUAL)와 `connectionPool`. 둘 다 CDS(cluster의 transport_socket, circuit_breakers)에 반영됨. SE에는 이런 필드가 없음 — MESH_EXTERNAL 목적지에 암호화를 명시하는 유일한 경로가 DR의 tls임. --- ## 부록 A. 미니 용어집 | 용어 | 쉬운 말 | |---|---| | xDS | istiod가 Envoy에 설정을 내려주는 API 묶음 (x = L/R/C/E) | | listener / LDS | 트래픽을 받는 입구 설정 / 그걸 내려주는 API | | route / RDS | HTTP 요청을 어디로 보낼지 규칙표 / 그 API | | cluster / CDS | 목적지 그룹과 연결 방식 설정 / 그 API | | endpoint / EDS | 목적지의 실제 IP 목록 / 그 API | | filter chain | listener 안에서 트래픽이 통과하는 처리 단계 묶음 — 매칭 조건에 따라 여러 개 중 하나를 탐 | | HCM | HTTP Connection Manager — HTTP를 파싱·라우팅하는 filter | | tcp_proxy | 내용을 보지 않고 바이트를 그대로 넘기는 L4 filter | | tls_inspector | TLS를 풀지 않고 ClientHello에서 SNI 등만 읽는 listener filter | | SNI | TLS 접속 시작 시 클라이언트가 평문으로 밝히는 목적지 서버 이름 | | protocol sniffing | protocol 미선언 포트에서 첫 바이트를 보고 HTTP 여부를 감지하는 동작 | | downstream / upstream | proxy 기준 트래픽이 들어오는 쪽 / 나가는 쪽 | | VIP | 가상 IP — 실제 서버가 아니라 판별·매칭용으로 부여하는 주소 | | virtualOutbound | sidecar가 가로챈 모든 outbound가 최초 도착하는 15001 listener | | SO_ORIGINAL_DST | REDIRECT되기 전의 원래 목적지 IP:port를 커널에서 되찾는 소켓 옵션 | | outboundTrafficPolicy | 미등록 목적지 처리 정책 — ALLOW_ANY(통과) / REGISTRY_ONLY(차단) | | PassthroughCluster | ALLOW_ANY에서 미등록 목적지를 원래 IP로 통과시키는 기본 cluster | | BlackHoleCluster | REGISTRY_ONLY에서 미등록 목적지를 폐기하는 cluster | | ORIGINAL_DST | "앱이 향하던 원래 IP로 그대로 보내라"는 cluster type (resolution: NONE) | | STRICT_DNS / LOGICAL_DNS | 모든 A 레코드 사용 / 연결 시점 하나만 사용하는 DNS cluster type | | WorkloadEntry | VM 등 비-Pod 워크로드를 registry에 등록하는 단위 — SE endpoints의 실체 | | transport_socket | cluster에서 upstream 연결의 (m)TLS 방식을 정의하는 부분 — DR tls가 채움 | | circuit breaker | 연결·요청 한도를 넘으면 즉시 차단하는 보호 장치 — DR connectionPool이 채움 | | mTLS / ISTIO_MUTUAL | 양방향 TLS / Istio가 워크로드 인증서로 자동 수행하는 mTLS 모드 | | TLS origination | 앱은 평문으로 보내고 proxy가 대신 TLS를 시작하는 패턴 | | PASSTHROUGH | gateway가 TLS를 종료하지 않고 통과시키는 모드 | | response flag | Envoy access log의 실패 사유 코드 — NC(cluster 없음), NR(라우트 없음), UF(upstream 연결 실패), UO(허용량 초과) | | subset | DR이 라벨로 쪼갠 목적지 하위 그룹 — 별도 cluster로 생성됨 | ## 부록 B. 명령어 치트시트 ```bash istioctl proxy-config listeners [-n ns] [--port N] [-o json] istioctl proxy-config routes [-n ns] [--name NAME] istioctl proxy-config clusters [-n ns] [--fqdn HOST] istioctl proxy-config endpoints [--cluster "outbound|80||host"] istioctl proxy-status # istiod<->proxy 동기화 상태 istioctl analyze [-n ns] # 리소스 정합성 사전 점검 kubectl -n istio-system get deploy istiod \ -o jsonpath='{.spec.template.spec.containers[0].env}' # pilot 플래그 확인 kubectl -n istio-system get cm istio -o jsonpath='{.data.mesh}' # meshConfig 확인 ``` ## 부록 C. 참고 자료 - ServiceEntry reference — https://istio.io/latest/docs/reference/config/networking/service-entry/ - DestinationRule reference — https://istio.io/latest/docs/reference/config/networking/destination-rule/ - Gateway reference — https://istio.io/latest/docs/reference/config/networking/gateway/ - Egress gateway task — https://istio.io/latest/docs/tasks/traffic-management/egress/egress-gateway/ - Envoy response flags — https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage - pilot feature flags 정의 (`PILOT_FILTER_GATEWAY_CLUSTER_CONFIG`, 기본 false) — https://pkg.go.dev/istio.io/istio/pilot/pkg/features - istio/istio#29131 (기본 활성화 논의와 blocking 이슈), istio/istio#44439 (1.18 누락 버그 수정) 관련 노트: [ServiceEntry 리스너 매칭 — 다섯 명제와 재현 시나리오](/docs/istio/egress/se-listener-matching-verification/)