Простой API gateway на базе PHP и Lumen

Термин «микросервисы» сегодня у всех на слуху — внезапно это стало очень модно, и многие компании объявляют переход на этот архитектурный паттерн даже толком не разобравшись в нём. Впрочем, обсуждение полезности микросервисов оставим за пределами этой статьи.

Традиционно перед коллекцией микросервисов предлагается дополнительный слой — так называемый API gateway, который решает сразу несколько проблем (они будут перечислены позже). На момент написания этой статьи open source реализаций таких gateway почти нет, поэтому я решил написать свой на PHP с использованием микрофреймворка Lumen (часть Laravel).

В этой статье я покажу насколько это простая задача для современного PHP!

Что такое API gateway?

Если говорить совсем коротко, то API gateway — это умный proxy-сервер между пользователями и любым количеством сервисов (API), отсюда и название.

Необходимость в этом слое появляется сразу же при переходе на паттерн микросервисов:

  • Единый адрес намного удобнее сотни (у Netflix их более 600) индивидуальных адресов API;
  • Логично проверять данные пользователя (token) в едином месте, на «входе»;
  • Удобно реализовывать ограничения на количество запросов в едином месте;
  • Вся система становится более гибкой — можно менять внутреннюю структуру хоть каждый день. Поддержка старых версий API становится тривиальным делом;
  • Можно кешировать или мутировать ответы;
  • Для удобства пользователя (или разработчиков front end) можно объединять ответы от разных сервисов. Facebook давно предлагает такую возможность.

Преимуществ больше — это просто те, что пришли на ум за 10–20 секунд.

Nginx выпустили неплохую бесплатную электронную книгу посвященную микросервисам и API gateway — советую почитать всем, кому интересен этот паттерн.

Существующие варианты

  • API Umbrella, Lua;
  • Kong, Lua;
  • AWS API Gateway — платный сервис от Amazon.

Как я уже сказал выше, вариантов очень мало, да и те появились сравнительно недавно. Многих возможностей в них пока нет.

Почему PHP и Lumen?

С выходом версии 7 PHP стал высокопроизводительным, а с появлением фреймфорков вроде Laravel и Symfony — PHP доказал миру, что может быть красивым и функциональным. Lumen, являясь «очищенной» быстрой версией Laravel здесь идеально подходит, ведь нам не нужны будут сессии, шаблоны и прочие возможности full stack приложений.

Кроме того, у меня просто больше опыта с PHP и Lumen, а разворачивая полученное приложение через Docker — будущим пользователям вообще будет не важен язык, на котором оно написано. Это просто слой, который выполняет свою роль!

Выбранная терминология

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

48aca9ee6ab5482bb0cd359a8a2647f9.jpg

Само приложение решил назвать Vrata, потому что «врата» на русском это почти «gateway», а ещё миру не хватает приложений с русскими названиями ;-)

Непосредственно за «вратами» находится количество N микросервисов — API сервисов, способных отвечать на web-запросы. У каждого сервиса может быть любое количество экземпляров, поэтому API gateway будет выбирать конкретный экземпляр через так называемый реестр сервисов.

Каждый сервис предлагает какое-то количество ресурсов (на языке REST), а у каждого ресурса может быть несколько возможных действий. Достаточно простая и логичная структура для любого опытного в REST программиста.

Требования к Vrata

Ещё не приступив к коду, можно сразу определить некоторые требования к будущему приложению:

  • Шлюз должен масштабироваться горизонтально, потому что на дворе 2016 год и все хотят масштабировать горизонтально. Следовательно — никакого состояния приложения не должно быть;
  • Шлюз должен уметь объединять запросы и вызывать микросервисы асинхронно;
  • Шлюз должен уметь ограничивать количество запросов в промежуток времени;
  • Шлюз должен уметь проверять достоверность токена аутентификации. Традиционно предлагается, что API gateway выполняет аутентификацию, а скрытые под ним микросервисы выполняют авторизацию на свои ресурсы;
  • Шлюз должен уметь автоматически импортировать доступные ресурсы с микросервисов. Для начала выберем формат Swagger, как самый популярный в мире на сегодня;
  • Шлюз должен уметь менять (мутировать) ответы микросервисов;
  • И напоследок: шлюз должен прекрасно запускаться напрямую из образа Docker и конфигурироваться через переменные окружения. Мы не хотим никаких дополнительных репозиториев, скриптов деплоя и так далее!

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

Реализация

Аутентификация

В этом направлении почти не пришлось работать — достаточно было поправить Laravel Passport под Lumen и мы получили поддержку всех современных OAuth2 фич, включая JWT. Мой маленький пакет-порт опубликован на GitHub/Packagist и кто-то его уже устанавливает.

Маршруты и контроллер

Все низлежащие маршруты с микросервисов импортируются в Vrata из конфигурационного файла в формате JSON. В момент запуска в service provider происходит добавление этих маршрутов:

// Получаем синглетный класс – база данных всех маршрутов
$registry = $this->app->make(RouteRegistry::class);

// Передаем наш Lumen контейнер этой базе, чтобы она могла зарегистрировать маршруты
$registry->bind(app());

А тем временем в базе маршрутов:

    /**
     * @param Application $app
     */
    public function bind(Application $app)
    {
        // Очень просто - маршрут за маршрутом добавляем в Lumen
        // Все запросы пойдут в один и тот же служебный контроллер
        // Добавляем middleware для аутентификации OAuth2, а также своего дополнительного помощника
        $this->getRoutes()->each(function ($route) use ($app) {
            $method = strtolower($route->getMethod());
            $app->{$method}($route->getPath(), [
                'uses' => 'App\Http\Controllers\GatewayController@' . $method,
                'middleware' => [ 'auth', 'helper:' . $route->getId() ]
            ]);
        });
    }

Теперь каждому публичному (и разрешенному в конфигах) маршруту с микросервисов соответствует маршрут на API gateway. Кроме того, добавлены также синтетические или объединенные запросы, которые существуют только на этом шлюзе. Все запросы уходят в один и тот же контроллер:

Вот так контроллер обрабатывает любой GET-запрос:

    /**
     * @param Request $request
     * @param RestClient $client
     * @return Response
     */
    public function get(Request $request, RestClient $client)
    {
        // Это наша баночка с параметрами, подробнее - позже
        $parametersJar = $request->getRouteParams();

        // Соберем финальный ответ из N ответов микросервисов
        $output = $this->actions->reduce(function($carry, $batch) use (&$parametersJar, $client) {
            // Соберем N ответов полученных асинхронно
            $responses = $client->asyncRequest($batch, $parametersJar);

            // Добавим необходимые новые параметры в баночку параметров
            $parametersJar = array_merge($parametersJar, $responses->exportParameters());

            // Склеим с текущим состоянием - делаем array reduce
            return array_merge($carry, $responses->getResponses()->toArray());
        }, []);

        // Отдаем ответ классу форматирования. Сейчас это только JSON
        return $this->presenter->format($this->rearrangeKeys($output), 200);
    }

В качестве HTTP-клиента выбран Guzzle, который прекрасно справляется с async-запросами, а также имеет готовые средства для integration-тестирования.

Составные запросы

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

// Boolean-флаг, обозначающий сложный маршрут
'aggregate' => true,
'method' => 'GET',
// Любой путь на наш вкус, параметры из него сразу попадут в "jar"
'path' => '/v2/devices/{mac}/extended',
// Массив с низлежащими маршрутами
'actions' => [
    'device' => [
        // Имя микросервиса из реестра сервисов
        'service' => 'core',
        'method' => 'GET',
        'path' => 'devices/{mac}',
        // Компоненты с одинаковым порядком будут запущены параллельно
        'sequence' => 0,
        // Если в составе есть критичные компоненты и они недоступны - весь маршрут недоступен
        'critical' => true
    ],
    'ping' => [
        'service' => 'history',
        // Вывод никак не участвует в нашем финальном ответе
        'output_key' => false,
        'method' => 'POST',
        'path' => 'ping/{mac}',
        'sequence' => 0,
        'critical' => false
    ],
    'settings' => [
        'service' => 'core',
        // Вставляем вывод под альтернативным JSON-ключом 
        'output_key' => 'network.settings',
        'method' => 'GET',
        // Используем параметр, добытый ранее в пункте 'device'
        'path' => 'networks/{device%network_id}',
        'sequence' => 1,
        'critical' => false
    ]
]

Как видим, сложные маршруты уже доступны и обладают неплохим набором фич — можно выделать критически важные из них, можно делать параллельные запросы, можно использовать ответ одного сервиса в запросе к другому и так далее. Помимо всего прочего, на выходе прекрасная производительность — всего 56 миллисекунд на получение суммарного ответа (загрузка Lumen и три фоновых запроса, все микросервисы с базами данных).

Реестр сервисов

Это пока самая слабая часть — реализован только один очень простой метод: DNS. Несмотря на всю его примитивность, он отлично работает в среде вроде Docker Cloud или AWS, где сам провайдер наблюдает за группой сервисов и динамически редактирует DNS-запись.

В настоящий момент Vrata просто берет hostname сервиса, не вникая — облако это или один физический компьютер. Самым популярным реестром на сегодня, пожалуй, является Consul, и именно его стоит добавить следующим.

Суть работы реестра очень проста — надо хранить таблицу живых и мертвых экземпляров сервиса, выдавая адреса конкретных экземпляров когда надо. AWS и Docker Cloud (и многие другие) умеют это делать за вас, предоставляя вам один «волшебный» hostname, который всегда работает.

Образ Docker

Говоря о микросервисах просто нельзя не упомянуть Docker — одну из самых «горячих» технологий последних пары лет. Микросервисы, как правило, тестируются и деплоятся именно как образы Docker — это стало стандартной практикой, поэтому мы быстро подготовили публичный образ в Docker Hub.

Одна команда, введённая в терминале любой OS X, Windows или Linux машины, и у вас работает мой шлюз Vrata:

$ docker run -d -e GATEWAY_SERVICES=... -e GATEWAY_GLOBAL=... -e GATEWAY_ROUTES=... pwred/vrata

Всю конфигурацию можно передать в переменных окружения в формате JSON.

Послесловие

Приложение (шлюз) уже используется на практике в компании, где я работаю. Весь код в репозитории на GitHub. Если кто-либо хочет поучаствовать в разработке — милости просим :)

Так как составные запросы как по идее, так и по реализации очень напоминают продвигаемый Facebook формат запросов GraphQL (в противовес REST), то одна из приоритетных будущих фич — поддержка GraphQL-запросов.

Комментарии (0)

© Habrahabr.ru