Istio ServiceEntry와 Envoy 설정 반영 범위 — sidecar vs gateway, 그리고 DestinationRule
읽는 법: 각 절은 “이 절이 답하는 질문” → 표(빠른 참조) → 필드/항목별 상세 해설(메커니즘) → 한 문장 요약 순서임. 표는 색인이고, 실제 이해는 상세 해설에 있음.
0. 한눈에 보기
용어: listener(트래픽을 받는 입구 설정) · route(HTTP 요청을 어디로 보낼지 규칙) · cluster(목적지 그룹과 연결 방식) · endpoint(목적지의 실제 IP 목록). LDS·RDS·CDS·EDS는 이 넷을 istiod가 Envoy에 내려주는 API(xDS = x Discovery Service)의 이름임. “CDS 반영” = “cluster 설정에 포함됨”. 낯선 용어는 부록 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 반영 조건 (모두 충족 필요):
- SE
exportTo(또는 meshConfigdefaultServiceExportTo)가 gateway 네임스페이스를 포함함 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는
SidecarCRD·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에 무관한 근본 원인
- 참고:
SidecarCRD는 이름대로 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은 항상 명시할 것. 미지정 시 portname접두(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:<port> | | | listener <addr>:<port> |
| http_connection_manager | | | (TLS: tls_inspector |
| └─ RDS route "<port>" | | | reads SNI, no terminate) |
| host header match | | | tcp_proxy |
+-------------+-------------+ | +-------------+-------------+
v v
cluster: outbound|<port>||<host> cluster: outbound|<port>||<host>
읽는 법: 앱의 연결이 가로채져 들어온 뒤, 선언된 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|<port>||<host>"
├─> 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|<port>|<subset>|<host>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:<port> 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조건)
- VS
gateways:에 해당 Gateway를 지칭 — 같은 네임스페이스면 이름만(egress-http), 다르면<ns>/<name>형식. 예약어mesh는 “모든 sidecar"를 뜻하며,gateways: [mesh, egress-http]처럼 쓰면 한 VS로 sidecar 구간과 gateway 구간 라우팅을 함께 정의함 - VS
hosts와 Gatewayservers[].hosts의 교집합이 존재해야 함 — 교집합이 없으면 라우트가 그 server에 부착되지 않음(증상: NR) - 라우트 섹션 유형(
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|<port>||<host>"
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|<port>|<subset>|<host>"
읽는 법: 위쪽이 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를 시작함. 이때 SEsubjectAltNames가 상대 검증에 쓰임(2.2)
- mesh 내부 홉의 mTLS:
- 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|<port>|<subset>|<host>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에는 적용하지 않는 구성
검증:
istioctl proxy-config clusters <pod> --fqdn <host> -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) 적용
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 형태
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 규칙표를 조회한다는 뜻
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 구조 변화
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을 어떻게 바꿔도 동일):
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 (환경 분기):
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도 이 시점 등장
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: 라우트)
재확인:
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 매핑이 기본값임 (배포 방식에 따라 상이)
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 일부를 확인:
istioctl proxy-config clusters deploy/sleep --fqdn httpbin.org -o json | head -40
→ circuitBreakers는 Istio 기본값(사실상 무제한에 가까운 큰 값), transportSocket 없음(평문)
DR 적용:
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
적용 후 재조회 (예상되는 변화 부분):
"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 환경 진단 커맨드 — “왜 안 보이지?” 판별 절차
# (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 <host>
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를 풀지 않고 통과) 예:
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. 핵심 요약
- sidecar: registry(K8s Service + SE) 전체에 outbound listener 자동 생성 — SE protocol이 filter chain(HCM vs tcp_proxy vs SNI)을 결정하고, 그 본질은 “판별자 선택”(Host 헤더 / SNI / 주소)임
- gateway: LDS = Gateway CR, RDS = VS, CDS = SE. 단 CDS는 exportTo와 pilot 플래그 스코핑을 통과해야 push됨
- flag=true 환경(현 환경): SE + Gateway + VS 세트 적용 → CDS 확인이 기본 절차. CDS 부재는 버그가 아니라 스코핑일 수 있음
- cluster = SE(등록·발견) + DR(연결 방식)의 합성 — 연결 제한·LB·mTLS·subset은 전부 DR에 씀. DR 단독으론 무효과, subsets만 cluster를 추가 생성하며, 같은 host의 DR은 병합되지 않고 우선순위로 하나만 선택됨
7. 자가 점검 (체화용)
먼저 스스로 답한 뒤 펼쳐서 확인할 것.
Q1. SE의 protocol을 HTTP → TCP로 바꾸면 sidecar listener는 구체적으로 어떻게 바뀌는가?
HCM + RDS route 구조가 사라지고 tcp_proxy가 cluster에 직결됨. 판별자가 Host 헤더에서 “주소"로 바뀌는 것이므로, addresses 미지정 시 0.0.0.0:port 전체가 그 cluster로 흡수됨.
Q2. flag=true 환경에서 SE만 만들고 gateway CDS를 조회하면 결과는? 이유는? 선행 조건 하나는?
출력 없음이 정상. gateway에 바인딩된 VS 라우트가 그 host를 참조해야 push 대상이 되기 때문. 선행 조건: exportTo가 gateway 네임스페이스를 포함해야 함(스코핑 관문 1).
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. 명령어 치트시트
istioctl proxy-config listeners <pod|deploy/name> [-n ns] [--port N] [-o json]
istioctl proxy-config routes <pod|deploy/name> [-n ns] [--name NAME]
istioctl proxy-config clusters <pod|deploy/name> [-n ns] [--fqdn HOST]
istioctl proxy-config endpoints <pod|deploy/name> [--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 리스너 매칭 — 다섯 명제와 재현 시나리오