Наш опыт создания API Gateway

    Некоторые компании, в том числе наш заказчик, развивают продукт через партнерскую сеть. Например, крупные интернет-магазины интегрированы со службой доставки — вы заказываете товар и вскоре получаете трекинговый номер посылки. Другой пример — вместе с авиабилетом вы покупаете страховку или билет на аэроэкспресс.

    Для этого используется один API, который нужно выдать партнерам через API Gateway. Эту задачу мы и решили. В этой статье расскажем подробности.

    Дано: экосистема и API-портал с интерфейсом, где пользователи зарегистрированы, получают информацию и т.п. Нам нужно сделать удобный и надежный API Gateway. В процессе нам нужно было обеспечить

    • регистрацию,
    • контроль подключения к API,
    • мониторинг того, как пользователи используют конечную систему,
    • учёт бизнес-показателей.



    В статье мы расскажем о нашем опыте создания API Gateway, в ходе которого мы решали следующие задачи:

    • аутентификация пользователя,
    • авторизация пользователя,
    • модификация исходного запроса,
    • проксирование запроса,
    • постобработка ответа.


    Есть два вида API-менеджмента:

    1. Стандартный, который работает следующим образом. Перед подключением пользователь тестирует возможности, затем оплачивает и встраивает на свой сайт. Чаще всего им пользуются в малом и среднем бизнесе.

    2. Крупный B2B API Management, когда компания сначала принимает бизнес-решение о подключении, становится партнером компании с контрактным обязательством, после чего подключается к API. И уже после улаживания всех формальностей компания получает тестовый доступ, проходит тестирование и выходит в прод. Но это невозможно без управленческого решения о подключении.



    Наше решение


    В этой части расскажем о создании API Gateway.

    Конечные пользователи создаваемого шлюза к API — партнёры нашего заказчика. Для каждого из них у нас уже хранятся необходимые контракты. Нам нужно будет лишь расширить функциональность, отмечая предоставленный доступ к шлюзу. Соответственно, нужен контролируемый процесс подключения и управления.

    Безусловно, можно было взять какое-нибудь готовое решение для решения задачи API Management и создания API Gateway в частности. Например, таким могло быть Azure API Management. Нам оно не подошло, потому что в нашем случае у нас уже был API-портал и огромная экосистема, построенная вокруг него. Все пользователи уже были зарегистрированы, они уже понимали, где и как они могут получить нужную информацию. В API-портале уже существовали нужные интерфейсы, нам лишь был необходим API Gateway. Собственно, его разработкой мы и занялись.

    То, что мы называем API Gateway — своего рода прокси. Тут у нас опять был выбор — можно написать свой прокси, а можно выбрать уже что-то готовое. В этом случае мы пошли вторым путём и выбрали связку nginx+Lua. Почему? Нам необходим был надежный, протестированный software, поддерживающий масштабирование. Мы не хотели после реализации проверять и корректность бизнес-логики, и корректность работы прокси.

    У любого веб-сервера есть конвейер обработки запроса. В случае nginx он выглядит следующим образом:



    (схема из GitHub Lua Nginx)

    Нашей целью было встроиться в этот конвейер в тот момент, где мы можем модифицировать исходный запрос.

    Мы хотим создать transparent proxy, чтобы функционально запрос оставался таким, каким и пришёл. Мы лишь контролируем доступ к конечному API, помогаем запросу добраться до него. В случае, если запрос был некорректным, ошибку должен показать конечный API, но не мы. Единственная причина, почему мы можем отклонить запрос — из-за отсутствия доступа у клиента.

    Для nginx уже существует расширение на Lua. Lua — скриптовый язык, он очень легковесный и лёгкий в освоении. Таким образом, необходимую логику мы реализовали с помощью Lua.

    Конфигурация nginx’a (аналогия route приложения), где и выполняется вся работа, вполне понятная. Примечательна здесь последняя директива — post_action.

    location /middleware {
          more_clear_input_headers Accept-Encoding;
          lua_need_request_body on;
          rewrite_by_lua_file 'middleware/rewrite.lua';
          access_by_lua_file 'middleware/access.lua';
          proxy_pass https://someurl.com;
          body_filter_by_lua_file 'middleware/body_filter.lua';
          post_action /process_session;
    }
    

    Рассмотрим, что происходит в этой конфигурации:
    more_clear_input_headers — очищает значение указанных после директивы хэдеров.
    lua_need_request_body — регулирует, следует ли прочитать исходное тело запроса перед тем, как выполнять директивы rewrite/access/access_by_lua или нет. По умолчанию nginx не читает тело запроса клиента, и если вам необходимо получить к нему доступ, то данная директива должна иметь значение on.
    rewrite_by_lua_file — путь до скрипты, в котором описана логика для модификации запроса
    access_by_lua_file — путь до скрипта, в котором описана логика, проверяющая наличие доступа к ресурсу.
    proxy_pass — url, на который будет проксироваться запрос.
    body_filter_by_lua_file — путь до скрипта, в котором описана логика для фильтрации запроса перед возвращением клиенту.
    И, наконец, post_action — официально недокументированная директива, с помощью которой можно выполнять ещё какие-либо действия после того, как ответ отдан клиенту.

    Далее расскажем по порядку, как мы решали свои задачи.

    Авторизация/аутентификация и модификация запроса


    Авторизация

    Авторизацию и аутентификацию мы построили с помощью доступов по сертификату. Есть корневой сертификат. Каждому новому клиенту заказчика генерируется его личный сертификат, с которым он может получить доступ к API. Этот сертификат настраивается в секции server настроек nginx.

    ssl on;
    ssl_certificate /usr/local/openresty/nginx/ssl/cert.pem;
    ssl_certificate_key /usr/local/openresty/nginx/ssl/cert.pem;
    ssl_client_certificate /usr/local/openresty/nginx/ssl/ca.crt;
    ssl_verify_client on;

    Модификация

    Может возникнуть справедливый вопрос: что делать с сертифицированным клиентом, если мы внезапно захотели отключить его от системы? Не перевыпускать же сертификаты для всех остальных клиентов.

    Так мы плавно и подошли к следующей задаче — модификация исходного запроса. Исходный запрос клиента, вообще говоря, не является валидным для конечной системы. Одна из задач состоит в том, чтобы дописать в запрос недостающие части, чтобы сделать его валидным. Соль в том, что недостающие данные — разные для каждого клиента. Мы знаем, что клиент приходит к нам с сертификатом, с которого мы можем взять отпечаток и вытянуть из базы необходимые данные клиента.

    Если в какой-то момент потребуется отключить клиента от нашего сервиса, его данные пропадут из базы и он ничего не сможет делать.

    Работа с данными клиента


    Нам нужно было обеспечить высокую доступность решения, особенно того, как мы получаем данные клиента. Сложность в том, что первоисточник этих данных — сторонний сервис, который не гарантирует бесперебойную и достаточно высокую скорость работы.

    Поэтому нам нужно было обеспечить высокую доступность данных клиентов. В качестве инструмента мы выбрали Hazelcast, который предоставляет нам:

    • быстрый доступ к данным,
    • возможность организовать кластер из нескольких нод с реплицированными на разных нодах данными.

    Мы пошли по самой простой стратегии доставки данных в кэш:



    Работа с конечной системой происходит в рамках сессий и есть лимит на максимальное количество. Если же клиент не закрыл сессию, это придётся делать нам.

    Данные об открытой сессии приходят от конечной системы и изначально обрабатываются на стороне Lua. Мы решили использовать Hazelcast для сохранения этих данных джобиком, написанным на .NET. Затем с некоторой периодичностью мы проверяем право на жизнь открытых сессий и закрываем протухшие.

    Доступ к Hazelcast и из Lua, и из .NET


    Клиентов на Lua для работы с Hazelcast нет, но у Hazelcast есть REST API, которым мы и решили воспользоваться. Для .NET же есть клиент, через который мы планировали получать доступ к данным Hazelcast на стороне .NET. Но не тут-то было.



    При сохранении данных через REST и извлечении через .NET клиента используются разные сериализаторы\десериализаторы. Поэтому невозможно положить данные через REST, а достать через .NET client и наоборот.

    Если будут заинтересованные, то подробнее расскажем об этой проблеме в отдельной статье. Спойлер — на схемке.



    Логирование и мониторинг


    Наш корпоративный стандарт для логирования через .NET — Serilog, все логи в итоге попадают в Elasticsearch, их анализ ведём через Kibanа. Нечто подобное хотелось сделать и в этом случае. Единственный клиент для работы с Elastic на Lua, который был найден, сломался на первом же require. И мы использовали Fluentd.

    Fluentd — open source решение для обеспечения единого слоя логирования приложения. Позволяет собирать логи с разных слоёв приложения, а потом транслировать их в единый источник.

    API Gateway работает в K8S, поэтому мы решили добавить контейнер с fluentd в ту же самую подy, чтобы писать логи в имеющийся открытый tcp port fluentd.

    Мы также исследовали, как будет вести себя fluentd, если у него не будет связи с Elasticsearch. В течение двух суток в шлюз непрерывно шли запросы, во fluentd отправлялись логи, но у fluentd был забанен IP Elastic. После восстановления связи fluentd отлично перегнал абсолютно все логи в Elastic.

    Заключение


    Выбранный подход к реализации позволил нам доставить реально работающий продукт в боевую среду всего за 2.5 месяца.

    Если когда-нибудь вам доведётся заниматься подобными вещами, советуем в первую очередь четко понять, какую задачу вы решаете и что из ресурсов у вас уже есть. Будьте внимательны к сложностям интеграции с существующими системами управления API.

    Поймите для себя, что именно вы собираетесь разрабатывать — только бизнес-логику обработки запросов или, как могло быть в нашем случае, прокси целиком. Не забывайте, что всё, что вы сделаете сами, должно быть впоследствии тщательно протестировано.
    True Engineering
    76,00
    Специалисты по цифровой трансформации бизнеса
    Поделиться публикацией

    Комментарии 3

      0

      Я думаю резонный вопрос, почему вы решили реализовать все сами, а не использовать множество из текущий решений вроде Kong или Tyk?

        0
        Плюс к тому есть вполне. NET Core пакет Ocelot. Зачем Lua?
          0

          Мы работаем с SOAP запросами, нам приходится парсить и модифицировать XML запросы, а Kong, согласно этой информации, не умеет с ними работать.


          В сторону Ocelot и Tyk не смотрели, поскольку с нашим набором условий (API-портал с зарегистрированными пользователями и огромная экосистема, построенная вокруг портала) показалось проще и надежнее сделать свой API Gateway. Вы их использовали? Поделитесь опытом?

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.