콘텐츠로 이동

AppProxy 워커 모드

이 문서는 Continuum Router가 Backend.AI AppProxy 추론 워커로 동작하는 방식을 명세합니다. 추론 워커는 AppProxy 코디네이터가 제어하는 데이터 플레인 노드로, 다수의 LLM 서빙 컨테이너를 하나의 OpenAI 호환 주소 뒤로 집약합니다.

워커 모드의 표준 레퍼런스로서, 어느 엔지니어(또는 에이전트)라도 프로토콜을 다시 유도하지 않고 각 구성 요소를 검증하거나 확장할 수 있을 만큼 정밀하게 작성되었습니다.

1. 동기

Backend.AI의 AppProxy는 투명한 L4/L7 프록시 워커로 모델 서비스의 앞단을 맡습니다. 서킷(circuit) 하나가 프런트엔드 슬롯 하나(포트 또는 와일드카드 서브도메인)에 매핑되어 그 뒤의 서빙 컨테이너로 바이트를 전달하고, traffic_ratio에 따라 복제본 간 로드 밸런싱을 수행합니다.

Continuum Router는 이 추론 데이터 플레인을 대신 맡아 그 위에 L7 수준의 LLM 인지 동작을 더할 수 있습니다.

  • 하나의 /v1 표면 뒤에서 이루어지는 모델 이름 라우팅과 엔드포인트 간 집약
  • 프로토콜 변환(OpenAI ↔ Anthropic ↔ Gemini), 스마트 라우팅, prefix/KV 캐시 인지 라우팅, 분리형 prefill/decode
  • 폴백 체인, 서킷 브레이커, 재시도, 응답 캐싱, Files API

워커 모드에서는 AppProxy 코디네이터가 표준 워커를 다루는 방식 그대로(등록, 하트비트, 서킷 할당) 런타임에 Continuum Router의 백엔드 집합을 제어하고, Continuum Router는 각 서킷을 LLM을 인지하며 헬스 체크와 가중치가 적용되는 백엔드 풀로 구현합니다.

2. 배경: AppProxy 아키텍처

AppProxy는 세 부분으로 구성됩니다(Backend.AI src/ai/backend/appproxy/).

  • 코디네이터: 컨트롤 플레인입니다. PostgreSQL(워커, 서킷, 엔드포인트, 토큰의 원천 데이터 저장소)을 기반으로 하는 aiohttp REST 서버로, 서킷을 워커에 스케줄링하고 라우팅 변경을 밀어냅니다.
  • 워커: 데이터 플레인입니다. 코디네이터에 등록하고 하트비트를 보내며, 자신에게 할당된 서킷의 트래픽을 프록시합니다.
  • Common: 공유 타입, 이벤트 버스, 설정을 담는 공통 모듈입니다.

핵심 엔티티

엔티티 의미
Worker 프록시 노드. 고유한 authority로 식별됩니다(HA 복제본 간에는 nodes 카운터로 공유). frontend_mode(wildcard/port), protocol(http/h2/tcp/…), hostname, api_port, 슬롯 공간(port_range 또는 wildcard_domain)을 가집니다. statusALIVE/LOST/TERMINATED.
Endpoint 추론 배포(모델 서비스). id == DeploymentID. 서킷과 1:1 관계이며, 선택적 health_check_config를 가집니다.
Circuit 워커로 푸시되는 중심 라우팅 객체. 프런트엔드 슬롯(port 또는 subdomain)을 백엔드 대상 목록(route_info)에 바인딩합니다. app_modeinteractive/inference. 추론의 경우 endpoint_idruntime_variant를 가집니다.
RouteInfo 서킷 내부의 백엔드 대상 하나: kernel_host, kernel_port, protocol, traffic_ratio, session_id, route_id.
Slot 프런트엔드 용량의 단위(범위 내 포트 하나 또는 서브도메인 하나). 코디네이터가 슬롯을 할당하고 워커는 이를 따릅니다.

전송 (코디네이터와 워커의 통신 방식)

서로 다른 세 가지 채널이 있습니다.

  1. 워커 → 코디네이터: HTTP REST. 등록, 하트비트, 등록 해제, 초기 서킷 풀(pull)에 사용합니다. 공유 X-BackendAI-Token: <api_secret> 헤더로 인증합니다.
  2. 코디네이터 → 워커: Redis Pub/Sub(레거시 모드): 서킷 생성/라우트 업데이트/제거를 events_all-appproxy 채널로 브로드캐스트하고, 생성 시에는 워커가 ack를 보냅니다.
  3. 코디네이터 → Traefik: etcd(Traefik 모드): 코디네이터가 Traefik 동적 설정을 etcd에 기록하고 Traefik이 프록시합니다. 이 모드에서는 워커에 서킷 단위 신호가 전달되지 않습니다.

모드는 코디네이터 전역 설정(proxy_coordinator.enable_traefik)입니다. 이 구분이 설계 결정 중 하나로 이어집니다(§4와 §5.5 참고).

3. 개념 매핑

추론 경로는 Continuum Router의 기존 모델에 거의 1:1로 매핑됩니다.

AppProxy Continuum Router
Worker (authority, frontend_mode, 슬롯 공간) 워커로 등록된 라우터 인스턴스
Endpoint (추론 모델 서비스) 모델 (해당 모델을 서빙하는 백엔드 집합)
Circuit (app_mode=inference, route_info[]) 모델 → Vec<BackendConfig> 매핑
RouteInfo {kernel_host, kernel_port, traffic_ratio} BackendConfig {url: http://host:port, weight ∝ ratio, models: [model]}
Slot (서브도메인/포트) 인그레스 주소 지정 키 (§5.2 참고)
RoutePool 가중 랜덤 + 헬스 체크 WeightedRoundRobin + HealthChecker + CircuitBreaker

AppProxy 추론 서킷은 'traffic_ratio로 가중치를 둔 한 모델의 N개 복제본'입니다. 이는 구성원이 models = [<model>]을 공유하고 복제본별 weight를 갖는 Continuum Router 백엔드 그룹과 정확히 같은 구조입니다. 따라서 변환은 기계적이고, 데이터 플레인(선택, 헬스, 브레이커, 폴백)은 그대로 재사용됩니다.

4. 설계 결정

네 가지 결정이 이 통합의 골격을 이룹니다.

  1. 외부 어댑터가 아닌 네이티브 모듈. 통합은 Cargo 피처(appproxy) 뒤에서 Continuum Router 내부에 위치합니다. 기존 핫 리로드 config_sender 채널을 통해 서킷을 주입하므로 데이터 플레인은 수정되지 않습니다.
  2. Continuum Router만 수정, 코디네이터는 그대로. Continuum Router가 기존 와일드카드 추론 워커 프로토콜을 따릅니다. 모델 이름은 각 복제본의 /v1/models를 자동 발견해 얻습니다.
  3. 슬롯을 존중하는 와일드카드 인그레스. 라우터는 와일드카드 슬롯 공간을 등록하고 각 요청을 HTTP Host(서브도메인)로 서킷에 해석하며, 캐치올(catch-all) 호스트에서는 모델 이름 집약을 사용할 수 있습니다. 코디네이터가 할당한 슬롯은 형식적인 잔재가 아니라 실제 인그레스 주소입니다(§5.2 참고).
  4. 두 전송 채널 모두 지원. 풀 기반 리컨사일(reconcile)을 기본으로 하고(어떤 코디네이터 모드에서도 동작), Redis Pub/Sub 이벤트 오버레이를 더합니다(레거시 모드 지원 + 낮은 지연). 풀은 누락된 이벤트의 안전망 역할도 합니다.

5. 아키텍처

5.1 구성 요소 개요

                wildcard DNS: *.models.example.com  ──►  continuum-router host
external client                   │   single socket (e.g. :443)
  POST https://ep-abc.models.example.com/v1/chat/completions
        │  Host: ep-abc.models.example.com
┌────────────────────────────────────────────────────────────────┐
│ continuum-router  (one worker, frontend_mode = wildcard)         │
│                                                                  │
│  appproxy module (feature = "appproxy")                          │
│   ├── coordinator client (REST: register/heartbeat/pull)         │
│   ├── worker service (lifecycle loops) ── circuit registry       │
│   ├── reconcile (circuit → BackendConfig → config_sender) ──┐    │
│   ├── events (Redis Pub/Sub subscribe + ack)                │    │
│   └── ingress middleware (Host subdomain → model) ──┐        │    │
│                                                     ▼        ▼    │
│  existing pipeline:  model router → backend pool ◄── hot reload   │
│                      (health, circuit breaker, fallback)         │
└────────────────────────────────────────────────────────────────┘
        │              │
        ▼              ▼
   kernel1:port    kernel2:port    (LLM serving containers = backends)

새로 추가되는 코드는 appproxy 모듈과 인그레스 미들웨어 하나뿐입니다. 서킷 상태는 기존 핫 리로드 메커니즘을 거쳐 백엔드 상태가 되고, 요청 라우팅은 기존 모델 라우터를 재사용합니다.

5.2 등록과 슬롯 모델

라우터는 와일드카드 추론 워커로 등록합니다. '단일 주소'와 '슬롯 존중'은 서로 충돌하지 않습니다. 와일드카드 도메인 자체가 단일 주소이고, 각 서킷의 서브도메인은 같은 소켓을 가리키는 가상 주소입니다.

등록 시 슬롯 공간을 알립니다.

frontend_mode         = wildcard
wildcard_domain       = ".models.example.com"
wildcard_traffic_port = 443         # the router's /v1 socket
hostname              = <router host>
available_slots       = -1          # wildcard → unbounded; never runs out
accepted_traffics     = [inference]

그러면 코디네이터는 해당 도메인 안에서 추론 서킷마다 서브도메인을 하나씩 할당하고, 기존 Circuit.get_endpoint_url()https://ep-abc.models.example.com/을 만들어 냅니다. 매니저가 사용자에게 전달하는 엔드포인트 URL 계약이 코디네이터 변경 없이 그대로 유지되는 것입니다. 운영자는 와일드카드 DNS(*.models.example.com → router)를 한 번만 설정하면 됩니다.

PORT 모드(서킷당 포트 하나)는 라우터가 리스닝 소켓을 동적으로 열고 닫아야 하므로 지원하지 않습니다(§10 참고). 외부에 서비스되는 추론에서는 와일드카드 + TLS가 일반적입니다.

5.3 서킷 → 백엔드 변환

각 추론 서킷은 RouteInfo 복제본마다 BackendConfig 하나로 변환되어 기존 런타임 변경 경로를 통해 적용됩니다.

for each circuit assigned to this authority:
    model = discover_model(circuit)            # from a replica's /v1/models, keyed by endpoint_id
    for each route in circuit.route_info:
        BackendConfig {
            name:         "appproxy-<circuit_id>-r<route_id>",
            backend_type: Generic,             # OpenAI-compatible; Vllm if known
            url:          "http://{route.kernel_host}:{route.kernel_port}",
            weight:       weight_from(route.traffic_ratio),
            models:       [model],             # what find_backends_for_model matches
            ..Default
        }

적용 단계는 관리자 API의 패턴(src/admin_config/backend_api.rs)을 그대로 재사용합니다.

let _guard = config_modification_lock().write().await;   // serialise with admin API
let cfg    = state.current_config();                      // re-read under lock
let mut new_cfg = (*cfg).clone();
reconcile new_cfg.backends so that the set of appproxy-* backends
    equals the desired set derived from the current circuits;
state.config_sender.send(Arc::new(new_cfg));              // drives hot reload

이어서 HotReloadService가 이전/새 설정을 비교해 새 백엔드를 추가하고, 제거된 백엔드를 점진적으로 드레인하며, 헬스 체커를 동기화하고, 모델 캐시를 무효화합니다. 백엔드 풀 코드는 전혀 건드리지 않습니다. 이 모듈이 소유한 백엔드는 appproxy- 접두사로 네임스페이스가 분리되어 있어, 리컨사일은 자신의 항목만 추가/제거할 뿐 정적으로 설정된 백엔드를 결코 건드리지 않습니다.

기존의 두 가지 특성이 이 방식과 잘 맞아떨어집니다.

  • 런타임 설정 변경은 메모리에만 적용됩니다(디스크에 기록되지 않음). 코디네이터가 데이터의 원천이고, 라우터는 재시작 시 초기 풀로 다시 동기화합니다. 이는 제약이 아니라 의도된 동작입니다.
  • 타입 지정(typed) 백엔드 풀은 핫 리로드되지 않지만 여기서는 무관합니다. 서빙 컨테이너는 URL 기반 풀로 라우팅되는 일반 OpenAI 호환 HTTP 백엔드이기 때문입니다.

5.4 인그레스 해석 (Host/서브도메인 → 모델)

새 Axum 미들웨어가 요청에서 대상 서킷/모델을 해석합니다.

  1. Host 헤더를 읽고, 설정된 wildcard_domain 접미사를 제거해 서브도메인을 얻습니다.
  2. 서브도메인을 메모리 내 서킷 레지스트리(워커 서비스가 소유하며 리컨사일/이벤트 시 갱신)에서 조회합니다 → 서킷 → 정식(canonical) 모델.
  3. IngressTarget { circuit_id, model } 요청 확장(extension)을 삽입합니다.
  4. 비공개 추론 서킷(open_to_public == false)의 경우 Authorization: Bearer <jwt>를 검증합니다(jwt_secret을 사용한 HS256, 디코딩된 id가 서킷 id와 일치해야 함). AppProxy 워커 인증과 동일한 방식입니다.

핸들러는 기존의 단일 읽기 지점(src/proxy/handlers.rspayload.get("model") 블록)과 그 형제 지점에서 바디의 model 필드보다 주입된 모델을 우선합니다. select_backend_with_retry 이하의 모든 동작은 변경되지 않는데, 선택 로직이 각 백엔드의 models 목록과 모델 이름을 대조해 해석하기 때문입니다.

와일드카드 도메인 자체(또는 설정된 집약 호스트)로 들어온 요청은 서브도메인 범위 지정을 건너뛰고 모든 서킷에 걸쳐 일반 모델 이름 라우팅을 사용합니다. 이것이 엔드포인트 간 집약 표면입니다.

폴백 참여 (범위 제한 폴백)

등록된 서킷은 fallback.fallback_chains참여합니다. 복제본이 모두 다운된 서킷(route_info가 비어 있어 살아 있는 백엔드가 없는 상태)으로 요청이 해석되더라도, 인그레스 미들웨어는 요청을 해당 서킷의 정식 모델에 고정해 일반 파이프라인으로 넘깁니다. 이어서 select_backend_with_retry가 그 모델의 백엔드를 찾지 못하면 FallbackService가 이어받아, 서킷의 모델을 키로 하는 체인(예: vllm-real-poc → gpt-4o-mini)에 도달합니다. '배포가 다운되면 트래픽이 OpenAI로 간다'는 동작입니다. 서킷별 인증(open_to_public, 베어러 토큰, allowed_client_ips)은 이 폴스루(fall-through) 이전에 적용되므로, 폴백 경로가 인증 우회 통로가 되는 일은 없습니다.

폴스루의 범위는 제한되어 있어 등록된 서킷에만 적용됩니다. 알 수 없는 서브도메인(레지스트리에 서킷이 없는 경우)으로의 요청은 여전히 404 endpoint_not_found입니다. 알 수 없는 서브도메인은 모델 이름이 아니라 서킷 식별자이므로, 모델 레지스트리/폴백 경로로 진입하지 않습니다.

5.5 업데이트 전송

워커는 서로 보완하는 두 메커니즘으로 서킷 집합을 최신으로 유지합니다.

  • 풀 리컨사일(기본, 항상 켜짐). 등록 후 워커는 /api/worker/{id}/circuitsGET해 리컨사일하고, 이후 타이머(reconcile_interval)로 반복합니다. 이것만으로도 Traefik 모드(코디네이터가 etcd에 기록하고 워커에 신호를 보내지 않는 모드)에서는 완전히 올바르게 동작하며, 누락된 이벤트의 안전망이 됩니다.
  • Redis Pub/Sub 오버레이(레거시 모드 + 낮은 지연). 워커는 events_all-appproxy를 구독해 생성/라우트 업데이트/제거 델타를 약 1초 안에 적용하고, 생성에는 ack를 보냅니다. 레거시 모드에서는 필수입니다. 코디네이터가 서킷 생성(initialize_legacy_circuit) 중 워커의 ack를 최대 15초까지 기다리며 블로킹하고, 타임아웃 시 E10001 Proxy worker not responding을 발생시키기 때문입니다. 라우트 업데이트와 제거는 fire-and-forget입니다.

네 가지 서킷 이벤트가 모두 브로드캐스트(Pub/Sub)이므로, 워커에는 SUBSCRIBE(인바운드 3종)와 PUBLISH(ack 1종) 있으면 됩니다. 서킷 라이프사이클에 Redis Streams나 컨슈머 그룹은 필요하지 않습니다.

6. 와이어 프로토콜 레퍼런스

6.1 코디네이터 REST API (워커 범위)

기본 URL은 coordinator_url입니다. 모든 요청에는 다음 헤더가 포함됩니다.

  • X-BackendAI-Token: <api_secret>
  • X-BackendAI-RequestID: <uuid4>
메서드와 경로 용도 비고
PUT /api/worker 등록/업서트 (authority 기준 멱등) {id, slots, …} 반환. HA: 재등록 시 nodes 증가
PATCH /api/worker/{id} 하트비트 바디 없음. heartbeat_period마다 전송(기본 10초). 코디네이터 타임아웃 30초
DELETE /api/worker/{id} 등록 해제 nodes 감소. 마지막 노드 → LOST
GET /api/worker/{id}/circuits 전체 서킷 스냅샷 {circuits: [SerializableCircuit, …]}
GET /api/circuit/{id} 서킷 1개 조회
DELETE /api/circuit/{id} 서킷 제거

와일드카드 모드의 등록 요청 바디(WorkerRequestModel)는 다음과 같습니다.

{
  "authority": "continuum-router-1",
  "frontend_mode": "wildcard",
  "protocol": "http",
  "hostname": "router.example.com",
  "tls_listen": false,
  "tls_advertised": true,
  "api_port": 8080,
  "accepted_traffics": ["inference"],
  "filtered_apps_only": false,
  "app_filters": [],
  "traefik_last_used_marker_path": null,
  "wildcard_domain": ".models.example.com",
  "wildcard_traffic_port": 443
}

응답에는 할당된 id(워커 UUID, 이후 호출을 위해 캐시)와 계산된 slots가 포함됩니다.

6.2 서킷과 라우트 데이터 모델

SerializableCircuit(REST 스냅샷이 반환하고 이벤트에 포함되는 JSON 형태):

필드 타입 비고
id UUID
app string 추론에서는 ""
protocol enum http/grpc/h2/tcp/preopen/vnc/rdp
worker UUID 호스팅 워커
app_mode enum interactive/inference
frontend_mode enum wildcard/port
port int? frontend_mode == port일 때만 설정
subdomain string? frontend_mode == wildcard일 때만 설정
endpoint_id UUID? 추론 전용
runtime_variant string? 추론 전용
open_to_public bool true면 인증 생략
allowed_client_ips string? 쉼표로 구분한 CIDR 목록
route_info RouteInfo[] 백엔드 대상 목록
session_ids UUID[]
envs object
created_at / updated_at datetime ISO-8601

RouteInfo:

필드 타입 비고
route_id UUID? 같은 host:port에서 route_id가 바뀌면 커널 교체를 의미
session_id UUID 필수
session_name string?
kernel_host string? Nonelocalhost
kernel_port int 1–65535
protocol enum
traffic_ratio float 기본값 1.0. 백엔드 weight로 매핑

Rust serde 관련 참고 사항:

  • 입력은 kebab-case와 snake_case 별칭을 모두 허용하고(예: route-idroute_id), 출력은 snake_case로 내보냅니다.
  • extra = "ignore" 의미론: 알 수 없는 필드를 허용해(#[serde(default)] / 알 수 없는 필드 무시) 코디네이터 측 필드 추가가 파싱을 깨뜨리지 않게 합니다.

6.3 Redis 이벤트 엔벨로프

네 가지 서킷 이벤트는 모두 events_all-appproxy로 PUBLISH되는 JSON 객체로 브로드캐스트됩니다.

{
  "name": "<event_name>",
  "source": "<agent-id>",
  "args": "<base64(msgpack(args_tuple))>",
  "metadata": "{\"request_id\":null,\"user\":null}"
}
  • args는 msgpack 배열의 base64입니다. 이 이벤트들에서 배열 원소는 문자열뿐입니다. msgpack ext 타입도 없고, msgpack 계층의 UUID/datetime/enum 인코딩도 없습니다(이런 값은 내부 JSON 안에 미리 인코딩되어 있음). Rust 구현에서는 JSON 객체 → args base64 디코딩 → 문자열로 이루어진 msgpack 배열 → 원소별 JSON 파싱만 처리하면 됩니다.
  • metadata는 정확히 request_iduser만 갖는 JSON 문자열입니다(키를 추가하면 코디네이터의 파서가 예외를 일으킵니다). {"request_id":null,"user":null}을 내보내거나 인바운드 request_id를 그대로 돌려보냅니다.
  • 워커가 내보내는 이벤트의 source"appproxy-worker"입니다. 라우팅에는 사용되지 않으며, 워커는 인바운드 이벤트를 target_worker_authority로 필터링합니다.

이벤트 페이로드:

name 방향 args 튜플
appproxy_circuit_created_event 인바운드 (authority, circuits_json), circuits_json = SerializableCircuit의 JSON 배열
appproxy_circuit_removed_event 인바운드 (authority, circuits_json)
appproxy_circuit_route_updated_event 인바운드 (authority, circuit_json, routes_json) (단일 서킷 + RouteInfo[])
appproxy_worker_circuit_added_event 아웃바운드 (ack) (authority, circuits_json), 인바운드 circuits_json을 그대로 되돌려 보냄

구체적인 ack 예시(authority = "worker01", circuits_json = "[]"): msgpack(["worker01","[]"]) = 92 a8 worker01 a2 5b 5d → base64 kqh3b3JrZXIwMaJbXQ==이며, 이를 name = appproxy_worker_circuit_added_event, source = appproxy-workerevents_all-appproxy에 PUBLISH합니다.

이벤트 버스용 Redis DB 인덱스는 배포 환경의 'stream' 역할 DB이며 반드시 설정해야 합니다(redis_url / DB 선택자). 코디네이터의 Redis 프로필과 맞는지 확인합니다.

7. 설정

라우터 설정에 appproxy 피처로 게이트되는 선택적 섹션이 새로 추가됩니다.

appproxy:
  enabled: true
  coordinator_url: "http://coordinator:10200"
  api_secret: "${APPPROXY_API_SECRET}"     # X-BackendAI-Token
  jwt_secret: "${APPPROXY_JWT_SECRET}"      # HS256 circuit/bearer verification
  redis_url: "redis://valkey:6379/4"        # event bus DB (stream role)
  authority: "continuum-router-1"
  hostname: "router.example.com"
  frontend_mode: "wildcard"
  wildcard_domain: ".models.example.com"
  aggregation_hosts: []                      # extra Hosts that skip subdomain scoping
  wildcard_traffic_port: 443
  tls_advertised: true
  heartbeat_period: "10s"
  reconcile_interval: "15s"
  events_enabled: true                       # Redis Pub/Sub overlay on/off

시크릿은 backends[].api_key와 동일하게 ${ENV_VAR} 보간을 지원합니다.

aggregation_hosts는 선택 사항이고 기본값은 빈 목록입니다. 와일드카드 정점(apex) 도메인(wildcard_domain에서 앞의 점을 뺀 것, 예: models.example.com)은 암묵적으로 항상 집약 표면이므로, 추가 베니티 호스트나 집약 호스트 이름이 필요할 때만 여기에 나열합니다. Host가 이 목록(또는 정점 도메인)과 일치하는 요청은 서킷별 서브도메인 범위 지정을 건너뛰고 모든 서킷에 걸쳐 일반 모델 이름 라우팅을 사용합니다(§5.4).

8. 모듈 구조

src/appproxy/                 # feature = "appproxy"
├── mod.rs                    # run_worker(cfg, state, shutdown_rx) entry; re-exports
├── config.rs                 # AppProxyWorkerConfig (also re-exported via core::config)
├── types.rs                  # SerializableCircuit, RouteInfo, enums (serde, dual aliases)
├── client.rs                 # coordinator REST client (X-BackendAI-Token)
├── worker.rs                 # lifecycle: register → pull → heartbeat → reconcile; circuit registry
├── reconcile.rs              # circuit set → Vec<BackendConfig> → config_sender (under the lock)
├── events.rs                 # Redis Pub/Sub subscribe + ack; msgpack/base64/JSON envelope codec
├── ingress.rs                # Host subdomain → IngressTarget middleware; handler override helper
└── jwt.rs                    # HS256 verify for non-public circuits

연결 지점(모두 #[cfg(feature = "appproxy")] 뒤에 위치):

  • Cargo.toml: appproxy = ["dep:redis", "dep:deadpool-redis", "dep:jsonwebtoken"] (Streams 불필요). 옵트인으로 유지되도록 full에는 포함하지 않습니다.
  • src/lib.rs: pub mod appproxy;.
  • src/core/config/models/config.rs: pub appproxy: Option<AppProxyWorkerConfig>.
  • src/server/mod.rs::build_router: 워커 /status 라우트를 등록하고, 해석된 모델이 속도 제한기에 보이도록 인그레스 미들웨어를 속도 제한 레이어 바로 바깥에 삽입합니다.
  • src/server/serve.rs: 핫 리로드 블록 다음에서 cfg.appproxy.enabled일 때 appproxy::run_worker(cfg, state.clone(), shutdown_rx.clone())를 스폰합니다.

9. 보안

  • 코디네이터 인증. 모든 REST 호출은 X-BackendAI-Token: <api_secret>을 보냅니다. 시크릿은 환경 변수/시크릿 저장소에 보관하고 절대 로그에 남기지 않습니다.
  • 데이터 플레인 인증. 비공개 추론 서킷은 디코딩된 id가 서킷 id와 일치하는 Authorization: Bearer <jwt>를 요구합니다(HS256, jwt_secret). 공개 서킷(open_to_public == true)은 생략합니다. jsonwebtokenValidation::new(Algorithm::HS256)과 함께 사용해야 하며, 트리의 다른 곳에 있는 미검증 페이로드 디코딩은 절대 사용하면 안 됩니다.
  • 클라이언트 IP 허용 목록. 서킷에 allowed_client_ips(쉼표로 구분한 CIDR)가 있으면 이를 적용합니다.
  • 공유 시크릿. api_secretjwt_secret은 AppProxy 클러스터 전체(코디네이터 + 워커)에서 동일해야 합니다.

10. 제약 사항

  • 모델 이름 출처. 서빙되는 모델 이름은 각 복제본의 /v1/models에서 자동 발견됩니다. 코디네이터는 모델 이름을 제공하지 않습니다(결정 2에 따라 통합은 Continuum Router 측에서만 이루어짐).
  • PORT 모드. 지원하지 않습니다. 포트별 동적 리스너가 필요하기 때문이며, 라우터는 와일드카드 워커로만 등록합니다.
  • 인터랙티브 앱. 서비스하지 않습니다. 라우터는 accepted_traffics = [inference]로만 등록하므로 인터랙티브 서킷은 기존 표준 워커에 남습니다.
  • 헬스/부하 보고. AppProxy 하트비트는 단순 keepalive입니다. 라우터는 서킷별 부하나 헬스 메트릭을 코디네이터로 내보내지 않습니다.