---
type: note
tags: [istio, envoy, xds]
created: 2026-06-07
---
# Envoy 정적 설정은 부트스트랩 시점에 고정되고 동적 설정은 컨트롤 플레인이 런타임에 푸시해 재시작 없이 반영한다
> [!abstract]
> 머릿속에 담을 한 장: **정적은 "xDS를 받는 통로", 동적은 "그 통로로 흘러드는 트래픽 설정"이다.** 정적(static)은 프로세스가 부팅하며 읽는 부트스트랩 YAML에 박제돼 재시작 전까지 불변이고, 동적(dynamic)은 컨트롤 플레인이 xDS로 런타임에 밀어넣어 프로세스 재시작 없이 listener/route/cluster/endpoint를 갈아끼운다. Istio에서 사이드카·gateway의 거의 모든 라우팅·보안 설정은 동적이며, 정적은 부트스트랩(노드 ID, xDS 채널, admin, 정적 stats sink)만 담는다. 이 문서는 "왜 둘로 나뉘고 무엇이 어느 쪽에 가는가"를 그 메커니즘(warming + atomic swap, ADS 의존 순서)까지 정리한다. 파일 기반 xDS의 운영 함정과 LDS/RDS/CDS/EDS 세부는 각각 다른 문서로 위임한다.
## 1. 배경 — 왜 설정을 "정적/동적" 둘로 나누는가
Envoy는 그 자체로는 정책을 모르는 **순수 데이터 평면 프록시**다. 어떤 포트로 받고(listener), 그걸 어디로 보내고(route), 누구에게 보내는지(cluster·endpoint)를 설정으로 주입받아야만 동작한다. 문제는 그 설정이 **두 종류의 수명**을 갖는다는 점이다.
- 어떤 설정은 **프로세스의 정체성**이다 — 나는 누구이며(node ID) 어디로 admin/metrics를 노출하는가. 이건 살아있는 동안 바뀔 이유가 없다.
- 어떤 설정은 **트래픽 정책**이다 — canary 가중치 5%→50%, Pod 스케일 2→4, circuit breaker 임계값. 이건 운영 중에 **끊임없이** 바뀐다.
이 둘을 같은 메커니즘으로 다루면 한쪽이 다른 쪽을 망친다. 정체성을 자주 바꿀 일은 없는데 트래픽 정책 하나 바꾸겠다고 프로세스를 통째로 재시작하면 진행 중 연결이 끊긴다. 반대로 정체성까지 런타임 푸시에 맡기면 "푸시를 받는 통로 자체"를 받을 길이 없는 부트스트랩 역설에 빠진다.
그래서 Envoy는 설정을 **로딩 시점**으로 가른다. 부팅 때 한 번 읽고 굳히는 **정적(static)**, 컨트롤 플레인이 런타임에 흘려보내는 **동적(dynamic)**. 이 문서를 읽기 전 알아둘 선행 개념은 셋뿐이다 — listener(진입점)·route(목적지 규칙)·cluster(백엔드 그룹)라는 데이터 평면의 3요소, 그리고 그것을 외부에서 밀어주는 프로토콜이 **xDS**라는 것. 그 위에서 "무엇이 정적이고 무엇이 동적인가"가 이 문서의 전부다.
## 2. 정적 부트스트랩: listener-route-cluster를 한 파일에 박제
먼저 동적이 없는 세계를 보면 동적의 존재 이유가 선명해진다. 가장 단순한 Envoy는 부트스트랩 YAML 하나로 데이터 평면 전체를 정의한다. `static_resources` 아래에 listener(트래픽 진입점) → route(어디로 보낼지) → cluster(백엔드 그룹)를 모두 적고, 프로세스는 부팅 시 이 파일을 1회 읽어 인메모리 설정으로 굳힌다. **모든 것이 파일에 박혀 있고, 파일은 부팅 후 불변이다.**
```yaml
static_resources:
listeners:
- name: listener_httpbin
address: { socket_address: { address: 0.0.0.0, port_value: 15001 } }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config: # 라우트가 listener 안에 인라인
name: local_route
virtual_hosts:
- name: vhost_all
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: httpbin_svc }
http_filters:
- { name: envoy.filters.http.router, typed_config: { "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router } }
clusters:
- name: httpbin_svc
type: LOGICAL_DNS
load_assignment: { ... } # 엔드포인트도 인라인
```
여기서 핵심은 **중첩 구조**다 — route가 listener 안에 인라인으로 박히고(`route_config`), endpoint가 cluster 안에 인라인으로 박힌다(`load_assignment`). 동적화란 바로 이 인라인을 끊고 "외부에서 받아오라"로 바꾸는 일인데(§3에서 다룸), 그 전에 이 인라인 구조의 **세 가지 한계**가 곧 동적 설정을 요구하는 동기다.
- **불변성**: 라우팅 가중치를 5%→50%로 바꾸려면 파일을 고치고 프로세스를 재시작해야 한다. 재시작은 기존 연결을 끊고 트래픽을 일시 단절시킨다(graceful drain 없이는 더 거칠다).
- **스케일 미반영**: 백엔드 Pod가 2→4로 늘어도 인라인 엔드포인트 목록은 그대로다. 새 IP가 트래픽을 못 받는다.
- **운영 비대칭**: 메시 안 프록시가 수백 개면 각 프록시의 부트스트랩을 따로 관리할 수 없다. 중앙에서 한 번 계산해 밀어주는 모델이 필요하다.
세 한계는 모두 같은 뿌리를 가진다 — **설정이 데이터 평면 프로세스 안에 갇혀 있고, 그걸 바꾸는 유일한 손잡이가 "재시작"뿐**이라는 것. 동적 설정은 이 손잡이를 "런타임 교체"로 바꾼다.
> 책(Istio in Action Ch.3.2)은 Envoy **v3 설정 API**(`typed_config`)만 다룬다. v1/v2는 폐기됐다. 정적 부트스트랩 실습 전문은 [정적/동적 xDS 실습](xds__src-envoy-static-dynamic-xds-lab.html) 참고.
## 3. 동적 설정의 메커니즘: 인라인을 끊고, 받아서, 무중단 교체
동적 설정의 핵심은 **설정의 출처를 listener 안 인라인이 아니라 외부 "config source"로 위임**하는 것이다. §2의 HCM이 라우트를 인라인(`route_config`) 대신 `rds`로 구독하면, 라우트는 부트스트랩 밖에서 흘러들어온다. cluster도 `cds_config`로, 엔드포인트도 EDS로 위임할 수 있다. 부트스트랩에는 이제 "어디서 받아올지"만 남는다.
```yaml
dynamic_resources:
ads_config: { api_type: GRPC, grpc_services: [{ envoy_grpc: { cluster_name: xds_cluster } }] }
lds_config: { ads: {} } # listener를 ADS로 구독
cds_config: { ads: {} } # cluster를 ADS로 구독
```
여기서 곧바로 드러나는 자기참조가 부트스트랩의 **닭-달걀** 본질이다: `ads_config`가 가리키는 `xds_cluster`는 컨트롤 플레인으로 가는 gRPC cluster인데, 이 cluster마저 동적으로 받으려 하면 "받을 통로를 받을 통로가 없다". 그래서 `xds_cluster`는 **반드시 정적**이어야 한다. 동적의 세계로 들어가는 문 하나만은 항상 손으로 박아둔다.
이제 핵심 질문 — 푸시된 설정이 **재시작 없이** 반영되는 메커니즘은 무엇인가. 답은 **warming(예열) + atomic swap**이다.
1. 컨트롤 플레인이 새 버전 리소스를 푸시한다(예: 가중치 바뀐 RouteConfiguration).
2. Envoy는 새 설정을 **별도로 준비(warm)** 한다. cluster라면 새 엔드포인트의 health check/DNS resolve가 끝날 때까지 트래픽을 안 보낸다.
3. 준비가 끝나면 기존 설정 객체를 **원자적으로 교체**한다. 이미 진행 중인 요청은 옛 설정으로 끝나고, 새 요청부터 새 설정이 적용된다.
4. Envoy가 컨트롤 플레인에 **ACK**(또는 거부 시 NACK)를 보낸다.
이 4단계가 "왜 무중단인가"의 전부다. 핵심은 2단계의 **별도 준비**다 — 새 설정을 옛 설정 *옆에* 따로 세워 미리 데우고, 다 데워진 다음에야 포인터 하나를 원자적으로 바꾼다. 그래서 동적 변경은 "프로세스를 재시작"하는 게 아니라 "프로세스 내부의 설정 객체를 바꿔치우는" 것이다. 소켓·연결 상태는 그대로 유지되고, 교체 순간에도 in-flight 요청은 자기가 시작한 옛 설정 위에서 끝까지 처리된다.
```mermaid
flowchart LR
CP[Control Plane
istiod] -- gRPC ADS push --> E[Envoy proxy]
subgraph E
W[warm new config
DNS/health resolve] --> S[atomic swap
old to new]
S --> ACK[send ACK/NACK]
end
ACK -- version tracked --> CP
Live[in-flight requests] -. finish on old config .-> S
```
> 트리거는 무엇인가: Istio에서는 사용자가 `VirtualService`·`DestinationRule` 등 CR을 바꾸거나, **엔드포인트가 변할 때**(Pod 추가/삭제 → EndpointSlice 변화)다. istiod가 이를 감지해 영향받는 프록시에 해당 계층만 푸시한다. CR이 입력이고 Envoy config가 진실인 모델은 [CR-xDS 멘탈모델](xds__src-cr-xds-model.html) 참고.
## 4. 무엇을 어느 쪽에 둘 것인가 — 혼합 기준과 xDS API 매핑
전부 동적일 필요는 없다. 판단 기준은 §1의 수명 구분 그대로 — **"이 설정이 런타임에 바뀌는가, 부팅 후 불변인가"** 다.
| 계층 | 정적/동적 | 이유 |
|---|---|---|
| node ID, admin, stats sink | **정적** | 프로세스 정체성. 런타임에 바뀔 일 없음 |
| xDS 채널 자체(`xds_cluster`) | **정적** | 동적 설정을 받으려면 그 통로는 먼저 부트스트랩에 있어야 함(닭-달걀) |
| Listener (LDS) | 보통 **동적** | 포트/필터체인이 정책에 따라 바뀜 |
| Route (RDS) | **동적** | 가중치/리트라이/타임아웃이 가장 자주 바뀜 |
| Cluster (CDS) | **동적** | 서비스 추가/삭제, circuit breaker 설정 변경 |
| Endpoint (EDS) | **동적** | Pod 스케일·롤링업데이트로 IP가 끊임없이 변함 |
실무 멘탈모델: **xDS로 들어가는 입구(부트스트랩의 ads_config + xds_cluster)는 반드시 정적, 그 너머의 트래픽 설정은 전부 동적.**
그리고 "동적화한다"는 건 추상이 아니라 구체적 구현을 가진다 — 그 계층을 **어떤 xDS API로 구독하는가**다. 각 "x"가 위 표의 한 계층에 1:1 대응한다.
| API | 동적화 대상 | Istio에서의 의미 |
|---|---|---|
| **LDS** | Listener(포트·필터체인) | 15001/15006 등 캡처 listener, mTLS 필터 |
| **RDS** | RouteConfiguration | `VirtualService`의 host/path/weight 매핑 |
| **CDS** | Cluster | `direction\|port\|subset\|fqdn` 규칙의 서비스 그룹 |
| **EDS** | ClusterLoadAssignment | cluster에 속한 실제 Pod IP 목록 |
| **SDS** | Secret(TLS 인증서) | mTLS workload cert, gateway cert |
| **ADS** | 위 전부를 단일 gRPC 스트림으로 | 의존 순서 보장(route가 가리키는 cluster가 먼저) |
ADS가 핵심인 이유는 **계층 간 의존 순서** 때문이다. RDS의 route가 아직 도착 안 한 cluster를 가리키면 그 트래픽은 갈 곳이 없다(NR 같은 response flag). 각 API를 별도 스트림으로 받으면 도착 순서를 보장할 수 없다. ADS는 LDS/RDS/CDS/EDS/SDS를 **하나의 gRPC 스트림**으로 묶어 "cluster 먼저, 그 다음 endpoint, 그 다음 route" 순서를 강제한다. Istio의 istiod는 항상 ADS로 푸시한다.
```mermaid
flowchart TD
ADS[ADS single gRPC stream] --> CDS
ADS --> EDS
ADS --> LDS
ADS --> RDS
ADS --> SDS
CDS[CDS: cluster groups] --> RDS[RDS: route to cluster]
EDS[EDS: pod IPs] --> CDS
SDS[SDS: TLS certs] --> LDS[LDS: listener filters]
RDS --> Traffic[serve request]
LDS --> Traffic
```
각 계층의 해부와 진단(어느 계층이 비었는지 `istioctl proxy-config`로 보는 법)은 [xDS API 계층](xds__note-xds-api-layers.html), 동기화 상태(SYNCED/STALE) 판독은 [데이터 플레인 sync 상태](xds__note-data-plane-sync-state.html), 실제 적용 설정 덤프는 [Envoy Admin API 진단](xds__note-envoy-admin-api-diagnosis.html) 참고.
## 5. 떴는지 한 번 확인 — Istio 사이드카 부트스트랩이 곧 교과서
이론을 실물로 검증해 보자. Istio 사이드카의 부트스트랩(`/etc/istio/proxy/envoy-rev0.json`)을 떠보면 §4의 패턴이 **정확히** 박혀 있다 — 정적부에 istiod로의 gRPC cluster와 admin·stats만 있고, listener/route/cluster/endpoint는 전부 ADS 구독이다.
```bash
# Istio 사이드카의 부트스트랩에서 정적 부분만 보기
kubectl exec deploy/sleep -c istio-proxy -- \
cat /etc/istio/proxy/envoy-rev0.json | jq '.static_resources.clusters[].name'
# 기대 출력(발췌): "prometheus_stats", "agent", "sds-grpc", "xds-grpc", "zipkin"
# → 트래픽용 outbound|*|* cluster는 여기 없음(전부 동적 CDS로 들어옴)
```
읽는 법: 정적 cluster 목록에 `xds-grpc`(컨트롤 플레인 채널)·`sds-grpc`(인증서 채널)·`prometheus_stats`/`zipkin`(관측)·`agent`만 보이고, **실제 트래픽이 향하는 `outbound|||` cluster는 단 하나도 없다.** 그것들은 전부 런타임에 CDS로 흘러든다. 즉 "정적 = 통로, 동적 = 트래픽"이라는 한 장이 부트스트랩 파일 한 줄 한 줄로 증명된다.
> 부분 동적화(파일 기반으로 LDS/RDS만 동적, CDS는 STRICT_DNS 정적)도 학습용으로 유효하다. 다만 파일 기반 xDS는 **move 교체만 감지**되고 **EDS는 클러스터당 CLA 1개** 제약이 있어 운영엔 부적합하다 — [파일 기반 xDS 제약](xds__note-file-based-xds-constraints.html)에 위임.
## 핵심 정리
- **정적 = 부트스트랩 1회 로드 후 불변, 동적 = 컨트롤 플레인이 런타임 푸시로 무중단 교체.** 둘로 가르는 기준은 설정의 수명(프로세스 정체성 vs 트래픽 정책)이다.
- 정적의 한계(재시작 필요·스케일 미반영·중앙관리 불가)가 동적 설정의 존재 이유다. 동적화란 인라인(`route_config`/`load_assignment`)을 끊고 외부 config source로 위임하는 것.
- 동적의 무중단은 **warming + atomic swap** 덕분 — 새 설정을 옆에 따로 데운 뒤 포인터만 원자 교체, in-flight 요청은 옛 설정으로 끝난다.
- **혼합 기준**: xDS로 들어가는 입구(ads_config + xds_cluster)와 프로세스 정체성은 정적, 그 너머 listener/route/cluster/endpoint/secret은 동적. 동적화 = LDS/RDS/CDS/EDS/SDS 중 해당 API로 구독.
- **ADS**가 이를 단일 gRPC 스트림으로 묶어 의존 순서(cluster→endpoint→route)를 보장한다. Istio 사이드카 부트스트랩은 이 패턴의 교과서: 정적부엔 xds-grpc/sds-grpc/stats만, 트래픽 설정은 전부 ADS.
## What you might be missing
- **"동적 = 재시작 없음"의 예외**: listener의 일부 변경(예: `listener_filters` 핵심 구조)이나 부트스트랩에만 존재하는 필드를 바꾸려면 결국 프록시 교체가 필요하다. 모든 변경이 hot-swap 되는 건 아니다. 또한 cluster warming이 끝나기 전 push가 또 오면 가장 최신 버전만 살아남는다.
- **NACK은 조용한 실패다**: 잘못된 동적 설정을 푸시하면 Envoy가 NACK하고 **직전 정상 설정을 유지**한다. 클러스터는 멀쩡해 보이는데 새 설정이 반영 안 되는 상황 — `istioctl proxy-status`에서 STALE/해당 버전 불일치로 드러난다. 적용했다고 끝이 아니라 sync 상태를 봐야 한다.
- **정적 부트스트랩은 사라지지 않는다**: Istio를 써도 부트스트랩은 여전히 정적이다. istiod 주소를 잘못 주입하거나 SDS 소켓 경로가 어긋나면 동적 설정을 한 줄도 못 받는다. "프록시가 통째로 비어있다"면 동적 CR이 아니라 정적 부트스트랩(특히 xds_cluster) 부터 의심하라.
- **닭-달걀 부트스트랩**: 동적 설정을 받는 통로(xDS gRPC cluster) 자체는 동적일 수 없다. 이 한 조각만은 영원히 정적이며, 여기에 의존하는 SDS(인증서)까지 부트스트랩에 정적 grpc로 박혀 있다.
- **파일 기반 xDS의 함정에 시간 낭비 금지**: 학습 목적이라면 파일 EDS 삽질 대신 Istio on kind에서 `istioctl proxy-config endpoints`로 실제 ADS를 관찰하는 편이 빠르다 — [파일 기반 xDS 제약](xds__note-file-based-xds-constraints.html)의 결론.