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

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

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

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

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


opf3aadequfubwpkiu4gci8j9nm.png

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

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


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

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

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

goqvoiroiq0yknutwbn9hrt7abe.jpeg

Наше решение


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

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

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

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

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

7pbu9y7z9ier1gtfhmu5pccawlu.png

(схема из 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, который предоставляет нам:

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


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

u6f6729km0g71ge4prl5ww1kes8.png

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

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

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


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

qrxnipywarywskcoslishyb8xtm.png

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

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

whl8rayewel52cotktzhfcejmbs.png

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


Наш корпоративный стандарт для логирования через .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.

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

© Habrahabr.ru