homelab89 Docs Logs Legacy Files ☰ TOC 🌓
noteistio 2026-06-10istioenvoyxdsmental-model

xDS는 Envoy 설정을 LDS/RDS/CDS/EDS/SDS 계층으로 나누고 ADS가 이를 단일 스트림으로 묶어 적용 순서를 보장한다

이 문서가 다루는 것

Envoy의 동적 설정은 단일 덩어리가 아니라 5개 xDS API(LDS/RDS/CDS/EDS/SDS)로 분리되어 “리스닝 지점 / 라우팅 / upstream pool / 실제 endpoint / TLS secret"이라는 서로 다른 계층을 각각 담당한다. 이 분리는 부분 업데이트를 가능하게 하지만 동시에 순서 문제를 낳는다 — route가 아직 없는 cluster를 가리키면 트래픽이 깨진다. ADS(Aggregated Discovery Service) 가 이 여러 xDS를 하나의 gRPC 스트림으로 묶어 적용 순서를 보장함으로써 그 문제를 푼다. 이 문서는 그 개념·멘탈모델에 집중하고, istioctl proxy-config로 각 계층을 진단하는 운영 상세는 xDS 5계층과 istioctl 진단으로 위임한다.

대상환경: Istio 1.30, sidecar mode, Envoy. · 대상독자: xDS를 “이름은 들었는데 왜 5개로 쪼개고 ADS가 뭘 해결하는지” 모르는 DevOps/SRE. · 범위: 계층 분리의 동기 → 5계층 역할 → 순서/일관성 메커니즘 → istiod 전달 경로. · 선행: Envoy listener/route/cluster 기본 개념.


01. 배경 — 왜 설정을 한 덩어리가 아니라 계층으로 쪼개나

한 문장 멘탈모델 (이 그림 하나만 잡으면 된다)

xDS = istiod가 K8s/Istio 상태를 Envoy 설정으로 컴파일해, 변화 빈도가 다른 5개 계층(LDS/RDS/CDS/EDS/SDS)을 각각 독립 갱신하는 채널이고, ADS는 그 5개를 한 스트림에 묶어 “참조 대상이 먼저, 참조하는 쪽이 나중"이라는 적용 순서를 강제하는 봉투다. 아래 모든 디테일은 이 한 그림의 세부다.

Envoy를 정적 부트스트랩 YAML로만 운영하면 listener/route/cluster/endpoint가 한 파일에 고정된다(정적/동적 경계는 정적 vs 동적 설정 참조). 메시 환경에서 이게 깨지는 이유는 단 하나 — 이 설정의 각 부분이 변화 빈도가 완전히 다르다.

Endpoint  : Pod가 뜨고 죽을 때마다 변함        → 초 단위로 자주
Cluster   : Service/DestinationRule 바뀔 때    → 가끔
Route     : VirtualService 바뀔 때             → 가끔
Listener  : Gateway/포트 바뀔 때               → 드물게
Secret    : 인증서 rotation 주기마다           → 드물게

설정이 단일 blob이라면 Pod 하나가 죽을 때마다 listener·route·cluster 전체를 다시 컴파일해 내려보내야 한다 — 가장 자주 바뀌는 부분(endpoint)이 가장 안 바뀌는 부분(listener)의 재전송을 끌고 다닌다. xDS는 이를 독립적으로 갱신 가능한 5개 API로 쪼개서, endpoint만 바뀌면 EDS만 push하고 나머지는 손대지 않는다. 이것이 계층 분리의 1차 동기 — 변화 빈도별 분리(decoupling by churn rate) 다. 분리에는 대가가 따르는데(03절의 순서 문제), 그 대가를 ADS가 갚는 구조다.


02. 5개 계층 각각이 담당하는 질문

요청 하나가 Envoy를 통과하는 흐름을 따라가면 다섯 계층의 역할이 한 줄로 꿰진다. 요청은 항상 Listener → Route → Cluster → Endpoint 순으로 resolve되고, mTLS가 걸리면 그 과정에서 Secret을 쓴다. 각 계층을 “필드"가 아니라 “그게 답하는 질문"으로 읽으면 외울 필요가 없다.

flowchart TD
  L["LDS / Listener<br/>where to receive"]
  R["RDS / Route<br/>which cluster"]
  C["CDS / Cluster<br/>upstream pool + policy"]
  E["EDS / Endpoint<br/>actual Pod IPs"]
  S["SDS / Secret<br/>mTLS cert / key / CA"]
  L --> R
  R --> C
  C --> E
  C -. mTLS .-> S
API 풀네임 답하는 질문 내려주는 것 Istio 리소스(대표)
LDS Listener Discovery Service 어디서 트래픽을 받나 listener + filter chain (15001 out / 15006 in / 포트별) Gateway, Sidecar, PeerAuthentication
RDS Route Discovery Service 이 HTTP 요청을 어느 cluster로 route config (virtual host, route entry) VirtualService, Gateway
CDS Cluster Discovery Service upstream 목적지 정의와 정책 cluster (upstream pool) + LB/timeout/outlier Service, ServiceEntry, DestinationRule
EDS Endpoint Discovery Service 그 cluster의 실제 Pod IP cluster 안의 endpoint 목록 EndpointSlice, WorkloadEntry
SDS Secret Discovery Service mTLS handshake에서 workload identity 증명 TLS cert / private key / root CA istiod CA, PeerAuthentication

가장 자주 혼동되는 두 지점을 메커니즘으로 못 박아 두자.

  • RDS의 D는 Discovery다 — “Route Direct Service"가 아니다. xDS 전체가 x + Discovery Service 패턴(L/R/C/E/S + DS)이라는 점을 기억하면 안 헷갈린다.
  • CDS와 EDS는 따로 온다 — cluster(outbound|9080|v1|reviews...)는 CDS로 정의되고, 그 cluster 안에 들어갈 endpoint 목록(10.244.1.12:9080 …)은 EDS로 별도 스트림으로 온다. 이게 분리돼 있기 때문에 “cluster 객체는 존재하는데 그 안의 endpoint가 비어서 503 UH(no healthy upstream)“라는, 처음엔 모순처럼 보이는 상태가 가능하다. cluster 이름 규칙 direction|port|subset|fqdn의 해부는 Cluster 해부 참조.

03. 계층 분리의 대가 — 참조 무결성과 순서 문제

계층을 나눈 대가로 참조 무결성(referential integrity) 문제가 생긴다. 각 계층은 윗 계층을 값이 아니라 이름으로 참조하기 때문이다.

LDS listener --참조-->  RDS route (RouteConfiguration 이름)
RDS route    --참조-->  CDS cluster (이름: outbound|9080|v1|reviews...)
CDS cluster  --참조-->  EDS endpoint set (같은 cluster 이름)

이름으로 참조하니 갱신 순서가 틀리면 댕글링 참조(dangling reference) 가 생긴다. route를 cluster Y로 바꾸는 변경을 5개 독립 스트림으로 보냈다고 하자.

[잘못된 순서]
1) RDS 먼저 push  →  route가 cluster Y를 가리킴
                     그런데 Envoy는 아직 cluster Y를 모름
                     →  이 순간 트래픽이 "존재하지 않는 cluster"로 가서 깨짐
2) CDS 나중에 push →  뒤늦게 cluster Y 생성 (이미 늦음)

핵심 불변식은 단 하나다 — “route가 아직 없거나 이미 사라진 cluster를 가리키는 순간이 생기면 안 된다.” 이 불변식을 지키려면 갱신을 순서대로 적용해야 하는데, 5개 API가 각각 독립 스트림이면 메시지 도착 순서를 제어할 방법이 없다(스트림 간에는 순서 보장이 없다). 여기서 ADS가 등장한다.


04. ADS — 단일 스트림으로 순서를 사다

Envoy 표준에서 CDS/EDS/LDS/RDS/SDS는 각각 독립 gRPC streaming endpoint를 가질 수 있다(개별 xDS). ADS(Aggregated Discovery Service) 는 이 여러 리소스 타입을 하나의 양방향 gRPC 스트림으로 묶는다. Istio(istiod)는 모든 프록시에 대해 예외 없이 ADS를 쓴다.

ADS가 사주는 것은 묶음 자체가 아니라 순서(ordering) 다. 단일 스트림이므로 istiod가 보낸 순서대로 Envoy가 받는다. 따라서 istiod는 참조 대상을 먼저, 참조하는 쪽을 나중에 보내는 식으로 댕글링 참조를 원천 차단한다.

sequenceDiagram
  participant K as K8s API + Istio CRD
  participant I as istiod (compiler)
  participant E as Envoy (single ADS stream)
  K->>I: Service/VS/DR/EndpointSlice change
  Note over I: compile to xDS
  I->>E: CDS (define cluster Y)
  E-->>I: ACK
  I->>E: EDS (fill cluster Y endpoints)
  E-->>I: ACK
  I->>E: RDS (route now points to cluster Y)
  E-->>I: ACK
  I->>E: LDS (refresh listener if needed)
  E-->>I: ACK

적용 순서의 비대칭 (add vs remove)

순서는 변경 방향에 따라 뒤집힌다. 같은 불변식을 양방향에서 지키려면 그래야 하기 때문이다.

ADD (추가)    : CDS → EDS → RDS → LDS   (bottom-up: 참조 대상부터 만든다)
REMOVE (삭제) : LDS → RDS → CDS → EDS   (top-down: 참조하는 손을 먼저 뗀다)

추가는 “가리킬 대상을 먼저 만든다”, 삭제는 “가리키던 손을 먼저 뗀다”. 두 방향 모두 같은 불변식(“빈/없는 cluster를 가리키는 route가 없다”)을 지키므로 추가·삭제 어느 쪽도 트래픽이 끊기지 않는다. 이 비대칭이 graceful한 라우팅 전환·드레인의 기반이다.


05. 순서만으론 부족하다 — warming과 ACK/NACK

ADS의 순서 보장만으로는 두 구멍이 남는다. ① cluster를 받았다고 그 endpoint가 즉시 채워지는 건 아니다. ② Envoy가 받은 설정이 항상 유효한 것도 아니다. 두 메커니즘이 각각을 막는다.

① Cluster warming. Envoy는 새 cluster를 받으면 곧바로 트래픽에 투입하지 않고 “warming” 상태에 둔다. warming 중에는 그 cluster가 의존하는 것들(EDS endpoint, 필요 시 SDS secret, active health check 1회)이 준비될 때까지 기다린다. 준비가 끝나야 cluster가 “warm"이 되어 실제로 사용된다. 덕분에 CDS는 받았지만 EDS가 아직 안 온 짧은 윈도우에도 트래픽이 빈 cluster로 쏟아지지 않는다. warming = “endpoint 없는 cluster로 라우팅하지 않게 하는 안전장치”. (ADS 순서는 “CDS가 RDS보다 먼저 도착"을, warming은 “도착한 CDS라도 채워지기 전엔 안 쓴다"를 보장 — 층이 다르다.)

② ACK / NACK. Envoy는 설정을 받으면 control plane에 응답한다.

ACK  : 설정을 받아들이고 적용함        (정상)
NACK : 설정이 유효하지 않아 거부함     → Envoy는 직전 good config를 유지

NACK는 중요한 fail-safe 속성을 준다 — 잘못된 설정을 push해도 Envoy가 멈추지 않고 마지막 정상 설정으로 계속 동작한다. 이 ACK/NACK 결과는 istioctl proxy-statusSYNCED(ACK) / STALE(보냈으나 ACK 못 받음) / NOT SENT(보낼 것 없음) 로 드러난다. 그래서 데이터 플레인은 eventually consistent 모델이고, 트러블슈팅 1단계가 “sync 됐나?“가 된다 — sync 상태값 의미와 진단 흐름은 데이터 플레인 sync 상태 참조.

함정 — warming/NACK는 “조용하다”

NACK된 설정은 적용이 안 됐을 뿐 에러가 사용자에게 안 보일 수 있다. proxy-config만 보면 설정이 멀쩡히 보여 정상처럼 착각하기 쉽다. 그래서 proxy-config(프록시가 실제로 받은 설정)와 analyze/proxy-status(istiod가 의도한 설정·sync 여부)를 함께 봐야 의도-반영 갭을 잡는다.


06. 예시 — 5계층을 눈으로 보고 “빈 cluster” 증상을 진단한다

추상 설명을 땅에 박자. bookinfo의 reviews 호출을 예로, 한 요청이 통과하는 4계층을 istioctl proxy-config로 그대로 들여다본다(검증은 productpage Pod 시점). 명령·증상 매핑의 풀 레퍼런스는 xDS 5계층과 istioctl 진단에 있다 — 여기선 멘탈모델을 출력에 붙이는 데만 집중한다.

Listener → Route (LDS/RDS) — 어디서 받아 어느 route로:

istioctl proxy-config route deploy/productpage-v1 -n default --name 9080 -o short
# NAME     VHOST NAME        DOMAINS                       MATCH    VIRTUAL SERVICE
# 9080     reviews:9080      reviews, reviews.default ...  /*       reviews.default

Cluster → Endpoint (CDS/EDS) — 어느 upstream pool로, 실제 Pod IP는 뭔지:

istioctl proxy-config cluster deploy/productpage-v1 -n default --fqdn reviews.default.svc.cluster.local -o short
# SERVICE FQDN                              PORT  SUBSET  ...  DESTINATION RULE
# reviews.default.svc.cluster.local         9080  -       ...  reviews.default
# reviews.default.svc.cluster.local         9080  v1      ...  reviews.default

istioctl proxy-config endpoint deploy/productpage-v1 -n default \
  --cluster "outbound|9080|v1|reviews.default.svc.cluster.local" -o short
# ENDPOINT             STATUS   CLUSTER
# 10.244.1.12:9080     HEALTHY  outbound|9080|v1|reviews.default.svc.cluster.local

여기까지가 정상 상태다. 이제 03/05절의 메커니즘이 어떻게 증상으로 드러나는지 — reviews-v1을 0 replica로 줄여 endpoint를 비워 본다.

kubectl scale deploy/reviews-v1 -n default --replicas=0

istioctl proxy-config endpoint deploy/productpage-v1 -n default \
  --cluster "outbound|9080|v1|reviews.default.svc.cluster.local" -o short
# ENDPOINT   STATUS   CLUSTER
# (empty)

cluster 객체는 그대로 있는데(CDS) endpoint만 사라졌다(EDS) — 정확히 02절에서 “CDS와 EDS는 따로 온다"가 가능하게 만든 상태다. 이 subset으로 가는 요청은 Envoy 응답 플래그 UH(no healthy upstream)와 함께 503이 된다.

kubectl exec deploy/productpage-v1 -n default -c istio-proxy -- \
  curl -s -o /dev/null -w "%{http_code}\n" http://reviews:9080/health
# 503        ← cluster는 있으나 EDS가 비어 UH

마지막으로 “이게 잘못된 설정 때문인가, 아니면 받아들여진 정상 설정 결과인가"를 sync 상태로 가른다.

istioctl proxy-status
# NAME                       CDS     LDS     EDS     RDS     ECDS    ISTIOD          ...
# productpage-v1...default   SYNCED  SYNCED  SYNCED  SYNCED  NOT SENT  istiod-xxx    ...

모든 계층이 SYNCED = 설정은 의도대로 ACK됐다. 즉 이 503은 push/NACK 문제가 아니라 “endpoint가 실제로 0개"라는 토폴로지 사실이다. 만약 여기서 EDS가 STALE이었다면 진단 방향은 정반대(istiod가 push했는데 프록시가 ACK 못 함)로 바뀐다 — 이 한 표가 “받은 것 vs 의도한 것"의 갭을 한눈에 가른다.


07. Istio가 실제로 push하는 경로 — istiod와 istio-agent

표준 Envoy는 외부 xDS management server에 직접 붙지만, Istio sidecar는 한 단계를 더 둔다. 각 Pod의 istio-agent(pilot-agent) 가 Envoy와 istiod 사이에 끼어든다.

flowchart LR
  ISTIOD["istiod<br/>(xDS server + CA)"]
  AGENT["istio-agent<br/>(per-pod, xDS proxy + SDS)"]
  ENVOY["Envoy<br/>(localhost ADS)"]
  ISTIOD -- ADS over mTLS --> AGENT
  AGENT -- ADS over localhost --> ENVOY
  AGENT -. SDS cert/key .-> ENVOY
  • Envoy는 localhost의 istio-agent에게 ADS를 요청한다(Envoy는 istiod를 직접 모른다).
  • istio-agent가 istiod와 mTLS gRPC 스트림을 유지하며 xDS를 중계한다.
  • SDS는 특히 agent 로컬에서 처리된다 — workload 인증서의 private key는 네트워크를 타지 않고 agent가 메모리에서 Envoy에 SDS로 건넨다(key가 istiod로 오가지 않음). mTLS/SPIFFE identity 발급 흐름은 mTLS와 SPIFFE identity 참조.

istiod 쪽에서는 K8s informer가 Service/Endpoint/Pod/Istio CRD 변화를 감지 → 내부 모델 갱신 → 영향받는 프록시에게만 증분(delta) 또는 전체 push를 ADS로 내보낸다. 한 번의 변경이 메시 전체 프록시 push로 번질 수 있어, 이 push 부하가 대규모 메시에서 istiod 성능의 핵심 변수다 — control plane 성능 요인 참조.


핵심 정리

계층 분리 이유 : 변화 빈도가 다른 설정을 독립 갱신 (endpoint만 바뀌면 EDS만 push)

5계층  : LDS(어디서 받나) RDS(어느 cluster로) CDS(upstream 정의) EDS(실제 IP) SDS(mTLS secret)
resolve: Listener → Route → Cluster → Endpoint (+ Secret)
주의    : RDS의 D=Discovery / CDS와 EDS는 따로 옴(cluster 있어도 endpoint 빌 수 있음 → UH 503)

ADS    : 5개 xDS를 단일 gRPC 스트림으로 묶어 순서 보장 (핵심 가치는 묶음이 아니라 순서)
순서    : ADD = CDS→EDS→RDS→LDS(bottom-up) / REMOVE = top-down
불변식  : route가 빈/없는 cluster를 가리키는 순간이 없다

일관성  : warming(준비 안 된 cluster 미사용) + ACK/NACK(나쁜 설정 거부, last good 유지)
        → eventually consistent → proxy-status SYNCED/STALE/NOT SENT로 표면화

Istio   : Envoy → localhost istio-agent → (mTLS) istiod. SDS는 agent 로컬(key 안 나감)

What you might be missing

  • ADS의 핵심 가치는 “묶음"이 아니라 “순서"다. 여러 xDS를 한 스트림에 넣는 것 자체가 목적이 아니라, 단일 스트림이라야 댕글링 참조(route → 없는 cluster)를 막는 적용 순서를 강제할 수 있기 때문이다. 그래서 Istio는 예외 없이 ADS만 쓴다.
  • warming과 ADS 순서는 다른 층의 안전장치다. ADS는 “CDS가 RDS보다 먼저 도착"을, warming은 “도착한 CDS cluster라도 endpoint가 채워지기 전엔 안 쓴다"를 보장한다. 둘이 합쳐져야 “라우팅 전환 중 트래픽 무중단"이 성립한다. 하나만으로는 짧은 깨짐 윈도우가 남는다.
  • NACK는 fail-safe다 — 잘못된 push가 메시를 멈추지 않는다. NACK된 프록시는 마지막 정상 설정으로 계속 돈다. 위험은 오히려 “겉으론 정상인데 새 설정이 조용히 거부됨"이다. proxy-config만 보면 옛 설정이 보여 정상처럼 착각하기 쉽다 — proxy-status(STALE 여부)와 istiod NACK 메트릭을 함께 봐야 한다.
  • SDS의 비대칭에 주의. LDS/RDS/CDS/EDS는 istiod가 컴파일해 push하는 “토폴로지” 설정이지만, SDS는 workload 신원과 묶여 있고 private key가 agent 로컬에 머문다. 그래서 인증서 만료/rotation 문제는 다른 4계층과 증상 양상이 다르다(handshake 단계 503 UF, AuthorizationPolicy principal mismatch 등).
  • proxy-config는 “받은 것”, analyze는 “의도한 것”. 이 둘의 갭이 곧 push/ACK 실패다. xDS를 진짜로 이해하려면 리소스 apply 전후로 proxy-config ... -o json을 diff 떠 보는 것이 가장 빠른 훈련이다(절차는 진단 src §08).

Files