Помню тот момент, когда стандартный Nginx начал трещать по швам под нагрузкой от сложных API. Клиенты требовали динамической маскировки данных в ответах, авторизации по JWT с проверкой в Redis, распределенных лимитов по пользователям и тарифам. Переписывать каждый бэкенд? Нет уж. Тогда я нырнул в OpenResty и его ngx_lua модуль. Это решение перевернуло мое понимание обратного прокси. Теперь прокси не просто перенаправляет трафик, он думает, фильтрует, защищает и адаптируется на лету. И все это без единого релоада конфига.
OpenResty это не просто Nginx с LuaJIT внутри. Это полноценный бандл, где lua-nginx-module позволяет внедрять код в разные фазы обработки запроса. Init_by_lua для загрузки библиотек при старте, rewrite_by_lua для изменения URI, access_by_lua для контроля доступа, content_by_lua для генерации ответа, header_filter_by_lua и body_filter_by_lua для модификации заголовков и тела, log_by_lua для пост-обработки. Каждая фаза дает точный контроль, а неблокирующий I/O означает, что даже запросы в Redis или MySQL не тормозят воркеры.
Я прошел путь от простых экспериментов до продакшн-кластеров на сотни тысяч RPS. И сейчас, в 2025 году, это все еще один из самых мощных инструментов для API Gateway без лишних зависимостей.
Фазы обработки и почему они меняют все
Понимание фаз это основа. Запрос проходит этапы, и Lua может вмешаться почти везде. Init_worker_by_lua_block идеально для создания глобальных объектов лимитеров из lua-resty-limit-traffic. Access_by_lua_block это место для сложной авторизации, где я проверяю токены, роли из Redis, черные списки IP. Body_filter_by_lua_block стал моим любимцем для динамической фильтрации контента, когда нужно менять JSON или HTML на лету.
Важный нюанс: никогда не смешивайте content_by_lua_block с proxy_pass в одном location. Nginx просто не знает, что делать. Выбирайте либо генерацию контента Lua, либо проксирование. А для потоковой фильтрации ответов всегда сбрасывайте Content-Length в header_filter_by_lua_block, иначе перейдете на chunked encoding и клиенты могут сломаться.
Динамическая фильтрация контента: от маскировки до полноценного WAF
Сколько раз возникала задача скрыть чувствительные данные в ответе от legacy-бэкенда? Email, номера карт, токены. Стандартный sub_filter слишком примитивный. А body_filter_by_lua_block позволяет работать с чанками потока.
Вот реальный пример, который я использую для маскировки email и карт:
location /sensitive {
proxy_pass http://legacy_backend;
header_filter_by_lua_block {
ngx.header.content_length = nil
}
body_filter_by_lua_block {
local chunk, eof = ngx.arg[1], ngx.arg[2]
if chunk then
local cjson = require "cjson.safe"
local ctx = ngx.ctx.buffer or ""
ctx = ctx .. (chunk or "")
ngx.ctx.buffer = ctx
if eof then
local data = cjson.decode(ctx)
if data then
if data.email then
data.email = data.email:gsub("(@.)", "@***.")
end
if data.ccn then
data.ccn = "****-****-****-" .. data.ccn:sub(-4)
end
ngx.arg[1] = cjson.encode(data)
end
else
ngx.arg[1] = nil # накапливаем
end
end
}
}
Для больших тел лучше избегать полной буферизации. Тогда использую string.gsub по паттернам или lua-resty-waf, который реализует правила ModSecurity на чистом Lua и работает в десятки раз быстрее оригинала. Я ставил его на продакшн, где он ловит XSS и SQL-инъекции прямо в прокси, не пропуская ничего к бэкенду.
Еще один трюк: инъекция скриптов или заголовков безопасности. Например, добавление CSP или HSTS на лету для старых приложений.
Продвинутый контроль доступа: JWT, OAuth2, внешние источники
Забудьте про базовую auth_basic. С lua-resty-jwt и lua-resty-openidc я вынес всю авторизацию в шлюз.
Полный пример с JWT, проверкой подписи и пробросом данных:
access_by_lua_block {
local jwt = require "resty.jwt"
local redis = require "resty.redis"
local auth_header = ngx.req.get_headers()["Authorization"]
if not auth_header or not auth_header:match("^Bearer%s+(.+)$") then
ngx.status = 401
ngx.say("{\"error\": \"missing token\"}")
return ngx.exit(401)
end
local token = auth_header:match("^Bearer%s+(.+)$")
local jwt_obj = jwt:verify("HMAC_SECRET_OR_RSA_PUBLIC", token, { })
if not jwt_obj.valid then
ngx.status = 401
ngx.say("{\"error\": \"invalid token\"}")
return ngx.exit(401)
end
local red = redis:new()
red:set_timeout(1000)
red:connect("127.0.0.1", 6379)
local revoked = red:get("revoked:" .. jwt_obj.payload.jti)
if revoked == "1" then
ngx.status = 401
ngx.say("{\"error\": \"token revoked\"}")
return ngx.exit(401)
end
ngx.req.set_header("X-User-ID", jwt_obj.payload.sub)
ngx.req.set_header("X-Roles", table.concat(jwt_obj.payload.roles or {}, ","))
}
Для OAuth2 беру lua-resty-openidc, она сама делает discovery, introspection и даже refresh токенов. А если нужны кастомные схемы по API-ключам с проверкой в PostgreSQL или внешним API, подключаю lua-resty-mysql или cosocket для HTTP-запросов.
Распределенный rate limiting: от leaky bucket до адаптивного throttling
Встроенный limit_req_zone хорош, но статичен и локален. Lua-resty-limit-traffic это другой уровень.
Ключевые модули из экосистемы:
-
resty.limit.req (leaky bucket)
-
resty.limit.conn (конкурентные соединения)
-
resty.limit.count (fixed window)
-
resty.limit.traffic (комбинирует все)
Пример tiered-лимитов по пользователю из Redis:
lua_shared_dict local_cache 50m;
access_by_lua_block {
local limit_traffic = require "resty.limit.traffic"
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local user_key = ngx.var.http_x_api_key or ngx.var.remote_addr
local tier = red:hget("user:"..user_key, "tier") or "free"
local rates = { free = 10, pro = 200, enterprise = 1000 }
local rate = rates[tier] or 10
local burst = rate * 2
local lim_req = require "resty.limit.req"
local lim1 = lim_req.new("local_cache", rate, burst)
local keys = { user_key }
local limits = { lim1 }
local delay, err = limit_traffic.combined(limits, keys, true)
if not delay then
if err == "rejected" then
ngx.status = 429
ngx.say("{\"error\": \"rate limit exceeded\"}")
return ngx.exit(429)
end
end
if delay > 0.001 then
ngx.sleep(delay)
end
}
Для полностью распределенного сценария заменяете shared_dict на Redis-бэкенд (есть готовые форки lua-resty-limit-req-redis). А если хотите адаптивный лимит, который сам снижает RPS при росте CPU, смотрите в сторону OpenResty XRay и их приватной библиотеки lua-resty-limit-traffic-dynamic. Она собирает метрики в log_by_lua и пересчитывает burst каждые секунды.
Готовые решения на базе OpenResty: Kong, APISIX и когда их использовать
Я пробовал все. Kong удобен плагинами, но тяжеловат. Apache APISIX быстрее, использует etcd и позволяет горячую замену правил. Но если нужна максимальная производительность и полный контроль, пишу сам на OpenResty. В высоконагруженных проектах разница в задержках ощутима.
Производительность и подводные камни, которые я познал на практике
LuaJIT дает почти нативную скорость, но избегайте блокирующих операций. Только resty-модули, никаких io.open или os.execute. Lua_code_cache on в продакшн обязательно, иначе каждый запрос компилирует скрипты заново. Shared_dict для кэша между воркерами, Redis для распределения. Тестируйте wrk или hey на реальных конфигах, потому что body_filter при полной буферизации может съесть память на больших файлах.
А еще мониторьте: в log_by_lua_block отправляйте метрики в Prometheus или StatsD. Сколько запросов отклонено, сколько отфильтровано, средний delay.
Итог: почему я до сих пор выбираю OpenResty
За годы работы я понял одну вещь. Когда задача выходит за рамки простого проксирования, Lua превращает Nginx в универсальный шлюз, который решает 90 процентов проблем API Gateway без лишних сервисов. Гибкость огромная, производительность бешеная, сообщество живое.
Если вы только начинаете, поставьте OpenResty, попробуйте простой access_by_lua с JWT. Потом добавьте rate limiting из lua-resty-limit-traffic. Дальше фильтрацию тела. Шаг за шагом поймете, как один сервер может заменить кучу микросервисов.
Для меня OpenResty это не инструмент, это философия: делай максимум на краю сети, оставляй бэкенды чистыми. И пока есть LuaJIT, этот подход будет жить еще долгие годы.