--- type: src tags: [istio, service-mesh, envoy, xds, mental-model] created: 2026-06-07 --- # Istio CR 멘탈 모델 — CR은 입력, Envoy config가 진실 (출처: LLM 답변 + Istio 1.30 공식 문서) > [!abstract] 이 문서의 목표 > Istio CR을 외우는 가장 빠른 길은 각 CR을 따로 암기하는 게 아니라, **"CR을 만들면 Envoy 설정의 어느 칸이 채워지는가"** 를 하나의 데이터 흐름으로 이해하는 것. 이 문서는 그 한 축으로 모든 CR을 꿰어, CR을 보면 Envoy에 무엇이 박힐지 자연스럽게 떠오르게 만드는 게 목표. - **대상환경:** Istio 1.30 / Envoy / `networking.istio.io/v1`·`security.istio.io/v1` - **대상독자:** CR은 써봤지만 "이게 내부에서 뭘 바꾸는지" 감이 안 잡히는 DevOps/SRE - **범위:** 트래픽·보안 핵심 6개 CR ↔ 5개 xDS 매핑 + `proxy-config` 검증 루틴 - **선행개념:** Kubernetes Service/Pod, TLS 핸드셰이크(downstream/upstream), Istio 사이드카가 트래픽을 가로챈다는 사실 --- ## 01. 배경 — 왜 CR을 외워도 동작이 안 보이나 Istio를 처음 다룰 때의 좌절은 거의 항상 같은 모양이다. VirtualService·DestinationRule·Gateway·PeerAuthentication을 문서대로 썼는데, **"이게 정확히 무슨 일을 일으키는지"** 가 머릿속에 그려지지 않는다. 그래서 안 되면 디버깅할 방향이 없고, CR마다 필드를 따로 외우다 양에 깔린다. 근본 원인은 한 가지다. **데이터 플레인을 실제로 굴리는 건 Istio가 아니라 Envoy인데, Envoy는 Istio CR이라는 개념 자체를 모른다.** Envoy 입장에서 VirtualService도 DestinationRule도 세상에 존재하지 않는다. Envoy가 아는 건 오직 자기 네이티브 설정 — listener, route, cluster, endpoint, secret뿐이다. 그 사이를 잇는 게 **istiod(컨트롤 플레인)**: CR을 watch하다가 Envoy 네이티브 설정(xDS)으로 **번역**해서 각 프록시에 gRPC로 push한다. ``` 사람이 쓰는 의도 번역기 Envoy가 실제로 따르는 것 (Kubernetes CR) ---> istiod ---> (xDS native config) VirtualService watch route(RDS) DestinationRule translate cluster(CDS) Gateway push(gRPC) listener(LDS) ... endpoint(EDS), secret(SDS) ``` 이 구조를 모르면 CR과 동작이 두 개의 분리된 세계로 보인다. **이 구조를 알면 둘이 하나의 데이터 흐름으로 합쳐진다** — 그게 이 문서가 세우려는 멘탈 모델이고, 디버깅·활용·학습이 전부 같은 한 질문으로 통일되는 이유다. --- ## 02. 핵심 멘탈 모델 — CR은 입력일 뿐, 진실은 Envoy config에 있다 > [!tip] 한 문장 앵커 (이 그림 하나만 머리에 박아라) > **Envoy는 Istio CR을 전혀 모른다.** istiod가 CR들을 읽어 **Envoy 네이티브 설정(xDS)으로 번역**해 각 프록시에 push할 뿐. 그러니 "CR을 어떻게 쓰지?"·"왜 안 되지?"의 답은 항상 **"이 CR이 Envoy의 어느 xDS 칸을 채우는가?"** 로 환원된다. 이 앵커 하나에서 나머지가 전부 따라 나온다. 디버깅이 통일되는 게 그 첫 결실이다 — 증상이 무엇이든 "어느 칸"을 묻는 질문으로 바뀐다. - 라우팅이 이상하다 → "route(RDS)에 뭐가 박혔지?" - 인증서가 안 먹는다 → "secret(SDS)에 로드됐나?" - 트래픽이 엉뚱한 데로 간다 → "cluster(CDS)와 endpoint(EDS)가 맞나?" CR 이름이 아니라 **xDS 칸**으로 생각하면, 처음 보는 CR을 만나도 "얘는 어느 칸을 건드리지?"만 물으면 된다. 그래서 학습 순서도 거꾸로 간다 — **결과물(xDS)을 먼저 보고, 그다음 그걸 만드는 입력(CR)을 본다.** ```mermaid flowchart LR subgraph CR["YAML에 쓰는 것 (CR)"] direction TB SE["ServiceEntry"] GW["Gateway"] VS["VirtualService"] DR["DestinationRule"] PA["PeerAuth / Authz"] end ISTIOD{{"istiod
CR 읽고 번역
→ xDS로 push"}} subgraph XDS["Envoy가 받는 것 (xDS)"] direction TB LDS["LDS — listener"] RDS["RDS — route"] CDS["CDS — cluster"] EDS["EDS — endpoint"] SDS["SDS — secret"] end CR -- "watch" --> ISTIOD -- "push (gRPC)" --> XDS ``` > 왼쪽(CR)은 사람이 쓰는 의도, 오른쪽(xDS)은 Envoy가 실제로 따르는 설정. istiod가 둘을 잇는 번역기다. > [!note] 이 문서의 진행 (멘탈 모델 구축 순서) > 1. **먼저 도착지를 본다** — Envoy의 5가지 설정 칸(xDS)이 무엇인지 (섹션 03) > 2. **그다음 다리를 놓는다** — 어떤 CR이 어느 칸을 채우는지 매핑 (섹션 04) > 3. **칸을 채우는 도구를 하나씩** — CR별 기능·설정·실제 반영 (섹션 05) > 4. **흐름으로 통합** — 요청 하나가 5칸을 어떻게 지나는지 (섹션 06) > 5. **직접 확인** — `istioctl proxy-config`로 진실을 보는 법 (섹션 07) --- ## 03. 도착지 먼저 — Envoy의 5가지 설정 (xDS) CR을 이해하려면 "CR이 만들어 내는 결과물"부터 알아야 한다. 그 결과물이 xDS다. xDS는 "x Discovery Service"의 묶음으로, Envoy가 동적으로 설정을 받아오는 API 종류들이다. Envoy는 이 설정만 보고 동작하지 Istio CR은 모른다. 5칸을 따로 외우려 하면 안 외워진다. **요청 하나가 Envoy를 통과하는 순서대로** 보면 자연스럽게 묶인다: **들어와서(LDS) → 어디로 보낼지 정하고(RDS) → 대상 그룹을 고르고(CDS) → 실제 IP를 찾고(EDS) → 필요하면 인증서를 꺼낸다(SDS).** 이 순서가 곧 섹션 06의 요청 흐름이자 디버깅 순서가 된다. | xDS | 칸 이름 | 무엇을 담나 (요청 처리 순서대로) | |---|---|---| | **LDS** | listener | Envoy가 **어떤 포트로 받고**, 그 포트에 **어떤 필터 체인**(TLS 종단/통과, HTTP 파싱, RBAC 등)을 걸지. "입구"를 정의 | | **RDS** | route | HTTP 요청을 **어느 cluster로 보낼지** 규칙(호스트·경로·헤더 매칭, 가중치, 재시도, 타임아웃). "길 안내" | | **CDS** | cluster | 보낼 **대상 그룹(upstream)** 정의 + 그 그룹에 대한 정책(로드밸런싱, 커넥션 풀, 서킷 브레이커, **upstream TLS**). "목적지 묶음" | | **EDS** | endpoint | 각 cluster의 **실제 Pod IP 목록**. Pod가 뜨고 죽을 때마다 가장 자주 갱신되는 칸 | | **SDS** | secret | TLS **인증서·키**를 동적으로 전달(mTLS 인증서, gateway용 cert). Envoy가 디스크 대신 SDS로 받음 | > [!warning] 전제 교정 — listener와 cluster의 TLS는 다른 TLS다 > 초보가 가장 헷갈리는 지점이자 이 문서 전체에서 반복될 구분: **LDS(listener)의 TLS = "내가 받는(downstream) 연결"**, **CDS(cluster)의 TLS = "내가 맺는(upstream) 연결"**. 같은 게이트웨이 Pod에서 이 둘은 완전히 독립이라, "들어오는 건 passthrough(listener), 나가는 건 mTLS(cluster)"처럼 따로 설정된다. egress 문서에서 "Gateway server tls.mode와 DestinationRule tls.mode가 다르다"고 한 게 바로 이 LDS/CDS 구분이다. --- ## 04. 다리 놓기 — 어떤 CR이 어느 xDS를 채우나 이제 CR과 xDS를 연결한다. 이 표 하나가 이 문서의 척추다. | CR | 주로 채우는 xDS 칸 | 한 줄 의미 | |---|---|---| | **Gateway** | LDS (listener) | 게이트웨이가 **어떤 포트로 받을지** + 그 포트의 TLS 모드(종단/통과) | | **VirtualService** | RDS (route) | 매칭된 트래픽을 **어느 cluster로** 보낼지 규칙 | | **DestinationRule** | CDS (cluster) | 그 대상 그룹의 **정책**: subset, 로드밸런싱, 커넥션풀, **upstream TLS** | | **ServiceEntry** | CDS + EDS | **외부 서비스**를 레지스트리에 등록 → cluster + endpoint 생성 | | **PeerAuthentication** | LDS filter chain | 받는 연결에 **mTLS 요구**(STRICT/PERMISSIVE)를 listener 필터로 주입 | | **AuthorizationPolicy** | LDS filter chain | listener에 **RBAC 필터** 추가(허용/거부 규칙) | | (인증서 일반) | SDS (secret) | mTLS·gateway 인증서가 SDS로 전달됨(특정 CR이 참조) | 표를 외우는 게 아니라 **여기서 두 가지 설계 패턴을 읽어내는 게** 핵심이다. 이 두 패턴이 "CR이 왜 이렇게 나뉘어 있나"를 설명한다. 1. **"라우팅(VirtualService=RDS)"과 "대상 정책(DestinationRule=CDS)"은 항상 분리**돼 있다 — 관심사 분리의 결과다. VirtualService가 "어디로 보낼지"라면 DestinationRule은 "거기로 보낼 때의 규칙". route(RDS)와 cluster(CDS)가 Envoy에서 원래 다른 칸이기 때문에 CR도 둘로 갈린 것. 그래서 둘은 거의 짝으로 다닌다(한쪽이 subset 이름을 만들면 다른 쪽이 그 이름을 가리킨다). 2. **보안 CR(PeerAuthentication·AuthorizationPolicy)은 새 칸을 만들지 않는다.** 기존 listener(LDS)에 **필터를 끼워 넣는다.** 트래픽 CR이 "어느 칸을 채우나"라면 보안 CR은 "기존 칸의 필터 체인 안에서" 동작한다. 그래서 보안 설정은 cluster/route를 바꾸지 않고 입구의 검문만 강화한다. > [!info] What you might be missing > 이 매핑은 "주로"이지 1:1 고정이 아니다. 실제로는 **여러 CR이 합쳐져 하나의 xDS가 만들어진다** — 한 cluster(CDS)에는 Kubernetes Service + DestinationRule subset + ServiceEntry가 모두 기여할 수 있다. 또 VirtualService도 listener(LDS)에 영향을 줄 때가 있고(TLS 라우팅의 SNI 매칭은 listener filter chain match에 반영), Gateway도 route 생성에 관여한다. 그래서 "CR→xDS"는 학습용 1차 근사이고, 정확한 진실은 항상 `proxy-config`(섹션 07)로 확인해야 한다. 멘탈 모델은 빠른 추론용, config 덤프는 검증용 — 이 둘을 같이 써야 한다. --- ## 05. CR 하나씩 — 기능 · 설정 · 실제 반영 각 CR을 같은 틀로 본다: **무엇을 위한 것인가 → 핵심 필드 → Envoy에 어떻게 박히나 → 확인 명령.** 순서는 요청 처리 흐름(입구→길→대상)을 따른다 — 섹션 03의 5칸 순서 그대로다. ### Gateway → LDS **기능:** 게이트웨이 Envoy(ingress/egress)가 **어떤 포트·프로토콜로 트래픽을 받을지**와 그 포트의 TLS 처리(종단/통과)를 정의. 사이드카가 아니라 **게이트웨이 Pod의 입구(listener)** 를 만든다는 게 핵심. ```yaml spec: selector: { istio: egressgateway } # 어느 게이트웨이 Pod에 적용할지 (label) servers: - port: { number: 443, name: tls, protocol: TLS } # 받을 포트/프로토콜 → listener hosts: [api.partner.example.com] # 이 listener가 받을 호스트 tls: { mode: PASSTHROUGH } # downstream TLS 처리 (LDS의 TLS) ``` **Envoy 반영:** `servers[].port`가 LDS의 listener로, `tls.mode`가 그 listener의 TLS 컨텍스트(종단할지 통과할지)로 박힌다. `selector`로 고른 Pod에만 push됨. ```bash istioctl proxy-config listener deploy/istio-egressgateway -n istio-system # PORT 443에 해당 listener가 생겼는지, TLS 모드가 맞는지 확인 ``` ### VirtualService → RDS **기능:** "이 호스트로 온 트래픽을 **어떤 조건으로 어디로 보낼지**" 규칙. Kubernetes Service의 단순 라우팅을 세밀하게(헤더·경로·가중치·재시도·타임아웃) 대체. ```yaml spec: hosts: [reviews] # 이 규칙이 적용될 대상 호스트 http: # L7(HTTP) 라우팅 — tls:/tcp: 도 가능 - match: [{ headers: { x-canary: { exact: "true" } } }] # 매칭 조건 route: - destination: { host: reviews, subset: v2 } # 어느 cluster(subset)로 - route: - destination: { host: reviews, subset: v1 } weight: 90 # 가중치 분배 - destination: { host: reviews, subset: v2 } weight: 10 ``` **Envoy 반영:** `match`+`route`가 RDS의 route 규칙으로 번역됨. 가중치는 route의 weighted_clusters로, `subset`은 대상 CDS cluster 이름으로 연결된다(그 cluster는 DestinationRule이 만듦). `http` 대신 `tls`+`sniHosts`를 쓰면 L4 SNI 라우팅이 되고, 이때는 route가 아니라 listener filter chain match에도 영향을 준다. ```bash istioctl proxy-config route deploy/myapp --name 80 -o json # match 조건과 weighted_clusters, 대상 cluster 이름 확인 ``` ### DestinationRule → CDS **기능:** 특정 호스트로 **보낼 때 적용할 정책.** ① subset 정의(라벨로 v1/v2 등 그룹 분할), ② 로드밸런싱, ③ 커넥션 풀·서킷 브레이커, ④ **upstream TLS**(외부로 나갈 때 TLS를 어떻게 맺을지). VirtualService가 "어디로"라면 DestinationRule은 "어떻게". ```yaml spec: host: reviews # 정책 대상 호스트 trafficPolicy: loadBalancer: { simple: ROUND_ROBIN } connectionPool: # 커넥션 풀/서킷브레이커 tcp: { maxConnections: 100 } http: { http2MaxRequests: 1000 } tls: { mode: ISTIO_MUTUAL } # upstream TLS (CDS의 TLS) — 내가 맺는 연결 subsets: # 라벨로 cluster를 분할 - name: v1 labels: { version: v1 } - name: v2 labels: { version: v2 } ``` **Envoy 반영:** 각 `subset`이 별도의 CDS cluster로 생성됨. 실제 cluster 이름은 `direction|port|subset|fqdn` 규칙을 따른다 — 예: `outbound|9080|v1|reviews.default.svc.cluster.local`, `outbound|9080|v2|reviews.default.svc.cluster.local` (정확한 이름 규칙은 [Cluster 해부](xds__src-cluster-anatomy.html) 참조). `trafficPolicy.tls`는 그 cluster의 transportSocket(upstream TLS)으로, `connectionPool`은 cluster의 커넥션 설정으로 박힌다. **subset을 만들었으면 VirtualService가 그 이름을 가리켜야** 실제로 쓰인다(짝으로 다니는 이유). ```bash istioctl proxy-config cluster deploy/myapp --fqdn reviews.default.svc.cluster.local # 정상: SUBSET 칼럼에 v1/v2가 각각 한 줄씩, DESTINATION RULE 칼럼에 해당 DR 이름이 떠야 한다 istioctl proxy-config cluster ... -o json | grep -A6 transportSocket # upstream TLS ``` ### ServiceEntry → CDS + EDS **기능:** 메시 **밖의 서비스**(외부 API, 레거시 VM 등)를 Istio 레지스트리에 등록. 등록되면 그 외부 호스트도 메시 내부 서비스처럼 cluster·endpoint를 갖게 되어 라우팅·정책 대상이 된다. ```yaml spec: hosts: [api.partner.example.com] ports: - { number: 443, name: tls, protocol: TLS } location: MESH_EXTERNAL resolution: DNS # 호스트→IP 해석 방식: DNS / STATIC / NONE # resolution: STATIC 이면 endpoints: 로 IP 직접 명시 ``` **Envoy 반영:** 외부 호스트용 CDS cluster가 생기고, `resolution`에 따라 EDS endpoint가 채워짐(DNS면 istiod/Envoy가 해석, STATIC이면 명시 IP, NONE이면 원래 목적지 IP). **ServiceEntry가 없으면** `REGISTRY_ONLY` 환경에서 그 외부는 BlackHole로 막힌다. ```bash istioctl proxy-config cluster deploy/myapp --fqdn api.partner.example.com istioctl proxy-config endpoint deploy/myapp | grep partner # 정상: outbound|443||api.partner.example.com cluster에 해석된 IP:443 ENDPOINT가 한 줄 이상 떠야 한다 (DNS resolution) ``` ### PeerAuthentication → LDS filter chain **기능:** 워크로드가 **받는(inbound) 연결에 mTLS를 요구할지**를 정함. STRICT(반드시 mTLS), PERMISSIVE(mTLS·평문 둘 다 수용), DISABLE(평문). 명시적 PeerAuthentication이 없으면 메시는 PERMISSIVE로 동작하며, 보내는 쪽이 가능하면 auto-mTLS로 mTLS를 우선 맺는다(단 install profile/메시 정책에 따라 달라질 수 있음). 받는 쪽 정책이라는 게 핵심 — 보내는 쪽 TLS는 DestinationRule이다(자세한 LDS/CDS TLS 구분은 섹션 03 warning 참조). ```yaml spec: selector: { matchLabels: { app: reviews } } # 없으면 네임스페이스 전체 mtls: { mode: STRICT } # STRICT / PERMISSIVE / DISABLE ``` **Envoy 반영:** 대상 워크로드의 inbound LDS listener filter chain에 mTLS 요구가 주입됨. STRICT면 평문 연결이 listener 단에서 거부된다. 새 cluster/route를 만드는 게 아니라 **기존 입구의 필터를 바꾸는** 것. ```bash istioctl proxy-config listener deploy/reviews --port 15006 -o json | grep -B2 -A5 transport_socket # inbound는 15006(inbound capture 포트)으로 캡처된다. mTLS는 HTTP 필터가 아니라 # filterChainMatch+transport_socket 레벨이라 --type HTTP로는 안 보일 수 있다. # 정상: STRICT면 filter chain의 transport_socket에 tls_context(또는 require_client_certificate)가 박혀 있어야 한다 ``` ### AuthorizationPolicy → LDS filter chain (RBAC) **기능:** "누가 누구에게 무엇을 할 수 있는지" 허용/거부 규칙. source(어느 워크로드/네임스페이스/principal)와 operation(메서드·경로)을 조건으로 ALLOW/DENY를 정함. ```yaml spec: selector: { matchLabels: { app: reviews } } action: ALLOW # ALLOW / DENY / AUDIT / CUSTOM rules: - from: [{ source: { principals: ["cluster.local/ns/default/sa/ratings"] } }] to: [{ operation: { methods: ["GET"], paths: ["/reviews/*"] } }] ``` **Envoy 반영:** 대상 listener filter chain에 **RBAC 필터**가 추가됨(LDS). principal 매칭은 mTLS로 검증된 신원(SPIFFE)을 쓰므로 **PeerAuthentication mTLS가 사실상 전제** — 평문이면 principal을 신뢰할 수 없으니까. ```bash istioctl proxy-config listener deploy/reviews -o json | grep -i rbac # 정상: envoy.filters.http.rbac (또는 network rbac) 필터 이름이 한 줄 이상, # action에 따라 shadow_rules/rules 안에 위 principals·operation 규칙이 떠야 한다 ``` > [!info] What you might be missing > - **"받는 TLS(PeerAuthentication=LDS)"와 "맺는 TLS(DestinationRule=CDS)"는 짝을 맞춰야 한다**(이 LDS/CDS 구분 자체는 섹션 03 warning이 정본). 받는 쪽을 STRICT로 했는데 보내는 쪽이 mTLS를 안 맺으면 연결이 깨진다. Auto mTLS가 대개 자동으로 맞춰주지만, DestinationRule에서 TLS를 명시하면 그 자동 조정이 꺼질 수 있어 충돌이 난다. > - **AuthorizationPolicy는 평가 순서가 있다**(CUSTOM → DENY → ALLOW). DENY가 ALLOW보다 먼저라, "허용했는데 막힌다"면 다른 DENY 정책을 의심해야 한다. > - **이 6개가 전부가 아니다** — Sidecar(워크로드별 설정 범위 축소, xDS push 양 감소), EnvoyFilter(xDS를 직접 패치하는 최후 수단), WorkloadEntry/WorkloadGroup(VM 통합), Telemetry, RequestAuthentication(JWT) 등이 더 있다. 하지만 위 6개 + xDS 매핑을 잡으면 나머지는 "얘는 어느 xDS 칸을 건드리지?"로 같은 틀에서 흡수된다. --- ## 06. 흐름으로 통합 — 요청 하나가 5개 xDS를 통과하는 길 멘탈 모델이 진짜로 굳는 건 "개별 CR"이 아니라 "요청이 xDS 칸들을 순서대로 지나는 그림"을 그릴 수 있을 때다. 여기가 앵커가 한 바퀴 도는 지점이다 — 섹션 03에서 본 5칸 순서가 실제 요청 한 건에서 어떻게 작동하는지 본다. 메시 내부 호출(reviews → ratings, 카나리 라우팅 + mTLS) 하나를 따라간다. ```mermaid flowchart TB subgraph FLOW["요청 처리 순서"] direction LR L["1. LDS
listener 수신
+ mTLS 검증"] --> R["2. RDS
route 매칭
→ subset 선택"] R --> C["3. CDS
cluster 확정
+ LB/풀 정책"] C --> E["4. EDS
실제 Pod IP
선택"] E --> S["5. SDS
mTLS 인증서로
upstream 연결"] end L -. 채움 .-> PA2["PeerAuthentication
(게이트웨이면 +Gateway)"] R -. 채움 .-> VS2["VirtualService"] C -. 채움 .-> DR2["DestinationRule
(외부면 ServiceEntry)"] E -. 채움 .-> SVC2["Service/Endpoints
(또는 ServiceEntry)"] S -. 채움 .-> DR3["DR tls + 인증서"] L -. inbound 인증서 .-> S ``` > 요청은 LDS→RDS→CDS→EDS→SDS 순으로 흐르고, 각 칸은 위에서 본 CR들이 채운다. 디버깅은 "몇 번 칸에서 끊겼나"를 찾는 일이다. 단 **SDS(인증서)는 순서상 마지막 칸이 아니다** — inbound mTLS 검증(1번 LDS)과 outbound 핸드셰이크(5번)에 모두 쓰여 양쪽에 걸친다(LDS→SDS 점선). 말로 풀면 이렇게 흐른다. 1. reviews의 사이드카가 ratings로 가는 요청을 잡고, **받는 쪽(ratings) listener**는 `PeerAuthentication`이 STRICT면 mTLS를 검증한다 (**LDS**). 2. 보내는 쪽 route 규칙(`VirtualService`)이 헤더를 보고 v2 subset을 고른다 (**RDS**). 3. 그 subset에 해당하는 cluster(`DestinationRule`이 만든 `outbound|8080|v2|ratings.default.svc.cluster.local` 같은 `direction|port|subset|fqdn` cluster)가 확정되고 로드밸런싱·커넥션풀 정책이 적용된다 (**CDS**). 4. 그 cluster의 실제 Pod IP를 EDS가 고른다 (**EDS**). 5. upstream 연결을 맺을 때 DestinationRule의 `tls` 설정대로 mTLS 인증서(**SDS**)를 써서 암호화한다. > [!tip] 이 흐름이 주는 디버깅 루틴 > 장애가 나면 **이 5칸을 순서대로 짚으면 된다.** > - "연결이 거부됨" → **LDS** (mTLS 미스매치?) > - "엉뚱한 버전으로 감" → **RDS** (route 규칙) > - "cluster를 못 찾음(NC)" → **CDS** (subset/DR 누락) > - "healthy upstream 없음(UH)" → **EDS** (endpoint 0) > - "핸드셰이크 실패" → **SDS** (인증서) > > CR 이름이 아니라 **xDS 칸 번호**로 생각하는 습관이 핵심. --- ## 07. 진실 확인 — proxy-config로 Envoy를 직접 들여다보기 (worked example) 멘탈 모델의 마지막 조각이자 가장 중요한 습관: **"내가 쓴 CR이 Envoy에 실제로 어떻게 박혔는지"를 항상 config로 확인**하는 것. 앵커가 "진실은 Envoy config에 있다"고 했으니, 그 진실을 직접 여는 창이 `istioctl proxy-config`다 — istiod가 그 프록시에 push한 실제 xDS를 그대로 보여준다. | 명령 | 보는 xDS | 언제 쓰나 | |---|---|---| | `proxy-config listener` | LDS | 포트가 열렸나, TLS 모드·필터(mTLS/RBAC)가 박혔나 | | `proxy-config route` | RDS | VirtualService 매칭·가중치·대상 cluster가 맞나 | | `proxy-config cluster` | CDS | subset cluster 생성, upstream TLS, 커넥션풀 확인 | | `proxy-config endpoint` | EDS | 실제 IP가 채워졌나(0이면 UH의 원인) | | `proxy-config secret` | SDS | 인증서가 로드됐나(핸드셰이크 실패 1차 점검) | | `proxy-status` | (전체) | 프록시가 istiod와 sync 됐나(SYNCED/STALE/NOT SENT) | 5칸을 순서대로 짚는 한 세션은 이렇게 흐른다 — 섹션 06의 요청 흐름과 같은 순서로 config를 연다. ```bash # 0) 정적 검증 먼저 — CR끼리 충돌/오류 잡기 istioctl analyze -n my-namespace # 1) 프록시가 최신 config를 받았나 istioctl proxy-status # 대상 Pod가 SYNCED인지 # 2) 내 CR이 만든 xDS를 순서대로 확인 istioctl proxy-config listener deploy/myapp -n my-namespace # Gateway/PeerAuth istioctl proxy-config route deploy/myapp -n my-namespace --name 80 # VirtualService istioctl proxy-config cluster deploy/myapp -n my-namespace --fqdn reviews.default.svc.cluster.local # DR/SE istioctl proxy-config endpoint deploy/myapp -n my-namespace # EDS istioctl proxy-config secret deploy/myapp -n my-namespace # SDS # 3) 더 깊게 — raw Envoy config 전체 덤프 istioctl proxy-config all deploy/myapp -n my-namespace -o json | less ``` `proxy-config cluster`를 실제로 돌리면 이런 표가 떠야 한다 — DestinationRule의 subset v1/v2가 각각 한 줄의 cluster로 박힌 걸 눈으로 확인하는 순간이 멘탈 모델이 닫히는 지점이다. ``` SERVICE FQDN PORT SUBSET DIRECTION DESTINATION RULE reviews.default.svc.cluster.local 9080 v1 outbound reviews.default reviews.default.svc.cluster.local 9080 v2 outbound reviews.default reviews.default.svc.cluster.local 9080 - outbound reviews.default ``` SUBSET 칼럼의 v1/v2가 곧 cluster 이름 `outbound|9080|v1|reviews...`의 그 subset 칸이고, **DESTINATION RULE 칼럼이 "이 cluster를 누가 만들었나"** 를 역으로 알려준다. **핵심은 `-o json`이다.** 요약 테이블로 안 보이는 세부(transportSocket의 TLS 모드, route의 정확한 match, RBAC 규칙)는 JSON으로 봐야 한다. "CR을 이렇게 썼는데 왜 안 되지?"의 답은 거의 항상 이 JSON 안에 있다 — 내가 의도한 값과 실제 박힌 값의 차이를 눈으로 대조하는 게 가장 빠른 학습이자 디버깅. > [!note] config_dump의 출처 표시 읽는 법 > `proxy-config`의 cluster 출력에는 **DESTINATION RULE** 칼럼이, route 출력에는 **VIRTUAL SERVICE** 칼럼이 있다 — 그 xDS 항목을 *어느 CR이 만들었는지* 역추적할 수 있다. 이게 "CR↔xDS" 멘탈 모델을 실전에서 닫는 고리: 의심나는 cluster를 보고 "아, 이건 저 DestinationRule이 만들었구나"를 즉시 연결할 수 있다. > [!info] What you might be missing > - `proxy-config`는 **그 프록시가 받은 설정**이지 다른 프록시 것이 아니다 — 같은 CR도 워크로드마다 다르게 박힐 수 있어서(Sidecar 리소스, 네임스페이스 scope), 문제가 난 바로 그 Pod를 지정해야 한다. > - config는 맞는데 트래픽이 안 되면 문제는 xDS 밖(노드 라우팅·방화벽·conntrack)이라, 그땐 Pod 안에서 `openssl s_client`·`ss`로 봐야 한다. > - **STALE 상태**는 Envoy가 그 설정을 거부(NACK)했다는 신호 — 컨트롤/데이터 플레인 버전 불일치나 잘못된 CR 조합이 원인이고, `istiod` 로그에서 rejected 메시지를 확인해야 한다. > - `EnvoyFilter`로 직접 패치한 항목은 `proxy-config`에 나타나지만 어느 CR이 만들었는지 추적이 어려우니, EnvoyFilter는 최후 수단으로만 쓰고 반드시 주석으로 의도를 남길 것. --- ## 정리 — 멘탈 모델 한 컷 CR은 **사람이 쓰는 의도**, xDS는 **Envoy가 따르는 진실**, istiod는 그 사이의 **번역기**. CR을 보면 "어느 xDS 칸을 채우나"를 묻고, 안 되면 "몇 번 칸에서 끊겼나"를 `proxy-config`로 확인한다 — 이 한 루프가 학습·활용·디버깅을 전부 덮는다. ## 핵심 정리 - **Envoy는 Istio CR을 모른다.** istiod가 CR을 **xDS(LDS/RDS/CDS/EDS/SDS)** 로 번역해 push하므로, 모든 CR 질문은 "이 CR이 어느 xDS 칸을 채우나"로 환원된다. - **매핑의 척추:** Gateway→listener, VirtualService→route, DestinationRule→cluster, ServiceEntry→cluster+endpoint, PeerAuth/Authz→listener 필터. 라우팅(VS)과 대상 정책(DR)은 관심사 분리로 항상 갈려 **짝으로** 다닌다. - **보안 CR은 새 칸을 안 만든다** — 기존 listener 필터 체인에 mTLS 요구/RBAC를 끼워 넣는다. - **TLS는 두 종류다:** listener(LDS) = 받는(downstream), cluster(CDS) = 맺는(upstream). 이 구분이 안 되면 mTLS 디버깅이 영원히 꼬인다. - **요청은 LDS→RDS→CDS→EDS→SDS** 순으로 흐른다(SDS만 inbound·outbound 양쪽). 디버깅은 "몇 번 칸에서 끊겼나"다. - **멘탈 모델은 추론용, `istioctl proxy-config -o json`은 검증용** — 둘을 같이 써야 의도와 실제의 차이가 보인다. ### CR → xDS → 확인 명령 치트시트 | CR | 채우는 xDS | 핵심 필드 | 확인 명령 | |---|---|---|---| | **Gateway** | LDS | `servers[].port`, `tls.mode` | `proxy-config listener` | | **VirtualService** | RDS | `http/tls.match`, `route.weight` | `proxy-config route` | | **DestinationRule** | CDS | `subsets`, `trafficPolicy.tls` | `proxy-config cluster` | | **ServiceEntry** | CDS+EDS | `hosts`, `resolution` | `proxy-config cluster/endpoint` | | **PeerAuthentication** | LDS | `mtls.mode` | `proxy-config listener -o json` | | **AuthorizationPolicy** | LDS | `action`, `rules` | `proxy-config listener \| grep rbac` | ### 다음 단계 (홈랩 검증 루틴) 이 멘탈 모델을 체득하는 가장 빠른 길은 홈랩 클러스터에서 **CR 하나 만들 때마다 proxy-config로 어느 xDS가 바뀌는지 직접 보는 것**이다. 1. DestinationRule에 subset 2개 추가 → `proxy-config cluster`에 cluster 2개 늘어나는지 2. VirtualService 가중치 변경 → `proxy-config route -o json`의 weighted_clusters 숫자가 바뀌는지 3. PeerAuthentication STRICT 적용 → `proxy-config listener`에 mTLS 요구가 생기고 평문 curl이 거부되는지 이 "CR 수정 → config diff 확인" 루프를 5~6번 돌리면 "CR을 보면 Envoy가 떠오르는" 상태가 된다. --- ## What you might be missing - **이 매핑은 1차 근사다.** 실제 xDS 한 칸에는 여러 CR이 합쳐지고(Service + DR + ServiceEntry → 한 cluster), VirtualService가 listener에 영향 줄 때도 있다. 빠른 추론에는 매핑을, 정확한 진실에는 `proxy-config -o json`을 쓴다 — 둘은 대체재가 아니라 짝이다. - **xDS 밖의 실패가 존재한다.** config가 의도대로 박혀도 트래픽이 안 되면 문제는 노드 라우팅·방화벽·conntrack 등 Envoy 바깥이다. 그땐 Pod 안에서 `openssl s_client`·`ss`로 본다. - **STALE = NACK 신호.** `proxy-status`가 STALE이면 Envoy가 그 설정을 거부한 것 — 멘탈 모델 매핑이 맞아도 버전 불일치/잘못된 CR 조합이면 적용 자체가 안 됐다. `istiod` 로그의 rejected 메시지가 1차 단서. - **EnvoyFilter는 멘탈 모델의 사각지대.** xDS를 직접 패치하므로 `proxy-config`엔 보여도 어느 CR이 만들었는지 추적이 어렵다. 최후 수단으로만, 반드시 의도 주석과 함께. --- ## See also - [Egress HTTP vs HTTPS](gw__src-egress-http-vs-https.html) — 외부 endpoint가 HTTP/HTTPS일 때 설정 차이 (후속) - [Cluster 해부](xds__src-cluster-anatomy.html) — `direction|port|subset|fqdn` cluster 이름 규칙의 정본 --- > [!quote] 출처 (검증 기준 Istio 1.30 / `networking.istio.io/v1`) > - CR→xDS 매핑(VirtualService→RDS, DestinationRule→CDS, Gateway→LDS, ServiceEntry→CDS+EDS, PeerAuthentication/AuthorizationPolicy→LDS filter chain) 및 Envoy가 Istio CR을 모르고 istiod가 xDS로 번역·push한다는 점 — Istio 아키텍처·xDS 해설 자료 및 Istio 공식 문서. > - `istioctl proxy-config {listener|route|cluster|endpoint|secret}` 및 `proxy-status`의 SYNCED/STALE/NOT SENT 의미 — Istio 디버깅 문서·proxy-config 레퍼런스. cluster 출력의 DESTINATION RULE 칼럼, route 출력의 VIRTUAL SERVICE 칼럼으로 역추적 가능. > - ※ "주로 채우는 xDS"는 학습용 1차 근사이며, 실제로는 여러 CR이 합쳐져 하나의 xDS를 구성함. 정확한 결과는 항상 `proxy-config -o json`으로 확인할 것.