--- title: 파일 기반 xDS는 DiscoveryResponse 포맷·move 교체만 감지하고 EDS의 "클러스터당 CLA 1개" 제약 때문에 디버깅이 까다롭다 date: 2026-06-07 type: note domain: istio tags: [istio, envoy, xds, filesystem-xds, eds] --- > [!abstract] > 머릿속에 둘 한 장면: **파일 기반 xDS는 gRPC ADS의 구독 계약을 그대로 둔 채 전송(transport)만 "로컬 파일"로 바꾼 것**이다. 그래서 세 가지 좁은 제약은 따로 생긴 규칙이 아니라 모두 *그 계약*에서 흘러나온다 — ① 파일은 인라인 조각이 아니라 루트에 `resources:` 배열을 둔 **DiscoveryResponse 응답**이어야 하고, ② 파일 watcher가 신뢰하는 변경 신호는 **move(rename) 이벤트** 하나뿐이며, ③ EDS만은 "응답 1개 = 클러스터 1개"라 **클러스터당 ClusterLoadAssignment 1개**만 허용된다. 결론: LDS/RDS 학습용으로는 훌륭하지만 **파일 EDS는 함정**이고, xDS를 진짜로 보려면 gRPC ADS(Istio면 `istioctl proxy-config`)로 우회하는 편이 빠르다. 깊은 실습 절차는 [정적/동적 xDS 실습](/docs/istio/xds-envoy/envoy-static-dynamic-xds-lab/)에 위임하고, 여기서는 **왜 그런 제약이 생기고 어떻게 판단할지**의 멘탈모델만 다룬다. --- ## 1. 배경 — 왜 파일 기반 xDS를 손으로 만져보는가 xDS는 한 문장으로 "**설정을 누가, 어떤 전송으로 Envoy에 주입하는가**"의 문제다. Envoy는 Listener(LDS)·Route(RDS)·Cluster(CDS)·Endpoint(EDS)를 부팅 시 한 번 읽고 끝내는 게 아니라, 런타임에 *구독*해서 받아온다. 이 구독을 누가 채워주느냐가 전송(transport)이고, 같은 LDS/RDS/CDS/EDS 리소스라도 전송은 세 가지로 나뉜다: | 전송 방식 | config_source | 푸시 주체 | 용도 | |---|---|---|---| | **static** | (없음, 부트스트랩 인라인) | 없음(고정) | 최소 부팅·테스트 | | **filesystem(파일)** | `path:` / `path_config_source:` | 로컬 파일 watcher | 단독 실습·엣지 케이스 | | **gRPC ADS** | `api_config_source: {api_type: GRPC}` | 컨트롤 플레인(istiod) | 프로덕션(Istio) | 프로덕션에서 Istio는 **gRPC ADS 한 채널**로 모든 리소스를 단일 스트림에 실어 보낸다. 그런데 그 ADS 스트림 안을 직접 들여다보긴 어렵다 — istiod가 KRM(VirtualService·DestinationRule 등)을 번역해 푸시하는 결과만 보일 뿐, "Envoy가 한 개의 RouteConfiguration을 받으면 무슨 일이 벌어지는가"를 손으로 한 줄씩 바꿔보긴 힘들다. **파일 기반 xDS는 그 ADS 스트림을 로컬 파일로 "외재화(externalize)"한 것**이다. 컨트롤 플레인 없이 `lds.yaml`·`rds.yaml`을 직접 쓰고, 파일 한 줄 바꾸면 Envoy가 재시작 없이 반영한다. 그래서 "동적 라우팅이 실제로 어떻게 갱신되는가"를 컨트롤 플레인의 추상화를 걷어내고 맨손으로 체득하는 최고의 실습 도구다. 선행 개념: 정적/동적의 경계와 어떤 계층을 동적화할지는 [정적 vs 동적 설정](/docs/istio/xds-envoy/envoy-static-vs-dynamic-config/), 계층 분할(LDS→RDS, CDS→EDS의 의존 사슬)의 전체 그림은 [xDS API 계층](/docs/istio/xds-envoy/xds-api-layers/)에 있다. 이 글은 그 위에서 **"파일로 바꾸면 어떤 함정이 새로 생기나"** 만 다룬다. --- ## 2. 핵심 멘탈모델 — "transport만 바꾼 것"에서 세 제약이 따라 나온다 붙잡을 단 하나의 그림: > **파일 기반 xDS는 gRPC ADS와 동일한 구독 계약을 쓰되, 메시지를 운반하는 채널만 gRPC 스트림 → 로컬 파일로 바꾼다.** Envoy 입장에선 "응답(DiscoveryResponse)을 어디서 받느냐"만 다르고, *받은 응답을 어떻게 검증·적용하느냐*는 똑같다. 이 한 줄을 쥐면 뒤의 세 제약이 전부 *연역*된다. 각 제약은 새로 만든 규칙이 아니라, 동일한 구독 계약의 어느 부분이 파일이라는 전송에서 드러나는가의 차이일 뿐이다: ```mermaid flowchart TD contract["동일한 xDS 구독 계약
(gRPC ADS와 공유)"] contract -->|"응답은 DiscoveryResponse 타입이다"| c1["제약 ① 포맷
루트 resources[] + @type"] contract -->|"새 응답 = 새 파일 도착 이벤트"| c2["제약 ② 갱신 신호
move(rename)만 안정"] contract -->|"EDS 응답 단위 = 클러스터 1개"| c3["제약 ③ EDS
클러스터당 CLA 1개"] c1 -.같은 인터페이스, 구현은 분리.-> mux["Subscription 인터페이스(추상)
구현·거부 로그는 전송별로 분리 — 파일은 FilesystemSubscriptionImpl"] c3 -.length 검사(파일 전용 구현·로그).-> mux ``` 세 제약을 하나씩 *계약에서 끌어내며* 보면: ### 2-1. 제약 ① — 파일은 DiscoveryResponse여야 한다 (포맷) ADS 스트림에서 오는 메시지는 `DiscoveryResponse`다. 전송만 바꿨으니 **파일의 모양도 정확히 DiscoveryResponse**여야 한다. 즉 루트에 `resources:` 배열이 있고, 각 원소는 `"@type"`으로 구체 리소스 타입을 명시한다: ```yaml # rds.yaml — 루트 resources[] + @type 이 계약 version_info: "0" # 선택, 디버깅·관찰용 resources: - "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration name: local_route virtual_hosts: [ ... ] ``` 여기서 오는 혼동: - **부분 설정 YAML이 아니다.** "Listener 한 조각"을 그냥 쓰면 안 되고, `Listener`를 `resources[]`로 감싼 응답 형태여야 한다. static 부트스트랩에서 쓰던 인라인 형식과 구조가 다르다는 점이 첫 번째 혼동 지점이다 — 같은 리소스라도 *인라인은 필드 값*, *파일 xDS는 응답 페이로드*라는 위상 차이다. - **경로는 부트스트랩 시점에 이미 존재**해야 한다. watcher는 초기 로드에서 파일을 읽으므로, 없으면 빈 구독 상태로 시작해 이후 생성에 반응이 어긋난다. - `@type`이 틀리면 조용히 무시되거나 reject된다. 타입 FQDN(`envoy.config.route.v3.RouteConfiguration` 등)은 v3 API 기준으로 정확히 적어야 한다. ### 2-2. 제약 ② — 갱신은 move(rename)로만 안정 감지된다 (이벤트) ADS에선 "새 응답이 왔다"가 명시적 메시지다. 파일 전송엔 그런 메시지가 없으니, **"파일이 바뀌었다"는 OS 파일시스템 이벤트로 대신**한다 — `inotify`(Linux)/`kqueue`(macOS). 문제는 에디터/스크립트의 in-place 저장이 발생시키는 이벤트가 플랫폼마다 다르고 불안정하다는 것이다. Envoy가 안정적으로 신뢰하는 트리거는 **rename(move) = `MOVED_TO`** 한 가지다. ```bash # 잘못: in-place 수정 — inotify가 일관되게 안 잡음(IN_MODIFY/CLOSE_WRITE 누락 가능) vi xds/rds.yaml # 옳음: 새 파일에 쓰고 atomic move 로 교체 — 항상 MOVED_TO 발생 cat > xds/rds.new <<'EOF' ... 새 DiscoveryResponse ... EOF mv xds/rds.new xds/rds.yaml # rename = 원자적 교체, watcher 확실히 감지 ``` **왜 move인가**: rename은 디렉터리 엔트리를 원자적으로 바꾸므로 "반쯤 쓰인 파일을 읽는" race가 없고, 단일 `MOVED_TO` 이벤트로 환원된다. in-place write는 truncate→write 사이의 중간 상태와 다중 이벤트(`IN_MODIFY` 여러 번, `CLOSE_WRITE`)를 만들어 watcher 구현이 빠뜨리기 쉽다. 그래서 ConfigMap을 마운트하는 Kubernetes도 내부적으로 **symlink swap(=rename)** 으로 갱신하며, 그 패턴을 안정 감지하라고 Envoy가 `path_config_source.watched_directory`를 추가했다. 즉 "rename으로만 갱신하라"는 임의의 규칙이 아니라 *원자적 교체만이 일관된 단일 이벤트를 보장*한다는 파일시스템 물리에서 나온다. ```mermaid sequenceDiagram participant Op as Operator/script participant FS as filesystem participant W as Envoy file watcher participant E as Envoy config Op->>FS: write rds.new (full DiscoveryResponse) Op->>FS: mv rds.new rds.yaml (rename) FS-->>W: MOVED_TO 이벤트(원자적) W->>FS: re-read rds.yaml W->>E: route_config 갱신(재시작 없음) Note over W,E: in-place write 면 이 화살표가 누락될 수 있음 ``` ### 2-3. 제약 ③ — EDS는 클러스터당 CLA 1개 (구독 단위 비대칭, 핵심 함정) 세 제약 중 가장 비자명하고 가장 자주 터지는 것. **이건 파일 전송의 한계가 아니라 EDS 구독 단위의 본질이 파일에서 노출되는 것**이다. EDS 리소스의 단위는 "클러스터 1개의 ClusterLoadAssignment(CLA) 1개"다. CDS/LDS/RDS는 한 파일(=한 DiscoveryResponse)에 여러 리소스를 넣어도 되지만 — CDS는 애초에 "클러스터 *목록*"을 받는 게 정상이니까 — **EDS는 한 구독 응답이 정확히 그 클러스터의 CLA 하나**여야 한다. "특정 클러스터에 대한 답"이라 1개여야 한다는 의미가, "리소스 목록"인 CDS와 충돌하는 게 함정의 본질이다. ```yaml # eds.yaml — 두 클러스터의 CLA를 한 파일에 욱여넣으면 거부 resources: - "@type": type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment cluster_name: httpbin_a # CLA #1 endpoints: [ ... ] - "@type": type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment cluster_name: httpbin_b # CLA #2 ← 이게 문제 ``` 이 경우 Envoy 로그에 다음이 뜬다(2026-07-05 실측, Envoy 1.30 — 아래 참고): ``` [warning][config] filesystem_subscription_impl.cc:63] Filesystem config update rejected: Unexpected EDS resource length: 2 ``` 여기서 두 가지를 동시에 이해해야 한다: - **"로그 접두어가 실제로는 뭘까?"** — (2026-07-05 실측 정정) 로그 머리는 `gRPC config`가 **아니라** `Filesystem config update rejected: ...`이고, 소스 위치도 `filesystem_subscription_impl.cc`다. filesystem 구독은 gRPC/ADS mux 코드 경로를 타지 않고, **자기 자신만의 `FilesystemSubscriptionImpl` 경로**에서 검증·거부 로그를 남긴다 — "구독 *계약*(Subscription 인터페이스)은 gRPC와 공유하지만, 그 *구현체*와 로그 문구는 전송별로 분리돼 있다"는 뜻이다. 홈랩 클러스터에서 두 클러스터의 CLA를 한 eds.yaml에 넣고 원자적 rename으로 교체해 재현한 결과, 전체 로그 어디에도 `gRPC config` 문자열은 한 번도 나타나지 않았다(grep 0건). "filesystem도 결국 공통 grpc-mux 경로를 타서 `gRPC config`가 찍힌다"는 서술은 이 실측으로 반증됐다. - **"왜 length 2?"** — 파일 EDS 구독은 클러스터별로 별도 응답을 기대하는데, 한 파일에 둘을 넣으면 "이 클러스터에 대한 CLA가 2개 왔다"로 해석돼 length 2로 reject된다. **회피책 — STRICT_DNS로 EDS 자체를 제거**: 학습/실습이라면 CDS에서 클러스터 타입을 `STRICT_DNS`(또는 `LOGICAL_DNS`)로 두고 엔드포인트를 인라인 `load_assignment`로 적으면 EDS 구독이 필요 없어진다. Envoy가 DNS로 IP를 직접 해석하므로 파일 EDS의 length 함정을 통째로 건너뛴다. ```yaml # cds.yaml — STRICT_DNS 면 EDS 불필요(엔드포인트 인라인) resources: - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: httpbin_a type: STRICT_DNS # EDS 대신 DNS 해석 lb_policy: ROUND_ROBIN load_assignment: cluster_name: httpbin_a endpoints: - lb_endpoints: - endpoint: { address: { socket_address: { address: httpbin-a, port_value: 8080 } } } ``` 부트스트랩 배선은 이 계약을 그대로 반영한다 — `lds_config: { path: ... }`, `cds_config: { path: ... }`로 가리키고, HCM 안에서 RDS를 `rds.config_source.path`로 건다. 즉 파일 하나마다 그게 채우는 구독이 1:1로 묶인다. ```mermaid flowchart LR subgraph FS["filesystem xDS"] boot["bootstrap.yaml"] lds["lds.yaml
(Listener)"] rds["rds.yaml
(Route)"] cds["cds.yaml
(Cluster)"] eds["eds.yaml
(CLA) — 함정"] end boot -->|lds_config path| lds boot -->|cds_config path| cds lds -->|rds.config_source path| rds cds -->|eds_config path| eds watcher["file watcher
(inotify/kqueue)"] -.move 이벤트.-> lds & rds & cds & eds ``` --- ## 3. 예시 — move로 가중치 전환을 관찰하고, 거부를 재현한다 세 제약이 실제로 어떻게 드러나는지 한 사이클로 묶어 본다. 전체 동작 실습(가중치 스위치·Admin API 덤프 전 과정)은 중복하지 않고 [정적/동적 xDS 실습](/docs/istio/xds-envoy/envoy-static-dynamic-xds-lab/) §3–4로 위임하고, 여기서는 *핵심 동작과 기대 출력*만 확인한다. **(a) move로 RDS 가중치 전환 — 제약 ②가 살아 있음을 확인.** A→B 트래픽을 100% B로 옮기는 새 RouteConfiguration을 atomic move로 교체한 뒤, Admin API에서 갱신을 확인한다: ```bash # 주의: admin 의 ?resource= 는 type-URL 필터지 'routes' 같은 약어가 아니다. # 전체 config_dump 를 받아 jq 로 추출해야 한다(정본 lab과 일치). curl -s http://127.0.0.1:15000/config_dump \ | jq '.. | .weighted_clusters? // empty' # 기대: httpbin_a weight 0, httpbin_b weight 100 으로 갱신 ``` `mv`로 교체하면 위 출력이 재시작 없이 바뀌고, `vi`로 in-place 저장하면 (플랫폼에 따라) 바뀌지 않는다 — 그 차이가 곧 제약 ②의 증명이다. **(b) 거부를 일부러 재현 — 제약 ③의 시그니처.** §2-3의 두-CLA `eds.yaml`을 그대로 넣어 보면 Envoy 로그에 `Unexpected EDS resource length: 2`가 뜬다. 이 줄을 보면 "한 파일에 CLA를 여러 개 넣었다"로 즉시 환원하고, 클러스터별 분리 또는 STRICT_DNS 회피로 간다. **(c) Istio 실환경에서 EDS를 제대로 보는 법(파일이 아니라 ADS 경로).** 파일 EDS의 함정을 우회해 "진짜 EDS가 런타임에 갱신되는 것"을 보려면 ADS로 간다: ```bash istioctl proxy-config clusters deploy/sleep -n default -o short istioctl proxy-config endpoints deploy/sleep -n default \ --cluster "outbound|8000||httpbin.default.svc.cluster.local" -o json # kubectl scale deploy/httpbin --replicas=2 후 endpoints 가 증가하면 EDS 동적 반영 확인 ``` cluster 이름은 `direction|port|subset|fqdn` 규칙(예: `outbound|8000||httpbin.default.svc.cluster.local`)을 따른다. 이 좌표로 어떤 클러스터의 EDS를 보는지 특정한다. 데이터플레인 동기화 상태(SYNCED/STALE)는 [데이터플레인 동기화 상태](/docs/istio/xds-envoy/data-plane-sync-state/)에서, 진단 도구 사용은 [Envoy admin API 진단](/docs/istio/xds-envoy/envoy-admin-api-diagnosis/)에서 다룬다. --- ## 4. 정리 — 무엇으로 우회할지의 의사결정 멘탈모델 한 줄로 되돌아가면: **파일 xDS는 "transport만 파일로 바꾼 ADS"** 이고, 그래서 LDS/RDS는 손으로 체득하기 훌륭하지만 EDS는 구독 단위 비대칭 때문에 함정이다. 그러니 *보고 싶은 것*에 따라 전송을 고른다: | 목적 | 권장 경로 | 이유 | |---|---|---| | LDS/RDS 동적성(라우팅 가중치·timeout·retry) "확인만" | **파일 LDS/RDS + CDS는 STRICT_DNS** | EDS 함정 회피, move 한 번으로 관찰 | | 엔드포인트가 런타임에 바뀌는 EDS 동작 자체 | **gRPC ADS** (go-control-plane 미니 CP 1~2h) | 파일 EDS length·이벤트 삽질보다 빠름 | | Istio 이해가 최종 목적 | **Istio on kind + `istioctl proxy-config`** | 진짜 ADS/gRPC xDS를 그대로 관찰 | ```mermaid flowchart TD q{무엇을 보고 싶나?} q -->|"LDS/RDS 동적 라우팅"| a["파일 LDS/RDS
CDS=STRICT_DNS"] q -->|"EDS 엔드포인트 변화"| b["gRPC ADS
(go-control-plane)"] q -->|"Istio 동작 자체"| c["kind + istioctl
proxy-config"] b -.파일 EDS 회피.-> a c -.프로덕션 경로.-> b ``` ## 핵심 정리 - **한 문장 앵커**: 파일 기반 xDS = gRPC ADS와 같은 구독 계약, 전송만 로컬 파일. 세 제약은 이 한 문장의 연역이다. - 파일 xDS의 계약 3가지: **(1) 루트 `resources:` + `@type`의 DiscoveryResponse 포맷, (2) move(rename) 이벤트만 안정 감지, (3) EDS는 클러스터당 CLA 1개.** - **`Unexpected EDS resource length: N`** 은 한 파일에 CLA를 여러 개 넣었다는 신호 → 클러스터별로 분리하거나 **STRICT_DNS로 EDS 자체를 제거**. 로그 접두어는 `gRPC config`가 아니라 **`Filesystem config update rejected`**다(2026-07-05 Envoy 1.30 실측) — filesystem 구독은 gRPC/ADS mux와 별개인 자신만의 검증·로그 경로(`FilesystemSubscriptionImpl`)를 타기 때문이다. - 파일은 **부트스트랩 시점에 존재**해야 하고, 갱신은 **`mv`(atomic rename)** 로. in-place 저장은 watcher가 놓칠 수 있다. ConfigMap이 symlink swap을 쓰는 이유, `watched_directory` 옵션이 생긴 이유가 모두 여기에 있다. - 학습 경로: 동적 라우팅 확인은 **파일 LDS/RDS(CDS=STRICT_DNS)**, 진짜 EDS/ADS는 **gRPC**(go-control-plane 또는 Istio `istioctl proxy-config`). ## What you might be missing - **`path` vs `path_config_source`는 다르다.** 단순 `path:`는 레거시 단일 파일 watcher라 위 이벤트 함정에 그대로 노출된다. 최신 Envoy는 `path_config_source: { path, watched_directory }`를 제공해 **디렉터리를 watch**하고 symlink swap(ConfigMap·Secret 마운트 패턴)을 안정적으로 감지한다. 파일 EDS를 굳이 써야 하면 이 쪽을 쓴다. - **macOS Docker Desktop의 파일 이벤트는 신뢰도가 더 낮다.** 가상 파일시스템(gRPC-FUSE/virtiofs) 경유라 `inotify` 전파가 누락되곤 한다. 같은 manifest가 Linux에선 되고 Mac에선 "왜 안 바뀌지"가 되는 흔한 원인 — Envoy 버그가 아니라 호스트 FS 이벤트 전달 문제다(이 홈랩은 Linux 워커 노드로만 구성돼 직접 재현·검증은 못 했다). - **Istio는 사이드카에 파일 xDS를 쓰지 않는다.** istiod가 **gRPC ADS** 단일 스트림으로 LDS/RDS/CDS/EDS를 푸시한다. **(2026-07-05 정정)** 이 단일 스트림에 **SDS는 포함되지 않는다** — SDS는 파드 안의 **istio-agent가 로컬 유닉스 도메인 소켓으로 별도 서빙**하는 채널이라, istiod와의 gRPC ADS 스트림과는 분리돼 있다. 파일 기반은 어디까지나 **Envoy 단독 학습/특수 임베디드** 용도다. 따라서 이 note의 함정들은 "Istio 운영 중 만날 버그"가 아니라 "xDS 멘탈모델을 손으로 체득할 때의 함정"으로 분리해 이해해야 한다. - **`version_info`는 의미적 버전이 아니라 관찰용 라벨**이다. 파일 갱신 자체(=move)가 reload를 트리거하고, `version_info`를 안 바꿔도 새 내용이 반영된다. `config_dump`에서 변경 추적을 쉽게 하려고 올려 두는 것일 뿐, "버전을 올려야 반영된다"는 오해를 하기 쉽다. - **EDS length 거부는 "잘못된 설정"이 아니라 "구독 단위 오해"** 다. CDS의 다중 리소스 습관을 EDS에 그대로 옮기면 터진다. EDS만 "응답 1개 = 클러스터 1개"라는 비대칭을 기억하면 대부분의 파일 EDS 삽질이 사라진다. ## 검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6) 검증 방법: Envoy/Istio 공식 문서(xds.md, config_source.proto, xds_protocol 등) 대조 + homelab k8s 클러스터(istio-verify 네임스페이스, 메시 밖 순수 Envoy 파드)에서의 실측을 병행했다. 15개 주장 중 12개는 문헌·실측으로 그대로 확인됐고, 1개(SDS가 istiod ADS 스트림에 같이 실린다는 서술)는 공식 아키텍처 문서와 배치돼 오류로 정정했으며, 1개(거부 로그의 `gRPC config` 접두어 서술)는 실측으로 반증돼 정정했다. macOS 파일 이벤트 신뢰도 주장은 이 홈랩(Linux 워커 전용)에서는 재현 대상이 없어 실측 불가로 남겼다. | 주장 | 판정 | 근거 | |---|---|---| | C1. 파일 기반 xDS는 gRPC ADS와 동일한 Subscription 인터페이스를 쓰고 전송만 로컬 파일로 바뀐 것이다 | ✅ 문헌 확인 | [github.com/envoyproxy/envoy/…/xds.md](https://github.com/envoyproxy/envoy/blob/main/source/docs/xds.md) | | C2. 파일은 부분 리소스가 아니라 루트 `resources:` + `@type`의 완전한 DiscoveryResponse여야 한다 | ✅ 문헌 확인 | [envoyproxy.io/…/configuration-dynamic-filesystem](https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/configuration-dynamic-filesystem) | | C3. 파일 경로는 부트스트랩(config load) 시점에 이미 존재해야 한다 | ✅ 문헌 확인 | [envoyproxy.io/…/config_source.proto](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/config_source.proto) | | C4. 파일 watcher는 move(MOVED_TO)만 신뢰하고, in-place 저장(IN_MODIFY/CLOSE_WRITE)은 감지가 누락될 수 있다 | ✅ 실측 확인 | [envoyproxy.io/…/configuration-dynamic-filesystem](https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/configuration-dynamic-filesystem) · [T66 실측](files/verify/T66/result.txt) — in-place(cat>)는 update_attempt 불변으로 고착, rename(mv)만 반영 | | C5. rename은 원자적 단일 이벤트를 만들고, K8s ConfigMap도 symlink swap을 쓰며 이를 위해 `watched_directory`가 생겼다 | ✅ 문헌 확인 | [envoyproxy.io/…/config_source.proto](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/config_source.proto) | | C6. EDS 구독 단위는 "클러스터 1개 = CLA 1개"라 한 응답에 CLA가 2개 이상이면 거부된다 | ✅ 실측 확인 | [github.com/envoyproxy/envoy/issues/23581](https://github.com/envoyproxy/envoy/issues/23581) · [T23 실측](files/verify/T23/result.txt) — 2-CLA eds.yaml 교체 시 `Unexpected EDS resource length: 2`로 즉시 거부, last-good 유지 | | C7. 거부 로그는 `gRPC config for ... rejected: ...` 접두어이며, filesystem 구독도 공통 grpc-mux 검증 경로를 타기 때문이다 | 🔬 실측 반증 — 본문 교정 | [github.com/envoyproxy/envoy/issues/23581](https://github.com/envoyproxy/envoy/issues/23581) · [T23 실측](files/verify/T23/result.txt) — 실제 접두어는 `Filesystem config update rejected: ...`(`filesystem_subscription_impl.cc`), 전체 로그에 `gRPC config` 문자열 0건. filesystem은 gRPC mux와 분리된 자체 구현 경로를 탐 | | C8. CDS를 STRICT_DNS/LOGICAL_DNS + 인라인 `load_assignment`로 두면 EDS 구독 자체가 필요 없다 | ✅ 문헌 확인 | [envoyproxy.io/…/service_discovery](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/service_discovery) | | C9. Istio 사이드카 클러스터 이름은 `direction\|port\|subset\|fqdn` 규칙을 따른다 | ✅ 문헌 확인 | [istio.io/…/proxy-cmd](https://istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/) | | C10. 레거시 `path:`는 단일 파일 watcher이고, 최신 `path_config_source: {path, watched_directory}`는 디렉터리를 watch해 symlink swap을 감지한다 | ✅ 문헌 확인 | [envoyproxy.io/…/config_source.proto](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/config_source.proto) | | C11. Istio 사이드카는 파일 기반 xDS를 쓰지 않고 istiod와의 단일 gRPC ADS로만 LDS/RDS/CDS/EDS를 받는다 | ✅ 실측 확인 | [envoyproxy.io/…/xds_protocol](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol) · [T22 실측](/docs/istio/xds-envoy/envoy-static-vs-dynamic-config/files/verify/T22/) — bootstrap 전체에 `path_config_source` 0건, `ads:{}` + DELTA_GRPC만 존재 | | C12. (원문) istiod의 gRPC ADS 단일 스트림이 SDS까지 포함해 LDS/RDS/CDS/EDS/SDS 전부를 푸시한다 | ❌ 오류 — 본문 교정 | [github.com/istio/istio/…/istio-agent.md](https://github.com/istio/istio/blob/master/architecture/security/istio-agent.md) — SDS는 istiod ADS 스트림이 아니라 파드 내 istio-agent가 로컬 소켓으로 별도 서빙 | | C13. `version_info`는 갱신을 게이팅하는 값이 아니라 관찰용 라벨이며, move 자체가 reload를 트리거한다 | ✅ 실측 확인 | [envoyproxy.io/…/xds_protocol](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol) · [T66 실측](files/verify/T66/result.txt) — 3개 파일 모두 `version_info` 동일하게 유지해도 rename 시점에만 갱신됨 | | C14. `istioctl proxy-config endpoints --cluster ...`는 EDS가 런타임에 실제 갱신되는 것을 보여준다 | ✅ 문헌 확인 | [istio.io/…/proxy-cmd](https://istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/) | | C15. macOS Docker Desktop의 가상 FS(gRPC-FUSE/virtiofs)는 inotify 전파가 덜 신뢰할 만해 Linux와 다르게 동작할 수 있다 | 실측 불가 | [github.com/docker/for-mac/issues/4999](https://github.com/docker/for-mac/issues/4999) — 홈랩은 Linux 워커 노드만 존재해 macOS 환경 재현 대상 없음 |