Agent Router: AAIF에 합류한 두 번째 에이전트 게이트웨이

지난 6월에 agentgateway 글을 올리면서 AAIF에 막 합류한 에이전트 게이트웨이라고 소개했는데요. 그런데 9월 9일, AAIF 블로그에 또 하나의 게이트웨이가 합류했다는 발표가 올라왔습니다. Envoy AI Gateway라는 이름으로 알려져 있던 프로젝트가 Agent Router로 이름을 바꾸고 재단에 들어온 거예요.
한 재단에 게이트웨이가 둘이라니 좀 이상하지 않나요? 🤔 둘 다 에이전트 앞에 앉아서 MCP 서버와 LLM 호출을 받아내겠다고 하니 겹치는 게 당연해 보이는데, 막상 들여다보면 출발점이 꽤 다릅니다. 이번 글에서는 Agent Router가 무엇인지, 노트북에서 어떻게 띄우는지, 그리고 agentgateway와 어디서 갈라지는지 정리해보겠습니다.
Envoy AI Gateway에서 Agent Router로
이 프로젝트는 2024년 10월에 Bloomberg 엔지니어들이 시작했습니다. LLM 제공업체마다 API 모양이 달라서 애플리케이션마다 어댑터를 따로 짜야 하는 문제를 게이트웨이 한 겹으로 풀어보자는 취지였는데요. 처음부터 Envoy의 하위 프로젝트로 출발했기 때문에 이름도 Envoy AI Gateway였습니다.
그 뒤로 안정 릴리스를 열다섯 번 냈고, 2026년 6월에 1.0.0을 내면서 1.x 시리즈 안에서는 호환성을 지키겠다고 약속했습니다. 8월에는 1.1.0이 나왔고요. 메인테이너 자리 아홉 개를 Bloomberg, Nutanix, AMD, Tetrate, Netflix가 나눠 갖고 있어서 어느 한 회사가 과반을 쥐고 있지 않고, 기여자는 21개 조직에서 132명입니다. 공개된 도입 사례로는 Bloomberg, Tetrate, Tencent Cloud, Nutanix, LY Corporation 등 열한 곳이 올라와 있어요.
AAIF로 옮기면서 이름은 바뀌었지만 배포물은 하나도 안 바뀌었습니다. CLI는 여전히 aigw고, 쿠버네티스 CRD의 API 그룹도 aigateway.envoyproxy.io 그대로예요. 컨테이너 이미지와 Helm 차트, Go 모듈 경로까지 예전 이름을 유지합니다. 저장소가 envoyproxy/ai-gateway에서 theagentrouter/agent-router로 옮겨가긴 했는데, 옛 링크는 전부 리디렉션됩니다. README에 적힌 “어제 쓴 매니페스트가 내일도 그대로 적용된다”는 문장이 프로젝트의 태도를 잘 보여주죠.
데이터 플레인이 아니라 컨트롤 플레인
Agent Router를 이해하는 열쇠는 README 첫 줄에 있습니다. “Agent Router controls. Envoy carries.” 그러니까 트래픽을 실제로 나르는 건 Envoy고, Agent Router는 그 Envoy에게 무엇을 어디로 보낼지 알려주는 컨트롤 플레인(control plane)이라는 뜻입니다.
agentgateway가 Rust로 데이터 플레인(data plane) 자체를 새로 썼다면, Agent Router는 이미 검증된 데이터 플레인을 그대로 가져다 쓰고 그 위에 AI 트래픽 전용 층만 얹은 셈이에요. 구체적으로는 세 조각으로 이루어집니다. 쿠버네티스에서 Gateway API를 구현하는 Envoy Gateway가 Envoy 프로세스를 관리합니다. Agent Router의 컨트롤러는 AIGatewayRoute 같은 자체 리소스를 Envoy Gateway가 알아듣는 HTTPRoute로 풀어주고요. 마지막으로 외부 프로세서(external processor, 이하 extproc)가 요청과 응답 본문을 들여다보며 OpenAI 형식을 제공업체 형식으로 바꾸고 토큰 수를 셉니다.
flowchart TB
accTitle: Agent Router의 구성 요소
accDescr: 에이전트의 요청은 Envoy 데이터 플레인이 받는다. Envoy는 외부 프로세서에게 본문을 넘겨 형식 변환과 토큰 계산을 맡기고, 결과를 LLM 제공업체나 MCP 서버로 보낸다. Envoy Gateway와 Agent Router 컨트롤러는 Envoy의 설정을 만들어 내려보내는 컨트롤 플레인이다.
agent["에이전트"] --> data
subgraph control["컨트롤 플레인"]
direction LR
ctrl["Agent Router 컨트롤러"] --> eg["Envoy Gateway"]
end
control -. "설정" .-> data
subgraph data["데이터 플레인"]
direction LR
envoy["Envoy"] <--> extproc["extproc<br/>형식 변환, 토큰 계산"]
end
data --> backends
subgraph backends["백엔드"]
direction LR
llm["LLM 제공업체"] ~~~ mcp["MCP 서버"]
end
classDef ar stroke-width:2px
class ctrl,extproc ar
이 구조가 주는 이점은 Envoy가 이미 갖고 있는 기능을 그대로 물려받는다는 점입니다. 속도 제한, 재시도, 서킷 브레이커, TLS, 액세스 로그 같은 걸 다시 만들 필요가 없고, Envoy Gateway의 정책 리소스를 AI 트래픽에도 붙일 수 있어요. 부족하면 Lua 스크립트나 Rust, Go로 짠 동적 모듈, 또는 별도의 extproc을 덧붙이는 확장 경로도 Envoy의 것을 그대로 씁니다.
aigw run으로 노트북에서 띄우기
쿠버네티스가 주 무대이긴 하지만, aigw run 명령으로 노트북에서도 단독으로 돌릴 수 있습니다. 설치는 GitHub 릴리스에서 플랫폼에 맞는 바이너리를 받는 방식이고, 도커 이미지(envoyproxy/ai-gateway-cli)로 돌리거나 Go가 있다면 소스에서 바로 빌드해도 됩니다.
curl -sL -o aigw https://github.com/theagentrouter/agent-router/releases/download/v1.1.0/aigw-darwin-arm64
chmod +x aigw
./aigw version
Envoy AI Gateway CLI: v1.1.0
설정 파일 없이 환경 변수만 넘겨도 뜹니다. OpenAI SDK가 읽는 변수 이름을 그대로 쓰기 때문에, 이미 OPENAI_API_KEY가 셸에 있다면 명령어 한 줄이면 끝이에요.
OPENAI_API_KEY=sk-your-key aigw run
AI Gateway External Processor is ready
Envoy AI Gateway listening on http://localhost:1975 (admin http://localhost:64525) after 1.1s
처음 실행할 때는 Envoy 바이너리를 내려받느라 10초 남짓 걸리고, 그다음부터는 1초 안에 뜹니다. 프록시는 1975번 포트에 열리고요. 로그에 함께 찍히는 admin 주소는 Envoy 자체의 관리 인터페이스이고, Agent Router가 지표와 헬스 체크를 내주는 관리 서버는 따로 1064번 포트에 뜹니다. 바꾸고 싶으면 --admin-port 플래그를 쓰면 돼요.
OPENAI_API_KEY 말고도 ANTHROPIC_API_KEY나 AZURE_OPENAI_API_KEY를 알아듣고, OPENAI_BASE_URL을 바꿔주면 Ollama처럼 OpenAI 호환 API를 내주는 로컬 서버에도 붙습니다.
이제 OpenAI 호환 클라이언트의 베이스 URL을 http://localhost:1975/v1로 돌리면 됩니다.
curl http://localhost:1975/v1/chat/completions \
-H 'content-type: application/json' \
-d '{
"model": "gpt-4o-mini",
"messages": [{ "role": "user", "content": "한 단어로 인사해줘" }]
}'
{
"id": "chatcmpl-ENQuw5wws8N9zg8ZMyW9TZlnIqcx3",
"object": "chat.completion",
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "안녕하세요!" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 15, "completion_tokens": 3, "total_tokens": 18 }
}
MCP 서버 묶기: 클라이언트 설정 파일 그대로
MCP 쪽이 재미있는데요. 게이트웨이 전용 설정 형식을 새로 배울 필요 없이, Claude Desktop이나 Cursor, VS Code에 넣던 mcpServers JSON을 그대로 넘기면 됩니다.
{
"mcpServers": {
"deepwiki": {
"type": "http",
"url": "https://mcp.deepwiki.com/mcp"
},
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/readonly",
"headers": {
"Authorization": "Bearer ${GITHUB_ACCESS_TOKEN}"
},
"includeTools": ["issue_read", "list_issues"]
}
}
}
headers 값에는 ${VAR} 형태로 환경 변수를 끼워 넣을 수 있어서 토큰을 파일에 적지 않아도 되고, includeTools로 서버가 내주는 도구 중 일부만 골라 노출할 수 있습니다. GitHub MCP 서버처럼 도구가 수십 개인 서버를 붙일 때 에이전트의 컨텍스트를 아끼는 데 유용해요.
GITHUB_ACCESS_TOKEN=$(gh auth token) aigw run --mcp-config mcp-servers.json
게이트웨이의 MCP 엔드포인트는 http://localhost:1975/mcp입니다. MCP Inspector로 도구 목록을 뽑아보면 두 서버가 하나로 합쳐져 나옵니다.
bunx @modelcontextprotocol/inspector@0.16.8 --cli http://localhost:1975/mcp --method tools/list
{
"tools": [
{ "name": "github__issue_read" },
{ "name": "github__list_issues" },
{ "name": "deepwiki__ask_question" },
{ "name": "deepwiki__read_wiki_contents" },
{ "name": "deepwiki__read_wiki_structure" }
]
}
도구 이름 앞에 서버 이름과 밑줄 두 개가 붙습니다. agentgateway는 밑줄 하나를 쓰니 클라이언트 쪽 도구 허용 목록을 옮길 때 이 차이를 기억해두세요. includeTools 효과도 보이는데, GitHub 읽기 전용 서버는 원래 도구가 훨씬 많지만 지정한 둘만 남았죠.
다만 등록할 수 있는 건 HTTP 서버뿐입니다. type에 넣을 수 있는 값이 http와 그 변형뿐이라, npx로 띄우는 stdio 서버는 등록할 수 없어요. Envoy는 네트워크 프록시지 프로세스 관리자가 아니니 당연한 제약이긴 한데, agentgateway는 stdio 대상을 직접 띄워주기 때문에 노트북에서 로컬 서버를 묶어 쓰는 용도라면 이 차이가 크게 느껴질 겁니다.
한 가지 더, 공식 문서 예제에 나오는 Context7이 제 환경에서는 붙지 않았습니다. 액세스 로그를 보니 업스트림 TLS 핸드셰이크가 BAD_ECC_CERT로 실패했는데, Context7 서버가 P-384 곡선의 ECDSA 인증서를 쓰는 반면 Envoy가 업스트림에 기본으로 내미는 곡선 목록에는 P-256까지만 있어서 생기는 문제로 보입니다. RSA 인증서를 쓰는 DeepWiki와 GitHub은 문제없이 붙었어요. 붙지 않는 서버가 있다면 인증서부터 의심해보세요.
인가 규칙에서 도구 인자까지 본다
agentgateway 글에서 도구 단위 인가(authorization)를 가장 눈여겨봤다고 했는데, Agent Router에도 같은 자리가 있습니다. MCPRoute 리소스의 securityPolicy.authorization에 규칙을 적는데요. 규칙 하나는 누가(source), 어떤 도구를(target), 어떤 조건에서(cel) 부를 수 있는지 세 부분으로 되어 있고, 위에서부터 순서대로 평가해서 처음 맞는 규칙이 허용 여부를 정합니다. 아무것도 안 맞으면 defaultAction을 따르고요.
spec:
securityPolicy:
authorization:
rules:
- source:
jwt:
scopes: ["read"]
target:
tools:
- backend: github
tool: list_issues
- source:
jwt:
claims:
- name: org.departments
valueType: StringArray
values: [engineering]
target:
tools:
- backend: github
tool: issue_read
cel: request.mcp.params.arguments.owner == "our-org"
read 스코프가 있는 토큰이면 이슈 목록을 볼 수 있고, org.departments 클레임에 engineering이 들어 있으면서 요청한 저장소 소유자가 우리 조직일 때만 이슈 본문을 읽을 수 있다는 규칙입니다. JWT 검증과 OAuth 흐름도 같은 securityPolicy 안에서 처리하니, MCP 인증 스펙과 그대로 이어지는 구조예요.
마지막 조건을 눈여겨봐주세요. agentgateway는 인가 시점에 도구 인자를 볼 수 없어서 인자 값으로 막으려면 가드레일을 따로 써야 했는데, Agent Router의 CEL 컨텍스트에는 request.mcp.params로 JSON-RPC 파라미터가 통째로 들어옵니다. 어떤 저장소인지, 어떤 경로인지 같은 인자 값까지 인가 규칙 하나에서 판단할 수 있는 거죠. 요청 헤더(request.headers)와 JWT(request.auth.jwt), MCP 메서드(request.mcp.method)도 같은 컨텍스트에 있습니다.
토큰을 세는 건 에이전트가 아니라 게이트웨이
AAIF 소개 글에 “에이전트는 토큰을 한 번도 세지 않는다. Agent Router가 센다”는 대목이 있습니다. extproc이 응답 본문에서 사용량을 뽑아 Envoy의 동적 메타데이터(dynamic metadata)에 실어주기 때문에 가능한 일인데요. 이 값이 지표, 액세스 로그, 그리고 속도 제한 세 군데로 흘러갑니다.
우선 지표는 관리 포트의 /metrics에서 Prometheus 형식으로 나옵니다. 아까 보낸 요청 하나가 이렇게 잡혔어요.
gen_ai_client_token_usage_sum{gen_ai_provider_name="openai",gen_ai_request_model="gpt-4o-mini",gen_ai_token_type="input"} 15
gen_ai_client_token_usage_sum{gen_ai_provider_name="openai",gen_ai_request_model="gpt-4o-mini",gen_ai_token_type="output"} 3
gen_ai_server_request_duration_seconds_sum{gen_ai_provider_name="openai",gen_ai_request_model="gpt-4o-mini"} 1.534362708
지표 이름이 gen_ai_로 시작하는 건 OpenTelemetry의 GenAI 시맨틱 컨벤션을 따르기 때문입니다. 추적(trace)도 OTEL_EXPORTER_OTLP_ENDPOINT만 지정하면 나가고, 액세스 로그에도 gen_ai.usage.input_tokens 같은 필드가 요청마다 찍힙니다. MCP 호출이면 어느 서버의 어떤 도구를 불렀는지가 남고요.
{
"request.path": "/mcp",
"mcp.method.name": "tools/call",
"mcp.provider.name": "deepwiki",
"mcp.tool.name": "read_wiki_structure",
"response_code": 200
}
속도 제한은 Envoy를 쓰는 보람이 드러나는 대목입니다. Envoy Gateway의 BackendTrafficPolicy에는 요청마다 비용(cost)을 다르게 매기는 기능이 있는데, 비용을 메타데이터에서 읽어오도록 하면 그게 곧 토큰 단위 속도 제한이 됩니다.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: token-ratelimit
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: aigw
rateLimit:
type: Global
global:
rules:
- clientSelectors:
- headers:
- name: x-tenant-id
type: Distinct
limit:
requests: 100000
unit: Hour
cost:
request:
from: Number
number: 0
response:
from: Metadata
metadata:
namespace: io.envoy.ai_gateway
key: llm_total_token
x-tenant-id 헤더 값마다 따로 예산을 잡고, 요청 시점에는 예산을 깎지 않되(number: 0) 응답이 돌아오면 llm_total_token 메타데이터에 적힌 만큼 차감하는 설정입니다. 시간당 10만 토큰을 넘기면 다음 요청부터 429가 돌아와요. 입력 토큰과 출력 토큰을 따로 제한하고 싶으면 llm_input_token, llm_output_token 키로 규칙을 하나씩 더 두면 됩니다. Global 방식은 카운터를 Redis에 두기 때문에 Envoy Gateway 쪽에 Redis 연동을 켜두어야 하고요.
폴백(fallback)도 같은 원리로 Envoy의 재시도 정책을 빌려 씁니다. AIGatewayRoute의 backendRefs에 priority를 매겨두고, BackendTrafficPolicy의 retry에서 어떤 상태 코드에 몇 번까지 다시 시도할지 정하면, 1순위 제공업체가 500을 내거나 연결이 안 될 때 2순위로 넘어갑니다. 제공업체마다 요청 형식이 다른 건 extproc이 매번 맞춰주니 에이전트는 요청 하나 보내고 답 하나 받을 뿐이죠.
쿠버네티스에서는 CRD로
aigw run이 환경 변수와 JSON 파일로 받은 설정은 내부적으로 쿠버네티스 리소스로 변환돼 돌아갑니다. 실제로 ~/.local/state/aigw/runs/ 아래를 열어보면 Gateway, HTTPRoute, Backend 같은 리소스가 YAML로 생성되어 있어요. 덕분에 쿠버네티스에 올릴 때 새로 배우는 게 없습니다. 노트북에서 돌린 설정이 곧 클러스터 설정이거든요.
핵심 리소스는 셋입니다. AIGatewayRoute가 어떤 모델 요청을 어느 백엔드로 보낼지 정하고, AIServiceBackend가 백엔드가 어떤 API 스키마(OpenAI, AWS Bedrock 등)를 말하는지 알려주고, BackendSecurityPolicy가 그 백엔드에 붙을 때 쓸 API 키나 클라우드 자격 증명을 지정합니다.
apiVersion: aigateway.envoyproxy.io/v1beta1
kind: AIGatewayRoute
metadata:
name: llm-route
spec:
parentRefs:
- name: aigw
kind: Gateway
group: gateway.networking.k8s.io
rules:
- matches:
- headers:
- type: Exact
name: x-ai-eg-model
value: gpt-4o-mini
backendRefs:
- name: openai
---
apiVersion: aigateway.envoyproxy.io/v1beta1
kind: AIServiceBackend
metadata:
name: openai
spec:
schema:
name: OpenAI
backendRef:
name: openai
kind: Backend
group: gateway.envoyproxy.io
---
apiVersion: aigateway.envoyproxy.io/v1beta1
kind: BackendSecurityPolicy
metadata:
name: openai-apikey
spec:
targetRefs:
- group: aigateway.envoyproxy.io
kind: AIServiceBackend
name: openai
type: APIKey
apiKey:
secretRef:
name: openai-apikey
라우팅 조건에 x-ai-eg-model이라는 헤더가 등장하는데, 클라이언트가 이 헤더를 직접 보내는 게 아닙니다. extproc이 요청 본문의 model 필드를 읽어서 이 헤더에 옮겨 적어주기 때문에 본문을 볼 수 없는 Envoy의 라우팅 규칙이 모델 이름으로 분기할 수 있게 되는 거예요. Gateway API의 HTTPRoute 매칭 문법을 그대로 쓰면서 본문 기반 라우팅을 얻어내는 방식이죠.
MCP 쪽도 마찬가지로 MCPRoute 리소스가 있고, aigw run --mcp-config로 넘긴 JSON이 사실 이 리소스로 변환됩니다.
그래서 둘 중 무엇을 쓸까
여기까지 보면 AAIF가 왜 둘 다 받아들였는지 감이 옵니다. 둘 다 에이전트 트래픽의 프록시라는 점은 같지만 서 있는 층이 달라요.
agentgateway는 Rust로 처음부터 짠 데이터 플레인입니다. 바이너리 하나에 프록시와 관리 UI, 설정이 다 들어 있어서 노트북에서 stdio MCP 서버를 묶어 쓰는 것부터 시작할 수 있고, A2A 통신까지 한 프로세스에서 받아냅니다. 반면 Agent Router는 Envoy 위의 컨트롤 플레인이라, 이미 Envoy Gateway로 쿠버네티스 인그레스를 다루고 있는 팀이라면 새 프록시를 들이지 않고 AI 트래픽까지 같은 정책 체계에 넣을 수 있습니다. 토큰 단위 속도 제한이 Envoy의 일반 속도 제한 정책에 비용 계산만 얹은 것이라는 점이 이 접근의 성격을 잘 보여주죠.
그래서 고르는 기준은 게이트웨이의 기능 목록보다 지금 인프라에 무엇이 깔려 있느냐에 가깝습니다. Envoy Gateway가 이미 있고 플랫폼 팀이 Gateway API에 익숙하다면 Agent Router가 자연스럽고, 쿠버네티스 없이 가볍게 시작하거나 stdio 서버와 A2A까지 한 곳에서 다루고 싶다면 agentgateway가 맞습니다. 물론 두 프로젝트가 한 재단 아래 있으니 앞으로 MCP 인가 규칙 같은 부분은 서로 닮아갈 가능성도 있고요.
마치며
Agent Router는 Bloomberg에서 시작해 Envoy 하위 프로젝트로 자란 AI 게이트웨이가 이름을 바꾸고 AAIF로 옮겨온 결과물입니다. Envoy가 나르고 Agent Router가 지휘한다는 구도 덕분에 속도 제한과 폴백, 관측 같은 인프라 기능을 다시 만들지 않고 물려받았고, OpenAI 호환 API와 MCP 다중화(multiplexing), 도구 인자까지 보는 인가 규칙을 그 위에 얹었습니다.
agentgateway 글에서 했던 말을 다시 하게 되는데요. 에이전트를 혼자 쓸 때는 게이트웨이가 필요 없지만, 사람과 서버가 늘어나는 순간 앞단에 한 겹이 있어야 합니다. 그 한 겹을 무엇으로 세울지는 이제 선택지가 둘이 됐으니, aigw run으로 한 번 띄워보고 손에 맞는 쪽을 고르시면 됩니다.
더 자세한 내용은 Agent Router 공식 문서를 참고하세요.
This work is licensed under CC BY 4.0