KrakenD: как мобильная логика расползлась по монолиту, а мы собрали её обратно
- среда, 29 июля 2026 г. в 00:00:22

Всем привет! Меня зовут Рома, я бэкенд‑инженер в Банки.ру. Мы перевели мобильное API на KrakenD, и сейчас через него идёт весь трафик приложения.
Начиналось всё не с выбора шлюза, а с довольно неприятного открытия: логика, обслуживающая мобильное приложение, потихоньку расползлась по монолиту — причём в те его места, где её никто не ждал. Дальше расскажу, как мы туда пришли, что делали, чтобы выбраться, и во что нам обошлось внедрение новой технологии в уже работающую инфраструктуру.
Содержание для быстрой навигации по статье:
1. С чего все начиналось? История одной оптимизации…
2. Что мы искали
3. Выпускайте кракена! Или погодите…
4. Как мы переезжали
5. Lua или плагины
6. Как устроены плагины
7. Примеры наших плагинов
8. Как устроен конфиг и пробросы
9. Что получилось
Кстати, подписывайтесь на наш телегам‑канал по ссылке или ищите: @bankirudev.
Тут на Хабре мы публикуем в основном лонгриды, а там — короткие посты про разработку, команду, мемы и всякое такое.
Мобильное приложение ходило в бэкенд через Spring Gateway. Он выполнял роль универсального входного шлюза: валидировал jwt токен, добавлял нужные заголовки и query‑параметры, делал rewrite путей, приводил параметры к нужному регистру. Схема была понятной и предсказуемой — ровно до момента, когда мобильному API начало требоваться что‑то за пределами стандартного проброса.
А требоваться начало быстро. Изменить структуру ответа, привести тип поля, переименовать параметры, обернуть данные в другой формат. Технически всё это решается на уровне фильтров Spring Gateway. Но шлюз принадлежал платформенной команде, у которой свой бэклог и свои приоритеты. Каждая такая задача — это межкомандное согласование, ожидание слота в спринте и код‑ревью в чужой кодовой базе. На стороне мобильного API цикл выпуска был быстрее, и этот разрыв в скорости постоянно создавал неудобства.
В какой‑то момент мы стали обходить это ограничение: добавляли нужную логику и новые методы прямо в монолит, к которому у команды был полный доступ. Сначала пара вспомогательных обёрток. Потом трансформации ответов. Потом фрагменты бизнес‑логики.
С точки зрения скорости поставки это работало отлично. Фичи ехали в прод без внешних зависимостей и блокеров. Но постепенно стало видно, к чему это ведёт: дублирование, бизнес‑логика в непрофильных местах, растущая связность и размытые границы ответственности. Классический случай, когда локально оптимальные решения дают глобально неоптимальную архитектуру.
Когда мы сели разбирать, что именно накопилось в монолите, картина оказалась показательной. Настоящей бизнес‑логики там было немного. Основная масса кода — это транспортная обвязка: трансформация ответов, переименование полей, приведение типов, конвертация форматов, добавление заголовков. То есть работа, которая не имеет отношения к предметной области, а нужна только для того, чтобы привести данные бэкенда к контракту мобильного приложения. Именно та задача, под которую и проектируются API‑шлюзы. Это определило стратегию: бизнес‑логику возвращаем в сервисы‑владельцы, а всю транспортную адаптацию выносим на шлюз.
Прежде чем что‑то выбирать, мы сформулировали критерии.
Нужен был современный стек, а конкретно Go: с ним нам комфортно работать. Нужна была производительность, потому что это оверхед на каждый запрос без исключения. Нужна была масштабируемость с прицелом на рост в соответствии со стратегией бизнеса. И нужна была возможность расширять функциональность, чтобы не упереться в стену через пару месяцев.
Дальше мы пошли смотреть, кто уже решал такую задачу, и почти сразу наткнулись на подробную статью коллег из МТС на Хабре. Возможно, вы её видели. Они закрывали ту же проблему и, что интереснее, по тем же критериям. Изобретать велосипед в такой ситуации смысла нет.
KrakenD подошёл по всем пунктам: понятная архитектура, официальный докер‑образ, простая конфигурация, система плагинов для расширения. Через пару минут чтения документации у нас уже работал локальный инстанс с парой проброшенных методов. Вот это и был решающий сигнал: инструмент понятный, осваивается легко и сразу даёт результат.
На сайте KrakenD есть график сравнения производительности с Kong и Tyk — по количеству запросов в секунду KrakenD показывает заметно лучшие результаты. Это вендорский бенчмарк, но общая картина совпадает с тем, что мы видели у себя.

Если коротко, KrakenD — высокопроизводительный API‑шлюз с открытым исходным кодом, написанный на Go. Конфигурируется декларативно через JSON (также поддерживает YAML и TOML). Спроектирован как stateless: не хранит состояние между запросами и не требует базы данных. Вся логика шлюза сводится к обработке входящего запроса — агрегации ответов от нескольких бэкендов, трансформации данных, фильтрации полей, авторизации и кэшированию. Там, где встроенных возможностей не хватает, KrakenD расширяется через Go‑плагины и Lua‑скрипты, а для более простых сценариев есть CEL‑выражения и Martian‑модификаторы.
Казалось бы, всё: инструмент есть, локально летает, конфиг настраивается. Бери и пробрасывай методы.
Реальность оказалась чуть скучнее.
Во‑первых, поток задач по мобильному API никуда не делся. Он оставался интенсивным, и команда по инерции продолжала решать всё в монолите, просто потому что так было быстрее. Рефлекс «да проще закатить в монолит живёт дольше, чем любые архитектурные договорённости.»
Во‑вторых, KrakenD был новой технологией не только для нас, но и для всей компании. Просто так притащить его в прод нельзя. Было необходимо сначала сформулировать обоснование и пройти обсуждение с платформенной командой: почему именно это решение, насколько оно масштабируемо, поддерживаемо, безопасно. Потом отдельный цикл проверки информационной безопасности, чтобы добавить образ к себе во внутренний container registry.
Это заняло время, и это нормально. Такова цена внедрения новой технологии в зрелой работающей инфраструктуре.
Когда согласования были пройдены, мы подошли к внедрению максимально осторожно. Нам нужно было сохранить стабильную работу мобильного API, иметь возможность оперативно править конфигурацию прямо на проде и при этом не замораживать фичи ради большого единовременного переезда. Подход «всё или ничего» тут не работал.
План получился такой.
Сначала мы развернули отдельный NGINX‑балансировщик, где по умолчанию весь трафик шёл на старый добрый Spring Gateway. Дальше договорились с платформой, что разработчики нашей команды получат права на редактирование конфига этого NGINX, и прошли мини‑обучение по безопасной правке, чтобы не ронять прод и не дёргать DevOps по каждому переключению.

Вот это дало главное: гибкость и скорость. Пробросили метод в конфиге KrakenD, указали новый
location в NGINX, направили трафик. Что‑то пошло не так, откатились так же быстро.
Стартовали с методов, которые уже ходили в монолит. Так мы одновременно разгружали монолит и проверяли работу KrakenD на кейсах, которые сами хорошо знали. Параллельно убирали эти методы из мобильного бандла монолита.
Довольно скоро выяснилось, что типовых возможностей KrakenD нам не хватает, и мы начали писать свои плагины.
Для добавления произвольной логики KrakenD предлагает два основных механизма: Lua‑скрипты и Go‑плагины.
У Lua возможности довольно ограниченные. Скрипты подходят для простых манипуляций с данными и валидации, но из них нельзя интегрироваться с внешними системами и нельзя использовать внешние библиотеки. Производительность тоже так себе. Единственный весомый плюс: порог входа ниже, и человек без опыта в Go напишет что‑то рабочее быстрее.
Плагины пишутся на Go. Для нашей команды и для компании в целом это плюс, экспертиза уже есть. По утверждению документации, производительность плагина до десяти раз выше, чем у Lua‑скриптов. Внешние библиотеки использовать можно, потому что по сути это обычная программа на Go. Влиять на запрос можно гораздо шире, а интеграции доступны любые: сходить во внешний сервис, в базу, в Redis. Из минусов только высокий порог входа, но в нашем случае это не было проблемой.

Выбор оказался очевидным.
Плагин в KrakenD это стандартный механизм Go plugin: заранее скомпилированная shared library с расширением.so, которую можно подключить в рантайме из другой программы и использовать экспортируемые функции, методы и переменные.
Типов плагинов три, и различаются они местом внедрения в пайплайн и тем, что в этом месте можно сделать.

1. HTTP server plugins относятся к уровню маршрутизатора. Они позволяют сделать что угодно сразу после того, как запрос попал в KrakenD: перехватить его, отменить, вернуть свой ответ, повлиять на финальный результат. Здесь удобно делать, например, кастомный rate limiter с нестандартной логикой, который обрубает трафик прямо на входе, не пропуская дальше по пайплайну.
2. Request/Response modifier plugins легковеснее остальных, поскольку выполняются на уровне proxy и не требуют дополнительных циклов кодирования/декодирования. Они модифицируют данные запросов и ответов: заголовки, query‑параметры, тело. Конкретный набор доступных полей зависит от режима кодирования ответа (output_encoding): в стандартном режиме доступны структурированные данные, в режиме no‑op — raw body, заголовки и статус‑код.
3. HTTP client plugins меняют способ взаимодействия клиента с конкретным бэкендом. Бэкендов может быть несколько, и клиентский плагин позволяет переопределить HTTP‑клиент для любого из них. На итоговый агрегированный ответ повлиять через них нельзя. Важно учитывать, что клиентский плагин заменяет дефолтный HTTP‑клиент KrakenD целиком, поэтому встроенные возможности вроде кэширования бэкенда или телеметрии придётся реализовывать самостоятельно. У нас такой плагин используется, чтобы передавать редирект клиенту эндпоинта: по умолчанию KrakenD ходит по редиректу сам, если бэкенд его вернул, а нам нужно было другое поведение.
Общий процесс создания плагина выглядит так:
Выбрать тип плагина и посмотреть в документации, какой у него должен быть интерфейс.
Написать go‑файл (пакет main), который реализует этот интерфейс и содержит нужную логику.
Скомпилировать в.so с флагом ‑buildmode=plugin.
Подключить плагин в KrakenD.
И сразу про ограничения, потому что на них легко налететь. Версия Go должна быть та же, что в KrakenD. Архитектура тоже. И версии всех общих библиотек, которые используются и в самом KrakenD, и в плагине, должны совпадать. Иначе плагин просто не загрузится.
Плагинов у нас уже несколько десятков. Все они решают разные задачи, но имеют одинаковую структуру, отличается только логика внутри обработчиков.
1. Есть плагин, который принимает JWT‑токен пользователя и валидирует его. Если токен невалиден, плагин возвращает ошибку. Если валиден, то в зависимости от настроек идёт за номером телефона, email или user‑id и кладёт значение туда, куда указано в конфигурации: в тело запроса, в query‑параметр или в заголовок с определённым названием. User‑id при этом достаётся сразу, а за остальным приходится ходить в профиль.
{ "convert-token-to-user-id": { "parameterName": "X-User-Id", "parameterDestination": "header" }, "convert-token-to-phone-number": { "parameterName": "phoneNumber", "parameterDestination": "body" }, "convert-token-to-email": { "parameterName": "email", "parameterDestination": "body" } }
2. Есть плагин для изменения формата даты. Мобилка ждёт дату в одном формате, какой‑то бэкенд возвращает в другом. Указываем поле, исходный формат и целевой:
{ "convert-date-format": { "date_expire": { "from": "2006-01-02 15:04:05", "to": "2006-01-02T15:04:05-07:00" } } }
3. Плагин конвертации пагинации. Мобильное приложение отправляет page и per_page, а бэкенд ожидает limit и offset. Плагин пересчитывает автоматически: page=3, per_page=15 превращается в limit=15, offset=30. Если параметры не переданы, подставляются значения по умолчанию. Конфигурации не требует — достаточно подключить:
{ "pagination-replacer": {} }
4. Плагин нормализации телефона. Мобильное приложение может отправить номер в любом формате: +79123456789, 89123456789, 79123456789. Бэкенд при этом ждёт только 10 цифр без префикса. Плагин убирает +7, 7 или 8 в начале и работает как с query‑параметрами, так и с полями в теле запроса:
{ "format-phone": { "source": "query", "path": "phone" } }
5. Плагин фильтрации кук. Работает на уровне HTTP server и фильтрует куки как в запросах, так и в ответах. Поддерживает whitelist и blacklist: можно либо явно перечислить разрешённые куки, либо указать те, которые нужно отсечь. Фильтрация запросных кук при этом применяется только к мобильным клиентам, определяемым по User‑Agent:
{ "cookie-filter": { "whitelist": ["SID", "DEVICE_TOKEN"] } }
Возможности плагинов широкие, но не безграничные. Внедриться можно только в те точки пайплайна, которые обозначены на схеме выше. Изменить поведение KrakenD за их пределами не получится. Например, нельзя вмешаться в логику мержа ответов от нескольких бэкендов: плагины работают до или после агрегации, но не во время неё. Написать свою стратегию мержа или условно включить ответ одного бэкенда в зависимости от результата другого через плагин не выйдет.
Основной файл конфигурации у нас krakend‑template.tmpl, потому что KrakenD поддерживает Flexible Configuration — шаблонизацию конфига на основе Go templates. Мы этим воспользовались и разложили каждый проброс метода в отдельный JSON‑файл, а файлы сгруппировали по папкам: сервис → метод. Чтобы добавить проброс, достаточно положить файл и добавить строчку в шаблон.
config/partials/ ├── catalog/ ├── currency/ ├── notifications/ ├── user_profile/ ├── offers/ ├── subscriptions/ └── ... (49 сервисов, 300+ файлов)
Звучит как мелочь, но когда пробросов становится несколько сотен, это очень удобный способ быстро находить, где какой метод живёт.
Плагины лежат в том же репозитории, в отдельной директории src/plugins/. Все они — часть одного Go‑модуля, что позволяет переиспользовать общий код: обёртки над запросами и ответами, JWT‑менеджер, клиенты к внутренним сервисам.
Сборка происходит при каждом билде Docker‑образа. В Dockerfile используется многоступенчатая сборка: на первом этапе официальный образ krakend/builder компилирует все плагины в цикле, на втором — из полученных.so‑файлов, конфига и бинарника KrakenD собирается финальный образ на базе Alpine.
Дальше разберу три проброса разной сложности.
Таких примерно две трети от всех. KrakenD выступает единой точкой входа для мобильного приложения. Указываем эндпоинт, метод, заголовки и query‑параметры, которые надо пробрасывать.
Тут есть особенность, на которую легко напороться: если не указать заголовки и query‑параметры явно в input_headers, KrakenD не пробросит ни одного. Это security by design — шлюз по умолчанию не доверяет клиенту.
Дальше указываем бэкенд: хост сервиса и URL, который дёргаем. Включаем return_error_code, чтобы KrakenD возвращал реальный код ошибки бэкенда, а не подменял его своим. Подключаем плагин convert‑token‑to‑user‑id и трансформацию регистра: запрос приводим к camelCase для бэкенда, ответ — обратно в snake_case для мобилки.
{ "endpoint": "/api/1.0/catalog/products/", "output_encoding": "json", "method": "GET", "input_query_strings": ["*"], "input_headers": [ "Authorization", "Cookie", "X-Request-Source" ], "backend": [ { "url_pattern": "/api/v1/products", "host": ["${CATALOG_SERVICE_URL}"], "method": "GET", "extra_config": { "backend/http": { "return_error_code": true }, "plugin/req-resp-modifier": { "name": [ "convert-token-to-user-id", "to-camel-case-transformer-request", "to-snake-case-transformer-response" ], "convert-token-to-user-id": { "parameterName": "user-id", "parameterDestination": "header" } } } } ] }
Всё. Кладём файл в папку сервиса, добавляем строчку в krakend‑template.tmpl, запускаем — работает.
KrakenD умеет не только параллельно дёргать несколько бэкендов, но и выстраивать их в цепочку, когда результат первого запроса нужен для второго. Это делается через sequential proxy.
В этом примере первый запрос создаёт фильтр и возвращает его id. Второй запрос использует этот id, чтобы получить агрегированные данные. Обратите внимание на {resp0_id} в URL второго бэкенда — это ссылка на поле id из ответа первого.
{ "endpoint": "/api/1.0/offers/filtered/", "method": "POST", "input_headers": [ "Authorization", "Content-Type", "X-Request-Source" ], "output_encoding": "json", "extra_config": { "proxy": { "sequential": true } }, "backend": [ { "url_pattern": "/api/v1/filters/", "host": ["${OFFERS_SERVICE_URL}"], "method": "POST", "extra_config": { "backend/http": { "return_error_code": true } } }, { "url_pattern": "/api/v1/offers?filter_id={resp0_id}", "host": ["${OFFERS_SERVICE_URL}"], "method": "GET", "extra_config": { "backend/http": { "return_error_code": true }, "plugin/req-resp-modifier": { "name": ["to-snake-case-transformer-response"] } } } ] }
Без sequential proxy пришлось бы либо делать два отдельных вызова на стороне мобильного приложения, либо писать BFF‑обёртку. Здесь всё решается конфигурацией.
Один из наших методов собирает данные из двух внешних сервисов параллельно: статус подписки из одного и уровень доступа пользователя из другого. KrakenD объединяет ответы в один JSON автоматически.
В этом примере сразу несколько приёмов. Через allow и mapping мы фильтруем лишние поля и переименовываем оставшиеся в формат мобильного приложения. Плагин convert‑date‑format приводит даты к нужному формату, а modify‑date сдвигает дату окончания подписки на час назад. Отдельного внимания заслуживает response‑status‑by‑condition: если один из бэкендов вернул 404, плагин проставляет этот код в итоговый ответ, а не глотает его молча.
{ "endpoint": "/api/1.0/subscription/details/", "output_encoding": "json", "method": "GET", "input_query_strings": ["*"], "input_headers": ["Authorization"], "extra_config": { "plugin/req-resp-modifier": { "name": [ "convert-token-to-user-id", "convert-token-to-phone-number", "convert-date-format", "modify-date", "response-status-by-condition" ], "convert-token-to-user-id": { "parameterName": "X-User-Id", "parameterDestination": "header" }, "convert-token-to-phone-number": { "parameterName": "phone", "parameterDestination": "query" }, "convert-date-format": { "created_at": { "from": "2006-01-02T15:04:05.999999Z07:00", "to": "2006-01-02T15:04:05-07:00" }, "expires_at": { "from": "2006-01-02T15:04:05.999999Z07:00", "to": "2006-01-02T15:04:05-07:00" } }, "modify-date": { "expires_at": { "format": "2006-01-02T15:04:05-07:00", "diff": "-1h" } }, "response-status-by-condition": { "trigger": "error_subscription_status", "condition": "\"http_status_code\":404", "response_status": "404" } } }, "backend": [ { "url_pattern": "/api/v1/subscriptions/status", "host": ["${SUBSCRIPTION_SERVICE_URL}"], "method": "GET", "allow": ["createdAt", "expiresAt", "planType", "isActive"], "mapping": { "createdAt": "created_at", "expiresAt": "expires_at", "planType": "plan_type", "isActive": "is_active" }, "extra_config": { "backend/http": { "return_error_details": "subscription_status" } } }, { "url_pattern": "/api/v1/users/access-level", "host": ["${USER_PROFILE_URL}"], "method": "GET", "allow": ["accessLevel"], "mapping": { "accessLevel": "access_level" }, "extra_config": { "backend/http": { "return_error_details": "user_access_level" } } } ] }
Здесь видно, как в одном пробросе сочетаются штатные средства KrakenD (allow, mapping) и наши плагины (конвертация токена, дат, условный статус‑код). Каждый инструмент делает своё, и конфиг остаётся читаемым.
По сравнению с точкой старта мы продвинулись далеко.
Spring Gateway погашен, весь трафик мобильного приложения идёт через KrakenD. Теперь это единая точка входа, которую конфигурирует и поддерживает наша команда, без зависимости от платформы.
Транспортная обвязка, которая раньше оседала в монолите — трансформации, маппинг полей, конвертация форматов — переехала в конфиги и плагины шлюза. Бизнес‑логика вернулась в сервисы‑владельцы.
Типовой проброс нового метода — один JSON‑файл и строчка в шаблоне. Большинство задач собирается из уже существующих плагинов, писать новый код не нужно. Саппорт делает типовые пробросы самостоятельно, без привлечения разработчиков.
Спасибо, что дочитали. Буду рад пообщаться в комментариях, особенно если у вас есть свой опыт с KrakenD или другими шлюзами.