Назад к блогу

Один шлюз вместо зоопарка: сценарии использования API Gateway

Один шлюз вместо зоопарка: сценарии использования API Gateway

api-gateway · практика

Бэкенд обрастает инфраструктурными компонентами незаметно. Сначала перед сервисом стоит один прокси. Потом нужна проверка токенов — и рядом появляется oauth2-proxy. Дальше требуются роли и права — и вот уже отдельный permission-сервис, к которому каждый запрос ходит по сети. Просят аудит действий — появляется воркер, читающий логи и рассылающий события. Сверху ложатся лимиты, CORS, автоматический TLS, service discovery, метрики и трейсинг. Каждый пункт — контейнер со своим конфигом, деплоем, мониторингом и собственным режимом отказа. Сопровождать этот зоопарк со временем дороже, чем сам сервис.

api-gateway построен вокруг другого допущения: перечисленные обязанности — это не внешние сервисы, а разделы одного конфига. Проверка JWT, роли из claims, аудит, лимиты, TLS, discovery и наблюдаемость включаются в config.yaml и работают на одном горячем пути. Ниже — пятнадцать типовых задач в формате «задача → конфиг → что получаете». Это практическое дополнение к документации: полные описания полей — в справочнике конфигурации, а те же рецепты с навигацией — на странице сценариев.

Кто пришёл: аутентификация и роли

Первое, что снимают с внешних компонентов, — вход. Вместо прокси и отдельного auth-сервиса токен проверяется прямо в шлюзе. Дело не только в меньшем числе контейнеров: проверка на месте убирает сетевой хоп с каждого запроса. Насколько это дешевле внешней авторизации, показывают замеры:

Цена авторизации: встроенная vs внешняязапросов / секунду · c300
api-gateway (встроенный JWT)нативный JWT (Envoy, Kong)внешний auth-сервис (Nginx, Traefik)светлее = без auth
010k20k28kapi-gateway · без auth · 24 304 запр/сapi-gateway · JWT встроен · 20 641 запр/сapi-gateway−15%Envoy · без auth · 25 272 запр/сEnvoy · jwt_authn · 20 290 запр/сEnvoy−20%Kong · без auth · 25 793 запр/сKong · плагин jwt · 18 331 запр/сKong−29%Nginx · без auth · 27 216 запр/сNginx · auth_request (внешний сервис) · 2 849 запр/сNginx−89%Traefik · без auth · 24 811 запр/сTraefik · ForwardAuth (внешний сервис) · 1 541 запр/сTraefik−94%
Валидный HS256-токен на каждый запрос. Нативная проверка стоит 15–29%; вынесенный auth-сервис (auth_request / ForwardAuth) — 89–94%.

1. Публичный сервис и JWT на выбранных маршрутах

Задача. Логин и регистрация должны быть открыты, а остальной API — закрыт токеном.

jwt:
  secret_key: "${JWT_SECRET}"
  algorithm: "HS256"
  validate_exp: true

targets:
  - name: "auth-api"
    url: "http://127.0.0.1:9001"
  - name: "client-api"
    url: "http://127.0.0.1:9002"

routing:
  rules:
    - path_prefix: "/api/v1/auth/login"
      target_name: "auth-api"
      methods: ["POST"]
      auth:
        required: false
    - path_prefix: "/api/v1/auth"
      target_name: "auth-api"
      auth:
        required: true
    - path_prefix: "/api/v1/client"
      target_name: "client-api"
      auth:
        required: true

Что получаете. /api/v1/auth/login доступен анонимно, остальные маршруты ждут валидный токен в Authorization: Bearer или в cookie cml_access. Невалидный токен, пришедший на публичный маршрут, запрос не блокирует.

Оговорки. Правило выбирается по самому длинному совпавшему префиксу, поэтому специфичный /login выносится отдельным правилом выше общего /api/v1/auth. Предъявленный токен проверяется всегда, даже если маршрут не требует auth: публичность не отключает валидацию.

2. Роли из claims токена

Задача. Пустить в админский API только носителей роли admin.

jwt:
  secret_key: "${JWT_SECRET}"
  claim_mappings: ["id", "roles"]

routing:
  rules:
    - path_prefix: "/api/v1/admin"
      target_name: "admin-api"
      auth:
        required: true
        roles: ["admin"]

Что получаете. Запрос без роли admin в claim roles получает 401. Claim roles может быть как массивом, так и строкой.

Оговорки. Требуются все перечисленные роли, а не любая из них. Если claim roles отсутствует или имеет неверный тип — тоже 401.

Кто что сделал: аудит

Аудит — вторая обязанность, которую обычно выносят наружу: события собирает отдельный воркер, который читает логи шлюза. В api-gateway источник событий и их доставка встроены, поэтому аудит описывается там же, где маршруты, и может быть разным для разных путей.

3. Аудит изменений: вебхук только на мутирующие методы

Задача. На каждый POST, PUT, PATCH и DELETE отправлять событие во внешний аудит.

webhooks:
  - name: "audit-mutations"
    transport: "webhook"
    webhook_url: "https://audit.example/events"
    trigger: "on_response"
    methods: ["POST", "PUT", "PATCH", "DELETE"]

Что получаете. На каждый ответ мутирующего запроса уходит POST с JSON-событием: method, path, user_id, status_code, changes.

Оговорки. changes заполняется телом запроса, только если это JSON-объект короче 64 KiB, а само чтение тела ограничено server.max_request_body_size. Для объёмного потока событий включайте батчинг — как в следующем сценарии.

4. Флагман: аудит админки — и чтение, и запись

Задача. Собрать полный аудит админских маршрутов: видеть не только изменения, но и просмотры. Кто что смотрел и кто что изменил.

targets:
  - name: "admin-api"
    url: "http://127.0.0.1:9003"

routing:
  rules:
    - path_prefix: "/api/v1/admin"
      target_name: "admin-api"
      auth:
        required: true
        roles: ["admin"]

webhooks:
  - name: "admin-audit"
    transport: "webhook"
    webhook_url: "https://audit.example/admin"
    trigger: "on_response"
    include_request_body: true
    include_response_body: true
    batch_size: 1000
    flush_interval: 100ms

Что получаете. Любой запрос к /api/v1/admin, включая GET, рождает событие с телом запроса и телом JSON-ответа. События копятся пачками и уходят одним POST {"count":N,"events":[...]}.

Оговорки. Тело ответа собирается только для JSON и обрезается на 64 KiB. Батчинг включает backpressure: при переполнении очереди (8192 события) постановка блокируется — потерь нет, но запросы замедляются. Тело запроса попадает в changes, только если это JSON.

Периметр: права, лимиты и доставка

Дальше — слой, который обычно собирают из трёх-четырёх отдельных сервисов: проверка прав, ограничение скорости, CORS, базовая защита служебных путей, раздача статики и сертификаты. В шлюзе это независимые секции конфига.

5. Внешний permission-сервис с кешем

Задача. Обогащать запрос эффективными правами пользователя, не дёргая сервис прав на каждый вызов.

jwt:
  secret_key: "${JWT_SECRET}"
  claim_mappings: ["id"]

permissions:
  enabled: true
  service_url: "http://permissions:8080"
  cache_ttl: 300s
  header_name: "X-User-Permissions"
  api_key: "${PERMISSIONS_KEY}"
  invalidate_token: "${INVALIDATE_TOKEN}"

Что получаете. Бэкенд получает заголовок X-User-Permissions со списком разрешений через запятую. Ответы кешируются по user_id на cache_ttl. Сбросить кеш можно через POST /_cache/permissions/invalidate с заголовком X-Invalidate-Token и опциональным ?user_id=….

Оговорки. Модулю нужен claim id, приводимый к целому. Если разрешений нет, заголовок не выставляется. Эндпоинт инвалидации закрыт Basic Auth, когда он включён, — добавьте путь в skip_paths либо шлите токен.

6. Рейт-лимитинг: на маршрут и глобальный

Задача. Зажать логин жёстче остального API и задать общий потолок на процесс.

routing:
  global_limit:
    requests_per_second: 1000
    burst: 2000
  rules:
    - path_prefix: "/api/v1/auth/login"
      target_name: "auth-api"
      methods: ["POST"]
      rate_limit:
        requests_per_second: 5
        burst: 10
    - path_prefix: "/api/v1/client"
      target_name: "client-api"
      rate_limit:
        requests_per_second: 100
        burst: 200

Что получаете. Логин ограничен 5 rps на IP, клиентский API — 100 rps на IP, весь процесс — 1000 rps.

Оговорки. Per-route лимит считается на IP клиента и отвечает 429 без Retry-After; глобальный считается на процесс и отдаёт Retry-After: 1. IP берётся из последнего значения X-Forwarded-For, иначе из RemoteAddr.

7. CORS для SPA

Задача. Разрешить браузерному приложению на конкретном домене обращаться к API с credentials.

headers:
  cors:
    enabled: true
    allowed_origins: ["https://app.example"]
    allowed_methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
    allowed_headers: ["Content-Type", "Authorization", "X-Request-ID"]
    expose_headers: ["X-Request-ID"]
    max_age: 86400

Что получаете. Запросы с origin https://app.example получают Access-Control-Allow-Origin с этим origin и Allow-Credentials: true; preflight OPTIONS отвечает 200.

Оговорки. При allowed_origins: ["*"] возвращается * без credentials — браузер не отправит куки. Точный origin в allowlist обязателен, если нужны credentials. Любой OPTIONS трактуется как preflight.

8. Basic Auth для внутренних путей

Задача. Закрыть служебные эндпоинты логином и паролем.

basic_auth:
  enabled: true
  username: "internal"
  password: "${BASIC_PASS}"
  skip_paths: ["/health"]

Что получаете. Все запросы требуют Basic Auth, кроме /health и его подпутей. Сравнение — constant-time.

Оговорки. Basic Auth стоит во внешнем слое и закрывает заодно /metrics, статику и эндпоинт инвалидации кеша. Пароль хранится в конфиге открытым текстом — подставляйте его из окружения.

9. Статика и SPA-fallback

Задача. Отдавать собранный SPA с fallback на index.html, а /api проксировать.

static:
  skip_prefixes: ["/api"]
  apps:
    - path_prefix: "/"
      root_dir: "/app/static"
      index_file: "index.html"
      max_age: 3600

targets:
  - name: "api"
    url: "http://127.0.0.1:9001"
    path_prefix: "/api"

Что получаете. Существующие файлы отдаются напрямую, несуществующие пути — index.html; /api/* уходит в прокси.

Оговорки. Статика обрабатывается раньше проксирования. Если маршрут совпал с правилом, у которого задан Host, статика пропускается — так host-based роутинг не перекрывается файлами. Поддерживаются и плоские .html (about.html для /about).

10. Автоматический TLS через ACME

Задача. Получить сертификаты Let's Encrypt и редиректить HTTP на HTTPS.

tls:
  enabled: true
  port: 443
  http_port: 80
  domains:
    - "api.example.com"
    - "*.example.com"
  email: "admin@example.com"
  cache_dir: "/var/lib/api-gateway/certs"
  staging: false
  redirect_http: true

Что получаете. HTTPS-сервер на 443, HTTP на 80 обслуживает ACME-challenge и редиректит на HTTPS. /health и /ready по HTTP отвечают 200.

Оговорки. Сначала прогоняйте на staging: true — это настоящий staging CA с отдельным кешем. directory_url перекрывает выбор CA по staging. При включении TLS домен и email обязательны.

Рантайм: маршруты, здоровье и наблюдаемость

Последний пласт — операционный. Пока сервисов мало, маршруты можно прописывать руками, но в проде они появляются и исчезают на каждом деплое. Ручной список таргетов быстро становится источником 502. Здесь шлюз берёт на себя обнаружение, распределение трафика и телеметрию.

11. Service discovery по labels

Задача. Поднимать и убирать бэкенды без правки конфига шлюза.

discovery:
  enabled: true
  provider: docker
  host: "unix:///var/run/docker.sock"
  label_prefix: "gateway"
  network: ""
  debounce: 500ms
  resync_interval: 5m
  state_file: "/var/lib/api-gateway/discovery-state.json"
services:
  blog:
    image: ghcr.io/sarnas-it/blog:latest
    labels:
      gateway.enable: "true"
      gateway.port: "8085"
      gateway.health: "/health"
      gateway.router.public.path_prefix: "/api/blog"
      gateway.router.public.auth.required: "false"
      gateway.router.admin.path_prefix: "/api/admin/blog"
      gateway.router.admin.auth.required: "true"
      gateway.router.admin.auth.roles: "admin"

Что получаете. Шлюз сам строит таргеты и правила из labels. Найденное дополняет статику; при конфликте имён побеждает статика.

Оговорки. Socket монтируется read-only, клиент делает только GET. Маркер включения — gateway.enable: "true" (true/1/yes). Чтобы пережить рестарт при недоступном Docker, каталог state_file должен быть writable. Секреты в labels не кладите — они видны через docker inspect.

Насколько быстро шлюз реагирует на изменения по сравнению с Traefik, у которого discovery — ключевая фича:

Service discovery: реакция на изменение подамиллисекунды
api-gatewayTraefikсветлее = под появился · насыщенно = под убран
05001000150020002500api-gateway · контейнер появился → роут готов · 720 мсapi-gateway · контейнер упал → роут убран · 650 мсapi-gateway720 / 650 мсTraefik · контейнер появился → роут готов · 310 мсTraefik · контейнер упал → роут убран · 2 210 мсTraefik310 / 2210 мс
Docker-провайдеры. api-gateway переключается симметрично ~0.65–0.72 с (debounce 500 мс, тюнится); Traefik быстро добавляет роут, но убирает за ~2.2 с.

api-gateway переключается симметрично: новый контейнер становится доступен примерно за 0.72 с, пропавший убирается за 0.65 с. Важно не только то, как быстро сервис появляется, но и как быстро шлюз перестаёт слать трафик на мёртвый: асимметрия означает окно, когда запросы уходят на контейнер, которого уже нет.

12. Взвешенная балансировка

Задача. Разделить трафик между канареечной и стабильной версиями сервиса в пропорции 1:4.

targets:
  - name: "api-stable"
    url: "http://127.0.0.1:9001"
    weight: 4
  - name: "api-canary"
    url: "http://127.0.0.1:9002"
    weight: 1

routing:
  rules:
    - path_prefix: "/api"
      target_name: "api-stable"
    - path_prefix: "/api"
      target_name: "api-canary"

Что получаете. Правила, совпавшие по (host, path_prefix, methods), образуют пул; запросы идут взвешенным round-robin по здоровым таргетам в пропорции 4:1.

Оговорки. Все правила пула обязаны совпадать по auth, strip_path и rate_limit. Без weight вес равен 1; weight: 0 или отрицательный исключает таргет. Нездоровые таргеты пропускаются; если здоровых нет — 503.

13. Проброс claims в заголовки

Задача. Передать бэкенду идентификатор, email и роли пользователя отдельными заголовками.

jwt:
  secret_key: "${JWT_SECRET}"
  claim_mappings: ["id", "email", "roles"]

headers:
  strip_authorization: true
  claim_to_header:
    id: "X-User-ID"
    email: "X-User-Email"
    roles: "X-User-Roles"
  add_headers:
    X-Gateway-Version: "1.0.0"

Что получаете. Бэкенд получает X-User-ID, X-User-Email, X-User-Roles из токена и X-Gateway-Version; Authorization удаляется.

Оговорки. Маппинг применяется только к извлечённым claim_mappings. Если список пуст, по умолчанию извлекается sub и маппится в X-User-ID. HMAC-подпись (headers.sign_header) требует permissions.api_key и claim id.

14. Health checks и circuit breaker

Задача. Не отправлять трафик на упавший бэкенд и разрывать цепь на ошибках транспорта.

application:
  health_check: true
  circuit_breaker: true

targets:
  - name: "api-a"
    url: "http://127.0.0.1:9001"
    health_check: "http://127.0.0.1:9001/health"
    weight: 1
  - name: "api-b"
    url: "http://127.0.0.1:9002"
    health_check: "http://127.0.0.1:9002/health"
    weight: 1

routing:
  rules:
    - path_prefix: "/api"
      target_name: "api-a"
    - path_prefix: "/api"
      target_name: "api-b"

Что получаете. Health-пробы идут раз в 30 секунд с таймаутом 5 секунд. Нездоровые таргеты исключаются из пула. Circuit breaker открывается после 3 ошибок транспорта и через 30 секунд пропускает один пробный запрос.

Оговорки. Любой HTTP-ответ, включая 5xx, считается признаком исправного транспорта — цепь рвут только ошибки соединения. health_check у таргета работает, только если application.health_check: true. Если здоровых таргетов в пуле нет — 503.

15. Метрики и трейсинг

Задача. Снимать метрики шлюза и отправлять трейсы в OTLP-коллектор.

application:
  metrics_enabled: true
  metrics_allowed_ips: ["10.0.0.5"]
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \
OTEL_EXPORTER_OTLP_INSECURE=true \
./api-gateway -config /etc/proxy/config.yaml

Что получаете. /metrics отдаёт счётчики в формате expvar (gateway_requests_total, gateway_request_duration_ms, gateway_rate_limit_denials_total, gateway_target_up, gateway_active_requests). Трейсы уходят по OTLP HTTP, заголовок traceparent пропагируется в бэкенд.

Оговорки. Метрики по умолчанию выключены — их сбор стоит работы на каждый запрос. При заданном metrics_allowed_ips /metrics доступен только этим IP. Без OTEL_EXPORTER_OTLP_ENDPOINT трейсинг выключен. pprof включается отдельно переменной PPROF_ADDR.

Что в итоге

Пятнадцать задач выше — это один и тот же приём: обязанность, которая обычно живёт в отдельном сервисе, переезжает в раздел конфига. Вход и роли, аудит чтений и записей, права с кешем, лимиты, CORS, Basic Auth, статика, сертификаты, discovery, балансировка, проброс claims, health checks и телеметрия — всё это включается по месту и не требует новых контейнеров на горячем пути. Вместо связки «прокси + auth-сервис + permission-сервис + воркер аудита + контроллер discovery» остаётся один компонент с одним конфигом.

Практический вывод простой. Если задача — только терминировать TLS и раздавать статику, хватит лёгкого прокси. Как только шлюзу нужно понимать, кто пришёл и что ему можно, и фиксировать, что он сделал, выигрывает тот, у кого эти обязанности встроены: их цена предсказуема, а конфигурация остаётся в одном файле. Начните с быстрого старта, сверяйте поля по справочнику конфигурации и забирайте готовые рецепты со страницы сценариев.

Рецепты адаптированы из документации api-gateway · примеры рассчитаны на config.local.yaml · продукт

Похожее