🏠 목록 Envoy 정적 설정은 부트스트랩 시점에 고정되고 동적 설정은 컨트롤 플레인이 런타임에 푸시해 재시작 없이 반영한다 📄 MD 원본 🌓 테마
istioenvoyxds

Envoy 정적 설정은 부트스트랩 시점에 고정되고 동적 설정은 컨트롤 플레인이 런타임에 푸시해 재시작 없이 반영한다

NOTE

머릿속에 담을 한 장: 정적은 "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)를 설정으로 주입받아야만 동작한다. 문제는 그 설정이 두 종류의 수명을 갖는다는 점이다.

이 둘을 같은 메커니즘으로 다루면 한쪽이 다른 쪽을 망친다. 정체성을 자주 바꿀 일은 없는데 트래픽 정책 하나 바꾸겠다고 프로세스를 통째로 재시작하면 진행 중 연결이 끊긴다. 반대로 정체성까지 런타임 푸시에 맡기면 "푸시를 받는 통로 자체"를 받을 길이 없는 부트스트랩 역설에 빠진다.

그래서 Envoy는 설정을 로딩 시점으로 가른다. 부팅 때 한 번 읽고 굳히는 정적(static), 컨트롤 플레인이 런타임에 흘려보내는 동적(dynamic). 이 문서를 읽기 전 알아둘 선행 개념은 셋뿐이다 — listener(진입점)·route(목적지 규칙)·cluster(백엔드 그룹)라는 데이터 평면의 3요소, 그리고 그것을 외부에서 밀어주는 프로토콜이 xDS라는 것. 그 위에서 "무엇이 정적이고 무엇이 동적인가"가 이 문서의 전부다.

2. 정적 부트스트랩: listener-route-cluster를 한 파일에 박제

먼저 동적이 없는 세계를 보면 동적의 존재 이유가 선명해진다. 가장 단순한 Envoy는 부트스트랩 YAML 하나로 데이터 평면 전체를 정의한다. static_resources 아래에 listener(트래픽 진입점) → route(어디로 보낼지) → cluster(백엔드 그룹)를 모두 적고, 프로세스는 부팅 시 이 파일을 1회 읽어 인메모리 설정으로 굳힌다. 모든 것이 파일에 박혀 있고, 파일은 부팅 후 불변이다.

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에서 다룸), 그 전에 이 인라인 구조의 세 가지 한계가 곧 동적 설정을 요구하는 동기다.

세 한계는 모두 같은 뿌리를 가진다 — 설정이 데이터 평면 프로세스 안에 갇혀 있고, 그걸 바꾸는 유일한 손잡이가 "재시작"뿐이라는 것. 동적 설정은 이 손잡이를 "런타임 교체"로 바꾼다.

책(Istio in Action Ch.3.2)은 Envoy v3 설정 API(typed_config)만 다룬다. v1/v2는 폐기됐다. 정적 부트스트랩 실습 전문은 정적/동적 xDS 실습 참고.

3. 동적 설정의 메커니즘: 인라인을 끊고, 받아서, 무중단 교체

동적 설정의 핵심은 설정의 출처를 listener 안 인라인이 아니라 외부 "config source"로 위임하는 것이다. §2의 HCM이 라우트를 인라인(route_config) 대신 rds로 구독하면, 라우트는 부트스트랩 밖에서 흘러들어온다. cluster도 cds_config로, 엔드포인트도 EDS로 위임할 수 있다. 부트스트랩에는 이제 "어디서 받아올지"만 남는다.

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 요청은 자기가 시작한 옛 설정 위에서 끝까지 처리된다.

Control PlaneistiodEnvoy proxywarm new configDNS/health resolveatomic swapold → newsend ACK/NACKgRPC ADSversion tracked → CPin-flight는 old에서 완료
그림 1. 동적 xDS 갱신: istiod가 ADS로 push → Envoy가 새 config를 warm(DNS/health resolve) → atomic swap → ACK(version 추적). in-flight 요청은 old config에서 마저 처리되어 무중단.

트리거는 무엇인가: Istio에서는 사용자가 VirtualService·DestinationRule 등 CR을 바꾸거나, 엔드포인트가 변할 때(Pod 추가/삭제 → EndpointSlice 변화)다. istiod가 이를 감지해 영향받는 프록시에 해당 계층만 푸시한다. CR이 입력이고 Envoy config가 진실인 모델은 CR-xDS 멘탈모델 참고.

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로 푸시한다.

ADS single streamCDScluster groupsEDSpod IPsRDSroute→clusterLDSlistener filtersSDSTLS certsserve requestEDS→CDSCDS→RDSSDS→LDSRDS/LDS → serve
그림 2. 단일 ADS stream이지만 xDS는 의존 순서가 있음: EDS가 CDS를, CDS가 RDS를, SDS가 LDS를 채운 뒤 RDS·LDS가 결합되어 요청을 처리. 순서가 어긋나면 warming/STALE.

각 계층의 해부와 진단(어느 계층이 비었는지 istioctl proxy-config로 보는 법)은 xDS API 계층, 동기화 상태(SYNCED/STALE) 판독은 데이터 플레인 sync 상태, 실제 적용 설정 덤프는 Envoy Admin API 진단 참고.

5. 떴는지 한 번 확인 — Istio 사이드카 부트스트랩이 곧 교과서

이론을 실물로 검증해 보자. Istio 사이드카의 부트스트랩(/etc/istio/proxy/envoy-rev0.json)을 떠보면 §4의 패턴이 정확히 박혀 있다 — 정적부에 istiod로의 gRPC cluster와 admin·stats만 있고, listener/route/cluster/endpoint는 전부 ADS 구독이다.

# 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|<port>||<fqdn> cluster는 단 하나도 없다. 그것들은 전부 런타임에 CDS로 흘러든다. 즉 "정적 = 통로, 동적 = 트래픽"이라는 한 장이 부트스트랩 파일 한 줄 한 줄로 증명된다.

부분 동적화(파일 기반으로 LDS/RDS만 동적, CDS는 STRICT_DNS 정적)도 학습용으로 유효하다. 다만 파일 기반 xDS는 move 교체만 감지되고 EDS는 클러스터당 CLA 1개 제약이 있어 운영엔 부적합하다 — 파일 기반 xDS 제약에 위임.

핵심 정리

What you might be missing