🏠 목록 Envoy Admin API(config_dump/clusters/stats)는 데이터 플레인이 실제로 적용한 설정과 런타임 상태를 보는 1차 진단 도구다 📄 MD 원본 🌓 테마
istioenvoyxdsadmin-apidiagnosis

Envoy Admin API(config_dump/clusters/stats)는 데이터 플레인이 실제로 적용한 설정과 런타임 상태를 보는 1차 진단 도구다

NOTE

istioctl·Kiali·access log가 모두 "추상화된 뷰"라면, Envoy Admin API는 데이터 플레인의 그라운드 트루스(ground truth) 다. istiod가 "의도한 설정"과 Envoy가 "실제로 적용한 설정"이 갈라질 때(STALE/NACK), 그 갈라짐은 결국 이 Admin API에서만 확정된다. 이 노트는 "Admin API가 무엇을 보여주고 왜 1차 도구인가" 의 멘탈모델을 세우고, config_dump·/clusters·/stats 세 면을 "설정 → 멤버 health → 트래픽 결과"의 인과로 묶어 읽는 법을 다룬다.

⚠️ 버전 skew 주의: 이 환경은 istiod 1.30.0 ↔ 로컬 istioctl 1.27.0이다. proxy-status의 VERSION 컬럼이나 proxy-config 일부 필드가 어긋나 보일 수 있으나 이는 client 표시 한계이며 메시 동작 이상이 아니다. 정밀 진단은 버전을 맞춘 1.30 istioctl 사용 권장.


01. 배경 — "설정을 줬다"와 "설정대로 돈다"는 다른 사건이다

Istio는 선언형이다. CRD(VirtualService, DestinationRule…)를 apply하면 istiod가 그걸 Envoy 설정으로 번역해 xDS로 각 프록시에 push한다. 그래서 디버깅할 때 우리는 본능적으로 CRD(의도) 를 들여다본다. 그런데 트래픽이 실패할 때 진짜 물어야 할 질문은 다르다 — "내가 의도한 그 설정이 지금 이 Envoy 안에 실제로 들어가 있고, 그 endpoint가 살아있고, 요청이 어디서 깨지나?"

이 질문이 중요한 이유는 xDS가 eventually consistent이기 때문이다. istiod와 Envoy는 비동기 스트림으로 연결돼 있고, push → ACK 사이엔 시간차가 있다. push가 실패하거나(연결 끊김), Envoy가 설정을 거부하거나(NACK), endpoint warming이 안 풀리면 — CRD는 완벽한데 Envoy 안엔 옛 설정이 박혀 있는 상태가 생긴다. CRD만 봐선 이걸 절대 못 잡는다. CRD는 "내 의도"일 뿐 "데이터 플레인의 현실"이 아니기 때문이다.

그래서 진단엔 세 개의 분리된 검증 축이 필요하다.

검증 축 무엇을 답하나 도구
의도 검증 내 CRD가 문법·정합성에 맞나 istioctl analyze
전달 검증 istiod가 push했고 Envoy가 ACK했나 istioctl proxy-status
적용 검증 그래서 Envoy 안에 지금 뭐가 들어 있나 Envoy Admin API

세 축이 모두 일치할 때만 "설정대로 동작 중"이라 단정할 수 있다. 그리고 마지막 적용 검증의 최종심급이 Admin API다 — 다른 무엇도 Envoy 내부를 직접 보여주지 못한다.

선행 개념: xDS 5계층(LDS/RDS/CDS/EDS/SDS), cluster 이름 규칙 direction|port|subset|fqdn, sync 상태값(SYNCED/STALE/NOT SENT). 5계층 운영 매핑은 xDS 5계층과 istioctl 진단, sync 상태값은 데이터 플레인 sync 상태에 위임한다.


02. 멘탈모델 ANCHOR — Admin API는 추상화 사다리의 맨 밑이다

★ 한 장의 그림

Istio 진단 도구는 추상화 사다리를 이룬다. 위로 갈수록 사람이 읽기 쉽고, 아래로 갈수록 프록시가 실제로 하는 일에 가깝다. 위의 모든 도구는 결국 맨 밑 Admin API(또는 동일 소스인 xDS) 위에 얹힌 가공물이다. 가공 과정에서 정보가 빠지거나 도구 버전이 어긋날 때, "진짜 Envoy 안에 뭐가 들어 있나"를 확정하는 최종심급이 맨 밑 칸이다.

Kiali / Grafana          ← 메트릭 집계 뷰 (mesh 전체 그래프)
istioctl analyze         ← istiod가 "의도"를 검증 (CRD 정합성)
istioctl proxy-status    ← 전달(delivery) 검증 (ACK/NACK 받았나)
istioctl proxy-config    ← Envoy 설정을 사람이 보기 좋게 가공
Envoy Admin API          ← Envoy 안의 raw 설정·런타임 상태 (ground truth)  ★

이 사다리가 진단 전략의 전부다. istioctl proxy-config는 내부적으로 Envoy의 config_dump를 호출해 보기 좋게 포맷팅·필터링한 것이고, proxy-status는 istiod가 가진 push 기록과 Envoy의 ACK를 대조한 것이다. 즉 상위 도구는 편의를 위해 raw를 깎아낸다 — 그래서 빠른 답엔 좋지만, 깎인 정보(warming 상태, outlier 통계, filter chain 세부)가 바로 원인일 땐 무력하다. 그때 맨 밑으로 내려간다.

핵심 멘탈모델은 이렇게 닫힌다. proxy-status가 "보냈는데 ACK 못 받음(STALE)" 을 알려주면, config_dump가 "그래서 지금 Envoy 안엔 옛날 설정이 있다" 를 증명한다. 전자는 istiod의 관점, 후자는 Envoy의 관점 — eventually consistent의 두 끝을 각각 확정하는 것이다.


03. Admin endpoint는 어디에 — localhost:15000과 포트 분리의 설계

Istio istio-proxy(sidecar ingress/egress gateway)에서 Envoy Admin API는 127.0.0.1:15000 에 바인딩된다. 127.0.0.1(localhost) 바인딩이 설계의 핵심이다 — admin 인터페이스는 인증이 없고 /quitquitquit(Envoy 종료)·/drain_listeners·/logging 같은 상태를 바꾸는 위험 엔드포인트를 포함하므로, Pod 네트워크 밖으로 절대 새면 안 된다. localhost 바인딩이 1차 방어선이고, 그래서 같은 Pod의 app 컨테이너(또는 kubectl exec로 istio-proxy 컨테이너 내부)에서만 닿는다.

+--------------------------- Pod (app + istio-proxy) ---------------------------+
|                                                                              |
|  app container                 istio-proxy (Envoy) container                 |
|     |  curl localhost:15000 ------> 127.0.0.1:15000  Admin API (no auth)      |
|                                     15001  outbound capture listener          |
|                                     15006  inbound capture listener           |
|                                     15020  merged metrics / health (Pilot agent)|
|                                     15021  status (readiness)                  |
|                                     15090  envoy prometheus (/stats/prometheus)|
+------------------------------------------------------------------------------+

왜 Istio가 Envoy의 한 포트(15000)에서 다 하지 않고 원포트들을 쪼개 뒀나 — 보안 표면을 면(面)별로 분리하려는 것이다. 15000(admin)은 위험하니 localhost-only로 가두고, 외부 Prometheus가 긁어야 하는 메트릭은 15090(/stats/prometheus) 으로, 헬스/probe는 15020/15021 로 따로 뺐다. 즉 15090은 15000의 /stats 중 안전한 일부만 외부 노출하는 전용 창구다 — 같은 데이터, 다른 목적, 다른 노출 정책.

접근 경로는 세 가지다.

# (1) sidecar 컨테이너 내부에서 직접 — 가장 원본
kubectl exec deploy/sleep -c istio-proxy -n default -- \
  curl -s localhost:15000/config_dump | jq '.configs | length'
# 기대: 7~8 (BootstrapConfigDump, ClustersConfigDump, ListenersConfigDump,
#             RoutesConfigDump, SecretsConfigDump, ... 의 개수)

# (2) istioctl이 감싼 형태 — admin API를 호출해 가공
istioctl proxy-config clusters deploy/sleep -n default -o short

# (3) 브라우저로 admin UI 전체를 보고 싶을 때 (localhost:15000 포워드)
istioctl dashboard envoy deploy/sleep.default
# → http://localhost:15000 로 config_dump/clusters/stats 링크 제공
⚠ 함정 — "admin = 15000"은 istio-proxy 관례일 뿐

비-Istio standalone Envoy(예: 정적/동적 xDS 실습)는 부트스트랩 YAML이 admin 포트를 직접 정하므로 흔히 9901(실습 설정에 따라 15000일 수도)이다. Istio sidecar와 gateway가 둘 다 15000인 건 istio-proxy가 같은 부트스트랩 템플릿을 쓰기 때문이지 Envoy의 보편 규칙이 아니다. standalone Envoy에선 그 부트스트랩의 admin.address.socket_address.port_value를 봐야 한다.


04. 세 면을 묶어 읽는 인과 — config_dump → /clusters → /stats

여기가 이 노트의 심장이다. Admin API의 세 엔드포인트는 따로 보는 게 아니라 "설정 → 멤버 health → 트래픽 결과" 의 인과 순서로 묶어 읽는다. 같은 cluster를 세 각도에서 보는 것이다 — config_dump는 정의, /clusters는 그 정의의 멤버가 살아있나, /stats는 그 멤버로 보낸 요청이 어떻게 됐나.

증상: 503 / 실패proxy-statusSTALE / 누락?config_dump설정 존재? warming?/statsrq_5xx / connect_fail/clustersendpoint HEALTHY? eject?원인 확정SYNCED인데 실패STALE/누락설정 있음0/ejectwarming/0/HEALTHY인데 실패 → /stats
그림 1. 503 진단 순서: proxy-status(전달)→ config_dump(설정 유무·warming)→ /clusters(endpoint 건강·eject)→ 다양한 분기가 /stats(rq_5xx·connect_fail)로 모여 원인 확정.

config_dump — 부트스트랩 + 동적 설정의 현재 스냅샷

/config_dump는 Envoy가 지금 이 순간 들고 있는 전체 설정을 JSON으로 토해낸다. xDS 5계층(LDS/RDS/CDS/EDS/SDS)이 각각 별도 섹션으로 들어가고, 더 중요한 건 각 동적 리소스를 상태(state)별로 구분해 보여준다는 점이다.

config_dump.configs[]:
  BootstrapConfigDump   ← Envoy가 시작할 때 받은 고정 설정 (어느 istiod, node metadata)
  ClustersConfigDump
    static_clusters     ← 부트스트랩에 박힌 cluster (예: xds-grpc=istiod, prometheus_stats)
    dynamic_active_clusters   ← CDS로 받아 "현재 활성"인 cluster   ★ 진짜 적용된 것
    dynamic_warming_clusters  ← 받았지만 아직 warming(endpoint/DNS 대기) — 트래픽 못 받음
  ListenersConfigDump   ← static / dynamic_active / dynamic_warming 동일 구조
  RoutesConfigDump      ← RDS route (dynamic_route_configs)
  SecretsConfigDump     ← SDS cert/key/CA (값은 마스킹, 메타데이터만)

진단에서 결정적 구분은 active vs warming 이다. 왜 Envoy가 cluster를 warming 단계에 묶어 두냐면 — endpoint나 DNS가 준비되기 전에 트래픽을 흘리면 즉시 실패하기 때문이다. CDS는 도착했지만 EDS endpoint나 DNS 해석을 못 받은 cluster는 dynamic_warming_clusters에 머물고, 그 cluster로 가는 트래픽은 실패한다. proxy-config cluster의 요약 출력은 cluster가 "있다"고만 나오므로, active인지 warming에 갇혔는지는 raw config_dump로만 갈린다.

# 특정 리소스만 떠서 노이즈 줄이기 (Envoy가 지원하는 resource 필터)
kubectl exec deploy/sleep -c istio-proxy -- \
  curl -s 'localhost:15000/config_dump?resource=dynamic_active_clusters' \
  | jq '.configs[].cluster.name' | head
# 기대 예: "outbound|8000||httpbin.default.svc.cluster.local"

# warming에 갇힌 cluster가 있나? (있으면 EDS/DNS 문제 의심)
kubectl exec deploy/sleep -c istio-proxy -- \
  curl -s localhost:15000/config_dump \
  | jq '.. | .dynamic_warming_clusters? // empty'
# 기대: 빈 결과(정상). 무언가 나오면 warming 정체 → /stats로 교차확인

bootstrap 섹션은 "이 Envoy가 어느 istiod에 붙어 시작했는가, node ID/metadata가 무엇인가"를 답한다 — injection 문제·잘못된 istiod 연결을 추적할 때 istioctl proxy-config bootstrap으로 같은 데이터를 본다.

/clusters — 그 cluster의 endpoint가 지금 살아있나

/config_dump가 "설정이 무엇인가"라면, /clusters"그 cluster의 엔드포인트가 지금 살아있나" 라는 런타임 상태를 준다. 같은 cluster라도 config_dump는 정의를, /clusters는 멤버 IP별 health/통계를 보여준다.

kubectl exec deploy/sleep -c istio-proxy -- \
  curl -s 'localhost:15000/clusters?format=json' \
  | jq '.cluster_statuses[]
        | select(.name=="outbound|8000||httpbin.default.svc.cluster.local")
        | {name, host: .host_statuses[].address.socket_address,
           health: .host_statuses[].health_status}'

기대 출력(정상 — 엔드포인트가 healthy):

{
  "name": "outbound|8000||httpbin.default.svc.cluster.local",
  "host": { "address": "10.244.1.12", "port_value": 8000 },
  "health": { "eds_health_status": "HEALTHY" }
}

여기서 봐야 할 신호:

/listeners는 더 단순하다 — 현재 바인딩된 listener 주소 목록(0.0.0.0:15001 등)을 주어, 기대한 포트의 listener가 실제로 떠 있는지 확인한다.

kubectl exec deploy/sleep -c istio-proxy -- curl -s localhost:15000/listeners | head
# 기대: virtualOutbound 0.0.0.0:15001, virtualInbound 0.0.0.0:15006, port별 listener ...

이 단계가 라우팅 체인의 cluster→endpoint 구간이다. listener→route→cluster→endpoint 전체 체인을 istioctl로 짚어 내려가는 절차는 Envoy 라우팅 체인 디버깅에서 다룬다.

/stats — 요청·실패를 카운터로 관측

/stats는 Envoy의 모든 카운터·게이지·히스토그램을 평문으로 쏟아낸다(수백~수천 줄). 진단은 이름 prefix로 grep해서 관심 축만 본다. Istio cluster 이름 규칙(direction|port|subset|fqdn)이 그대로 stat 키에 박히므로, 특정 upstream의 요청·에러를 정확히 좁힐 수 있다 — 이게 config_dump/clusters와 stats를 같은 cluster로 꿰는 접착제다.

# 특정 upstream cluster의 요청/에러 카운터만
kubectl exec deploy/sleep -c istio-proxy -- \
  curl -s 'localhost:15000/stats?filter=outbound.8000.*httpbin' \
  | grep -E 'upstream_rq_(total|2xx|4xx|5xx)|upstream_cx_connect_fail|pending_overflow'

기대 출력(예):

cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_total: 42
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_2xx: 40
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_5xx: 2
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_connect_fail: 0

진단에 자주 쓰는 stat 축:

stat prefix 무엇을 말하나 비정상 신호
cluster.<name>.upstream_rq_5xx 그 cluster로 보낸 요청 중 5xx 증가 = 백엔드 장애 또는 mTLS 실패
cluster.<name>.upstream_cx_connect_fail TCP 연결 자체 실패 endpoint 죽음/네트워크/방화벽
cluster.<name>.upstream_rq_pending_overflow connection pool circuit breaker 발동 Circuit breaking 한도 초과
cluster.<name>.ssl.connection_error upstream TLS/mTLS handshake 실패 SDS cert 문제, mode 불일치
listener.0.0.0.0_15006.* inbound listener 처리량 downstream 연결 거부 흐름
server.* / control_plane.* Envoy 자기 상태·xDS 연결 control_plane.connected_state=0 = istiod 미연결
# istiod 연결 살아있나 (proxy-status에서 누락된 proxy를 Envoy 쪽에서 교차확인)
kubectl exec deploy/sleep -c istio-proxy -- \
  curl -s localhost:15000/stats | grep -E 'control_plane.(connected_state|rq_total)'
# 기대: control_plane.connected_state: 1  (0이면 sidecar가 istiod에 안 붙음)

control_plane.connected_state가 0이면, 이는 proxy-status에서 그 proxy가 목록에서 누락되는 현상의 Envoy 쪽 근거다 — istiod 연결이 끊긴 동안 push를 못 받으니 config_dump도 옛날 설정으로 고정된다. 운영용 메트릭은 보통 15000이 아니라 15090의 /stats/prometheus 를 Prometheus가 긁는다 — 진단은 15000에서 한 Envoy를 깊게, 모니터링은 15090에서 전체를 집계.


05. 예시 — 503 한 건을 세 면으로 끝까지 좁히기

세 엔드포인트가 어떻게 인과로 이어지는지, 전형적 분기를 따라가 본다.

가장 흔한 황금 경로를 한 줄로 묶으면:

proxy-status SYNCED → config_dump active(=warming 아님) → /clusters HEALTHY → /stats rq_5xx↑
  ⇒ 설정·endpoint는 멀쩡, 문제는 백엔드 응답 또는 mTLS handshake. 5xx vs ssl.connection_error로 분기.

응답 flag(NR/NC/UH/UF)로 어느 면부터 볼지 시작점을 좁히는 표는 xDS 5계층과 istioctl 진단에 정리돼 있다.


핵심 정리

멘탈모델   = 진단 도구는 추상화 사다리. 상위(Kiali/proxy-config/proxy-status)는 전부 맨 밑 Admin API의 가공물.
            Admin API = 데이터 플레인 ground truth, 적용 검증의 최종심급.
세 검증 축 = 의도(analyze) / 전달(proxy-status) / 적용(Admin API). 셋이 일치할 때만 "설정대로 동작".
노출       = istio-proxy localhost:15000 (auth 없음 → localhost-only). 메트릭 15090, probe 15020/15021로 분리.
config_dump = 현재 설정 스냅샷. dynamic_active vs dynamic_warming 구분이 핵심(warming=트래픽 못 받음).
/clusters   = endpoint health (HEALTHY / outlier eject / 0개=503 UH). config_dump가 정의면 이건 런타임.
/stats      = 카운터로 결과 관측. upstream_rq_5xx / cx_connect_fail / control_plane.connected_state.
읽는 순서   = proxy-status(전달) → config_dump(설정) → /clusters(멤버 health) → /stats(트래픽 결과).

What you might be missing