Назад к документации

Конфигурация

Полный справочник по всем секциям и полям, которые читает код api-gateway. Для каждого поля указаны тип, значение по умолчанию, допустимые значения и короткий пример. Значения по умолчанию применяются, когда ключ не задан в YAML; явно заданные значения имеют приоритет.

Общая структура

Конфиг — один YAML-файл. Путь задаётся флагом -config (по умолчанию /etc/proxy/config.yaml). Секции верхнего уровня:

СекцияНазначение
applicationРежим работы, метрики, пул соединений
serverHTTP-сервер: порт, таймауты, лимит тела
tlsHTTPS и автоматические сертификаты
staticРаздача статики и SPA
targetsБэкенды
jwtПроверка JWT
basic_authBasic Auth на служебные пути
loggingУровень, формат, access log
headersCORS, проброс claims, подпись, заголовки
routingПравила маршрутизации и глобальный лимит
permissionsИнтеграция с permission-сервисом
webhooksПубликация событий (webhook/NATS)
discoveryОбнаружение сервисов по labels

Длительности записываются строками Go duration: 500ms, 5s, 5m, 1h.

Переменные, флаги и сигналы

Подстановка ${VAR}. Перед разбором YAML шлюз раскрывает ${VAR} и $VAR из окружения процесса. Если переменная не задана или пуста, шлюз не стартует и возвращает ошибку с именем отсутствующей переменной — молчаливого фолбэка на литерал ${VAR} нет, чтобы его нельзя было случайно использовать как секрет. Чтобы оставить ${VAR} в конфиге буквально, экранируйте: $${VAR}.

jwt:
  secret_key: "${JWT_SECRET}"
permissions:
  service_url: "${PERMISSIONS_URL}"

Секреты удобно передавать через env_file в docker-compose или переменные окружения контейнера — в самом YAML хранится только ссылка.

Переменные окружения. Их читает не конфиг, а сам процесс:

ПеременнаяЗначениеСмысл
PPROF_ADDRадрес, напр. 127.0.0.1:6060Включает pprof-сервер; пусто — выключен
OTEL_EXPORTER_OTLP_ENDPOINTURL, напр. http://localhost:4318Включает OTLP-трейсинг; пусто — выключен
OTEL_EXPORTER_OTLP_INSECUREtrueОтправлять OTLP без TLS
OTEL_INSECUREtrueТо же, альтернативное имя

Флаги. -config <path> — путь к файлу конфигурации.

Сигналы. SIGHUP перечитывает конфиг и применяет его атомарно (старое состояние сохраняется при ошибке). SIGINT и SIGTERM запускают graceful shutdown с таймаутом 30 секунд.

Неизвестные ключи. Ключи, которых нет в структуре конфига, не считаются ошибкой: шлюз пишет в лог предупреждение Unknown config key с точечным путём (например headers.forward_headers). Строгий режим намеренно не включён.

application

Общие настройки процесса.

ПолеТипПо умолчаниюСмысл и значенияПример
envstring""dev включает мягкий CORS (reflect origin + credentials) и dev-логи. Любое другое значение — обычный режимprod
health_checkboolfalseВключает фоновые health-проверки таргетов, у которых задан health_checktrue
circuit_breakerboolfalseВключает circuit breaker на ошибках транспортаtrue
metrics_enabledboolfalseСобирает метрики и открывает /metricstrue
metrics_allowed_ips[]string[] (все)Allowlist клиентских IP для /metrics["10.0.0.5"]
max_idle_conns_per_hostint1000Размер пула keep-alive соединений к каждому таргету. Должен быть не меньше пиковой конкурентности1000

Метрики выключены по умолчанию, потому что их сбор добавляет работу на каждый запрос. Включайте metrics_enabled осознанно; metrics_allowed_ips ограничивает доступ к /metrics, если эндпоинт смотрит наружу.

server

HTTP-сервер.

ПолеТипПо умолчаниюСмысл и значенияПример
portint8080Порт HTTP-сервера (в TLS-режиме не используется)8080
read_timeoutduration5sТаймаут чтения запроса5s
write_timeoutduration10sТаймаут записи ответа30s
idle_timeoutduration120sТаймаут keep-alive соединения120s
max_request_body_sizeint64 (байты)10485760 (10 MiB), если ключ не заданЛимит тела запроса. 0без лимита, >0 — лимит в байтах1048576

Семантика max_request_body_size. Различаются «ключ не задан» и «явный ноль»: не задан — 10 MiB, 0 — без ограничения, положительное число — точный лимит. Тот же лимит применяется к чтению тела для аудита, чтобы вебхуки не вычитывали тело неограниченно.

tls

HTTPS с автоматическими сертификатами Let's Encrypt (ACME).

ПолеТипПо умолчаниюСмысл и значенияПример
enabledboolfalseВключает HTTPS-серверtrue
portint443HTTPS-порт443
http_portint80HTTP-порт для ACME-challenge и редиректа80
domains[]stringДомены сертификата; обязательны при enabled: true. Поддерживаются wildcard["api.example.com"]
emailstringEmail для регистрации в Let's Encrypt; обязателенadmin@example.com
cache_dirstring/var/lib/api-gateway/certsКаталог кеша сертификатов; при staging используется подкаталог staging/var/lib/api-gateway/certs
stagingboolfalsetrue — ACME staging CA (для отладки), false — production Let's Encryptfalse
directory_urlstring""Свой ACME directory URL; если задан, перекрывает выбор CA по staginghttps://acme-staging-v02.api.letsencrypt.org/directory
redirect_httpboolfalseРедиректит HTTP на HTTPS; /health и /ready на HTTP-порту отвечают 200true

При включённом TLS запускаются два сервера: HTTPS на port и HTTP на http_port для ACME-challenge и редиректа. staging: true использует реальный staging CA и отдельный подкаталог кеша, чтобы staging-сертификаты не смешивались с production.

static

Раздача статики и SPA-приложений. Обрабатывается до проксирования, но после глобального лимита.

ПолеТипПо умолчаниюСмысл и значенияПример
apps[]object[]Список SPA/статических приложенийсм. ниже
skip_prefixes[]string[]Пути, которые не отдаются статикой (уходят в прокси)["/api"]

Поля элемента apps:

ПолеТипПо умолчаниюСмысл и значенияПример
path_prefixstringURL-префикс приложения/
root_dirstringКаталог со статикой/app/static
index_filestringindex.htmlFallback-файл SPAindex.html
max_ageint (сек)3600Значение Cache-Control: max-age3600
static:
  skip_prefixes: ["/api"]
  apps:
    - path_prefix: "/"
      root_dir: "/app/static"
      index_file: "index.html"
      max_age: 3600

Если маршрут совпадает с правилом, у которого задан Host, статика пропускается — так host-based роутинг работает при смонтированной статике. Для несуществующих путей отдаётся index_file (SPA fallback); поддерживаются и плоские .html-файлы (about.html для /about).

targets

Список бэкендов. Имя и URL обязательны; имена уникальны.

ПолеТипПо умолчаниюСмысл и значенияПример
namestringУникальное имя таргетаapi
urlstringБазовый URL бэкендаhttp://127.0.0.1:9001
timeoutduration30sТаймаут запроса к таргету10s
path_prefixstring""Префикс; при пустых routing.rules из него создаётся правило/api
strip_prefixboolfalseУдалять префикс при проксированииfalse
weightint1, если ключ не заданВес в пуле маршрута: nil — 1, 0 и отрицательные исключают таргет, >0 — вес2
health_checkstring""URL health-проверки; работает при application.health_check: truehttp://127.0.0.1:9001/health

Семантика weight. Таргеты, чьи правила совпадают по (host, path_prefix, methods), образуют один пул и распределяются взвешенным round-robin по здоровым кандидатам. Отсутствие ключа weight — это вес 1, а явный weight: 0 (или отрицательный) исключает таргет из выбора. Различайте эти два случая.

jwt

Проверка JWT. Токен берётся из Authorization: Bearer … или из cookie cml_access.

ПолеТипПо умолчаниюСмысл и значенияПример
secret_keystring""Симметричный ключ для HMAC"${JWT_SECRET}"
public_key_filestring""PEM-файл публичного ключа для RSA/ECDSA/Ed25519/etc/proxy/public.pem
algorithmstringHS256HS256/384/512, RS256/384/512, ES256/384/512, EdDSA/ED25519RS256
validate_expboolfalseПроверять срок действия; принудительно true, если required: truetrue
validate_issboolfalseПроверять issuerfalse
expected_issstring""Ожидаемый issuer"https://auth.example"
validate_audboolfalseПроверять audiencefalse
expected_audstring""Ожидаемый audience"api"
claim_mappings[]string["sub"]Claims, извлекаемые в контекст; при пустом значении дополнительно маппится sub → X-User-ID["id", "email", "roles"]
requiredboolfalseТребовать токен глобальноfalse

Глобальный required перекрывается настройкой маршрута auth.required. Предъявленный токен всегда проверяется криптографически: на маршруте без требования невалидный токен игнорируется (запрос идёт как анонимный), на требующем — возвращается 401.

basic_auth

Basic Auth для служебных путей. Проверка — constant-time по SHA-256 от логина и пароля.

ПолеТипПо умолчаниюСмысл и значенияПример
enabledboolfalseВключает Basic Authtrue
usernamestring""Логинinternal
passwordstring""Пароль"${BASIC_PASS}"
skip_paths[]string[]Пути без Basic Auth; совпадение точное или по префиксу путь/["/health"]

Basic Auth стоит во внешнем слое цепочки и защищает всё, включая /metrics и статику, кроме skip_paths. При успехе заголовок Authorization удаляется перед проксированием.

logging

ПолеТипПо умолчаниюСмысл и значенияПример
levelstringinfodebug, info, warn, error, panic, fataldebug
formatstringtextconsole, text (алиас console), json; регистр не важен. Иное значение — ошибка запускаjson
access_logboolfalseПострочный лог каждого запроса; выключен по умолчанию из-за аллокацийtrue

Неверный format не «молча» превращается в консоль: загрузка конфига завершается ошибкой logging.format must be one of console, text, json.

headers

ПолеТипПо умолчаниюСмысл и значенияПример
strip_authorizationboolfalseУдалять Authorization перед проксированиемtrue
claim_to_headermap[string]string{}Claim → имя заголовка; при пустых claim_mappings по умолчанию sub → X-User-ID{id: X-User-ID}
add_headersmap[string]string{}Заголовки, добавляемые в каждый проксируемый запрос{X-Gateway: sarnas}
sign_headerstring""Имя заголовка для HMAC-SHA256(id, permissions.api_key) в hexX-User-Signature
corsobjectнетНастройки CORS (см. ниже)см. ниже

Поля cors:

ПолеТипПо умолчаниюСмысл и значенияПример
enabledboolfalseВключает CORStrue
allowed_origins[]string[]* или точные origin; при * credentials не выставляются["https://app.example"]
allowed_methods[]stringвстроенный списокРазрешённые методы["GET", "POST"]
allowed_headers[]stringвстроенный списокРазрешённые заголовки["Authorization"]
expose_headers[]stringвстроенный списокЗаголовки, видимые браузеру["X-Request-ID"]
max_ageint (сек)86400Время кеширования preflight86400
headers:
  strip_authorization: true
  claim_to_header:
    id: "X-User-ID"
    roles: "X-User-Roles"
  add_headers:
    X-Gateway-Version: "1.0.0"
  sign_header: "X-User-Signature"
  cors:
    enabled: true
    allowed_origins: ["https://app.example"]
    max_age: 86400

Правила CORS: при точном совпадении origin он отражается и выставляется Access-Control-Allow-Credentials; при * возвращается * без credentials; не попавший в allowlist origin CORS-заголовков не получает. Любой OPTIONS обрабатывается как preflight и возвращает 200. В env: "dev" при отсутствии секции cors включается мягкий режим с отражением origin.

sign_header вычисляется только если заданы и sign_header, и permissions.api_key, и в токене есть claim id.

routing

ПолеТипПо умолчаниюСмысл и значенияПример
rules[]object[]Правила маршрутизации; при пустом списке генерируются из targets с path_prefixсм. ниже
global_limitobjectнетГлобальный лимит на весь процесссм. ниже

Поля правила rules[]:

ПолеТипПо умолчаниюСмысл и значенияПример
hoststring""Матч по Host; поддерживается wildcard *.example.comapi.example.com
path_prefixstringПрефикс пути; обязателен/api
target_namestringИмя таргета; обязателен и должен существоватьapi
methods[]stringвсеФильтр HTTP-методов (регистр не важен)["GET", "POST"]
strip_pathboolfalseУдалять префикс при проксированииtrue
authobjectнетАутентификация маршрутасм. ниже
rate_limitobjectнетЛимит маршрутасм. ниже

auth: required (bool, по умолчанию наследует глобальный jwt.required), roles ([]string, достаточно любой из перечисленных ролей), roles_all ([]string, должны присутствовать все перечисленные роли), strip_token (bool, наследует headers.strip_authorization).

roles и roles_all читают claim roles токена (строка или массив строк). Если заданы оба списка, роль должна пройти оба условия: (любая из roles) И (все из roles_all). Если claim roles отсутствует или его тип не строка и не массив строк, маршрут с требованиями ролей отвечает 401.

auth:
  required: true
  roles: ["admin", "support"]   # достаточно любой из двух
  roles_all: ["staff", "mfa"]   # и при этом обязательны обе

rate_limit: requests_per_second (float, token bucket), burst (int). global_limit имеет те же поля, но применяется ко всем запросам процесса, а не на IP.

routing:
  global_limit:
    requests_per_second: 1000
    burst: 2000
  rules:
    - path_prefix: "/api"
      target_name: "api"
      strip_path: true
      auth:
        required: true
        roles: ["user"]
      rate_limit:
        requests_per_second: 50
        burst: 100

Выбирается правило с самым длинным совпадающим path_prefix среди тех, у кого совпали host и methods. Если ничего не совпало — 404. Правила, совпадающие по (host, path_prefix, methods), делят один пул балансировки и обязаны совпадать по auth, strip_path и rate_limit, иначе запуск завершается ошибкой.

permissions

Интеграция с внешним permission-сервисом: шлюз подмешивает в проксируемый запрос заголовок с эффективными разрешениями пользователя. Модуль включается permissions.enabled: true; при этом service_url обязателен, иначе загрузка конфига завершается ошибкой permissions.service_url is required when permissions.enabled is true.

Как это работает

JWT (Authorization: Bearer / cookie cml_access)
        │
        ▼
проверка подписи и claims
        │  claim id из jwt.claim_mappings
        ▼
   toInt(id) ── нет claim / не приводится к int ──► заголовок не выставляется,
        │                                            запрос идёт дальше (не 401)
        │ ok
        ▼
   кеш по user_id ── hit ────────────────────────────┐
        │ miss                                        │
        ▼                                             │
{method} {service_url}{path}  ({user_id} подставляется)
        │                                             │
        ├─ 200: кешируем, берём permissions ──────────┘
        └─ иное: ошибка, заголовок не выставляется, запрос идёт дальше
        │
        ▼
X-User-Permissions: "orders:read,orders:write"   (только если список непуст)
  1. После криптографической проверки токена шлюз берёт из него claim id. Claim должен быть перечислен в jwt.claim_mappings (например ["id", "email", "roles"]), иначе он не попадёт в извлечённый набор и заголовок не выставится.
  2. Значение id приводится к int: принимаются JSON-число (float64), int и числовая строка. Если claim отсутствует или не приводится — заголовок не выставляется, запрос продолжается без 401.
  3. При успешном приведении шлюз смотрит кеш по user_id. При промахе вызывается permission-сервис (см. контракт ниже), результат кладётся в кеш.
  4. Если список permissions непуст, выставляется заголовок header_name со значениями, склеенными через запятую. Пустой список заголовок не выставляет.
  5. Любая ошибка (сеть, таймаут, не-200, ошибка декодирования) логируется как failed to set permissions header; запрос всё равно проксируется без заголовка.

Блок выполняется только при предъявленном токене, прошедшем проверку, — на анонимных запросах заголовок не появляется.

Контракт permission-сервиса

Шлюз вызывает один эндпоинт, собранный из method, service_url и path:

{method} {service_url}{path}
{api_key_header}: {api_key}        # только если api_key задан
  • {method}permissions.method (по умолчанию GET).
  • {path}permissions.path (по умолчанию /api/v1/users/{user_id}/effective-permissions); подстрока {user_id} заменяется на целое число, полученное из claim id. Плейсхолдер {user_id} обязателен, иначе загрузка конфига завершается ошибкой.
  • {api_key_header}permissions.api_key_header (по умолчанию X-API-Key); заголовок добавляется, только если api_key непустой.
  • Таймаут HTTP-клиента — 5 секунд.

Если method, path и api_key_header не заданы, поведение прежнее: GET {service_url}/api/v1/users/{user_id}/effective-permissions с заголовком X-API-Key. Существующему permission-сервису менять ничего не нужно — достаточно не добавлять новые поля.

Успешный ответ — HTTP 200 с JSON-объектом:

{
  "user_id": 42,
  "permissions": ["orders:read", "orders:write", "reports:view"],
  "inherited_from_role": ["orders:read", "reports:view"],
  "direct_allowed": ["orders:write"],
  "direct_denied": ["admin:all"]
}
ПолеТипОбязательноСмысл
user_idintнетИдентификатор пользователя
permissions[]stringдаИтоговый список эффективных разрешений; единственное поле, которое использует шлюз
inherited_from_role[]stringнетРазрешения, унаследованные от роли
direct_allowed[]stringнетРазрешения, выданные пользователю напрямую
direct_denied[]stringнетЯвно запрещённые разрешения

Шлюз читает только permissions, остальные поля можно не возвращать. Любой не-200 (включая 401/403/404/5xx) считается ошибкой: заголовок не выставляется, запрос продолжается. Тело ответа должно быть валидным JSON.

Кеш и инвалидация

  • Эффективные разрешения кешируются по user_id на cache_ttl (по умолчанию 300s). После прогрева на одного пользователя приходится один вызов сервиса за TTL.
  • Чтобы изменения прав не «залипали» до истечения TTL, permission-сервис (или оператор) вызывает инвалидацию:
POST /_cache/permissions/invalidate
X-Invalidate-Token: {invalidate_token}
  • ?user_id=42 сбрасывает запись одного пользователя; без параметра — весь кеш.
  • Неверный или отсутствующий X-Invalidate-Token — 401; нечисловой user_id — 400.
  • Эндпоинт доступен, только когда модуль включён, и обрабатывается во внешнем слое цепочки — до Basic Auth, поэтому basic_auth его не защищает. Доступ ограничен только X-Invalidate-Token.
  • Модуль стоит на горячем пути, поэтому сервис должен отвечать быстро: кеш сводит нагрузку к одному вызову на пользователя за TTL.

Поля конфигурации

ПолеТипПо умолчаниюСмысл и значенияПример
enabledboolfalseВключает модульtrue
service_urlstringБазовый URL permission-сервиса; обязателен при enabledhttp://permissions:8080
methodstringGETHTTP-метод запроса к permission-сервисуPOST
pathstring/api/v1/users/{user_id}/effective-permissionsШаблон пути; {user_id} заменяется на идентификатор пользователя. Плейсхолдер {user_id} обязателен/v2/users/{user_id}/permissions
api_key_headerstringX-API-KeyЗаголовок, в который кладётся api_key, если он заданAuthorization
cache_ttlduration300sTTL кеша разрешений по пользователю300s
header_namestringX-User-PermissionsЗаголовок с разрешениями (через запятую)X-User-Permissions
invalidate_tokenstringзначение api_keyТокен для инвалидации кеша; если пуст — равен api_key"${INVALIDATE_TOKEN}"
api_keystring""API-ключ сервиса (отправляется в api_key_header); также секрет для headers.sign_header"${PERMISSIONS_KEY}"

Пример конфигурации

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

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

Кастомный эндпоинт — другой метод, путь и заголовок ключа:

permissions:
  enabled: true
  service_url: "http://permissions:8080"
  method: "POST"
  path: "/v2/users/{user_id}/permissions"
  api_key_header: "Authorization"
  api_key: "${PERMISSIONS_KEY}"

webhooks

Публикация событий на каждый запрос или ответ. Транспорты — HTTP webhook и NATS.

ПолеТипПо умолчаниюСмысл и значенияПример
namestringУникальное имя вебхукаaudit
transportstringwebhook или natswebhook
webhook_urlstring""URL приёмника; обязателен для webhookhttps://audit.example/events
nats_urlstring""URL NATS; обязателен для natsnats://nats:4222
subjectstring""NATS subject; обязателен для natsaudit.events
triggerstringon_request или on_responseon_response
methods[]stringвсеФильтр методов (регистр не важен)["POST", "PUT", "PATCH", "DELETE"]
on_status_codes[]intвсеДля on_response: публиковать только эти коды[200, 201]
exclude_paths[]string[]Префиксы путей, исключённые из публикации["/health"]
asyncboolfalseОтправлять в горутине, не блокируя запросtrue
include_request_bodybooltrue, если ключ не заданПубликовать тело запроса как changes (только JSON)false
include_response_bodyboolfalseПубликовать тело ответа как response_body (только JSON, до 64 KiB)true
batch_sizeint0/1>1 включает батчинг HTTP-вебхука1000
flush_intervalduration200msМаксимальная задержка перед отправкой неполной пачки100ms

Структура события

Тело события (одно событие, а также элемент массива events в пачке):

ПолеТипВсегдаСмысл
methodstringдаHTTP-метод запроса
pathstringдаПуть запроса
querystringнетСтрока запроса без ?
user_idstringнетИз заголовка X-User-ID (заполняется маппингом claims)
user_emailstringнетИз заголовка X-User-Email
user_rolesstringнетИз заголовка X-User-Roles (роли через запятую)
request_idstringдаX-Request-ID запроса
status_codeintдля on_responseКод ответа
timestampstring (RFC3339)даВремя события
changesobjectнетТело запроса как JSON (если include_request_body, по умолчанию включено)
response_bodyobjectнетТело ответа как JSON, до 64 KiB (если include_response_body)

Поля с omitempty (всё, кроме method, path, request_id, timestamp) отсутствуют в JSON, если пусты. headers зарезервировано и сейчас не заполняется. changes и response_body попадают в событие, только если тело — валидный непустой JSON-объект (для response_body ещё и Content-Type: application/json).

Пример одного события (HTTP-вебхук без батчинга или сообщение NATS):

{
  "method": "POST",
  "path": "/api/v1/admin/users",
  "user_id": "42",
  "user_email": "admin@example.com",
  "user_roles": "admin",
  "request_id": "9f2c1e7a4b3d5c60",
  "status_code": 201,
  "timestamp": "2026-09-13T12:34:56.789Z",
  "changes": { "email": "new@example.com", "role": "editor" }
}

Батчинг (batch_size > 1) шлёт один POST с телом:

{
  "count": 2,
  "events": [
    { "method": "POST", "path": "/api/v1/admin/users", "request_id": "…", "status_code": 201, "timestamp": "2026-09-13T12:34:56.789Z", "changes": { "role": "editor" } },
    { "method": "GET", "path": "/api/v1/admin/users/42", "request_id": "…", "status_code": 200, "timestamp": "2026-09-13T12:34:56.812Z" }
  ]
}

Очередь батчера ограничена (8192 события); при переполнении постановка блокируется — события не теряются, но запросы замедляются (backpressure). Батчинг применяется только к HTTP-вебхукам; NATS шлёт по одному событию. Соединение NATS устанавливается по nats_url первого вебхука, поэтому у всех NATS-вебхуков URL должен совпадать.

Примеры

Аудит изменений (только мутации, с телом запроса):

webhooks:
  - name: audit-mutations
    transport: webhook
    webhook_url: "https://audit.example/events"
    trigger: on_response
    methods: ["POST", "PUT", "PATCH", "DELETE"]
    include_request_body: true
    batch_size: 1000
    flush_interval: 100ms
    async: true

Аудит чтений и записей по админским путям (тело ответа включено):

webhooks:
  - name: audit-admin
    transport: webhook
    webhook_url: "https://audit.example/admin"
    trigger: on_response
    methods: ["GET", "POST", "PUT", "PATCH", "DELETE"]
    include_response_body: true
    exclude_paths: ["/health", "/metrics"]
    async: true

Публикация в NATS:

webhooks:
  - name: audit-nats
    transport: nats
    nats_url: "nats://nats:4222"
    subject: "audit.events"
    trigger: on_response
    methods: ["POST", "PUT", "PATCH", "DELETE"]
    async: true

discovery

Обнаружение бэкендов по labels контейнеров Docker/Podman. Обнаруженные таргеты и правила дополняют статические; при конфликте имени таргета побеждает статика. Если обнаруженный сервис описывает тот же маршрут (host, path_prefix, methods), он попадает в общий пул с статикой.

ПолеТипПо умолчаниюСмысл и значенияПример
enabledboolfalseВключает discoverytrue
providerstringdockerdocker или podman (один клиент)docker
hoststringunix:///var/run/docker.sockSocket Docker/Podman APIunix:///var/run/docker.sock
api_versionstringv1.41Версия API; "" — запросы без версииv1.41
label_prefixstringgatewayПрефикс labelsgateway
service_name_labels[]stringcompose-сервис Docker и PodmanЦепочка фолбэков имени таргета["com.docker.compose.service"]
networkstring""Учитывать только контейнеры этой сети; пусто — всеproxy
debounceduration500msЗадержка перед ре-синком по событиям500ms
resync_intervalduration5mПериод полного ре-синка5m
default_timeoutduration30sТаймаут обнаруженного таргета по умолчанию30s
state_filestring/var/lib/api-gateway/discovery-state.jsonФайл последнего удачного результата (аварийный фолбэк); "" отключает персист/var/lib/api-gateway/discovery-state.json

Если discovery.enabled: true, статические targets и routing.rules можно не заполнять. При включённом discovery достаточно даже пустой секции discovery: { enabled: true } — остальные поля подставятся.

Аварийный фолбэк. Последний удачный результат discovery атомарно пишется в state_file и применяется при старте, поэтому маршруты переживают рестарт при недоступном Docker/Podman. Каталог state_file должен быть writable (смонтируйте volume). Ограничение: первый холодный старт без файла состояния и с недоступным Docker останется без обнаруженных маршрутов.

Labels

Маркер контейнера — gateway.enable: "true" (принимаются true/1/yes, регистр не важен).

LabelОбязателенСмыслПо умолчанию
gateway.enableдаОпт-ин контейнера
gateway.nameнетИмя таргетаservice_name_labels, иначе имя контейнера
gateway.portдаПортединственный exposed TCP-порт
gateway.schemeнетhttp или httpshttp
gateway.timeoutнетТаймаут запроса к целиdiscovery.default_timeout
gateway.healthнетHealth-путь или полный URL
gateway.weightнетВес таргета (0/отрицательный исключает)1

Роутеры задаются коротко (gateway.<field> → роутер default) или именованно (gateway.router.<id>.<field>): host, path_prefix, methods, strip_path, auth.required, auth.roles, auth.roles_all, auth.strip_token, rate_limit.rps, rate_limit.burst. Роутер без host и path_prefix пропускается.

Поведение и оговорки

  • Порядок middleware. Снаружи внутрь: инвалидация кеша permissions, Basic Auth, recovery, request id, tracing, метрики активных запросов, /metrics, глобальный лимит, статика, CORS-preflight, проксирование. Поэтому Basic Auth защищает и /metrics, и статику.
  • Рейт-лимиты. Per-route лимит — token bucket на IP клиента; IP берётся из последнего непустого значения X-Forwarded-For, иначе из RemoteAddr. Глобальный лимит — один на процесс. Per-route отдаёт 429 без Retry-After; глобальный — 429 с Retry-After: 1. Лимитеры по IP очищаются после часа простоя.
  • Access log. Выключен по умолчанию: каждая запись — это аллокации на горячем пути. Включайте для отладки, а не в проде под нагрузкой.
  • Тело запроса для аудита. Читается только для POST, PUT, PATCH, QUERY и ограничено max_request_body_size; в событие попадает только JSON-объект короче 64 KiB.
  • Метрики. /metrics в формате expvar; при metrics_allowed_ips доступ ограничен по IP.