Простий 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 – раджу почитати всім, кому цікавий цей патерн.

Існуючі варіанти



Як я вже сказав вище, варіантів дуже мало, та й ті з'явилися порівняно недавно. Багатьох можливостей у них поки немає.

Чому PHP і Lumen?

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

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

Обрана термінологія

Мною пропонується наступна архітектура і відповідна їй термінологія. У коді буду дотримуватися цих термінів, щоб не заплутатися:



Сам додаток вирішив назвати 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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.