Помню тот момент, когда стандартный 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, этот подход будет жить еще долгие годы.