Представь десяток сервисов: auth, billing, catalog, search, notifications. У каждого свой порт, свой деплой, свои таймауты. Если выпустить их в мир напрямую, клиент должен знать все адреса, ты должен на каждом повесить TLS, rate limit, проверку токена, CORS и логи. Это копипаста, которая разъезжается через неделю. Шлюз решает боль одной фразой: одна точка входа, за которой прячется вся внутренняя топология. Снаружи виден один домен api.example.com, внутри - маршрутизация по пути, хосту и заголовку, единая аутентификация и единые наблюдаемость и лимиты.
Именно в этой роли nginx живёт уже пятнадцать лет, и связка nginx api gateway давно стала индустриальным дефолтом. На нём же построены Kong, APISIX и частично Traefik - то есть когда ты учишь nginx как шлюз, ты заодно понимаешь, что у них под капотом. В этом уроке разберём паттерны API-gateway на чистом nginx и на OpenResty, а потом честно поговорим про Kubernetes: про nginx ingress, про то, что в 2026 с ним случилось, и куда индустрия переезжает.
Ключевая мысль: gateway - это не "ещё один прокси". Это слой политик. Маршрут - политика. Лимит - политика. Проверка JWT - политика. CORS - политика. Чем чище ты отделяешь политики от бэкендов, тем дольше живёт конфиг.

Паттерны API-gateway на nginx: маршрутизация, версии, лимиты
Начнём с маршрутизации - сердца шлюза. nginx умеет разводить трафик по трём осям: по пути (location), по хосту (server_name) и по заголовку (через map плюс переменную в proxy_pass или через if по $http_*). Базовый каркас выглядит так.
Код: Выделить всё
upstream auth_svc { server 10.0.1.10:8080; keepalive 32; }
upstream billing_svc { server 10.0.2.10:8080; keepalive 32; }
upstream catalog_svc { server 10.0.3.10:8080; keepalive 32; }
server {
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
http3 on;
server_name api.example.com;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
# маршрут по пути
location /auth/ { proxy_pass http://auth_svc/; }
location /billing/ { proxy_pass http://billing_svc/; }
location /catalog/ { proxy_pass http://catalog_svc/; }
}
Версионирование API. Две рабочие схемы. Первая - версия в пути, /v1/ и /v2/ как отдельные location на разные апстримы. Прозрачно, кэшируется, видно в логах. Вторая - версия в заголовке Accept или X-API-Version, разруливается через map.
Код: Выделить всё
map $http_x_api_version $catalog_backend {
default catalog_v2;
"1" catalog_v1;
"2" catalog_v2;
}
location /catalog/ {
proxy_pass http://$catalog_backend/;
}
Rate limit на токен, а не только на IP. Лимит по $binary_remote_addr защищает от грубого флуда, но за NAT или CDN тысяча клиентов выглядит одним IP. Профессиональный gateway лимитирует по идентификатору потребителя - по API-ключу или по subject из JWT. В эталонном ngx-trace-gateway это сделано через map в $api_consumer_id и несколько зон:
Код: Выделить всё
map $http_authorization $api_consumer_id { default anon; }
map $http_x_api_key $rl_api_key { default ""; }
limit_req_zone $binary_remote_addr zone=public_api_ip:10m rate=60r/m;
limit_req_zone $api_consumer_id zone=public_api_user:10m rate=120r/m;
limit_req_zone $rl_api_key zone=public_api_key:10m rate=10r/s;
location /catalog/ {
limit_req zone=public_api_ip burst=20 nodelay;
limit_req zone=public_api_key burst=20 delay=10;
proxy_pass http://catalog_svc/;
}
JWT, OAuth2, CORS и трансформация запроса
Шлюз - правильное место для аутентификации: проверил токен один раз на входе, дальше внутренний трафик уже доверенный. На чистом nginx это делается двумя способами.
auth_request. nginx делает внутренний субзапрос к сервису-валидатору; 2xx - пускаем, 401/403 - режем. Бэкендам не нужно знать про токены вообще.
Код: Выделить всё
location = /_authz {
internal;
proxy_pass http://auth_svc/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
location /billing/ {
auth_request /_authz;
auth_request_set $user $upstream_http_x_user;
proxy_set_header X-User $user;
proxy_pass http://billing_svc/;
}
Проверка JWT прямо в gateway. В чистом open-source nginx нативной проверки JWT нет (она есть в NGINX Plus). На OpenResty её делают через Lua. Здесь жёсткое правило безопасности: библиотека lua-resty-jwt имеет историю уязвимостей alg-confusion и alg=none, поэтому НИКОГДА не доверяй заголовку alg из самого токена - явно фиксируй допустимый алгоритм и ключ.
Код: Выделить всё
access_by_lua_block {
local jwt = require "resty.jwt"
local h = ngx.var.http_authorization
if not h then return ngx.exit(401) end
local token = h:gsub("^Bearer%s+", "")
local obj = jwt:verify(SECRET, token, {
valid_issuers = { "https://idp.example.com" }
})
-- критично: проверяем сами, а не верим токену
if not obj.verified or obj.header.alg ~= "RS256" then
ngx.log(ngx.WARN, "jwt rejected: ", obj.reason)
return ngx.exit(401)
end
ngx.req.set_header("X-User", obj.payload.sub)
}
CORS. Браузерный preflight - это OPTIONS-запрос, на который gateway должен ответить сам, не дёргая бэкенд.
Код: Выделить всё
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 600 always;
return 204;
}
Агрегация и трансформация. Если фронту нужен один ответ из нескольких сервисов, на OpenResty это делают через ngx.location.capture_multi (параллельные субзапросы) или через njs. Для njs учти: в 2026 классический JS-движок помечен deprecated, рекомендуемый - QuickJS (js_engine qjs;), у него ES2023, async, Fetch и WebCrypto. Async в njs работает только в js_content и js_access, не в js_set.
Nginx ingress в Kubernetes: что сломалось в 2026
Теперь самое важное и самое запутанное. Когда говорят "nginx kubernetes" или "kubernetes ingress nginx", под одним именем прячутся ТРИ совершенно разных продукта. Их путают постоянно, и в 2026 эта путаница стоит денег и безопасности.
- community ingress-nginx (репозиторий kubernetes/ingress-nginx, тот, что ставят через helm install ingress-nginx) - самый популярный исторически. В 2026 он МЁРТВ.
- F5 NGINX Ingress Controller (NIC, репозиторий nginxinc/kubernetes-ingress) - отдельный продукт от F5, OSS и Plus-редакции. ЖИВ и развивается.
- NGINX Gateway Fabric (NGF) - реализация нового Kubernetes Gateway API на дата-плейне nginx, тоже от F5. ЖИВ, это будущее.
Важная оговорка: сам ресурс Ingress в Kubernetes никуда не делся, спецификация Ingress остаётся в ядре. Умер именно контроллер kubernetes/ingress-nginx. Контроллеры Traefik, HAProxy, Kong и F5 NGINX Ingress Controller продолжают обслуживать Ingress-ресурсы как ни в чём не бывало.
Переезд на Kubernetes Gateway API: ingress2gateway 1.0
Куда переезжать? Индустрия выбрала nginx gateway api как направление, точнее - Kubernetes Gateway API как стандарт на смену Ingress. Gateway API решает то, на чём Ingress буксовал: ролевую модель (инфра-команда владеет Gateway, продуктовые команды - своими HTTPRoute), выразительную маршрутизацию по заголовкам, методам, весам без зоопарка вендорских аннотаций. Ingress конфигурировался десятками annotation-строк, которые у каждого контроллера свои; Gateway API выносит это в типизированные ресурсы Gateway, HTTPRoute, GRPCRoute.
Сравни философию. Ingress:
Код: Выделить всё
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"
Код: Выделить всё
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: catalog
spec:
parentRefs:
- name: api-gateway
rules:
- matches:
- path: { type: PathPrefix, value: /catalog }
backendRefs:
- name: catalog-v1
weight: 80
- name: catalog-v2
weight: 20
Код: Выделить всё
ingress2gateway print --input-file ingress.yaml --providers ingress-nginx > gateway.yaml
Canary и blue-green на gateway без Kubernetes. Если ты на голом nginx, тот же эффект дают split_clients (стабильное разбиение по хешу) и веса в upstream:
Код: Выделить всё
split_clients "$request_id" $catalog_pool {
20% catalog_v2;
* catalog_v1;
}
location /catalog/ { proxy_pass http://$catalog_pool/; }
Типичные грабли
- Забытый слеш в proxy_pass. http://svc и http://svc/ ведут себя по-разному с префиксом location. Симптом - бэкенд получает удвоенный путь и отдаёт 404.
- Доверие к alg из JWT. Если проверяешь токен в Lua и не фиксируешь алгоритм, атакующий подсунет alg=none или RS256->HS256 и подделает подпись. Всегда задавай ожидаемый alg явно.
- Rate limit только по IP за CDN/NAT. $remote_addr там - адрес ближайшего прокси, а не клиента. Нужен модуль realip с set_real_ip_from ТОЛЬКО на доверенные сети, иначе клиент подделает X-Forwarded-For и обойдёт лимит.
- Установка community ingress-nginx в новый кластер в 2026. Это установка софта без патчей безопасности на самый периметр. Не делай так - бери F5 NIC или Gateway API.
- Путаница трёх "nginx ingress". Гуглишь баг, находишь решение - проверь, к какому из трёх продуктов оно относится. Аннотации community-контроллера не работают в F5 NIC и наоборот.
- add_header в if. Заголовки внутри if могут не наследоваться так, как ты ждёшь. Для CORS-preflight это терпимо (там return), но в общем случае держи add_header вне if и используй always.
Соберём минимальный шлюз на OpenResty по шагам. Понадобится Docker.
- Шаг 1. Подними два бэкенда-заглушки: docker run -d --name svc-a -p 8001:80 kennethreitz/httpbin и аналогично svc-b на 8002.
- Шаг 2. Создай gateway.conf с двумя upstream (svc_a -> host.docker.internal:8001, svc_b -> :8002) и server на 8080.
- Шаг 3. Сделай маршруты: location /a/ -> proxy_pass http://svc_a/ и location /b/ -> http://svc_b/. Не забудь слеши.
- Шаг 4. Добавь на /b/ зону limit_req по $binary_remote_addr с rate=2r/s burst=2 nodelay.
- Шаг 5. На /a/ повесь access_by_lua_block, который требует заголовок Authorization и при его отсутствии делает ngx.exit(401). Пока без полной проверки подписи - просто факт наличия.
- Шаг 6. Запусти openresty/openresty:latest, смонтировав конфиг ro: docker run -d -p 8080:8080 -v $PWD/gateway.conf:/etc/nginx/conf.d/default.conf:ro openresty/openresty:latest.
- Шаг 7. Проверь: curl localhost:8080/a/get -> 401; curl -H "Authorization: Bearer x" localhost:8080/a/get -> 200. Пробей /b/ циклом из 10 быстрых запросов - увидишь 503 после лимита.
Контрольные вопросы
- Почему лимитировать только по $remote_addr опасно за CDN, и что нужно настроить, чтобы лимит считался по реальному клиенту?
- Чем отличаются три продукта со словом "nginx ingress" и какой из них в 2026 нельзя ставить в новый прод и почему?
- Зачем при проверке JWT в Lua явно фиксировать алгоритм подписи, и какие атаки это закрывает?
- Как nginx ведёт себя со слешем в proxy_pass при префиксном location, и почему это частый источник 404?
API-gateway на nginx - это слой политик над микросервисами: маршрутизация по пути/хосту/заголовку, версионирование, rate limit по токену, проверка JWT/OAuth2, CORS и трансформация. На чистом nginx хватает auth_request, map и limit_req; на OpenResty добавляются Lua-проверки и агрегация, но с дисциплиной безопасности (фиксируй alg, не храни cosocket на уровне модуля). В Kubernetes 2026 запомни главное: community ingress-nginx ретайрнут и без CVE-патчей, F5 NGINX Ingress Controller и NGINX Gateway Fabric живы, а будущее - Kubernetes Gateway API, на который переезжают инструментом ingress2gateway 1.0. За образец production-структуры держи ngx-trace-gateway: модульные globals, сквозная трассировка и лимиты по consumer-id.