Некоторые компании, в том числе наш заказчик, развивают продукт через партнерскую сеть. Например, крупные интернет-магазины интегрированы со службой доставки — вы заказываете товар и вскоре получаете трекинговый номер посылки. Другой пример — вместе с авиабилетом вы покупаете страховку или билет на аэроэкспресс.
Для этого используется один API, который нужно выдать партнерам через API Gateway. Эту задачу мы и решили. В этой статье расскажем подробности.
Дано: экосистема и API-портал с интерфейсом, где пользователи зарегистрированы, получают информацию и т.п. Нам нужно сделать удобный и надежный API Gateway. В процессе нам нужно было обеспечить
В статье мы расскажем о нашем опыте создания 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.
Рассмотрим, что происходит в этой конфигурации:
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.
Модификация
Может возникнуть справедливый вопрос: что делать с сертифицированным клиентом, если мы внезапно захотели отключить его от системы? Не перевыпускать же сертификаты для всех остальных клиентов.
Так мы плавно и подошли к следующей задаче — модификация исходного запроса. Исходный запрос клиента, вообще говоря, не является валидным для конечной системы. Одна из задач состоит в том, чтобы дописать в запрос недостающие части, чтобы сделать его валидным. Соль в том, что недостающие данные — разные для каждого клиента. Мы знаем, что клиент приходит к нам с сертификатом, с которого мы можем взять отпечаток и вытянуть из базы необходимые данные клиента.
Если в какой-то момент потребуется отключить клиента от нашего сервиса, его данные пропадут из базы и он ничего не сможет делать.
Нам нужно было обеспечить высокую доступность решения, особенно того, как мы получаем данные клиента. Сложность в том, что первоисточник этих данных — сторонний сервис, который не гарантирует бесперебойную и достаточно высокую скорость работы.
Поэтому нам нужно было обеспечить высокую доступность данных клиентов. В качестве инструмента мы выбрали Hazelcast, который предоставляет нам:
Мы пошли по самой простой стратегии доставки данных в кэш:
Работа с конечной системой происходит в рамках сессий и есть лимит на максимальное количество. Если же клиент не закрыл сессию, это придётся делать нам.
Данные об открытой сессии приходят от конечной системы и изначально обрабатываются на стороне Lua. Мы решили использовать Hazelcast для сохранения этих данных джобиком, написанным на .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.
Поймите для себя, что именно вы собираетесь разрабатывать — только бизнес-логику обработки запросов или, как могло быть в нашем случае, прокси целиком. Не забывайте, что всё, что вы сделаете сами, должно быть впоследствии тщательно протестировано.
Для этого используется один 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.
Поймите для себя, что именно вы собираетесь разрабатывать — только бизнес-логику обработки запросов или, как могло быть в нашем случае, прокси целиком. Не забывайте, что всё, что вы сделаете сами, должно быть впоследствии тщательно протестировано.