devlos
Devlos Archive
devlos
전체 방문자
오늘
어제
10-13 00:01

최근 글

  • 분류 전체보기 (131)
    • 프로젝트 (1)
    • MSA 설계 & 도메인주도 설계 (9)
    • AI (15)
      • [LLMSO] Hands-On LLM 서빙과 최적.. (15)
    • 클라우드 컴퓨팅 & NoSQL (96)
      • [K8S Deploy] K8S 디플로이 스터디 (8)
      • [Cilium Study] 실리움 스터디 (8)
      • [KANS] 쿠버네티스 네트워크 심화 스터디 (12)
      • [T101] 테라폼 4기 스터디 (8)
      • [CICD] CICD 맛보기 스터디 (3)
      • [T101] 테라폼 기초 입문 스터디 (6)
      • [AEWS] Amazon EKS 워크숍 스터디 (7)
      • [PKOS] 쿠버네티스 실무 실습 스터디 (7)
      • Kubernetes (13)
      • Docker (7)
      • Redis (1)
      • Jenkins (3)
      • Terraform (1)
      • Ansible (4)
      • Kafka (1)
    • 프로그래밍 (7)
      • Spring Boot (5)
      • Broker (1)
    • 성능과 튜닝 (1)
    • ALM (0)
    • 기타 (2)

인기 글

태그

  • ansible
  • DevOps
  • LLMSO
  • kOps
  • Kubernetes
  • MSA
  • 쿠버네티스 스터디
  • LLM 서빙
  • 도커
  • CloudNet@
  • 데브옵스
  • docker
  • t101 4기
  • K8s
  • 쿠버네티스

티스토리

최근 댓글

hELLO · Designed By 정상우.
devlos

Devlos Archive

[7주차-LLMSO] Envoy: Agent Router (2026.09.13)
AI/[LLMSO] Hands-On LLM 서빙과 최적화 스터디

[7주차-LLMSO] Envoy: Agent Router (2026.09.13)

2026. 9. 20. 02:01
반응형
[7W-1] 프록시 하나로 LLM 앞문을 세우기까지 — LLMSO 7주차
7W-1 · Envoy와 Agent Router 들어가며 1 Envoy 2 xDS 3 타임아웃·재시도 4 요청의 일생 5 필터 체인 6 속도 제한 7 AI Gateway 8 토큰 제한 9 ext_proc 10 실습 ① 11 실습 ② 마치며
LLMSO 7주차 · [7W-1] Envoy: Agent Router

프록시 하나로
LLM 앞문을 세우기까지.

OpenAI, Anthropic, 사내 vLLM. 모델이 늘어날수록 앱마다 키를 복사하고, 재시도 로직을 새로 짜고, 비용은 청구서가 와야 압니다.

10년 전 마이크로서비스가 겪은 일과 같은 문제이고, 그때 답이었던 Envoy가 이번에도 가운데에 있습니다. Envoy의 기본기부터 Agent Router(구 Envoy AI Gateway) 실습까지 한 줄로 이어 봅니다.

들어가며

읽는 시간 40분 대상 쿠버네티스는 쓰지만 Envoy 설정은 낯선 개발자 사전 지식 HTTP 요청·응답, 파드와 Service 정도 그림 11개

AI Gateway 문서를 열면 ext_proc, xDS, HCM, ORIGINAL_DST 같은 말이 설명 없이 나옵니다. 전부 Envoy의 말입니다. Envoy를 모르고 읽으면 "AI 게이트웨이가 알아서 해 준다"로 뭉개지고, 막혔을 때 어디를 봐야 할지 모릅니다.

그래서 이번 스터디 자료는 Istio 스터디의 Envoy 정리(책 3장·책 14장)로 시작해서 AI Gateway로 넘어갑니다. 이 페이지도 그 순서를 따릅니다. 앞의 여섯 장이 뒤의 다섯 장을 읽기 위한 사전입니다.

결론부터 세 줄
  1. Envoy는 "요청을 이해하는" 프록시입니다. Listener로 받고, 필터 체인으로 가공하고, Route로 고르고, Cluster로 보냅니다. 재시도·타임아웃·메트릭을 앱 대신 처리하고, 설정은 xDS로 재시작 없이 바뀝니다.
  2. AI Gateway는 이 구조에 "LLM을 아는 판단"을 바깥에서 끼운 것입니다. Envoy 코어는 그대로 두고 ext_proc 필터로 외부 프로세스에 본문을 보여 줍니다. 모델 이름 추출, provider별 형식 변환, 토큰 수 집계가 거기서 일어납니다.
  3. LLM 요청은 요청 수로 세면 틀립니다. 한 건이 수십 토큰일 수도 수만 토큰일 수도 있어서, 속도 제한도 부하 분산도 토큰과 대기열 기준으로 바뀌어야 합니다. 그 마지막 조각(InferencePool·EPP)이 [7W-2] llm-d로 이어집니다.

접는 상자(▸)는 안 열어도 됩니다. 안에는 YAML 설정과 명령어, 원문 출력이 들어 있습니다. 본문만 읽어도 흐름은 끊기지 않습니다.

그림은 만져보라고 넣었습니다. 슬라이더를 끝까지 밀어 보면 설정 하나가 어떤 장애로 번지는지 보입니다. 노란 상자에 "해볼 것"이 적혀 있습니다.

"책 3장"은 『Istio in Action』의 장 번호이고, 그냥 "3장"은 이 페이지의 장입니다.

먼저 말해둘 것 — 버전이 빠르게 바뀝니다

원문의 책 실습은 Envoy v1.19 · Istio 1.17이고, Agent Router 실습은 Envoy Gateway v1.8.1 · Agent Router v1.1.0 · Envoy distroless-v1.38.1로, 5년 차이가 납니다. 개념은 그대로지만 필드 이름과 기본값은 다를 수 있으니 설정을 옮길 때는 쓰는 버전의 문서를 같이 보세요. 프로젝트 이름도 2026년 9월에 Envoy AI Gateway → Agent Router로 바뀌어서, 문서·이미지·CRD 그룹명에 옛 이름이 섞여 있습니다.

먼저 프록시가 무엇을 대신해 주는지부터 봅니다. AI Gateway의 기능 대부분이 이미 여기 있습니다.

01Envoy는 무엇을 대신해 주나

한 줄 요약: Envoy는 앱 옆에서 요청을 읽고 이해하는 L7 프록시이고, 재시도·부하 분산·메트릭처럼 모든 서비스가 똑같이 필요한 일을 대신 합니다.

왜 이걸 배우나
"타임아웃, 재시도, 메트릭 — 이걸 서비스마다 언어별로 또 짜야 하나?"

마이크로서비스가 열 개면 Java, Go, Python 코드에 같은 네트워크 로직이 열 번 들어갑니다. Lyft도 이 문제를 겪었고, 그래서 2016년에 Envoy를 C++로 만들어 오픈소스로 공개했습니다(2017년 CNCF 합류).

Envoy의 첫 번째 원칙은 이것입니다. "네트워크는 앱에게 투명해야 하고, 문제가 생기면 원인을 찾기 쉬워야 한다." 뒤에서 볼 AI Gateway가 LLM 앱들에게 해 주려는 약속과 똑같습니다.

이 장에 나오는 낯선 말
Proxy · Reverse proxy
클라이언트와 서버 사이에 서서 요청을 대신 전달하는 중개자. 서버 앞에서 여러 인스턴스를 하나의 주소로 감추면 reverse proxy라고 부릅니다.
L3/L4 · L7
네트워크 계층 번호. L3/L4 프록시는 IP·포트·연결만 보고, L7 프록시는 HTTP 헤더·경로·gRPC 메서드까지 읽습니다. 요청 단위 재시도는 L7에서만 가능합니다.
Downstream · Upstream
Envoy 기준 방향. 요청을 보내오는 쪽이 downstream(클라이언트), 받아 처리하는 쪽이 upstream(백엔드)입니다.
Listener
Envoy가 포트를 열고 요청을 받는 입구. 예: 15001번 포트 리스너.
Route
들어온 요청을 어느 cluster로 보낼지 정하는 규칙. 예: 경로가 /catalog로 시작하면 catalog cluster로.
Cluster · Endpoint
Cluster는 같은 일을 하는 upstream 묶음(쿠버네티스 Service와 비슷), Endpoint는 그 안의 실제 IP:포트 하나입니다.
Outlier detection · Circuit breaker
연속으로 5xx를 내는 endpoint를 풀에서 잠시 빼는 기능과, 연결·요청 수가 임계값을 넘으면 기다리지 않고 바로 실패시키는 기능입니다.

비유 — 식당의 홀 매니저

손님은 주방에 요리사가 몇 명인지, 누가 지금 바쁜지 모릅니다. 홀 매니저에게 주문만 넣습니다. 매니저는 한가한 요리사에게 주문서를 넘기고, 주문서에 번호를 붙이고, 20분이 넘으면 다시 확인합니다.

Envoy가 그 매니저입니다. 손님(downstream)은 입구(Listener)로 들어오고, 매니저는 메뉴판 규칙(Route)을 보고 주방 파트(Cluster)를 정한 뒤, 그 안의 요리사(Endpoint)를 고릅니다. 매니저가 주문 내용을 읽을 수 있다는 것이 L7 프록시의 핵심입니다.

Envoy가 기본으로 해 주는 것

기능무엇을 하나뒤에서 다시 나오는 곳
Service discovery디스커버리 API에서 endpoint 목록을 받아 옵니다. 앱은 주소를 몰라도 됩니다. 목록은 결국엔 맞춰진다(eventual consistency)는 전제로 설계됐습니다.2장 xDS
Load balancingRandom, (Weighted) Round robin, Weighted least request, Consistent hashing(Ring hash·Maglev), 그리고 locality-aware.이 장 그림 1
Routing호스트·경로·헤더·우선순위로 cluster 선택. 재시도·타임아웃·fault injection도 여기서.3장
Traffic shifting · Shadowing비율로 나눠 보내기(카나리), 그리고 복사본을 보내고 응답은 버리기(fire and forget).—
Resilience요청 타임아웃, 요청 단위 재시도, 연결·요청 수 제한, outlier detection.3장
HTTP/2 · gRPC다운스트림·업스트림 양쪽에서 HTTP/1.1과 HTTP/2를 서로 바꿔 가며 프록시합니다.9장 ext_proc는 gRPC
Metrics · Tracingcounter·gauge·histogram 통계, x-request-id 생성, 트레이스 스팬 보고.3장 Admin API
TLS 종료·시작앱 대신 TLS를 풀고, 앱 대신 upstream으로 TLS(mTLS 포함)를 겁니다.4장 istiod 인증서
Rate limiting연결 단위(네트워크)와 요청 단위(HTTP) 모두에서 외부 속도 제한 서비스와 연동합니다.6장
확장C++ 필터를 컴파일해 넣거나, Lua 스크립트·Wasm 모듈로 덜 침습적으로 확장합니다.5장, 9장
복원력은 "맡길 수 있지만 조정은 앱의 몫"

Envoy가 재시도를 대신 해 주지만, 재시도가 겹치면 장애가 증폭됩니다(retry storm). 그래서 Envoy는 재시도 횟수를 제한할 수 있게 해 두었고, 원문은 "파라미터를 설정하고 잘 조정하는 것은 애플리케이션의 책임"이라고 못 박습니다. 앱 수준 재시도가 완전히 필요 없어지는 것도 아닙니다.

작은 숫자로 한 번

인스턴스 3개(A·B·C)에 요청 6개를 보냅니다. B만 3배 느리다고 합시다(B는 한 번에 한 건씩만 처리한다고 봅니다).

# Round robin — 순서대로 돌린다. 느린지 모른다. 요청 1→A 2→B 3→C 4→A 5→B 6→C B에 2건, B는 건당 3배 걸림 B의 두 번째 요청은 첫 번째가 끝날 때까지 기다린다 → 가장 늦은 응답은 B에서 나온다 # Least request — 지금 처리 중인 요청 수가 적은 곳으로 요청 5가 도착한 순간 A:0 B:1(아직 처리 중) C:0 → B를 피해 A나 C로 결과 A:3 B:1 C:2 느린 B에는 1건만

6건으로는 차이가 작습니다. 요청이 초당 수십 건이 되고 느린 인스턴스가 감당하지 못하면, round robin은 그 인스턴스 앞에 줄을 계속 쌓습니다.

아래 그림 1은 이 상황을 키운 것입니다 — endpoint 4개(ep-1~ep-4), ep-3만 4배 느림, 그리고 endpoint마다 동시에 두 건씩 처리한다고 봅니다.

그림 1

느린 endpoint 하나가 섞였을 때 — LB 알고리즘별 결과

버튼 · 슬라이더
해볼 것 — ① 기본값(Round robin, ep-3이 4배 느림)에서 ep-3의 평균 지연이 몇 초까지 치솟는지 보세요. ② Least request로 바꾸면 ep-3에 가는 건수가 줄고 전체 p99가 확 내려갑니다. ③ 느림 배수를 1로 내리면 Round robin·Random·Least request 셋의 차이가 거의 사라집니다. Ring hash만 평균이 조금 높게 남는데, 느림과 상관없이 배정 자체가 ep-3으로 치우쳐 있어서입니다(⑤). ④ 초당 요청을 60까지 올리면 Least request에서도 p99가 1초를 넘기 시작합니다 — 알고리즘은 용량을 늘려 주지 않습니다. ⑤ Ring hash는 사용자별로 고정 배정이라 ep-3에 걸린 사용자는 계속 느립니다(이 모형은 가상 노드가 적어 ep-3에 요청이 더 몰립니다).
단순화한 모형입니다

endpoint마다 동시 처리 2개, 건당 약 100 ms, 30초 동안 흘린 결과입니다(같은 씨앗으로 고정). 숫자는 관계를 보이기 위한 추정치이고 실측이 아닙니다. Least request는 Envoy 기본 동작처럼 무작위로 두 곳을 뽑아 덜 바쁜 쪽을 고릅니다.

그림에 나오는 말 p99 요청 100건 중 가장 느린 1건 수준의 지연 · 대기 30초가 끝났을 때 아직 처리되지 못하고 줄 서 있는 요청 수 · Ring hash 요청의 키(여기서는 사용자 ID)를 해시해 원 위의 가장 가까운 endpoint로 보내는 방식. 같은 사용자는 늘 같은 곳으로 갑니다
통계 이름 예시와 다른 프록시와의 비교 (원문 표)
통계설명
downstream_cx_total총 커넥션 개수
downstream_cx_http1_active활성 HTTP/1.1 커넥션 개수
downstream_rq_http2_total총 HTTP/2 요청 개수
cluster.<name>.upstream_cx_overflow커넥션 circuit breaker가 임계값을 넘겨 발동한 횟수
cluster.<name>.upstream_rq_retry총 요청 재시도 횟수
cluster.<name>.ejections_detected_consecutive_5xx5xx가 이어져 퇴출로 감지된 횟수(실제로 시행되지 않은 경우 포함)

통계는 StatsD, DogStatsD, Hystrix 형식, generic metrics service로 내보낼 수 있습니다. 분산 트레이싱을 쓰려면 앱이 x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags 헤더를 다음 호출로 전파해야 합니다. 이건 Envoy가 대신 못 합니다.

원문이 꼽는 Envoy의 강점: Wasm 확장, 열린 커뮤니티, 모듈식 코드베이스, 업·다운스트림 HTTP/2, 깊은 프로토콜 메트릭, C++(가비지 컬렉션 없음), 동적 설정으로 hot restart 불필요.

1장 세 줄 요약
  1. 정의 — Envoy는 요청 경로 가운데 끼워 넣는 L7 프록시입니다. Listener(입구) → Route(규칙) → Cluster(묶음) → Endpoint(실제 주소) 순으로 요청이 흐릅니다.
  2. 성질 — HTTP를 읽을 수 있어서 요청 단위 재시도·타임아웃·메트릭이 가능하고, 앱의 언어와 상관없이 같은 기능을 줍니다. 부하 분산은 알고리즘에 따라 느린 endpoint를 피하기도, 못 피하기도 합니다.
  3. 쓰임 — 클러스터 입구(edge), 공유 프록시, 서비스마다 붙는 사이드카(Istio)로 쓰입니다. 뒤에서 볼 AI Gateway의 데이터 플레인도 Envoy입니다.

여기까지는 "Envoy가 무엇을 할 수 있나"였습니다. 그런데 이 규칙들은 어떻게 Envoy에 들어갈까요 — 파일로, 아니면 실시간으로?

02설정은 어떻게 들어가나 — 정적 설정과 xDS

한 줄 요약: Envoy 설정은 YAML 파일로도 되지만, 운영에서는 xDS라는 gRPC 스트림으로 재시작 없이 계속 밀려 들어옵니다. 조각이 따로 오기 때문에 순서가 어긋나면 잠깐 503이 납니다.

왜 이걸 배우나
"파드가 하나 늘 때마다 프록시 설정 파일을 고쳐서 재시작해야 하나?"

1장의 Listener·Route·Cluster를 파일에 적는 건 한두 개일 때 얘기입니다. 쿠버네티스에서는 endpoint가 수시로 바뀌고, 사이드카가 수천 개입니다. 그래서 Envoy는 "어디서 설정을 받아 올지"만 적힌 작은 부트스트랩 파일로 시작하고, 나머지는 서버가 밀어 줍니다.

9장과 10장의 Agent Router도 정확히 이 구조입니다. Envoy Gateway 컨트롤러가 xDS 서버이고, 실습에서 리스너 하나를 추가하면 재시작 없이, 5초 뒤 확인해 보면 이미 Envoy에 나타나 있습니다.

이 장에 나오는 낯선 말
static_resources · bootstrap
bootstrap은 Envoy가 시작할 때 읽는 설정 전체입니다. 그 안의 static_resources에 리스너·클러스터를 직접 적거나, dynamic_resources에 "설정 서버 주소"만 적습니다.
HCM (HTTP connection manager)
바이트를 HTTP 요청·응답으로 바꿔 주는 네트워크 필터. 이 장에서는 이름만 쓰고, 4·5장에서 안쪽을 봅니다.
xDS
"x Discovery Service"의 묶음 이름. x 자리에 L·R·C·E·S·A가 들어갑니다. v3 API는 gRPC 스트리밍이라 서버가 변경을 바로 밀어 줍니다(폴링 불필요).
LDS · RDS · CDS · EDS
각각 Listener · Route · Cluster · Endpoint를 받아 오는 API. RDS는 리스너 설정의 일부, EDS는 클러스터 설정의 일부입니다.
SDS · ADS
SDS는 인증서(secret)를 배포하는 API, ADS(Aggregated)는 나머지 전부를 한 스트림에 순서대로 실어 보내는 API입니다.
Eventual consistency
"지금 당장은 어긋날 수 있지만 결국엔 올바른 상태로 수렴한다"는 약속. 그 사이 틈이 문제입니다.
Race condition
두 작업의 도착 순서에 따라 결과가 달라지는 상황.
NC (response flag)
Envoy 액세스 로그의 응답 플래그. No Cluster — 라우트가 가리키는 클러스터가 없어서 503을 냈다는 뜻입니다.

정적 설정 — 파일 하나에 다 적기

책 실습의 첫 설정은 네 줄로 요약됩니다. 15001 포트에 리스너를 열고, HTTP connection manager 필터를 붙이고, 모든 호스트(*)의 모든 경로를 httpbin_service 클러스터로 보내고, 그 클러스터는 DNS 이름 httpbin:8000을 LOGICAL_DNS로 찾고 ROUND_ROBIN으로 부하 분산합니다.

원문 정적 설정 전체 (simple.yaml)
static_resources:
  listeners:                                  # (1) 리스너 정의
  - name: httpbin-demo
    address:
      socket_address: { address: 0.0.0.0, port_value: 15001 }
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager   # (2) HTTP 필터
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          http_filters:
          - name: envoy.filters.http.router
          route_config:                       # (3) 라우팅 규칙
            name: httpbin_local_route
            virtual_hosts:
            - name: httpbin_local_service
              domains: ["*"]                  # (4) 와일드카드 가상 호스트
              routes:
              - match: { prefix: "/" }
                route:
                  auto_host_rewrite: true
                  cluster: httpbin_service    # (5) 클러스터로 라우팅
  clusters:
    - name: httpbin_service                   # (6) 업스트림 클러스터
      connect_timeout: 5s
      type: LOGICAL_DNS
      dns_lookup_family: V4_ONLY
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: httpbin
        endpoints:
        - lb_endpoints:
          - endpoint:
              address:
                socket_address: { address: httpbin, port_value: 8000 }

책은 v3 설정만 다룹니다. v1·v2는 더 이상 쓰지 않고, v3가 Istio가 쓰는 버전이기 때문입니다.

동적 설정 — 조각마다 따로 받기

API받아 오는 것관계
LDS어떤 리스너를 열어야 하나—
RDS리스너에 붙일 라우트LDS의 부분집합
CDS클러스터 목록과 각 클러스터 설정—
EDS클러스터 안의 endpointCDS의 부분집합
SDS인증서—
ADS위 전부를 하나의 직렬화된 스트림으로순서 보장용

전부 쓸 필요는 없고 골라서 조합하지만, 문제는 조각이 따로 도착한다는 것입니다. 원문 예시 그대로 — RDS로 "클러스터 foo로 보내라"는 새 라우트가 먼저 왔는데, foo를 정의한 CDS는 아직 안 왔다면? CDS가 올 때까지 라우팅 오류가 납니다.

도착 순서에 따라 결과가 갈리는 이런 상황을 race condition이라고 부릅니다.

비유 — 따로 오는 공지 문자

본사가 매장에 "오늘부터 신메뉴 판매"(RDS)와 "신메뉴 재료 공급처 등록"(CDS)을 각각 문자로 보냅니다. 판매 문자가 먼저 오면 주문은 받는데 재료가 없습니다. 반대로 메뉴를 내릴 때 "공급처 해지"가 먼저 오면, 아직 메뉴판에 있는 걸 못 만듭니다.

ADS는 한 봉투에 순서대로 넣어 보내는 방식입니다. 추가할 때는 재료부터, 내릴 때는 메뉴판부터. xDS 문서는 이걸 make-before-break라고 부릅니다.

작은 숫자로 한 번

# 초당 10건이 들어오는 서비스. 라우트를 v1 → v2 로 바꾼다. t = 1.9 s RDS 도착: 이제 v2 로 보내라 t = 3.1 s CDS 도착: v2 클러스터는 이렇게 생겼다 # 1.9 ~ 3.1 s 사이(1.2초)에 들어온 요청 → 보낼 클러스터가 없다 10 건/s × 1.2 s = 약 12건이 503 (NC) # ADS: 서버가 CDS → RDS 순으로 보내면 틈이 0
그림 2

RDS와 CDS가 따로 도착할 때 생기는 503 구간

버튼 · 슬라이더
해볼 것 — ① 기본값(추가, CDS가 1.2초 늦음)에서 분홍 구간과 분홍 점(503)을 보세요. ② ADS 순서 보장을 켜면 RDS 적용이 CDS 뒤로 밀리고 503이 0이 됩니다. ③ ADS를 끄고 시간차를 음수로 밀면(CDS가 먼저) 추가 시나리오는 안전해집니다. ④ 삭제 시나리오로 바꾸면 반대로 양수일 때 안전하고 음수일 때 503이 납니다 — 안전한 순서가 방향마다 다릅니다.
그림에 나오는 말 v2 추가 라우트를 새 클러스터 v2로 돌리면서 v2를 새로 만드는 변경 · v1 삭제 라우트를 이미 있는 v2로 돌리면서 옛 클러스터 v1을 지우는 변경 · 적용 시각 Envoy가 그 조각을 받아 반영한 순간(여기서는 도착 즉시로 단순화)
동적 설정 부트스트랩 예시 — LDS만 서버에서 받기 / Istio의 ADS 부트스트랩
dynamic_resources:
  lds_config:                     # 리스너는 LDS로 받는다
    api_config_source:
      api_type: GRPC
      grpc_services:
        - envoy_grpc:
            cluster_name: xds_cluster   # 이 클러스터로 가서 리스너 API를 확인
clusters:
- name: xds_cluster               # LDS를 구현하는 gRPC 클러스터 — 이것만은 정적으로 적어야 한다
  connect_timeout: 0.25s
  type: STATIC
  lb_policy: ROUND_ROBIN
  http2_protocol_options: {}
  hosts: [{ socket_address: { address: 127.0.0.3, port_value: 5678 }}]

리스너는 몰라도 되지만 설정 서버로 가는 클러스터 하나는 정적으로 알아야 합니다. 닭과 달걀 문제를 이렇게 풉니다.

bootstrap:
  dynamicResources:
    ldsConfig: { ads: {} }        # 리스너용 ADS
    cdsConfig: { ads: {} }        # 클러스터용 ADS
    adsConfig:
      apiType: GRPC
      grpcServices:
      - envoyGrpc: { clusterName: xds-grpc }
      refreshDelay: 1.000s
  staticResources:
    clusters:
    - name: xds-grpc
      type: STRICT_DNS
      connectTimeout: 10.000s
      hosts: [{ socketAddress: { address: istio-pilot.istio-system, portValue: 15010 } }]
      circuitBreakers:            # 설정 채널은 끊기면 안 되니 한도를 사실상 해제
        thresholds:
        - { maxConnections: 100000, maxPendingRequests: 100000, maxRequests: 100000 }
        - { priority: HIGH, maxConnections: 100000, maxPendingRequests: 100000, maxRequests: 100000 }
      http2ProtocolOptions: {}

Istio는 LDS·CDS를 모두 ADS로 받습니다. 10장에서 보는 Agent Router의 Envoy 파드도 같은 모양이고, 거기서는 xds_cluster가 envoy-gateway.envoy-gateway-system.svc:18000을 가리키며 DELTA_GRPC(바뀐 것만 보내는 방식)를 씁니다.

2장 세 줄 요약
  1. 정의 — 정적 설정은 파일에 리스너·라우트·클러스터를 다 적는 방식, 동적 설정은 xDS(LDS·RDS·CDS·EDS·SDS·ADS)로 서버가 gRPC 스트림으로 밀어 주는 방식입니다.
  2. 성질 — 조각이 따로 오고 결국엔 맞춰지는(eventual consistency) 구조라, 순서가 어긋나면 잠깐 503(NC)이 납니다. ADS는 한 스트림에 순서를 실어 이 틈을 없앱니다. 추가는 클러스터 먼저, 삭제는 라우트 먼저.
  3. 쓰임 — Istio의 istiod, Envoy Gateway 컨트롤러가 xDS 서버입니다. 설정이 "안 먹는다"면 먼저 xDS 연결 상태와 update 카운터를 봅니다(10장).

개념은 여기까지입니다. 이제 컨테이너로 Envoy를 직접 띄워서, 설정 한 줄이 응답을 어떻게 바꾸는지 봅니다.

03직접 띄워 보기 — 타임아웃·재시도·Admin API

한 줄 요약: httpbin 앞에 Envoy를 세우고 라우트에 timeout: 1s와 retry_policy를 넣으면, 앱 코드 한 줄 없이 응답이 바뀝니다. 무슨 일이 있었는지는 Admin API 통계가 말해 줍니다.

왜 이걸 배우나
"앱은 그대로 두고 설정만 바꿔서 타임아웃과 재시도를 걸 수 있나?"

1장에서 "Envoy가 대신 해 준다"고 했던 것을 손으로 확인하는 장입니다. 그리고 실습 끝에서 curl에는 아무 출력도 없는데 Envoy는 세 번 재시도했다는 걸 통계로 찾아냅니다.

이 "보이지 않는 재시도"를 알아 두는 게 중요합니다. 7장에서 LLM provider 장애 시 fallback이 똑같이 일어나고, 그때 비용과 지연이 어디서 늘었는지 찾으려면 프록시 쪽 통계를 보는 습관이 필요합니다.

이 장에 나오는 낯선 말
httpbin
요청을 받으면 받은 헤더를 돌려주거나(/headers), 일부러 늦게 답하거나(/delay/N), 원하는 상태 코드를 내는(/status/500) 테스트용 서버. 실습은 ARM에서도 도는 mccutchen/go-httpbin을 8000 포트로 씁니다.
X-Request-Id
Envoy가 요청마다 새로 붙이는 고유 ID. 여러 서비스를 거치는 요청을 한 줄로 이어 추적할 때 씁니다.
X-Envoy-Expected-Rq-Timeout-Ms
upstream에게 주는 힌트 헤더. "나는 이 요청을 N ms 뒤에 포기할 거야"라는 뜻이라, upstream이 그 전에 작업을 접을 수 있습니다(deadline).
route timeout
라우트 단위 전체 제한 시간. 기본 15초. 재시도까지 포함한 전체 시간입니다.
retry_policy
retry_on(어떤 실패에 재시도할지, 예: 5xx)과 num_retries(최대 재시도 횟수)로 구성됩니다. 재시도 사이에는 짧은 무작위 대기(backoff)가 들어갑니다.
Admin API
Envoy가 따로 여는 관리 포트(실습은 15000). /stats, /logging, /clusters, /listeners, /config_dump 등으로 내부 상태를 봅니다.

실습 흐름

  1. httpbin 띄우고 직접 호출docker run -d -e PORT=8000 --name httpbin mccutchen/go-httpbin 후 curl 컨테이너로 /headers를 부르면 보낸 헤더가 돌아옵니다.
  2. 설정 없이 Envoy 실행 → 실패"At least one of --config-path or --config-yaml … should be non-empty" — 설정 파일은 필수입니다.
  3. 2장의 simple.yaml로 실행15001 포트 리스너가 뜨고 로그에 all dependencies initialized. starting workers가 찍힙니다.
  4. 프록시 경유 호출httpbin이 돌려준 요청 헤더 목록에 X-Request-Id와 X-Envoy-Expected-Rq-Timeout-Ms: 15000이 새로 붙어 있습니다. Envoy가 upstream으로 보내면서 붙인 것이고, 앱이 붙인 게 아닙니다.
  5. 라우트에 timeout: 1s힌트 헤더가 1000으로 바뀌고, /delay/2는 upstream request timeout으로 끊깁니다.
  6. retry_policy: {retry_on: 5xx, num_retries: 3}/status/500을 부르면 curl에는 아무것도 안 보이지만, /stats | grep retry에 upstream_rq_retry: 3이 남습니다.

비유 — 택배 기사의 배송 기한

택배 기사(Envoy)가 물건을 받아 창고(upstream)에 맡기면서 "오늘 6시까지 안 나오면 취소합니다"라고 쪽지를 붙입니다. 이게 힌트 헤더입니다. 창고는 쪽지를 보고 6시가 넘으면 포장을 멈출 수 있습니다 — 어차피 버려질 일에 사람을 묶어 두지 않으려고요.

창고가 "파손"(500)이라고 돌려주면 기사는 세 번까지 다시 맡겨 봅니다. 손님은 기사가 몇 번 다녀왔는지 모르고, 기사 운행 일지(Admin API)에만 남습니다. 단, 다시 맡기는 시간도 6시 기한 안에 들어갑니다.

작은 숫자로 한 번

# timeout: 1s, 재시도 없음 /delay/0.5 0.5 s 에 200 기한 안 /delay/2 1.0 s 에 504 upstream request timeout httpbin은 힌트 헤더를 보지 않아 2초짜리 일을 계속했다 # retry_on: 5xx, num_retries: 3, 기한 15 s /status/500 시도 1 → 500 → 재시도 1 → 500 → 재시도 2 → 500 → 재시도 3 → 500 시도 4번, 재시도 3번 → 클라이언트는 마지막 500 을 받는다 stats: upstream_rq_retry: 3 retry.upstream_rq_500: 3
그림 3

한 요청의 시간표 — 시도, 재시도, 그리고 기한

버튼 · 슬라이더
해볼 것 — ① 기본값(/delay/2, 기한 1초)에서 분홍 기한선에서 시도가 잘리는 모습을 보세요. ② 프리셋 /status/500 + retry 3을 누르면 짧은 막대 네 개와 upstream_rq_retry: 3이 나옵니다. ③ 그 상태에서 실패 확률을 50%로 내리고 "다시 보내기"를 여러 번 눌러 보세요. 재시도 횟수가 0~3으로 바뀌면서도 대부분 200으로 끝납니다 — 재시도가 실패를 흡수하고 있다는 뜻입니다. ④ 처리 시간 1.2초 · 재시도 3 · 기한 3초 · 실패 확률 100%로 두면 재시도가 기한을 다 써 버려 재시도했는데도 504가 됩니다 — 재시도 시간은 기한 안에 포함됩니다.
단순화한 모형입니다

재시도 간 대기는 Envoy 기본값(기준 25 ms, 지수 증가, 무작위)을 흉내 낸 추정치입니다. 실패는 처리 시간만큼 걸린 뒤 500을 낸다고 가정했습니다. 시도별 기한(per_try_timeout)은 넣지 않았습니다.

실습 명령어와 원문 출력
# 이미지 (citizenstig/httpbin 은 ARM 미지원이라 go-httpbin 사용)
docker pull envoyproxy/envoy:v1.19.0
docker pull curlimages/curl
docker pull mccutchen/go-httpbin
docker run -d -e PORT=8000 --name httpbin mccutchen/go-httpbin

# Envoy 실행 (설정은 인라인 YAML)
docker run --name proxy --link httpbin envoyproxy/envoy:v1.19.0 --config-yaml "$(cat ch3/simple.yaml)"
docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15001/headers
#  "X-Envoy-Expected-Rq-Timeout-Ms": ["15000"],
#  "X-Request-Id": ["..."]

# timeout: 1s 로 바꾼 설정
docker run --name proxy --link httpbin envoyproxy/envoy:v1.19.0 --config-yaml "$(cat ch3/simple_change_timeout.yaml)"
curl http://proxy:15001/headers      #  "X-Envoy-Expected-Rq-Timeout-Ms": ["1000"]
curl -X POST http://proxy:15000/logging?http=debug
curl http://proxy:15001/delay/2      #  upstream request timeout

# retry_policy 설정
#   route: { cluster: httpbin_service, retry_policy: { retry_on: 5xx, num_retries: 3 } }
curl http://proxy:15001/status/500   #  (아무 출력 없음)
curl http://proxy:15000/stats | grep retry
#  cluster.httpbin_service.retry.upstream_rq_500: 3
#  cluster.httpbin_service.retry.upstream_rq_5xx: 3
#  cluster.httpbin_service.retry.upstream_rq_completed: 3
#  cluster.httpbin_service.retry_or_shadow_abandoned: 0
#  cluster.httpbin_service.upstream_rq_retry: 3

Admin API로 자주 보는 것 — /stats(전체 통계), /stats/prometheus(프로메테우스 형식), /clusters(클러스터와 endpoint 상태), /listeners(열린 리스너), /config_dump(지금 적용된 설정 전체), /logging(로그 레벨 변경).

3장 세 줄 요약
  1. 정의 — 라우트에 timeout과 retry_policy를 적으면 Envoy가 앱 대신 기한을 지키고 재시도합니다. 요청에는 X-Request-Id와 기한 힌트 헤더가 자동으로 붙습니다.
  2. 성질 — route timeout은 재시도까지 포함한 전체 기한입니다. 재시도는 클라이언트에게 보이지 않고 upstream_rq_retry 같은 통계에만 남습니다.
  3. 쓰임 — "응답이 왜 늦지?", "왜 upstream 호출이 늘었지?"의 첫 확인은 Admin API의 /stats입니다. 이 습관이 10장의 Agent Router 파드 점검에서 그대로 쓰입니다.

손으로 파일을 넣어 봤으니, 이제 이걸 수천 개 사이드카에 자동으로 해 주는 쪽을 봅니다. 그리고 요청 한 건이 Envoy 안에서 정확히 어떤 길을 가는지도.

04Istio 안의 Envoy — istiod와 요청 한 건의 일생

한 줄 요약: Istio에서 Envoy 설정을 쓰는 건 사람이 아니라 istiod입니다. 그리고 요청 한 건은 Envoy 안에서 listener → network filter(마지막이 HCM) → HTTP filter(마지막이 router) → cluster 순으로 지나가고, 응답은 그 역순으로 돌아옵니다. (Envoy 문서 Life of a Request)

왜 이걸 배우나
"VirtualService만 썼는데 Envoy 설정은 누가 만들었고, 내 요청은 그 안에서 어디를 지나가나?"

Envoy는 혼자서는 반쪽입니다. xDS 서버, 서비스 목록, 인증서 발급처, 메트릭 저장소가 필요합니다. Istio에서는 그걸 istiod가 맡습니다.

요청 경로를 알아야 하는 이유는 5장과 9장 때문입니다. "필터를 router 앞에 끼운다", "ext_proc가 요청 헤더 단계에서 개입한다" 같은 말은 이 경로 그림이 머릿속에 있어야 읽힙니다. 원문도 이 공식 문서를 "강력 추천"으로 표시해 두었습니다.

이 장에 나오는 낯선 말
Control plane · Data plane
설정을 만들어 내려주는 쪽이 control plane(istiod), 실제 트래픽이 지나가는 쪽이 data plane(Envoy)입니다.
istiod · istio-proxy
istiod는 Istio의 control plane 프로세스로 xDS 서버·인증서 발급을 겸합니다. istio-proxy는 파드 옆에 붙는 Envoy 사이드카 컨테이너 이름입니다.
Worker thread
Envoy가 연결을 실제로 처리하는 스레드. 연결 하나는 처음부터 끝까지 한 worker가 맡습니다.
Listener filter · Transport socket
Listener filter는 TLS를 풀기 전에 SNI 같은 정보를 읽어 어떤 필터 체인을 쓸지 고릅니다. Transport socket은 TLS 암·복호화를 맡는 층입니다.
Network filter · HCM
바이트 스트림을 다루는 필터. 그중 HTTP connection manager(HCM)가 바이트를 HTTP 요청으로 바꾸며, 네트워크 필터 체인의 마지막에 옵니다.
Codec · Stream
Codec은 HTTP/1·2·3 프레임을 풀고 싸는 부분, Stream은 요청 하나와 응답 하나의 쌍입니다. HTTP/2 연결 하나에 stream 여러 개가 섞여 흐릅니다.
Router filter
HTTP 필터 체인의 맨 끝에서 라우트를 고르고 cluster로 요청을 내보내는 필터.
Connection pool
upstream endpoint로 가는 연결을 재사용하는 풀. 비어 있거나 모자라면 새 연결을 만듭니다.

Envoy 혼자서는 못 하는 네 가지 — istiod가 채운다

Envoy에 필요한 것Istio에서 누가 주나
xDS 서버 — 리스너·라우트·클러스터를 동적으로 받기istiod가 쿠버네티스 API에서 VirtualService 같은 Istio 설정을 읽어 xDS로 변환해 밀어 줍니다.
서비스 목록 — endpoint를 찾기istiod가 쿠버네티스의 service registry를 씁니다. Envoy는 그 구현을 전혀 모릅니다.
메트릭·트레이스 목적지Istio가 Prometheus 같은 시계열 저장소, Jaeger·Zipkin 같은 트레이싱 엔진과 연결되도록 data plane을 설정합니다.
인증서 — TLS 종료·시작, mTLSistiod가 인증서를 생성·서명·교체합니다(SDS).

그래서 원서는 책 3장 이후로 Envoy를 "Istio 프록시"라 부르고 Istio API를 통해 다룹니다. 하지만 실제로 일하는 건 대부분 Envoy라는 점을 잊지 말라고 덧붙입니다. 원문의 정리도 같습니다 — Service Mesh나 Gateway API 구현체들이 내부적으로 Envoy를 쓰고, xDS 동기화 API로 네트워크 정책을 구성합니다.

비유 — 공항 출국 절차

터미널 입구(Listener)에 들어서면 먼저 여권 국적만 보고 어느 줄로 갈지 나눕니다(listener filter). 짐 X-ray로 가방 속을 들여다보고(TLS 복호화), 항공사 카운터(HCM)에서 비로소 "승객 한 명"으로 등록됩니다.

그다음 여러 검사대(HTTP filter)를 차례로 지나고, 마지막 검사대가 탑승구를 배정합니다(router). 어느 게이트 줄이 짧은지 보고 보내는 게 LB입니다.

응답은 같은 검사대를 거꾸로 지나 나옵니다. 그리고 검사대 한 곳에서 "탑승 거부"를 받은 승객은 탑승구까지 가지 않고 그 자리에서 돌아 나옵니다 — 요청을 막는 필터가 하는 일이 이것입니다.

작은 숫자로 한 번

# HTTP/2 연결 1개 위에 요청 3개가 섞여 온다 TCP 연결 1 → TLS 복호화 1번 → HCM 의 HTTP/2 codec 이 풀어서 → stream 3개 stream 마다 HTTP 필터 체인이 따로 만들어진다 → 필터 체인 실행 3번 # 필터 순서가 A → B → router 일 때 요청: A → B → router → upstream 응답: upstream → router → B → A 역순
그림 4

Life of a Request — 요청 한 건을 한 단계씩

이전·다음·재생
해볼 것 — ① 다음을 눌러 12단계를 따라가 보세요. 색이 칠해진 상자가 지금 위치입니다(요청은 파랑, 응답은 주황). ② 6단계에서 router가 HTTP 필터의 맨 끝이라는 것, 7단계에서 LB와 circuit breaker가 cluster 단계라는 것을 확인하세요. ③ 10단계(원문 목록의 11번)에서 화살표가 주황으로 바뀌며 거꾸로 흐릅니다. ④ "CustomFilter가 401로 막기"를 켜고 다시 재생하면, 6단계에서 router·cluster·upstream을 건너뛰고 바로 응답이 돌아갑니다 — 5장의 RBAC·9장의 ext_proc가 요청을 막는 방식입니다.
원문 Request flow 12단계 (Envoy 문서) — 요지 번역 (그림은 8·9번을 묶고 "downstream으로 전달"을 한 단계로 떼어 12단계로 나눴습니다)
  1. downstream의 TCP 연결을 worker thread에서 도는 리스너가 받는다.
  2. listener filter 체인이 돌며 SNI 등 TLS 이전 정보를 얻고, 목적지 IP·SNI·ALPN·출발지 포트 조합으로 network filter 체인 하나를 고른다. 이 체인에 TLS transport socket이 붙어 있다.
  3. TLS transport socket이 읽은 데이터를 복호화한다.
  4. network filter 체인이 돈다. HTTP에서 가장 중요한 필터는 체인의 마지막인 HCM이다.
  5. HCM의 HTTP/2 codec이 복호화된 스트림을 독립된 stream 여러 개로 분리한다. stream 하나 = 요청 하나 + 응답 하나.
  6. stream마다 downstream HTTP filter 체인이 만들어져 돈다. 요청은 CustomFilter를 먼저 지나고, 체인 끝의 router 필터가 decodeHeaders에서 라우트와 cluster를 고른다.
  7. cluster의 LB로 endpoint를 고르고, circuit breaker로 새 stream이 허용되는지 보고, connection pool이 비었으면 새 연결을 만든다.
  8. stream마다 upstream HTTP filter 체인이 돈다. 기본은 codec 필터뿐이지만, 설정되어 있으면 재시도와 shadow 요청마다 별도 체인이 만들어진다.
  9. upstream 연결의 HTTP/2 codec이 이 stream을 같은 upstream으로 가는 다른 stream들과 TCP 연결 하나에 섞어 보낸다.
  10. upstream 연결의 TLS transport socket이 암호화해서 TCP 소켓에 쓴다.
  11. 요청이 upstream으로, 응답이 downstream으로 프록시된다. 응답은 HTTP 필터를 요청과 반대 순서로 지난다 — codec → upstream 필터 → router → CustomFilter.
  12. 응답이 끝나면 stream이 정리되고, 통계 갱신·액세스 로그 기록·트레이스 스팬 마무리가 일어난다.
4장 세 줄 요약
  1. 정의 — Istio에서 istiod는 Envoy에게 xDS·서비스 목록·인증서·텔레메트리 목적지를 제공하는 control plane이고, Envoy(istio-proxy)는 실제 요청을 처리하는 data plane입니다.
  2. 성질 — 요청은 listener → network filter(HCM이 마지막) → HTTP filter(router가 마지막) → cluster(LB·circuit breaker·pool) → upstream 순으로 가고, 응답은 필터를 역순으로 지납니다. 필터가 중간에 응답을 만들면 뒤 단계는 실행되지 않습니다.
  3. 쓰임 — "필터를 어디에 끼울지", "누가 요청을 막았는지"를 판단하는 지도입니다. 5장의 tap 필터 위치, 9장의 ext_proc 개입 시점이 전부 이 그림 위의 한 점입니다.

4장 그림의 "HTTP filter" 상자를 확대할 차례입니다. 저 상자에 무엇을 어떤 순서로 넣느냐가 Envoy 확장의 전부입니다.

05필터 체인과 EnvoyFilter — tap 필터 끼워 보기

한 줄 요약: Envoy의 기능은 전부 필터이고, 필터는 체인으로 순서대로 돕니다. HTTP 필터 체인은 반드시 router로 끝나야 하고, 새 필터를 "어디에" 끼우느냐가 결과를 정합니다.

왜 이걸 배우나
"Istio API에 없는 Envoy 기능을 쓰고 싶으면, Envoy 소스를 고쳐서 빌드해야 하나?"

Istio의 VirtualService·AuthorizationPolicy는 결국 Envoy 설정과 필터로 번역됩니다. 하지만 Istio는 Envoy의 모든 필터를 노출하지 않습니다. 그 틈을 메우는 게 EnvoyFilter 리소스이고, 원문 예제는 요청 디버깅용 tap 필터를 webapp 사이드카에 끼웁니다.

이 장의 결론은 9장으로 곧장 이어집니다. 코어를 다시 빌드하지 않고 기능을 넣는 세 가지 길 — 외부 처리(ext_proc), Lua, Wasm — 중에서 AI Gateway가 고른 것이 첫 번째입니다. 책을 쓸 당시엔 "아직 아무것도 하지 않는 필터"였던 게 지금은 주인공이 됐습니다.

이 장에 나오는 낯선 말
Network filter
바이트 스트림을 인코딩·디코딩하는 가장 기본 필터. MongoDB, Redis, Thrift, Kafka, 그리고 HTTP connection manager(HCM)가 있습니다.
HTTP filter
HCM 안에서 HTTP 요청 단위로 도는 필터. CORS, CSRF, ExtAuthz, RateLimit, Fault injection, gRPC-JSON transcoding, Gzip, Lua, RBAC, Tap, Wasm, Router 등.
Terminal filter
체인의 맨 끝에 와야 하는 필터. HTTP 필터 체인에서는 요청을 upstream cluster로 보내는 router입니다.
RBAC
Role-Based Access Control. 조건에 맞지 않는 요청을 403으로 막는 필터입니다.
Tap filter
지나가는 요청·응답의 헤더·바디·트레일러를 복사해 관리 API로 스트리밍하는 디버깅 필터.
EnvoyFilter
Istio가 고수준 API로 노출하지 않은 Envoy 설정을 직접 패치하는 리소스. 원문 표현으로 "break glass(비상 수단)"입니다.
workloadSelector · configPatches
어떤 파드에 적용할지 고르는 라벨 조건, 그리고 Envoy 설정의 어디를(match) 어떻게(patch) 바꿀지 적는 목록입니다.

Envoy는 결국 바이트를 옮기는 기계다

1장에서는 Envoy를 L7 프록시라고 불렀습니다. 원문은 한 층 아래에서 이렇게 말합니다 — "Envoy는 궁극적으로 네트워크 연결에서 바이트를 꺼내 어떤 방식으로 처리하는 L3/L4 프록시"입니다.

두 말은 어긋나지 않습니다. 바닥은 바이트를 옮기는 L3/L4이고, L7 기능은 그 위에 필터로 쌓입니다. 바이트를 HTTP 헤더·바디·트레일러로 바꾸는 HCM도 네트워크 필터 중 하나일 뿐입니다.

HCM 안에는 다시 HTTP 필터 체인이 있고, 이 체인은 반드시 router 필터로 끝나야 합니다. router가 타임아웃·재시도 파라미터와 함께 요청을 cluster로 내보내기 때문입니다(3장의 설정이 적용되는 곳).

Envoy를 확장하는 네 가지 길 — 빌드가 필요한 하나와 필요 없는 셋

방법어떻게이 페이지에서
C++ 필터소스에 넣고 Envoy를 빌드. Istio 프록시가 이 방식이라 유지 보수 부담과 C++가 필요합니다.—
External processing필터가 요청을 외부 gRPC 서비스에 보여 주고 결정을 받습니다. 6장의 속도 제한 서비스 호출도 같은 계열입니다.9장의 주인공
Lua설정에 Lua 스크립트를 넣어 요청·응답을 조작합니다.—
WasmWebAssembly 모듈을 불러와 샌드박스 안에서 실행합니다.—

EnvoyFilter를 쓰기 전에 알아야 할 세 가지

  1. 기본 적용 범위는 네임스페이스 전체istio-system에 만들면 메시 전체에 적용됩니다. 좁히려면 workloadSelector를 씁니다.
  2. 다른 Istio 리소스가 다 번역된 뒤에 적용VirtualService·DestinationRule이 먼저 data plane에 들어가고, EnvoyFilter는 그 결과 위에 덧칠됩니다.
  3. 잘못 쓰면 메시가 멈춘다밑단 Envoy API는 Istio 버전 사이에 언제든 바뀔 수 있고 하위 호환을 가정하면 안 됩니다. 배포하는 EnvoyFilter마다 검증이 필요합니다.

비유 — 컨베이어 라인의 검사대

공장 라인에 검사대가 순서대로 서 있고, 맨 끝은 출하대(router)입니다. 기록용 카메라(tap)를 어디에 세우느냐에 따라 찍히는 게 달라집니다. 불량 판정대(RBAC) 뒤에 두면 불량품은 카메라 앞까지 오지 않고, 앞에 두면 불량품도 찍힙니다.

그리고 출하대 뒤에 검사대를 두는 건 의미가 없습니다. 물건이 이미 트럭에 실렸으니까요. 그래서 Envoy는 router가 끝이 아닌 체인을 설정 단계에서 거부합니다.

작은 숫자로 한 번

# 체인: CORS → RBAC → Lua → tap → router 요청 3건 정상 요청 (x-app-tap: true) CORS ✓ RBAC ✓ Lua ✓ tap 기록 router → 200 권한 없는 요청 (x-app-tap: true) CORS ✓ RBAC ✗ 403 tap 은 요청을 못 봤다 → 기록 0 헤더 없는 요청 CORS ✓ RBAC ✓ Lua ✓ tap 통과(조건 불일치, 기록 안 함) router → 200 # 원문 예제의 선택: tap 을 router "바로 앞"(INSERT_BEFORE router)
그림 5

HTTP 필터 체인 — tap을 어디에 끼울까

버튼
해볼 것 — ① 기본값(tap이 router 바로 앞)에서 요청 종류를 바꿔 가며 tap 기록 여부를 보세요. 권한 없는 요청은 RBAC에서 403으로 돌아가 tap이 기록하지 못합니다. ② tap ◀를 두 번 눌러 RBAC 앞으로 옮기면 403 요청도 기록됩니다. ③ "router를 끝에서 빼기"를 켜면 Envoy가 그 설정을 거부(NACK)하고, 직전 설정 그대로 트래픽을 계속 처리합니다.
그림에 나오는 말 CORS 다른 출처(origin)에서 온 브라우저 요청을 허용할지 판단 · RBAC 권한 없는 요청을 403으로 차단 · Lua 설정에 적은 스크립트로 헤더를 더하거나 고치는 필터 · tap 조건(x-app-tap: true)에 맞는 요청을 복사해 기록 · router cluster로 요청을 내보내는 마지막 필터 · NACK Envoy가 받은 xDS 설정을 거부했다는 응답
원문 EnvoyFilter와 tap 실습 명령
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: tap-filter
  namespace: istioinaction
spec:
  workloadSelector:
    labels:
      app: webapp                       # 1) 적용할 워크로드
  configPatches:
  - applyTo: HTTP_FILTER                # 2) 패치할 위치: HTTP 필터
    match:
      context: SIDECAR_INBOUND          #    인바운드 리스너의
      listener:
        portNumber: 8080                #    8080 포트에 바인딩된
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
            subFilter:
              name: "envoy.filters.http.router"   # router 필터를 기준으로
    patch:
      operation: INSERT_BEFORE          # 3) 그 바로 앞에 끼운다
      value:
        name: envoy.filters.http.tap
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.http.tap.v3.Tap"
          commonConfig:
            adminConfig:
              configId: tap_config
# 적용 후 실제로 router 앞에 들어갔는지 확인
kubectl apply -f ch14/tap-envoy-filter.yaml
istioctl proxy-config listener deploy/webapp.istioinaction --port 15006 -o json   # ... tap → router 순서

# 터미널 1: Admin API로 tap 시작 (tap-config.json 에 x-app-tap: true 헤더 매칭 조건)
kubectl port-forward -n istioinaction deploy/webapp 15000 &
curl -X POST -d @./ch14/tap-config.json localhost:15000/tap

# 터미널 2: 헤더를 달아 호출
curl -s -H "x-app-tap: true" -H "Host: webapp.istioinaction.io" http://$EXT_IP/api/catalog
#  → 터미널 1 에 요청의 헤더·바디·트레일러가 흘러나온다

실습 환경은 kind(k8s 1.23.17) + MetalLB + Istio 1.17.8입니다. 원문은 kubectl get envoyfilter -A로 Istio가 이미 깔아 둔 stats-filter-1.13~1.17 같은 EnvoyFilter들이 있다는 것도 보여 줍니다. Istio 자체도 이 방식으로 필터를 넣습니다.

5장 세 줄 요약
  1. 정의 — Envoy 기능은 network filter와 HTTP filter의 체인입니다. HCM은 network filter 체인의 끝, router는 HTTP filter 체인의 끝이며, EnvoyFilter는 Istio 위에서 이 체인을 직접 패치하는 비상 수단입니다.
  2. 성질 — 필터는 순서대로 돌고, 앞 필터가 요청을 막으면 뒤 필터는 요청을 못 봅니다. router가 끝이 아니면 설정 자체가 거부됩니다. EnvoyFilter는 네임스페이스 전체 적용이 기본이고, 다른 Istio 리소스 뒤에 적용됩니다.
  3. 쓰임 — 기존 필터(tap 등)로 되면 EnvoyFilter로 설정하고, 없으면 ext_proc·Lua·Wasm으로 확장합니다. AI Gateway는 ext_proc를 골랐습니다(9장).

필터 목록에 RateLimit이 있었습니다. 이 필터는 혼자 판단하지 않고 바깥 서비스에 묻습니다. AI Gateway의 "토큰 한도"가 바로 이 구조 위에 서 있습니다.

06전역 속도 제한 — descriptor와 RLS

한 줄 요약: Envoy의 rate limit 필터는 요청에서 속성(descriptor)을 뽑아 바깥의 속도 제한 서버(RLS)에 묻고, RLS는 Redis에 카운터를 둡니다. 그래서 파드가 몇 개든 한도는 하나로 지켜집니다.

왜 이걸 배우나
"분당 10건으로 막았는데, 파드를 3개로 늘리니 30건이 통과한다 — 왜?"

한도를 각 파드가 따로 세면(local) 파드 수만큼 곱해집니다. 공유 서비스, 비싼 호출, 지연이 예측 불가능한 리소스를 보호하려면 모든 복제본이 같은 카운터를 봐야 합니다. 원문이 강조하듯 재시도가 켜진 환경에서는 한 장애가 과도하게 증폭되는 것도 막아야 합니다.

8장의 토큰 기반 속도 제한이 이 구조를 그대로 씁니다. 차이는 한 가지 — 요청 1건을 1로 세지 않고 응답에 적힌 토큰 수만큼 셉니다.

이 장에 나오는 낯선 말
Local · Global rate limit
Local은 각 Envoy가 자기 메모리에서 세는 방식, Global은 모든 Envoy가 같은 속도 제한 서버에 물어 한 곳에서 세는 방식입니다. Envoy에는 네트워크 필터 방식까지 세 가지가 있고, 원문은 global을 봅니다.
RLS (Rate Limit Server)
Envoy 속도 제한 API(rls.proto)를 구현한 gRPC 서버. Envoy 커뮤니티의 envoyproxy/ratelimit이 대표입니다. 카운터는 Redis(또는 Memcached)에 둡니다.
Descriptor
요청에서 뽑아 RLS로 보내는 속성 또는 속성 묶음. 원격 주소, 요청 헤더, 목적지 등이 될 수 있습니다. RLS는 이걸 미리 정의한 트리와 비교해 카운터를 올립니다.
domain
RLS 설정에서 한도 규칙 묶음의 이름공간. 필터 설정의 domain과 같아야 규칙이 적용됩니다.
Rate limit action
Envoy 라우트 쪽 설정. "이 경로의 요청에서는 어떤 descriptor를 만들어 보낼지"를 정합니다. 예: x-loyalty: gold면 gold_request.
429 · x-envoy-ratelimited
한도를 넘으면 받는 Too Many Requests 응답과, Envoy가 막았다는 표시 헤더입니다.

원문 예제 — 등급별 한도

x-loyalty 헤더descriptor 값한도
goldgold_request분당 10건
silversilver_request분당 5건
bronzebronze_request분당 3건
없음(식별 불가)no_loyalty분당 1건

설정은 두 곳에 나뉩니다 — RLS 쪽(ConfigMap)에는 "descriptor 값별 한도"를, Envoy 쪽(EnvoyFilter 두 개)에는 "rate limit 필터를 HTTP 체인에 넣기"와 "라우트에서 헤더를 보고 descriptor를 만들기"를 적습니다. 원문이 짚듯 RLS는 헤더를 직접 보지 않고, Envoy가 보낸 descriptor만 봅니다. 책을 쓸 당시 Istio에 전용 API가 없어서 Envoy 쪽 두 설정을 EnvoyFilter로 넣습니다.

비유 — 체인점 쿠폰 사용 한도

"회원 1인당 하루 한 번 할인"을 매장마다 종이 장부로 세면, 매장 세 곳을 돌면 세 번 받습니다. 그래서 본사 전산(RLS + Redis)에 매번 조회합니다. 매장 직원(Envoy)은 회원 등급 카드(헤더)를 보고 "골드 회원 요청"이라는 코드(descriptor)만 본사에 보냅니다.

작은 숫자로 한 번

# silver(분당 5건) 사용자가 1분에 6번 호출, catalog 파드 3개 global: RLS 카운터 1 2 3 4 5 → 6번째 429 허용 5 local : 파드마다 따로 셈 → 파드당 2건씩 → 한도 5에 안 걸림 허용 6 (한도 × 3 = 15까지 뚫림) # 헤더 없이 연속 호출 → no_loyalty 분당 1건 1번째 200, 2번째 429 Too Many Requests (x-envoy-ratelimited: true)
그림 6

등급별 한도 — 한 곳에서 세기 vs 파드마다 세기

버튼 · 슬라이더
해볼 것 — ① 기본값(global, 등급마다 분당 6건, 파드 3개)에서 분홍 점(429)이 silver·bronze·no_loyalty에만 생기는 걸 보세요. ② 파드마다 세기로 바꾸면 silver·bronze의 429가 사라집니다 — 한도가 파드 수만큼 곱해졌습니다. ③ 파드를 1개로 줄이면 두 방식이 같아집니다. ④ 요청 수를 20으로 올리면 local에서도 결국 막히는데, 그 지점이 한도 × 파드 수입니다.
단순화한 모형입니다

요청은 1분 동안 고르게 도착하고, local 모드에서는 파드에 round robin으로 나뉜다고 가정했습니다. 실제 local rate limit은 token bucket 방식이라 몰려 들어오는 순간의 모양은 다릅니다.

원문 RLS 설정 · rate limit action · 배포 명령
# ch14/rate-limit/rlsconfig.yaml — RLS 가 읽는 한도 규칙
apiVersion: v1
kind: ConfigMap
metadata: { name: catalog-ratelimit-config, namespace: istioinaction }
data:
  config.yaml: |
    domain: catalog-ratelimit
    descriptors:
      - key: header_match
        value: no_loyalty
        rate_limit: { unit: MINUTE, requests_per_unit: 1 }
      - key: header_match
        value: gold_request
        rate_limit: { unit: MINUTE, requests_per_unit: 10 }
      - key: header_match
        value: silver_request
        rate_limit: { unit: MINUTE, requests_per_unit: 5 }
      - key: header_match
        value: bronze_request
        rate_limit: { unit: MINUTE, requests_per_unit: 3 }
# ch14/rate-limit/catalog-ratelimit-actions.yaml — 라우트에서 descriptor 만들기
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata: { name: catalog-ratelimit-actions, namespace: istioinaction }
spec:
  workloadSelector: { labels: { app: catalog } }
  configPatches:
    - applyTo: VIRTUAL_HOST
      match:
        context: SIDECAR_INBOUND
        routeConfiguration: { vhost: { route: { action: ANY } } }
      patch:
        operation: MERGE
        value:
          rate_limits:
            - actions:
              - header_value_match:
                  descriptor_value: no_loyalty
                  expect_match: false          # x-loyalty 헤더가 "없으면"
                  headers: [{ name: "x-loyalty" }]
            - actions:
              - header_value_match:
                  descriptor_value: bronze_request
                  headers: [{ name: "x-loyalty", exact_match: bronze }]
            - actions:
              - header_value_match:
                  descriptor_value: silver_request
                  headers: [{ name: "x-loyalty", exact_match: silver }]
            - actions:
              - header_value_match:
                  descriptor_value: gold_request
                  headers: [{ name: "x-loyalty", exact_match: gold }]
kubectl apply -f ch14/rate-limit/rlsconfig.yaml -n istioinaction
kubectl apply -f ch14/rate-limit/rls.yaml -n istioinaction          # ratelimit + redis 파드
kubectl apply -f ch14/rate-limit/catalog-ratelimit.yaml -n istioinaction          # rate limit 필터를 HTTP 체인에
kubectl apply -f ch14/rate-limit/catalog-ratelimit-actions.yaml -n istioinaction

kubectl exec -it deploy/sleep -n istioinaction -c sleep -- curl http://catalog/items -v
#  < HTTP/1.1 429 Too Many Requests
#  < x-envoy-ratelimited: true
kubectl exec -it deploy/sleep -n istioinaction -c sleep -- curl -H "x-loyalty: silver" http://catalog/items -v
6장 세 줄 요약
  1. 정의 — 전역 속도 제한은 Envoy가 요청에서 descriptor를 만들어 RLS에 묻고, RLS가 Redis 카운터로 한도를 판정하는 구조입니다. 넘으면 429 + x-envoy-ratelimited: true.
  2. 성질 — 카운터가 한 곳에 있어 파드 수와 상관없이 한도가 지켜집니다. 파드마다 세면 한도가 파드 수만큼 곱해집니다. RLS는 헤더가 아니라 descriptor만 봅니다.
  3. 쓰임 — 비싼 호출, 공유 자원, 공정성이 필요한 곳을 보호합니다. AI Gateway의 팀·모델별 토큰 한도가 이 구조 위에 "1건 = 토큰 N개"로 세는 규칙만 바꿔 얹은 것입니다(8장).

여기까지가 Envoy의 기본기입니다. 이제 질문을 바꿉니다 — upstream이 마이크로서비스가 아니라 OpenAI·Bedrock·vLLM이라면, 이 기능들 중 무엇이 그대로 쓰이고 무엇이 모자랄까요.

07왜 AI Gateway인가 — 모델 앞에 다시 선 프록시

한 줄 요약: 앱마다 provider 키를 복사하고 재시도를 짜고 비용을 따로 추적하던 문제는, 10년 전 API Gateway가 풀었던 문제와 같습니다. AI Gateway는 대상만 "백엔드"에서 "모델"로 바꾼 같은 개념입니다.

왜 이걸 배우나
"LLM API 하나 부르는데 게이트웨이까지 세워야 하나?"

원문이 소개하는 발표(AI(LLM) Gateway 소개)는 한 팀이 겪는 일을 시간 순서로 보여 줍니다. 첫 provider의 용량 한도에 걸려 두 번째 provider를 붙이고, 팀원이 늘며 키를 복사하고, 재시도 로직을 매번 새로 짜고, 청구서로는 누가 얼마 썼는지 모릅니다.

약 3개월 뒤엔 모든 팀이 같은 벽 앞에 섭니다. 서로 다른 모델에 인증·재시도·키·로그라는 같은 로직을 다시 구현해야 하고, provider 한 곳이 멈추면 제품이 같이 멈춥니다. 1~6장에서 본 Envoy 기능 목록과 거의 겹친다는 게 이 장의 요점입니다.

이 장에 나오는 낯선 말
Provider
모델을 API로 제공하는 곳. OpenAI, Anthropic, AWS Bedrock, Google Vertex, 그리고 사내에 직접 띄운 vLLM(self-hosted)도 provider로 취급합니다.
API Gateway
여러 백엔드 앞에서 인증·라우팅·속도 제한·로그를 공통으로 처리하는 입구. 마이크로서비스 시대에 나왔습니다.
Unified API
provider마다 다른 요청 형식을 하나의 형식(대부분 OpenAI 방식)으로 통일한 입구. 모델 이름만 바꿔 다른 provider를 쓸 수 있습니다.
Virtual key (일회용 키)
게이트웨이가 앱에게 주는 대리 키. 실제 provider 키는 게이트웨이만 갖고, 대리 키를 바꾸거나 취소해도 다른 앱은 모릅니다.
Failover · Fallback
한 provider가 실패하면 다른 provider나 다른 endpoint로 넘겨 처리하는 것.
Guardrails
프롬프트·응답을 검사해 금지된 내용이나 개인정보를 막는 정책.

AI Gateway가 맡는 여섯 가지

역할무엇을 해결하나Envoy에서 온 것
Unified APIprovider별 요청 형식 차이. 대부분이 OpenAI 방식을 따르므로 모델 이름만 바꾸면 다른 provider로 갑니다.라우팅(1장) + 형식 변환(9장 ext_proc)
Holds the keys키 복사·유출. 앱에는 대리 키, 실제 키는 게이트웨이에. App·Agent·IDE마다 다른 인증 방식도 가능합니다.TLS·인증 필터
Retry & failover앱마다 재시도 코드. provider 문제 시 게이트웨이가 재시도·전환합니다.retry_policy(3장)
Track everything누가 토큰을 얼마나 썼는지. 토큰 흐름을 추적해 예산을 관리합니다.전역 속도 제한(6장)
Observability지연·오류·지출을 한 곳에서. 이를 위해 게이트웨이를 우회하는 경로는 막아야 합니다.메트릭·트레이싱(1장)
Guardrails프롬프트·응답 정책 검사.필터 확장(5장)
"우회 경로 차단 필수"가 왜 중요한가

게이트웨이를 세워도 한 팀이 provider API를 직접 부르면, 그 팀의 토큰·오류·비용은 대시보드에 없습니다. 원문 발표가 이 점을 따로 짚었습니다.

관측은 트래픽이 전부 한 문을 지날 때만 완전합니다. 쿠버네티스라면 egress 정책으로 provider 도메인 직접 호출을 막는 식입니다.

비유 — 회사 법인카드 창구

팀마다 법인카드 실물을 복사해 나눠 주면, 카드 한 장이 유출될 때 모든 팀이 카드를 바꿔야 하고, 명세서에는 가맹점만 찍혀 누가 썼는지 모릅니다. 그래서 구매 창구 한 곳을 둡니다. 팀은 사내 구매 번호(대리 키)로 신청하고, 창구가 실제 카드로 결제합니다.

한 카드사가 결제를 거절하면 창구가 다른 카드로 결제합니다(failover). 팀은 모릅니다. 명세서는 팀별로 정리됩니다.

작은 숫자로 한 번

# 아래 그림 7과 같은 상황 — 팀 4개, provider 4곳(OpenAI·Anthropic·Bedrock·vLLM). 두 곳을 같이 쓰는 팀이 있어 팀들이 든 키는 모두 5개 게이트웨이 없음: 실제 키 사본 = 5개(팀들에 흩어짐), 재시도 구현 4벌, OpenAI 키 유출 시 재배포할 팀 = 2곳(4팀 중 OpenAI 를 쓰는 팀) 게이트웨이 있음: 실제 키 사본 = provider당 1개 = 4개(게이트웨이 안), 재시도 구현 1벌, 재배포 0
그림 7

게이트웨이가 있을 때와 없을 때 — 장애와 키 유출

버튼 · 슬라이더
해볼 것 — ① 기본값(게이트웨이 없음)에서 OpenAI 장애를 켜 보세요. OpenAI를 첫 번째로 쓰는 팀이 모두 실패합니다. ② 게이트웨이 사용을 켜면 같은 장애에서 요청이 다른 provider로 넘어가(주황 선) 성공합니다. ③ OpenAI 키 유출을 켜고 게이트웨이를 껐다 켜 보세요 — 교체해야 할 곳이 "팀 여러 개"에서 "게이트웨이 한 곳"으로 줄어듭니다. ④ 팀 수를 10으로 늘리면 키 사본과 재시도 구현 수가 게이트웨이 없는 쪽에서만 늘어납니다.
단순화한 모형입니다

게이트웨이가 없을 때 각 팀은 자기가 쓰는 첫 번째 provider만 부른다고 가정했습니다(직접 fallback을 짠 팀은 없음). 게이트웨이의 failover는 같은 역할의 모델이 다른 provider에 있다고 가정합니다.

원문이 함께 본 발표들 — 한 줄씩

  • Evolution of Envoy AI Gateway (Google·Tetrate) — 해결하려는 문제, 프로젝트 역사, control plane 구조. 핵심 기능 셋: 토큰 기반 전역 속도 제한, provider·모델 간 failover, self-hosted LLM용 지능형 부하 분산. 8장에서 이어집니다.
  • LLM이 프로덕션에 올 때: AI 게이트웨이가 필요한 이유 (Nutanix) — "GenAI 트래픽은 다르다". Unified API, 토큰 기반 속도 제한(현재 4가지 타입), 모델별 사용량 한도, 공급자 장애 시 자동 failover, CRD, 본문 기반 지능형 라우팅, MCP Gateway.
  • 블룸버그의 하이브리드 클라우드 LLM 확장 — 필터 순서가 Pre-Process → 인증 → AI GW ExtProc → Rate Limit → Router, 응답은 ExtProc → Rate Limit → Post-Process로 적혀 있습니다(4장의 "응답은 역순" 규칙과 달리 발표 도식에선 응답에서도 ExtProc가 먼저입니다). 안정성, HA 모델 endpoint, 속도 제한 운영.
  • EnvoyCon: Envoy AI Gateway 2026의 새 기능 (Bloomberg) — 비용 최적화 3종(Quota-aware routing(QuotaPolicy), Context caching, Batch processing)과 확장 API(Responses·Embeddings·멀티모달).

원문은 추천 글로 인프런의 사내 AI Gateway 구축기(Envoy AI Gateway vs LiteLLM Proxy vs Portkey 비교)도 걸어 두었습니다.

7장 세 줄 요약
  1. 정의 — AI Gateway는 LLM provider들 앞에 선 API Gateway입니다. 역할은 Unified API, 키 보관, 재시도·failover, 토큰 추적, 관측, guardrails.
  2. 성질 — 키 사본·재시도 구현·장애 영향이 팀 수에 비례하던 것을 게이트웨이 한 곳으로 모읍니다. 대신 관측이 완전하려면 우회 경로를 막아야 합니다.
  3. 쓰임 — 기능 대부분이 1~6장의 Envoy 기능과 짝을 이룹니다. 모자란 부분(본문 읽기, 토큰 세기, 형식 변환)이 8·9장의 주제입니다.

겉보기엔 기존 API Gateway에 provider 목록만 붙이면 될 것 같습니다. 그런데 LLM 요청에는 기존 규칙이 안 맞는 지점이 있습니다 — 요청 1건의 무게가 제각각이라는 것.

08LLM 트래픽은 다르다 — 토큰으로 세고, 본문을 읽고, 대기열을 본다

한 줄 요약: LLM 요청은 1건의 무게가 수십 배씩 다릅니다. 그래서 속도 제한은 토큰 수로, failover는 형식 변환과 함께, 부하 분산은 모델 서버의 대기열을 보고 해야 합니다.

왜 이걸 배우나
"팀마다 분당 10건으로 막았는데, 왜 GPU 비용은 팀마다 열 배씩 차이가 나지?"

6장의 속도 제한은 요청 1건을 1로 셉니다. 마이크로서비스에선 요청 무게가 비슷하니 괜찮았습니다.

LLM에선 "안녕"도 1건, 300쪽 문서 요약도 1건입니다. Nutanix 발표의 제목이 그대로 요점입니다 — GenAI traffic is different.

원문의 Evolution of Envoy AI Gateway 발표는 이 차이에서 나온 핵심 기능 셋을 꼽습니다. 토큰 기반 전역 속도 제한, provider·모델 간 failover, self-hosted LLM용 지능형 부하 분산. 하나씩 봅니다.

이 장에 나오는 낯선 말
Token · usage
모델이 읽고 쓰는 글자 조각 단위. 응답 본문의 usage 필드에 input_tokens(읽은 양)·output_tokens(쓴 양)가 적혀 옵니다. 비용과 GPU 시간은 이 숫자에 비례합니다.
Streaming (SSE)
응답을 한 번에 주지 않고 토큰이 생길 때마다 조금씩 흘려보내는 방식. Server-Sent Events. 사용량은 스트림이 끝나야 확정됩니다.
apply_on_stream_done · hits_addend
Envoy rate limit 설정 두 가지. apply_on_stream_done은 "스트림이 끝난 뒤에 RLS와 통신", hits_addend는 "카운터를 1이 아니라 이만큼 올려라"(여기선 토큰 사용량 메타데이터)입니다.
Schema translation
OpenAI 형식 요청을 Anthropic·Bedrock 형식으로 바꿔 주는 것. 인증 방식도 provider마다 달라 함께 바꿔야 합니다.
Body-based routing
헤더가 아니라 요청 본문(JSON)의 내용(예: "model" 필드)을 보고 목적지를 정하는 것.
Gateway API Inference Extension · InferencePool · EPP
쿠버네티스 Gateway API에 LLM 서빙용 라우팅·부하 분산을 표준화한 확장. 모델 서버 파드 묶음이 InferencePool, 그중 어느 파드로 보낼지 고르는 서비스가 EPP(Endpoint Picker)입니다.

① 토큰 기반 전역 속도 제한

구조는 6장과 같습니다. 달라진 건 두 줄입니다. 스트림이 끝난 뒤(apply_on_stream_done) RLS에 보고하고, 카운터를 토큰 사용량만큼(hits_addend) 올립니다.

토큰 수는 응답 본문의 usage에서 읽어 메타데이터로 넘깁니다 — 본문을 읽는 일은 9장의 ext_proc가 합니다.

여기서 생기는 성질 하나. 요청이 들어올 땐 아직 몇 토큰짜리인지 모르니 한도를 넘기는 마지막 요청은 통과하고, 그 다음 요청부터 막힙니다. 한도는 그래서 "대략" 지켜집니다.

Nutanix 발표 기준으로 토큰 기반 속도 제한은 현재 4가지 타입을 지원하고, 모델별 사용량 한도는 Envoy 자체 속도 제한 기능을 씁니다.

② provider·모델 간 failover — 형식이 달라서 어렵다

3장의 재시도는 같은 cluster로 같은 요청을 다시 보냈습니다. provider를 바꾸면 그게 안 됩니다. API 스키마도, 인증도 다르기 때문입니다.

같은 모델을 다른 사업자나 다른 endpoint로 쓰는 경우, DeepSeek를 로컬과 외부 사업자에 동시에 두는 경우 등이 원문의 사용 사례입니다.

Envoy AI Gateway의 답은 upstream 단계의 ext_proc 필터입니다. 원문 예시는 Anthropic endpoint의 Primary와 Secondary 메타데이터를 보고 변환 로직을 다르게 적용합니다. 9장에서 볼 "ExtProc가 router 단계와 upstream 단계 두 곳에서 동작한다"는 설계가 이것 때문입니다.

③ self-hosted LLM용 지능형 부하 분산

일반 HTTP 요청은 헤더만 보고 판단합니다. 추론 요청은 본문을 봐야 판단할 수 있고, 원문 발표 자료에 따르면 그 확인에 일반 HTTP 요청 대비 약 3배의 컴퓨팅 비용이 듭니다. 그리고 1장의 round robin을 그대로 쓰면, 긴 요청을 처리하느라 대기열이 쌓인 파드에도 똑같이 요청을 보내 지연이 늘어납니다.

그래서 Gateway API Inference Extension이 나왔습니다. 요청을 EPP에 넘기고, EPP가 모델 서버 메트릭을 모아 적절한 파드를 고릅니다. 원문에는 inference extension과 일반 k8s Service의 벤치마크 비교 자료가 있고, 이 구조는 11장 실습과 [7W-2] llm-d에서 본격적으로 다룹니다.

비유 — 택배 요금은 건수가 아니라 무게

택배 회사가 "하루 10건까지 무료"라고 하면, 누구는 편지 10통, 누구는 냉장고 10대를 보냅니다. 트럭(GPU)을 거덜내는 건 냉장고 쪽입니다.

그래서 무게(토큰)로 한도를 겁니다. 다만 저울은 배송이 끝나야 찍혀서, 한도를 넘기는 마지막 짐 하나는 이미 실려 갑니다.

작은 숫자로 한 번

# 팀 A: 짧은 요청(500 토큰) 10건 팀 B: 긴 요청(12,000 토큰) 10건 요청 수 한도 10건/분 → 둘 다 통과. A = 5,000 토큰, B = 120,000 토큰 (24배 차이) 토큰 한도 30,000/분 → A 10건 전부 통과(5,000) B: 12k → 24k → 36k(3번째는 이미 통과, 끝난 뒤 한도 초과 확인) → 4번째부터 429 B = 36,000 토큰 — 초과분 6,000, 마지막 한 요청(12,000) 안쪽이다
그림 8

요청 수로 막기 vs 토큰으로 막기

버튼 · 슬라이더
해볼 것 — ① 기본값(요청 수 한도 10건/분, 긴 요청 50%)에서 아래 토큰 누적선이 예산선을 한참 넘는 걸 보세요. ② 토큰 한도로 바꾸면 누적선이 예산선 근처에서 멈추고, 마지막 한 요청만큼 살짝 넘습니다(스트림이 끝나야 세기 때문). ③ 요청 수 한도로 되돌린 뒤 긴 요청 비율을 0%로 내리면 반대 문제가 보입니다 — 요청 수 한도는 토큰을 거의 안 쓴 짧은 요청까지 막습니다. ④ 토큰 한도에서 긴 요청 크기를 29,000으로 올리면 소비가 58,000까지 뜁니다 — 한도를 28,000이나 넘습니다. 딱 30,000이면 첫 요청이 한도를 꽉 채워 초과가 0이 됩니다 — 초과폭은 한도 직전 마지막 요청의 크기를 넘지 않습니다.
단순화한 모형입니다

한 팀이 1분에 20건을 고르게 보내고, 짧은 요청은 500 토큰, 긴 요청은 슬라이더 값이라고 가정했습니다. 어떤 요청이 긴지는 고정 씨앗으로 정했습니다. 한도 값(10건·30,000 토큰)은 설명용 예시입니다.

2026년에 더해진 것 — 비용 최적화

기능한 줄 설명
Quota-aware routingQuotaPolicy로 provider별 할당량을 보고, 여유 있는 쪽으로 보냅니다.
Context cachingprovider의 컨텍스트 캐시를 활용해 반복되는 긴 프롬프트 비용을 줄입니다.
Batch processing급하지 않은 요청을 배치 API로 돌려 단가를 낮춥니다.
Extended APIsChat 외에 Responses API, Embeddings API, 멀티모달 입력.

원문에는 이 부분이 발표 슬라이드 캡처와 제목으로만 정리돼 있어, 설명은 기능 이름의 일반적인 뜻 수준으로 적었습니다. 동작 세부는 발표 영상에서 확인이 필요합니다.

8장 세 줄 요약
  1. 정의 — 토큰 기반 속도 제한은 6장의 전역 속도 제한에 apply_on_stream_done과 hits_addend를 더해 토큰 수만큼 세는 방식입니다.
  2. 성질 — 사용량은 스트림이 끝나야 알 수 있어 최대 마지막 요청 하나만큼 한도를 넘습니다. provider failover는 스키마·인증 변환이 필요하고, self-hosted 부하 분산은 본문과 대기열을 봐야 해서 비용이 더 듭니다.
  3. 쓰임 — 팀·모델별 예산 관리, provider 장애 대응, vLLM 파드 간 분산. 셋 다 "본문을 읽는 누군가"가 필요하고, 그게 다음 장의 ext_proc입니다.

8장에서 세 번 "본문을 읽는다"고 했습니다. Envoy 코어는 LLM을 모릅니다. 그렇다면 누가, 어디서, 어떻게 읽을까요.

09ext_proc와 Agent Router 구조 — LLM 로직은 Envoy 밖에 둔다

한 줄 요약: ext_proc 필터는 요청·응답을 단계마다 바깥 gRPC 서버에 보여 주고 결정을 받습니다. Agent Router(구 Envoy AI Gateway)는 이 필터 위에 모델 라우팅·형식 변환·토큰 집계를 올리고, 설정은 Envoy Gateway 위에 한 겹 더 얹습니다.

왜 이걸 배우나
"Envoy에 LLM 기능을 넣는데, 왜 Envoy 소스는 한 줄도 안 고쳤다고 할까?"

원문의 한 줄이 이 장의 전부입니다. "AI 로직이 Envoy 코어에 침투하지 않는다." 5장의 확장 네 가지 길 중, 빌드가 필요 없는 셋 가운데 external processing을 택했기 때문입니다. C++ 필터를 컴파일해 넣는 대신, 어떤 언어로든 gRPC 서비스만 구현하면 트래픽을 가로채 조작할 수 있습니다.

그리고 이 필터가 [7W-2] llm-d의 연결점입니다. llm-d의 EPP도 ext_proc 서버이고, 원문은 [7W-1]·[7W-2] 두 자료에 그 Envoy 설정 파일을 똑같이 싣습니다. ext_proc를 한 번 이해하면 두 프로젝트를 같은 그림으로 읽을 수 있습니다.

이 장에 나오는 낯선 말
ext_proc · External Processor
envoy.filters.http.ext_proc 필터와, 그 필터가 호출하는 바깥 gRPC 서버. 요청·응답 처리 로직을 Envoy 프로세스 밖으로 위임합니다.
처리 단계 (phase)
RequestHeaders → RequestBody → ResponseHeaders → ResponseBody(+ trailers). 단계마다 gRPC 스트림으로 데이터를 보내고 답을 받습니다.
CONTINUE · mutation · immediate response
외부 서버의 세 가지 답. 그대로 통과, 헤더·바디 수정, 즉시 응답 반환(예: 401로 차단).
processing_mode
어느 단계를 보낼지 정하는 설정. 헤더는 SEND/SKIP, 바디는 NONE·BUFFERED(다 모아서 한 번)·BUFFERED_PARTIAL·STREAMED·FULL_DUPLEX_STREAMED(버퍼 없이 양방향 실시간)입니다.
ORIGINAL_DST cluster
endpoint 목록이 고정돼 있지 않고 요청마다 목적지를 정하는 Envoy 특수 클러스터. 헤더에 적힌 주소로 바로 연결할 수 있습니다.
Envoy Gateway · Extension Server
Envoy Gateway는 쿠버네티스 Gateway API를 Envoy 설정(xDS)으로 바꿔 주는 컨트롤러. Extension Server는 그 변환 도중에 끼어들어 xDS를 미세 조정하게 해 주는 훅입니다.
UDS (Unix Domain Socket)
같은 머신 안의 프로세스끼리 네트워크 스택을 거치지 않고 통신하는 소켓 파일.

ext_proc는 어떻게 동작하나

  1. Envoy가 요청을 잠시 붙잡는다4장 그림의 HTTP filter 자리에서, 단계마다 데이터를 gRPC 스트림으로 외부 서버에 보냅니다.
  2. 외부 서버가 보고 답한다CONTINUE · 수정 · 즉시 응답 중 하나. processing_mode로 "헤더만 볼지, 바디를 버퍼로 볼지, 스트리밍으로 볼지"를 정합니다.
  3. Envoy가 그 답을 적용하고 계속 간다수정된 헤더로 라우팅이 달라질 수도 있습니다. 이게 모델 이름 기반 라우팅의 원리입니다.
주요 용도LLM 게이트웨이에서의 모습
인증·인가 (커스텀 토큰 검증)앱별 대리 키 검증
요청·응답 변환 (헤더 삽입, 바디 rewrite)OpenAI 형식 ↔ Anthropic·Bedrock 형식 변환
본문 기반 라우팅"model" 필드를 읽어 x-ai-eg-model 헤더로 옮기고, 그 헤더로 라우트 매칭
콘텐츠 검사·차단 (모더레이션, PII 마스킹)프롬프트 가드레일
사용량 계측응답 usage에서 토큰 수를 뽑아 8장의 토큰 속도 제한에 넘김
공짜는 아니다

gRPC라서 요청마다 네트워크 왕복(hop)이 추가되어 지연 오버헤드가 있습니다. 그래서 실무에서는 워크로드마다 붙는 메시 사이드카에 넣기보다 Envoy Gateway·Istio가 관리하는 gateway 레벨에 붙이는 경우가 많고, Agent Router는 ExtProc를 그 gateway 파드 안에 두고 UDS로 통신해 이 비용을 줄입니다(10장).

llm-d의 Envoy 설정에서 본 두 가지 트릭

하나, 바디는 FULL_DUPLEX_STREAMED. vLLM의 SSE 응답을 토큰 단위로 그대로 흘려보내면서 외부 서버도 볼 수 있게 합니다. 전체 응답을 기다렸다 처리하면 스트리밍 UX가 깨지기 때문입니다.

같은 설정 블록의 message_timeout: 1000s는 긴 생성 스트림을 고려한 값입니다.

둘, ORIGINAL_DST 클러스터. 외부 서버(EPP)가 요청 헤더에 x-gateway-destination-endpoint: <파드IP>:<포트>를 심으면 Envoy는 그 주소로 바로 연결합니다.

부하 분산 결정을 Envoy 알고리즘이 아니라 EPP가 요청 단위로 100% 위임받는 구조입니다. kube-proxy처럼 "연결 단위로 배정한 뒤 keep-alive로 고착"되는 문제가 구조적으로 생길 수 없습니다.

원문 llm-d envoy.yaml 발췌 — ext_proc 필터, ORIGINAL_DST, ext_proc 클러스터
http_filters:
  - name: envoy.filters.http.ext_proc
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
      grpc_service:
        envoy_grpc: { cluster_name: ext_proc, authority: localhost:9002 }
        timeout: 10s
      processing_mode:
        request_header_mode: SEND            # 헤더는 전부 EPP에 보낸다
        response_header_mode: SEND
        request_body_mode: FULL_DUPLEX_STREAMED   # 버퍼 없이 양방향 실시간
        response_body_mode: FULL_DUPLEX_STREAMED
        request_trailer_mode: SEND
        response_trailer_mode: SEND
      message_timeout: 1000s                 # 긴 생성 스트림 고려

clusters:
- name: original_destination_cluster
  type: ORIGINAL_DST                         # 고정 endpoint 없음
  lb_policy: CLUSTER_PROVIDED
  original_dst_lb_config:
    use_http_header: true
    http_header_name: x-gateway-destination-endpoint   # EPP가 심은 파드 주소로 직행
  # circuit_breakers.max_connections: 40000 등 — 오래 열려 있는 스트리밍 연결을 견디도록 사실상 해제

- name: ext_proc
  type: STATIC
  lb_policy: LEAST_REQUEST
  health_checks:
    - grpc_health_check: { service_name: "envoy.service.ext_proc.v3.ExternalProcessor" }
  load_assignment:
    endpoints: [{ address: 127.0.0.1, port_value: 9002 }]   # Envoy와 EPP가 같은 호스트에 동거

no-kubernetes 배포 예제라 EPP가 127.0.0.1:9002에 같이 떠 있고, 같은 머신 내부 통신이라 TLS도 생략했습니다. HTTP/2 window 크기 설정은 ext_proc의 양방향 스트리밍 처리량 조정용입니다.

비유 — 통역사를 옆에 앉힌 창구 직원

창구 직원(Envoy)은 외국어 서류를 못 읽습니다. 그래서 서류를 한 장씩 옆자리 통역사(ext_proc)에게 보여 주고 "이건 3번 창구로", "이 줄은 가려서" 같은 지시를 받습니다. 직원을 새로 교육(코어 수정)할 필요가 없고, 통역사는 아무 언어로 일해도 됩니다.

지시를 받는 방법이 두 가지입니다. 서류 뭉치를 다 받은 뒤 한 번에 보여 주면(BUFFERED) 손님은 그동안 아무것도 못 받습니다. 한 장씩 즉시 보여 주면(FULL_DUPLEX_STREAMED) 손님도 한 장씩 받습니다.

작은 숫자로 한 번

# vLLM: 첫 토큰 200 ms, 이후 토큰마다 20 ms, 300 토큰. ext_proc 왕복 2 ms 마지막 토큰 생성 = 200 + 299 × 20 = 6,180 ms NONE 클라이언트 첫 토큰 200 ms ext_proc는 바디를 못 봄 → 토큰 수 집계 불가 BUFFERED 클라이언트 첫 토큰 6,182 ms 다 모일 때까지 아무것도 안 감 FULL_DUPLEX_STREAMED 클라이언트 첫 토큰 202 ms 청크마다 2 ms 추가, 토큰 수 집계 가능
그림 9

response_body_mode에 따라 클라이언트가 토큰을 받는 시점

버튼 · 슬라이더
해볼 것 — ① FULL_DUPLEX_STREAMED(기본값)에서 아래 두 줄의 눈금이 거의 겹치는 걸 보세요. ② BUFFERED로 바꾸면 클라이언트 줄이 맨 끝에 한 번에 몰립니다 — 스트리밍 UX가 깨집니다. ③ NONE은 빠르지만 ext_proc 줄이 비어 토큰을 셀 수 없습니다(8장의 토큰 한도 불가). ④ ext_proc 왕복을 20 ms로 올리면 스트리밍 모드에서 모든 청크가 20 ms씩 늦게 도착하고, 오른쪽의 gRPC 메시지 수는 토큰 수만큼입니다. 청크마다 왕복이 붙으니 한 번의 왕복을 짧게 만드는 게 중요합니다 — Agent Router가 같은 파드에서 UDS를 쓰는 이유입니다.
단순화한 모형입니다

토큰마다 청크 하나, 모델의 첫 토큰 시간(TTFT)은 200 ms로 고정했습니다. 스트리밍 모드의 왕복 지연은 청크마다 누적되지 않고 각 청크에 한 번씩 붙는다고 보았고, 버퍼 크기 제한은 넣지 않았습니다(10장 실습에서 버퍼 한도를 50 MiB로 올립니다).

Agent Router — 이름과 버전

Envoy AI Gateway는 CNCF의 Envoy 하위 프로젝트로 시작해 2025-08-25 Linux Foundation에 벤더 중립 거버넌스로 기증됐고, 2026년 9월 10일부로 독립 프로젝트 "Agent Router"로 전환되어 Agentic AI Foundation(AAIF)으로 이전했습니다. AAIF는 에이전틱 AI를 엔터프라이즈 규모에서 실제로 운영할 수 있게 만드는 것을 미션으로 내건 LF 산하 재단이고, 8개 워킹그룹을 운영합니다.

Capabilityv0.1 (2025년 2월)1.0
AI providers2 (OpenAI, AWS Bedrock)16, provider 간 요청·응답 변환
API surfaceChat completionsChat, completions, embeddings, image generation, audio, OpenAI Responses API
MCP—MCP gateway — 서버 멀티플렉싱, tool 라우팅·필터링, 세밀한 인가
Multimodal—이미지·오디오·비디오 입력
Observability기본 메트릭OpenTelemetry tracing, OpenInference, GenAI 토큰 메트릭, reasoning 토큰 별도 집계
Multi-tenancy & routing토큰 속도 제한호스트명 기반 라우팅, model virtualization, quota-aware 속도 제한
Control-plane APIv1alpha1 (실험)v1beta1 (stable)

원문은 공식 블로그의 참조 아키텍처(Two-Tier Gateway, KServe로 self-hosted 서빙), Endpoint Picker 지원, OpenTelemetry 트레이싱, "stateless" MCP 라우팅 글도 함께 링크합니다. Two-Tier 구조는 [7W-2] 6장에서 llm-d와 나란히 봅니다.

구조 — control plane 둘, data plane 셋

AI Gateway(Agent Router) ControllerEnvoy Gateway Controller
관심사AI 전용 — 변환·검증 같은 도메인 로직일반 게이트웨이 — 라우팅, 속도 제한
설정 대상External Processor 설정 + Extension Server로 xDS 미세 조정Envoy Proxy(xDS) + Rate Limit Service

설정 흐름은 다섯 단계입니다. ① Agent Router Controller가 AI Gateway CR 변경을 감시 → ② 대응하는 Envoy Gateway 설정과 HTTPRoute 생성 → ③ Envoy Gateway가 xDS로 변환 → ④ Agent Router Controller가 Extension Server 프로토콜로 그 xDS를 다시 미세 조정 → ⑤ Envoy Proxy가 최종 설정을 받아 적용. 10장 그림에서는 ①과 ②를 한 단계로 묶고, 앞뒤에 kubectl apply와 extproc의 Secret 수신을 더해 여섯 단계로 재생해 봅니다.

Data plane은 Envoy Proxy, AI Gateway External Processor(모델 선택·검증, 응답에서 토큰 사용량 추적, provider 간 형식 변환), Rate Limit Service(토큰 기반 한도 관리)입니다. 요청 경로는 경로·헤더·모델명 라우팅 → provider 형식 변환 → provider별 인증 → 속도 제한 확인, 응답 경로는 공통 형식 변환 → 토큰 사용량 추출 → 한도용 메트릭 저장입니다.

핵심 설계 — ExtProc가 두 번 끼는 이유

External Processor는 router 단계와 upstream 단계 두 곳에서 동작합니다. 한 provider 요청이 실패해 다른 provider로 재시도할 때, router 단계를 다시 평가하지 않고 upstream 단계에서 변환·인증만 다시 적용하면 됩니다. 4장 그림의 "upstream filter" 칸이 여기서 쓰입니다(8장 ②의 답).

CRD 세 개

Client Request
     ↓
AIGatewayRoute         통합 진입점 — 입력 스키마, 라우팅 규칙, 요청·응답 변환, LLM 비용 모니터링
     ↓
AIServiceBackend       개별 백엔드 — 기대하는 출력 스키마, K8s Service 또는 Envoy Gateway Backend 참조
     ↓ (선택적으로 참조)
BackendSecurityPolicy  인증 — API Key, AWS 자격증명 등
Local Quick Start — 컨테이너 하나로 띄워 보기
export OPENAI_API_KEY=###
docker run -d --name agentrouter -p 1975:1975 -e OPENAI_API_KEY=$OPENAI_API_KEY \
  envoyproxy/ai-gateway-cli:latest run

docker logs agentrouter -f
#  AI Gateway External Processor is ready
#  Envoy AI Gateway listening on http://localhost:1975 (admin http://localhost:42387) after 4.6s

curl http://localhost:1975/v1/chat/completions -H "Content-Type: application/json" \
  -d '{"model": "gpt-5", "messages": [{"role": "user", "content": "Say this is a test!"}]}'
#  "model": "gpt-5-2025-08-07", ... "content": "this is a test!"

docker rm -f agentrouter

로그 첫 줄에 External Processor가 먼저 뜨는 게 보입니다. Envoy와 ExtProc가 한 컨테이너에 같이 들어 있는 형태입니다.

9장 세 줄 요약
  1. 정의 — ext_proc는 요청·응답을 단계마다 외부 gRPC 서버에 보여 주는 HTTP 필터이고, Agent Router는 그 위에 모델 라우팅·형식 변환·토큰 집계를 구현한 프로젝트입니다(구 Envoy AI Gateway, 이제 AAIF 소속).
  2. 성질 — Envoy 코어는 그대로입니다. 대신 hop 비용이 있고, processing_mode가 스트리밍 UX와 토큰 집계 가능 여부를 함께 정합니다. ExtProc는 router·upstream 두 단계에 끼어 failover 변환을 깔끔하게 처리합니다.
  3. 쓰임 — 설정은 AIGatewayRoute·AIServiceBackend·BackendSecurityPolicy로 쓰고, 컨트롤러 둘이 Extension Server 훅으로 협력해 xDS를 만듭니다. llm-d의 EPP도 같은 ext_proc 자리에 꽂힙니다.

이제 손으로 확인할 차례입니다. kind 클러스터에 Envoy Gateway와 Agent Router를 올리고, 파드 속 컨테이너 셋과 설정이 흘러가는 길을 따라가 봅니다.

10실습 ① Agent Router 올리기 — 파드 속 컨테이너 셋과 설정의 길

한 줄 요약: kind 위에 Envoy Gateway와 Agent Router 컨트롤러를 올리면 control plane만 생기고, Gateway 리소스를 만들어야 비로소 Envoy 파드(컨테이너 3개)가 뜹니다. 설정은 CRD → 컨트롤러 둘 → xDS로, 요청은 Envoy → ext_proc(UDS) → upstream으로 흐릅니다.

왜 이걸 배우나
"kubectl apply 한 번에 게이트웨이가 생겼는데, 안에서 누가 무엇을 만들었지?"

설치 문서대로 따라 하면 몇 분이면 됩니다. 하지만 요청이 안 갈 때 볼 곳을 모르면 그 몇 분이 몇 시간이 됩니다. 원문 실습은 설치 후에 파드를 describe로 해부하고, Admin API로 xDS 연결을 확인하고, 리스너를 하나 추가해 반영되는 순간을 카운터로 잡습니다.

1~9장에서 본 개념이 전부 실물로 나옵니다. xds_cluster(2장), Admin API(3장), 필터 체인(5장), ext_proc와 UDS(9장), Extension Server(9장).

이 장에 나오는 낯선 말
kind · MetalLB
kind는 도커 컨테이너를 노드로 쓰는 로컬 쿠버네티스, MetalLB는 클라우드가 없는 환경에서 LoadBalancer Service에 IP를 나눠 주는 구현체입니다.
GatewayClass · Gateway · HTTPRoute
쿠버네티스 Gateway API 리소스. GatewayClass는 어떤 컨트롤러가 처리할지, Gateway는 실제 입구(리스너·주소), HTTPRoute는 경로 규칙입니다.
EnvoyProxy · ClientTrafficPolicy · Backend
Envoy Gateway의 확장 CRD. 각각 Envoy 파드 배포 방식, 클라이언트 쪽 연결 정책(버퍼 한도 등), Service 대신 FQDN·IP로 직접 적는 백엔드 주소(클러스터 안 mock, 밖의 api.openai.com 모두)를 정의합니다.
Native sidecar
restartPolicy: Always가 붙은 initContainer. 초기화 후 끝나지 않고 메인 컨테이너와 함께 계속 도는 사이드카입니다.
shutdown-manager
파드가 내려갈 때 Envoy가 새 연결을 거부하고 기존 연결을 다 처리할 때까지 기다리게 하는 보조 컨테이너(drain).
mock upstream (testupstream)
실제 LLM 대신 정해진 가짜 응답을 돌려주는 테스트 서버. API 키 없이 경로·스키마를 검증할 때 씁니다.

설치 순서

  1. kind 클러스터kind v0.33.0 기본 노드 이미지가 Kubernetes 1.37.0(요구 1.32+). 원문은 macOS라면 Rosetta를 켠 VM 안에 K3s나 kind를 올리라고 안내합니다.
  2. Envoy Gateway v1.8.1 (Helm)values 파일이 Extension Server 통로를 여는 역할입니다 — enableBackend: true, extensionManager.hooks.xdsTranslator(post: Translation → Cluster → Route), 연결 대상 ai-gateway-controller.envoy-ai-gateway-system.svc.cluster.local:1063.
  3. Agent Router v1.1.0 — CRD, 컨트롤러CRD 6종(aigatewayroutes, aiservicebackends, backendsecuritypolicies, gatewayconfigs, mcproutes, quotapolicies)과 ai-gateway-controller(9443·1063·8080·18002 포트).
  4. 이 시점엔 data plane이 없다컨트롤러 둘만 있습니다. Gateway 리소스를 만들어야 그걸 보고 전용 Envoy 파드가 생깁니다.
  5. MetalLB v0.16.1kind 도커 네트워크 172.18.0.0/16에서 172.18.255.200~250 풀을 L2로 광고. 테스트 nginx가 172.18.255.200을 받는지 확인 후 삭제.
  6. basic.yaml 적용GatewayClass·Gateway(HTTP:80) · EnvoyProxy(리소스 요청 제거) · ClientTrafficPolicy(버퍼 32 KiB → 50 MiB) · AIGatewayRoute(x-ai-eg-model: some-cool-self-hosted-model) · AIServiceBackend+Backend(schema OpenAI) · testupstream Deployment. Gateway 주소 172.18.255.200.

Envoy 파드 해부 — 컨테이너 셋

컨테이너무엇눈여겨볼 설정
ai-gateway-extproc
init · native sidecar
AI Gateway External Processor (ai-gateway-extproc:v1.1.0)-extProcAddr unix:///etc/ai-gateway-extproc-uds/run.sock — Envoy와 UDS로 통신(emptyDir 공유) · -configBundlePath /etc/filter-config-bundle — 컨트롤러가 만든 필터 설정 Secret · -endpointPrefixes openai:,cohere:/cohere,anthropic:/anthropic — provider별 스키마 변환 경로 · readiness failureThreshold=1(잠깐만 응답 못 해도 트래픽 차단)
envoyEnvoy Proxy (envoy:distroless-v1.38.1)컨테이너 포트는 metrics 19001 · readiness 19003뿐(admin 19000은 127.0.0.1 전용). 부트스트랩에는 사용자 트래픽 리스너가 없고, 리스너·라우트·클러스터를 xDS(ADS, DELTA_GRPC)로 envoy-gateway…:18000에서 받습니다. xds_cluster는 SDS 인증서로 mTLS. 부트스트랩의 EDS 클러스터(least_request)는 local cluster이고, 실제 백엔드는 xDS로 오는 httproute/…/rule/0입니다.
shutdown-managergraceful shutdown 보조drain-strategy immediate, drain-time-s 60

9장에서 본 data plane 셋 중 Rate Limit Service는 이번 실습에서 설치하지 않았습니다(Envoy Gateway의 Rate Limiting addon 미설치). 설치하더라도 이 파드가 아니라 envoy-gateway-system에 별도 배포로 뜹니다 — 이 파드의 컨테이너는 위 셋입니다.

볼륨도 흥미롭습니다. 필터 설정 Secret이 1 MiB 크기 제한을 넘으면 part-000 ~ part-007처럼 여러 조각으로 쪼개 Projected 볼륨으로 합칩니다. LLM 라우팅 규칙이 많아지면 설정 자체가 커진다는 뜻입니다.

xDS가 정말 흐르는지 — 리스너 하나 추가해 보기

원문은 2장의 개념을 여기서 증명합니다. Admin 포트(19000)를 포트포워딩하고 control_plane.connected_state: 1을 확인한 뒤, LDS 카운터를 적어 둡니다(update_attempt: 9, update_success: 7).

Gateway에 8081 리스너를 kubectl patch로 추가하고 5초 뒤 다시 보면 파드 재시작 없이 10 / 8로 늘어 있고, /listeners에 default/envoy-ai-gateway-basic/http-test::0.0.0.0:8081이 나타납니다. 확인 후 원복합니다.

호출해 보기 — mock과 실제 OpenAI

호출결과의미
POST /v1/chat/completions (model: some-cool-self-hosted-model)200 · "I see dead people." · usage prompt 1 / completion 100 / total 300(mock이 임의로 채운 값이라 합이 맞지 않습니다)mock이 고정 데이터를 줌. 답의 내용이 아니라 스키마 모양(choices[].message)이 검증 포인트
POST /v1/embeddings200 · embedding: [0.1 … 0.5]Embeddings API 경로 동작
POST /v1/completionsunknown path: /v1/completionsmock 이미지가 이 경로의 가짜 응답을 구현하지 않음
POST /cohere/v2/rerankextproc 로그에 cannot set backend: failed to create translatorrerank는 Cohere 스키마 백엔드가 필요 — OpenAI 스키마 백엔드로는 변환기가 없음
model: gpt-4o-mini (openai.yaml 적용 후)200 · "Hello! How can I assist you today?"Backend api.openai.com:443 + BackendTLSPolicy + BackendSecurityPolicy(APIKey)로 실제 provider 도달

mock 파드 로그에서 한 가지를 꼭 보세요. 클라이언트는 본문에 "model"만 넣었는데, upstream이 받은 요청엔 X-Ai-Eg-Model: some-cool-self-hosted-model 헤더가 붙어 있습니다.

이 헤더가 9장의 "본문을 읽어 헤더로 옮기고, 그 헤더로 라우트를 매칭"이 실제로 일어난 흔적입니다.

로그를 하나 더 보면, Envoy 액세스 로그의 downstream_local_address는 :10080 — Gateway의 80번이 파드 안에선 10080 리스너입니다.

비유 — 새 매장 오픈

본사(control plane) 사무실 두 곳을 먼저 차립니다. 한 곳은 일반 매장 운영팀(Envoy Gateway), 한 곳은 AI 메뉴 전담팀(Agent Router).

아직 매장은 없습니다. "매장 하나 열어라"(Gateway)는 지시서가 와야 매장 건물(Envoy 파드)이 섭니다.

운영팀이 매장 매뉴얼을 쓰다가, AI 메뉴 부분은 전담팀에게 넘겨 고치게 합니다(Extension Server). 매장엔 계산원(envoy) 옆에 통역 담당(extproc)이 붙어 앉고(UDS), 폐점 정리 담당(shutdown-manager)이 한 명 더 있습니다.

작은 숫자로 한 번

# Gateway 에 리스너를 하나 추가하면 LDS 카운터가 어떻게 변하나 (원문 값) 변경 전 listener_manager.lds.update_attempt: 9 update_success: 7 patch spec.listeners += {name: http-test, port: 8081} sleep 5 변경 후 listener_manager.lds.update_attempt: 10 update_success: 8 /listeners → default/envoy-ai-gateway-basic/http-test::0.0.0.0:8081 # attempt 가 success 보다 2 큰 이유는 원문에 없다 — 내용이 빈 응답이나 거부된 업데이트가 앞서 있었을 수 있다
그림 10

설정이 가는 길, 요청이 가는 길

버튼 · 이전·다음·재생
해볼 것 — ① 설정 흐름에서 다음을 눌러 6단계를 따라가세요. 4단계에서 Envoy GW Controller가 AI GW Controller에게 되묻는 화살표(Extension Server)가 핵심입니다. ② 6단계에서 extproc가 설정을 xDS가 아니라 Secret 볼륨으로 받는 것도 보세요. ③ 요청 흐름으로 바꾸면 요청이 파드 안에서 envoy ↔ extproc를 UDS로 두 번(요청 때, 응답 때) 오갑니다. ④ 재생으로 흐름을 한 번에 보고, 초기화로 처음으로 돌아갑니다.
원문 명령 요약 — 설치부터 호출까지
# Envoy Gateway (Extension Server 통로를 연 values)
helm upgrade -i eg oci://docker.io/envoyproxy/gateway-helm --version v1.8.1 \
  -n envoy-gateway-system --create-namespace \
  -f https://raw.githubusercontent.com/theagentrouter/agent-router/main/manifests/envoy-gateway-values.yaml

# Agent Router CRD + 컨트롤러
helm upgrade -i aieg-crd oci://docker.io/envoyproxy/ai-gateway-crds-helm --version v1.1.0 -n envoy-ai-gateway-system --create-namespace
helm upgrade -i aieg     oci://docker.io/envoyproxy/ai-gateway-helm      --version v1.1.0 -n envoy-ai-gateway-system --create-namespace

# MetalLB
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-native.yaml
#   IPAddressPool 172.18.255.200-172.18.255.250 + L2Advertisement

# 기본 예제
kubectl apply -f https://raw.githubusercontent.com/theagentrouter/agent-router/main/examples/basic/basic.yaml
kubectl get pod -n envoy-gateway-system -l app.kubernetes.io/component=proxy     # READY 3/3

# xDS 확인
kubectl port-forward -n envoy-gateway-system pod/$POD --address 0.0.0.0 19000:19000 &
curl -s "$ADMIN/stats?filter=control_plane"          # control_plane.connected_state: 1
curl -s "$ADMIN/clusters" | grep -A1 "^xds_cluster"  # ...envoy-gateway.envoy-gateway-system.svc.cluster.local:18000
kubectl patch gateway envoy-ai-gateway-basic -n default --type=json \
  -p='[{"op":"add","path":"/spec/listeners/-","value":{"name":"http-test","port":8081,"protocol":"HTTP","allowedRoutes":{"namespaces":{"from":"Same"}}}}]'
curl -s "$ADMIN/listeners" | grep 8081

# 호출
export GATEWAY_URL=$(kubectl get gateway/envoy-ai-gateway-basic -o jsonpath='{.status.addresses[0].value}')
curl -H "Content-Type: application/json" \
  -d '{"model":"some-cool-self-hosted-model","messages":[{"role":"system","content":"Hi."}]}' \
  $GATEWAY_URL/v1/chat/completions
kubectl logs -n envoy-gateway-system -l app.kubernetes.io/component=proxy -c ai-gateway-extproc --tail=20 -f

# 실제 OpenAI (Secret 에 API 키)
kubectl apply -f https://raw.githubusercontent.com/theagentrouter/agent-router/main/examples/basic/openai.yaml
curl -s http://<admin>:19000/clusters | grep -i openai    # ...api.openai.com ... rq_success::2
10장 세 줄 요약
  1. 정의 — Agent Router 설치 = Envoy Gateway(일반 게이트웨이 control plane) + Agent Router 컨트롤러(AI 전용 control plane). data plane Envoy 파드는 Gateway 리소스를 만들 때 생기고, 컨테이너는 extproc(native sidecar)·envoy·shutdown-manager 셋입니다.
  2. 성질 — Envoy는 리스너·라우트를 전부 xDS(ADS, DELTA_GRPC, :18000)로 받고, extproc는 필터 설정을 Secret 볼륨으로 받으며 둘은 UDS로 통신합니다. 리스너 추가는 재시작 없이 LDS 카운터로 확인됩니다.
  3. 쓰임 — 안 될 때 볼 곳: xDS 연결(control_plane.connected_state), extproc 로그(스키마 변환 오류), Envoy 액세스 로그(upstream_cluster), upstream이 받은 헤더(X-Ai-Eg-Model).

여기까지는 mock과 외부 provider였습니다. 마지막으로 upstream을 "직접 띄운 모델 서버 묶음"으로 바꿉니다. 8장에서 예고한 InferencePool과 EPP가 등장합니다.

11실습 ② InferencePool 연결 — 모델 이름으로 풀을 고르고, EPP가 파드를 고른다

한 줄 요약: HTTPRoute나 AIGatewayRoute의 backend를 일반 Service 대신 InferencePool로 두면, Envoy는 "EPP에게 물어봐야 하는 백엔드"로 인식합니다. AIGatewayRoute를 쓰면 모델 이름으로 풀을 나누고, 풀 안에서는 EPP가 파드를 고릅니다.

왜 이걸 배우나
"직접 띄운 모델 서버 앞에 게이트웨이를 둘 때, 그냥 Service로 보내면 안 되나?"

8장 ③에서 본 문제입니다. Service(kube-proxy)는 파드가 바쁜지, KV 캐시에 무엇이 있는지 모릅니다.

Gateway API Inference Extension은 그 판단을 EPP에게 맡기는 표준입니다. 원문 표현으로 "Agent Router가 지능형 라우팅과 부하 분산으로 추론 요청을 효율적으로 전달"하는 기능입니다.

반가운 점 하나. 원문은 이 실습이 GPU 없이 CPU만으로 가능하다고 적었습니다.

CPU만으로 되는 건 llama.cpp 서버에 1B·0.5B짜리 작은 GGUF 모델을 올리기 때문입니다. 그리고 EPP 이미지가 llm-d-router-endpoint-picker입니다 — [7W-2]로 넘어가는 문이 여기서 열립니다.

이 장에 나오는 낯선 말
InferencePool
같은 모델을 서빙하는 파드들을 라벨 셀렉터로 묶은 리소스(inference.networking.k8s.io/v1). 어느 포트로 보낼지(targetPorts), 어느 EPP에게 물을지(endpointPickerRef)를 적습니다.
EPP (Endpoint Picker)
InferencePool 안에서 이번 요청을 받을 파드 하나를 고르는 gRPC 서비스(9002). 9장의 ext_proc 서버입니다.
BBR (Body Based Router)
Inference Extension의 다른 구성 요소. 본문을 읽어 헤더로 올려 라우팅에 쓰게 합니다. Agent Router에선 extproc가 이 일을 합니다.
failureMode: FailClose
EPP가 응답하지 않으면 요청을 실패시킨다는 설정. 반대는 FailOpen(EPP 없이 아무 파드로 보냄).
KV 캐시
모델이 앞서 읽은 토큰의 중간 계산 결과를 파드 메모리에 쌓아 둔 것. 같은 프롬프트가 다시 오면 그 파드가 훨씬 빨리 답합니다([7W-2] 5장).
llama.cpp · GGUF
llama.cpp는 CPU에서도 도는 C++ 추론 서버, GGUF는 그 서버가 읽는 양자화 모델 파일 형식입니다. 실습 모델은 Llama-3.2-1B-Instruct-Q4_K_M과 Qwen2.5-0.5B-Instruct.
extensionManager.backendResources
Envoy Gateway에게 "HTTPRoute의 backendRef로 InferencePool 종류도 받아들여라"고 알려 주는 설정.

두 가지 연결 방식

HTTPRoute + InferencePoolAIGatewayRoute + InferencePool
성격표준 Gateway API에 직접 연결. 단순한 추론 라우팅Agent Router 전용 라우트. AI 특화 기능과 함께
매칭 기준경로(예제는 / 전체)헤더 x-ai-eg-model — 본문의 model에서 extproc가 올린 값(원문 curl은 헤더도 직접 붙였습니다)
실습 Gatewayinference-pool-with-httproute · 172.18.255.201inference-pool-with-aigwroute · 172.18.255.202
모델 수풀 하나(llama32-1b)풀 둘(llama32-1b, qwen25-05b)

요청이 지나는 여섯 단계 (원문 흐름)

  1. Client Requestcurl이 Gateway 주소로 /v1/chat/completions를 보냅니다.
  2. Route MatchingHTTPRoute가 경로로(또는 AIGatewayRoute가 모델 헤더로) 규칙을 고릅니다.
  3. InferencePool ResolutionEnvoy Gateway가 backendRef가 일반 Service가 아니라 InferencePool임을 알아보고 "EPP에게 물어봐야 하는 특수 백엔드"로 처리합니다.
  4. Endpoint SelectionEnvoy가 ext_proc로 EPP에게 묻고, EPP가 파드별 큐 길이·캐시 상태를 점수화해 하나를 고릅니다. 실습은 파드가 1개라 선택지가 없습니다.
  5. Request ForwardingEPP가 고른 파드 IP로 Envoy가 실제 요청을 보냅니다.
  6. Response Returnllama.cpp가 만든 답이 Envoy를 거쳐 그대로 돌아옵니다.

실습에서 본 것

설치 — Inference Extension v1.6.1 CRD(inferencepools v1, inferencepoolimports v1alpha1)를 넣고, Envoy Gateway를 --reuse-values로 업그레이드하며 backendResources에 InferencePool을 추가한 뒤 재시작합니다.

HTTPRoute 방식 — "Say this is a test"에 "This is definitely a test. I'm here to provide information…"가 돌아왔고, usage는 prompt 40 / completion 24, cached_tokens: 39. 응답의 model은 bartowski/Llama-3.2-1B-Instruct-GGUF입니다.

한 가지 로그 — EPP 로그에 dispatch failed … 501이 계속 찍힙니다. llama.cpp가 vLLM 전용 메트릭 프로토콜을 구현하지 않아서이고, 원문 판단은 "정상. 스케줄링 신호 품질만 떨어질 뿐 라우팅은 문제없다"입니다.

이 말은 EPP가 파드 상태를 모르면 점수를 매길 근거가 없어진다는 뜻입니다. EPP가 모델 서버에 요구하는 메트릭 목록과 함께 [7W-2] 2장에서 다시 봅니다.

AIGatewayRoute 방식 — 두 번째 풀(qwen25-05b + EPP)을 올리고 규칙 두 줄을 씁니다. x-ai-eg-model: llama-3.2-1b → 풀 llama32-1b, qwen2.5-0.5b → 풀 qwen25-05b. 같은 질문에 한쪽은 bartowski/Llama-3.2-1B-Instruct-GGUF, 다른 쪽은 "I am Qwen, … created by Alibaba Cloud"와 Qwen/Qwen2.5-0.5B-Instruct-GGUF로 답했습니다.

비유 — 병원 접수와 진료실 배정

접수(Gateway)는 증상 칸(model)을 보고 진료과(InferencePool)를 정합니다. 내과면 내과, 이비인후과면 이비인후과. 진료과 안에서 어느 의사 방으로 갈지는 그 과의 간호사(EPP)가 대기 인원을 보고 정합니다.

간호사가 대기 현황판을 못 보면(메트릭 없음) 늘 같은 방(명단 첫 번째)으로 보냅니다. 환자는 진료를 받지만 대기가 긴 방에 갈 수도 있습니다. 현황판이 없다고 접수가 막히진 않는다는 게 501 로그의 뜻입니다.

작은 숫자로 한 번

# AIGatewayRoute 규칙 2줄, 풀 2개, 풀마다 파드 1개 body "model": "qwen2.5-0.5b" → extproc 가 헤더 x-ai-eg-model: qwen2.5-0.5b → 규칙 2번 일치 → InferencePool qwen25-05b → EPP(9002) → 파드 1개 중 1개 body "model": "gpt-4o-mini" → 일치하는 규칙 없음 → 라우트 없음 # 같은 요청을 HTTPRoute(.201)로 보내면 — 규칙은 path "/" 하나 어떤 model 이든 → InferencePool llama32-1b 모델 이름은 라우팅에 쓰이지 않는다
그림 11

model 한 줄이 풀과 파드를 고르는 과정

버튼 · 슬라이더
해볼 것 — ① AIGatewayRoute에서 model 버튼을 바꿔 가며 어느 풀로 가는지 보세요. gpt-4o-mini는 규칙이 없어 라우트 없음이 됩니다. ② HTTPRoute로 바꾸면 무엇을 보내든 llama32-1b 풀로 갑니다. ③ llama-3.2-1b를 고르고 풀당 파드를 3으로 늘리고 "모델 서버 메트릭"을 켜면, EPP가 대기열이 가장 짧은 파드를 고릅니다. "다른 순간의 대기열"을 누르면 대기열이 바뀌고 선택도 따라 바뀝니다. ④ 메트릭을 끄면(llama.cpp 상황) 점수가 모두 같아져 대기열과 상관없이 첫 번째 파드가 뽑힙니다 — 라우팅은 되지만 판단 근거가 없습니다.
단순화한 모형입니다

파드별 대기열 숫자는 설명용으로 만든 값입니다. 실제 EPP는 대기열 외에 KV 캐시 사용률·prefix 캐시 적중 등 여러 점수를 가중합하고, 동점일 때의 선택 규칙도 구현에 따라 다릅니다. 풀당 파드 1개가 원문 실습 구성입니다.

원문 매니페스트 발췌 — InferencePool, EPP, AIGatewayRoute
# kubectl get inferencepool llama32-1b -o yaml (발췌)
spec:
  appProtocol: http
  endpointPickerRef:
    failureMode: FailClose
    kind: Service
    name: llama32-1b-epp
    port: { number: 9002 }
  selector:
    matchLabels: { app: llama32-1b }
  targetPorts:
  - number: 8000

# llama32-1b-epp Deployment (발췌)
image: ghcr.io/llm-d/llm-d-router-endpoint-picker:main
ports: 9002 (grpc), 9003 (grpc health), 9090 (metrics)
args: --pool-name llama32-1b --pool-namespace default --grpc-port 9002 --grpc-health-port 9003
      --config-file /config/default-plugins.yaml

# llama32-1b Deployment (발췌)
image: ghcr.io/ggml-org/llama.cpp:server
args: --hf-repo bartowski/Llama-3.2-1B-Instruct-GGUF --hf-file Llama-3.2-1B-Instruct-Q4_K_M.gguf --port 8000 --host 0.0.0.0
# aigwroute.yaml — 모델 이름으로 풀을 나눈다
apiVersion: aigateway.envoyproxy.io/v1beta1
kind: AIGatewayRoute
metadata: { name: inference-pool-with-aigwroute, namespace: default }
spec:
  parentRefs:
    - { name: inference-pool-with-aigwroute, kind: Gateway, group: gateway.networking.k8s.io }
  rules:
    - matches: [{ headers: [{ type: Exact, name: x-ai-eg-model, value: llama-3.2-1b }] }]
      backendRefs: [{ name: llama32-1b, group: inference.networking.k8s.io, kind: InferencePool }]
    - matches: [{ headers: [{ type: Exact, name: x-ai-eg-model, value: qwen2.5-0.5b }] }]
      backendRefs: [{ name: qwen25-05b, group: inference.networking.k8s.io, kind: InferencePool }]
# Envoy Gateway 에 InferencePool 을 backend 로 받아들이게
helm upgrade -i eg oci://docker.io/envoyproxy/gateway-helm --version v1.8.1 -n envoy-gateway-system --reuse-values \
  --set config.envoyGateway.extensionManager.backendResources[0].group=inference.networking.k8s.io \
  --set config.envoyGateway.extensionManager.backendResources[0].kind=InferencePool \
  --set config.envoyGateway.extensionManager.backendResources[0].version=v1
kubectl rollout restart -n envoy-gateway-system deployment/envoy-gateway

# 정리
kind delete cluster --name agentrouter
11장 세 줄 요약
  1. 정의 — InferencePool은 같은 모델 서버 파드의 라벨 묶음이고, backendRef가 InferencePool이면 Envoy가 EPP(ext_proc, 9002)에게 파드 선택을 맡깁니다. HTTPRoute는 경로로, AIGatewayRoute는 모델 이름 헤더로 풀을 고릅니다.
  2. 성질 — EPP의 판단 품질은 모델 서버가 내놓는 메트릭에 달렸습니다. llama.cpp처럼 vLLM 메트릭을 안 주면 501 로그와 함께 신호가 떨어지지만 라우팅은 됩니다. failureMode: FailClose면 EPP 자체가 죽을 때는 요청이 실패합니다.
  3. 쓰임 — 여러 self-hosted 모델을 한 입구로 묶고(AIGatewayRoute), 모델마다 파드를 똑똑하게 고르게(EPP) 합니다. EPP가 무엇을 보고 어떻게 점수를 매기는지는 [7W-2] llm-d가 답합니다.

프록시의 기본기에서 모델 앞문까지 왔습니다. 정리하고 [7W-2]로 넘깁니다.

마치며

사실 스터디 전에는 이 흐름이 잘 납득되지 않았습니다. 이미 API Gateway가 있고 그것 하나 운영하기도 벅찬데, AI 부서는 왜 게이트웨이를 하나 더 세우자고 하는가. 기존 게이트웨이에 provider 주소와 키를 몇 줄 더 넣으면 될 일로 보였습니다.

정리하고 나니 그 요구가 어디서 나온 것인지 보입니다. AI Gateway에서 LLM API를 사용하는 키를 관리할 수 있고, 효율적인 호출을 위해 HTTP Body 수준의 요청을 분석하고 라우팅하며, 파드마다 대기열과 캐시가 달라 고르는 기준 자체가 달라집니다. 리소스 경로 설정 몇 줄이 아니라, 요청을 다루는 방식이 다른 구조였습니다.

이번 주차 스터디 내용을 통해 지금 쓰는 API Gateway의 AI 서빙시 어느 부분까지 맡기고 LLM을 위해 어디서부터 별도의 라우팅과 선택을 맡길지 판단할 수 있게 됐습니다.

제보 부탁드립니다

이 페이지는 [7W-1] Envoy: Agent Router 스터디 자료의 정리본입니다. 참고한 것은 원문이 인용한 『Istio in Action』 책 3장·책 14장 스터디 정리, Envoy 공식 문서의 Life of a Request, Agent Router 아키텍처 문서와 블로그, Gateway API Inference Extension, 그리고 원문에 링크된 EnvoyCon 등 발표들입니다.

설명이 틀렸거나 최신 버전에서 다르게 동작하는 부분이 있으면 알려주세요.

[7W-1] Envoy: Agent Router — LLMSO 스터디 7주차 학습자료
반응형

'AI > [LLMSO] Hands-On LLM 서빙과 최적화 스터디' 카테고리의 다른 글

[7주차-LLMSO] llm-d 란 (2026.09.13)  (0) 2026.09.20
[6주차-LLMSO] Scaling LLM Inference with vLLM and AWS Trainium (2026.09.06)  (1) 2026.09.13
[5주차-LLMSO]CH10. Advancements in LLM Serving (2026.08.30)  (1) 2026.09.05
[5주차-LLMSO]CH9. LLM Optimization in Practice (2026.08.30)  (0) 2026.09.05
[4주차-LLMSO]CH8. LLM Serving Frameworks (2026.08.23)  (0) 2026.08.30
    devlos
    devlos
    안녕하세요, Devlos 입니다. 새로 공부 중인 지식들을 공유하고, 명확히 이해하고자 블로그를 개설했습니다 :) 여러 DEVELOPER 분들과 자유롭게 지식을 공유하고 싶어요! 방문해 주셔서 감사합니다 😀 - DEVLOS -

    티스토리툴바