Какво, защо и как на API шлюзовете
API шлюзът е компонент, който насочва трафика към бекенда и отделя клиентите от API договорите. Той капсулира сложна архитектура на приложение, като го обединява със сплотен API интерфейс. Освен капсулирането и обратното проксииране, те могат също така да разтоварят междусекторните проблеми от отделните услуги, като удостоверяване, ограничаване на скоростта и регистриране на заявки.
Криза на идентичността
През последните години се появиха много инструменти за управление и обработка на заявки. Както при системите за данни, където хранилищата на данни се използват като опашки за съобщения и опашките за съобщения имат гаранции за издръжливост, подобни на бази данни, границите между проксита, мрежи и шлюзове се размиват.
За да се избегне объркването, тази статия поема следните основни отговорности за всяко решение:
- Сервизна мрежа. Специализирана мрежова инфраструктура, която се наслоява върху вашите услуги, разтоварвайки комуникационните функции между услугите, като криптиране, наблюдаемост и механизми за устойчивост.
- API шлюз. Компонент, който осигурява сплотена абстракция в архитектурата на приложението, докато разтоварва граничните функции от името на отделни услуги.
Сега ще разгледаме „защо“ на API Gateways и ще разгледаме пример за код по-късно.
Разработката е поддръжка
Добре известно е, че по-голямата част от цената на софтуера е в неговата текуща поддръжка. Всички подобрения и поправки след доставката се считат за „работа по поддръжката“ — поддържане на системите в работно състояние, анализиране на повреди, адаптиране към различни платформи, коригиране на грешки и изплащане на „технически дълг“. Трябва да проектираме системи, които могат лесно да се адаптират към променящите се изисквания, правейки режима на поддръжка малко по-малко болезнен.
Винаги кодирайте, сякаш човекът, който в крайна сметка поддържа вашия код, ще бъде жесток психопат, който знае къде живеете - Джон Уудс
Промяната е неизбежна
Бизнес приоритетите се променят, основните платформи се променят, законовите и регулаторните изисквания се променят, вашите потребители се променят! Не е съвпадение, че по-голямата част от принципите на дизайна се фокусират върху това да направят нещата „по-лесни за промяна“. Независимо дали става въпрос за изолиране на проблемите между модулите („отделяне“), маскиране на сложни взаимодействия зад прости „фасади“ или просто да не се повтаряме („СУХО“); Трябва да се стремим към развиващи се системи, които позволяват лесни бъдещи адаптации.
С всяко критично решение екипът на проекта се ангажира с версия на реалността, която има по-малко възможности.
API поддръжка
Тъй като организациите преминават от монолитни към по-разпределени или „споделени нищо“ архитектури, където услугите са самоуправляеми, „облачно управлявани“ или „„функции без сървър““, оперативната сложност нараства и предлагането на надеждно API изживяване става предизвикателен. Тази сложност забавя екипа на проекта, като допълнително увеличава разходите за поддръжка.
При управлението на API директната комуникация клиент-услуга има тенденция да увеличава сложността на API и може да доведе до непредвидени последици:
- Тясно свързване. Клиентските приложения зависят пряко от непрекъснато променящите се API договори на бекенд услугите.
- Дублиране на знания. Всяка изложена услуга прилага свои собствени крайни функции, като прекратяване на SSL и ограничаване на скоростта.
- Много двупосочни пътувания. Прекомерният брой двупосочни пътувания по мрежата поради сложни потоци на композиране на API може да влоши производителността.
- Увеличена повърхност за атака. Вече са отворени много повече портове, повече услуги са изложени и удостоверяването също се превърна в разпространен проблем.
Подобряването на поддръжката не означава непременно намаляване на функционалността; това може също да означава намаляване на сложността.
Фасада за задната част
Правилно проектираните абстракции могат да скрият голяма част от детайлите на изпълнението зад прости фасади. API шлюзовете осигуряват тази абстракция чрез капсулиране на сложни бекенд архитектури, като същевременно излагат последователен и удобен за клиента API интерфейс.
За да се поддържа API сложността на (разпределените) системи на управляемо ниво, API шлюзовете помагат чрез:
- Отделяне на клиентите от задните договори. Маршрутите на API се управляват чрез отделна конфигурация за маршрутизиране на заявки, поддържайки клиентския интерфейс последователен.
- Консолидиране на междусекторни проблеми в едно ниво. Шлюзовете намаляват дублирането и опростяват всяка услуга чрез централизиране на отговорността за критичните периферни функции.
- Обобщаване на данни в услуги. Вече можем да приложим съставяне на API „от страна на сървъра“, като изпратим една клиентска заявка до няколко вътрешни услуги и отговорим с агрегиран полезен товар.
- Скриване на вътрешни услуги от външния свят. Излагането само на шлюза намалява повърхността на мрежовата атака и позволява централизирано управление на сигурността на API.
Декларативни API шлюзове с KrakenD
Останалата част от тази публикация демонстрира няколко общи задачи, изпълнявани от API шлюз чрез примерен проект.
Някои от популярните API шлюзове (с отворен код), налични днес, включват:
Избрах KrakenD заради:
- Простота. „Docker изображение“ с един конфигурационен файл е всичко, от което се нуждаете.
- Бездържание и неизменност. Да бъдеш без състояние, неизменен и независим от заобикалящите работни натоварвания опростява поддръжката и намалява свързването.
- Ефективност. Сдодопълнителен мрежов скок, през който ще трябва да премине всяка заявка, искате да е бързо. KrakenD е създаден с оглед на производителността (~18 000 заявки/секунда).
Той се придържа към по-голямата част от практиките за приложения с дванадесет фактора, идеален кандидат за контейнерни среди.
Проектът
Примерното приложение представлява част от „по-голям проект за микро-услуга за електронна търговия“ с интерфейс за iOS, с който експериментирам. Ще използваме само следните части:
Приложение:
- Обслужване на колички. Услуга REST, написана на GO, която управлява пазарски колички за регистрирани клиенти.
- Услуга за самоличност. REST услуга, написана на Typescript, която управлява клиентски акаунти и издава JSON уеб токени (JWT).
- Шлюз. KrakenD (Community Edition) API шлюз, който обработва маршрутизиране на заявки, оторизация, валидиране на полезен товар и ограничаване на скоростта.
Разширих слоя за съхранение за тази статия, за да не зависи от Postgres и Redis и да поддържа нещата леки.
Инфраструктура:
- Kubernetes, Helm & Skaffold. Всички работни натоварвания са пакетирани чрез диаграми на Kubernetes Helm. Skaffold управлява работния поток за изграждане и внедряване на целия проект във вашия клъстер.
- Docker Compose. Вместо Kubernetes, проектът може да бъде изграден и внедрен чрез Docker Compose.
За простота ще използваме Docker Compose, докато конструираме нашия манифест на шлюза в следващите раздели, въпреки че в хранилището е предоставена напълно работеща конфигурация на Kubernetes.
Project: . ├── identity-service/ (nodejs microservice) │ ├── src/ │ ├── Dockerfile │ └── Makefile ├── cart-service/ (golang microservice) │ ├── src/ │ ├── Dockerfile │ └── Makefile ├── kubernetes-helmcharts/ │ ├── identity-service/ │ ├── cart-service/ │ └── gateway/ ├── krakend.yaml ├── docker-compose.yaml └── skaffold.yaml Versions: Kubernetes: 1.21.2 Helm: 3.3.3 Skaffold: 1.27.0 Docker: 20.10.5 Go: 1.15.2 NodeJS: 12.19.0 KrakenD: 1.2
Пълният изходен код на примерното приложение е достъпен на GitHub.
Изграждане и внедряване с „Docker Compose“
Инсталирайте Docker и стартирайте:
$ docker compose up ... [+] Running 3/3 ⠿ Container cart-service Started 4.0s ⠿ Container identity-service Started 5.8s ⠿ Container gateway Started 7.0s
Изградете и внедрите с Kubernetes, Helm и Skaffold
Създайте клъстер на Kubernetes локално или в облака.
Docker Desktop включва самостоятелен Kubernetes сървър и клиент, който работи на вашата машина. За да активирате Kubernetes, отидете на Docker › Preferences › Kubernetes и след това щракнете върху Активиране на Kubernetes.
Инсталиране на Skaffold и Helm и разгръщане на всички схеми на кормилото:
$ skaffold run --port-forward=user --tail ... Waiting for deployments to stabilize... - deployment/cart-service is ready. - deployment/gateway is ready. - deployment/identity-service is ready. Deployments stabilized in 19.0525727s
API конфигурация като код
Създайте нов манифест на KrakenD на адрес project-root/krakend.yaml
със следното съдържание:
krakend.yaml --- version: 2 endpoints: []
Всичко, което правя тук, е да посоча версията на файловия формат.
Маршрутизиране
Добавете обект на крайна точка под масива endpoints
и разкрийте крайната точка на услугата за самоличност GET /users
:
krakend.yaml --- ... endpoints: - endpoint: /users method: GET output_encoding: no-op backend: - url_pattern: /users encoding: no-op sd: static method: GET host: - http://identity-service:9005
- Кодирането
no-op
(без операция) гарантира, че клиентските заявки се препращат към бекенда както е и обратно. - Разделителната способност
static
е настройката за откриване на услуги по подразбиране и това ще използваме за нашата мрежа Docker Compose.
За внедрявания на Kubernetes, задайте sd
на dns
(активиране на режим DNS SRV)
Рестартирайте шлюза и изпълнете GET /users
заявка:
$ docker compose restart gateway $ curl 'http://0.0.0.0:8080/users' \ --request GET \ --include HTTP/1.1 401 Unauthorized ... { "status": 401, "message": "No Authorization header" }
Написах „персонализиран междинен софтуер за оторизация на JWT“ за услугата за идентичност за декодиране и валидиране на „JWT“ полезни товари, конфигурируеми чрез:
docker-compose.yaml --- ... identity-service: environment: - JWT_VALIDATION_ENABLED=true - JWT_PATHS_WHITELIST=/auth/register,/auth/login,/jwks.json
Оставете горното както е и разширете krakend.yaml
с register
и login
маршрути, за да издавате JWT токени от услугата за самоличност:
krakend.yaml --- ... endpoints: - endpoint: /users ... - endpoint: /auth/register method: POST output_encoding: no-op backend: - url_pattern: /auth/register encoding: no-op sd: static method: POST host: - http://identity-service:9005 - endpoint: /auth/login method: POST output_encoding: no-op backend: - url_pattern: /auth/login encoding: no-op sd: static method: POST host: - http://identity-service:9005
Издайте JWT токен, като регистрирате нов потребител и го експортирайте в средата на вашата обвивка за по-късна употреба:
$ docker compose restart gateway $ curl 'http://0.0.0.0:8080/auth/register' \ --request POST \ --header "Content-type: application/json" \ --include \ --data '{ "email": "[email protected]", "password": "pass" }' HTTP/1.1 201 Created ... { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVzZX...", "expiry": 1623536812 } $ export TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVzZX...
Инжектирайте токена си в заглавкатаAuthorization
и опитайте отново да извлечете всички потребители:
$ curl 'http://0.0.0.0:8080/users' \ --request "GET" \ --header "Authorization: Bearer ${TOKEN}" \ --include HTTP/1.1 401 Unauthorized ... { "status": 401, "message": "No Authorization header" }
KrakenD не изпраща клиентски заглавки към бекенда по подразбиране.
Добавете свойство headers_to_pass
под обекта на крайна точка /users
, за да препратите заглавката на заявката Authorization
към бекенда:
krakend.yaml --- ... endpoints: - endpoint: /users ... headers_to_pass: - Authorization backend: ...
С препратеното заглавие Authorization
вече можем да извлечем всички потребители:
$ docker compose restart gateway curl 'http://0.0.0.0:8080/users' \ --request "GET" \ --header "Authorization: Bearer ${TOKEN}" \ --include HTTP/1.1 200 OK [{ "id":"f06b084b-9d67-4b01-926b-f90c6246eed9", "email":"[email protected]" }]
Нашият манифест на KrakenD досега:
Разрешение за разтоварване
Нека не пишем персонализиран мидълуер за оторизация на GO за услугата количка, а да предпазим нейните крайни точки, като прехвърлим тази междусекторна грижа към шлюза.
Форматът JSON Web Key Set се използва за излагане на нашия ключ(ове) за проверка на целостта на токена на шлюза. За простота избрах генериране на симетричен подпис с алгоритъма HS256 (HMAC-SHA256), когато пишех услугата за самоличност (нашият „доставчик на идентичност“):
$ echo -n 'secret' | openssl base64 c2VjcmV0
🤫
Нашият JWKS съдържа същия симетричен ключ, статично хостван на identity-service/jwks.json
:
# identity-service/jwks.json { "keys": [ { "kty": "oct", # key type (octet string) "kid": "userid", # key id (identify the key in the set) "k": "c2VjcmV0", # key "alg": "HS256". # algorithm } ] }
За повече информация относно стандарта JWK вижте RFC документа.
Добавете крайна точка PUT /cart
към манифеста и го предпазете от нерегистрирани клиенти чрез приставката за валидатор krakend-jose
:
krakend.yaml --- ... - endpoint: /cart method: PUT output_encoding: no-op extra_config: github.com/devopsfaith/krakend-jose/validator: alg: HS256 jwk-url: http://identity-service:9005/jwks.json disable_jwk_security: true kid: userid backend: - url_pattern: /cart encoding: no-op sd: static method: PUT host: - http://cart-service:9002
Тук посочваме „ID на ключа“ и разрешаваме HTTP достъп до нашия частно хостван JWKS, като зададем disable_jwk_security
на false
.
Ако сте затворили сесията на обвивката си, изпълнете заявка
/login
и експортирайте повторно токена си в средата на обвивката:curl ‘http://0.0.0.0:8080/auth/login' -H “Content-type: application/json” -d ‘{“email”: “[email protected]”,”password”: “pass”}’
Изпълнете заявка PUT /cart
с валиден токен, за да актуализирате количката на потребителя:
$ docker compose restart gateway $ curl 'http://0.0.0.0:8080/cart' \ --request "PUT" \ --header "Content-type: application/json" \ --header "Authorization: Bearer ${TOKEN}" \ --include \ --data '{ "items": [{ "productid": "94e8d5de-2192-4419-b824-ccbe7b21fa6f", "quantity": 2, "price": 200 }] }' HTTP/1.1 400 Bad Request ... { "message": "Bad request: no userID" }
Услугата за кошница очаква потребителският идентификатор на клиента да бъде добавен пред всички пътища. Идентификационният номер на потребителя може да бъде извлечен ръчно от полезния товар на JWT като Аз го вградих сам под претенцията JWT за потребителски идентификатор.
За щастие можем да получим достъп до валидирани JWT полезни натоварвания чрез променливата KrakenD JWT
и да я предадем на бекенд обекта на крайната точка на количката:
krakend.yaml --- ... - endpoint: /cart ... backend: - url_pattern: /{JWT.userid}/cart ...
Ако изпълним заявката PUT /cart
отново, тя трябва успешно да създаде или актуализира cart
:
$ docker compose restart gateway $ curl 'http://0.0.0.0:8080/cart' \ --request "PUT" \ --header "Content-type: application/json" \ --header "Authorization: Bearer ${TOKEN}" \ --include \ --data '{ "items": [{ "productid": "94e8d5de-2192-4419-b824-ccbe7b21fa6f", "quantity": 2, "price": 200 }] }' HTTP/1.1 201 Created ... { "items": [ { "productid": "94e8d5de-2192-4419...", "quantity": 2, "price":200 } ] }
Следващата стъпка ще бъде рефакторинг на GET /users
, за да разтоварите JWT валидирането и от identity-service
.
Ще оставя това като упражнение. Преди да започнете обаче, уверете се, че сте деактивирали JWT валидирането на нивото на услугата в услугата за самоличност:
docker-compose.yaml --- identity-service: ... - JWT_VALIDATION_ENABLED=false # offloaded to the gateway! ... $ docker compose down && docker compose up
Валидиране
Следващият пример е само с илюстративна цел и показва как KrakenD може да извърши базирано на „схема“ JSON валидиране. Може да е разумно да не свързвате шлюза с бизнес логиката (за разлика от този пример), като гарантирате, че услугите остават в техните граници.
За илюстративни цели нека уточним, че полетата email
и password
на крайната точка /register
са required
и трябва да са от тип string
:
krakend.yaml --- ... - endpoint: /auth/register method: POST output_encoding: no-op extra_config: github.com/devopsfaith/krakend-jsonschema: type: object required: - email - password properties: email: type: string password: type: string backend: ... ...
Първо променете ключа email
, за да изпратите невалиден полезен товар:
$ docker compose restart gateway $ curl 'http://0.0.0.0:8080/auth/register' \ --request POST \ --header "Content-type: application/json" \ --include \ --data '{ "emai": "[email protected]", "password": "pass" }' HTTP/1.1 400 Bad Request
Коригирайте полезния си товар и проверете дали заявката е успешна:
$ curl 'http://0.0.0.0:8080/auth/register' \ --request POST \ --header "Content-type: application/json" \ --include \ --data '{ "email": "[email protected]2", "password": "pass" }' HTTP/1.1 201 Created { "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVzZXJ...", "expiry":1625254416 }
Ограничаване на скоростта
И накрая, ще се съсредоточим върху управлението на трафика. За да защитим услугите си от прекомерна употреба – независимо дали е предвидена или непреднамерена, ние можем да „ограничим скоростта“ на критични или неекранирани пътища и да установим квота за използване за нашите клиенти.
Първо бомбардирайте нашата крайна точка /register
със 100 заявки и вижте какво ще се случи:
for i in {1..100}; do curl 'http://0.0.0.0:8080/auth/register' \ --request POST \ --header "Content-type: application/json" \ --include \ --data '{ "email": "d@d.os", "password": "pass" }'; done HTTP/1.1 201 Created HTTP/1.1 409 Conflict HTTP/1.1 409 Conflict ... HTTP/1.1 409 Conflict # 100
Всяка заявка се обработва от услугата за идентичност, което води до прекомерна обработка и комуникация с базата данни.
Сега добавете ограничение от 5 заявки в секунда (на IP адрес) и ограничение от общо 100 заявки в секунда за крайната точка /register
:
krakend.yaml --- ... - endpoint: /auth/register ... github.com/devopsfaith/krakend-jsonschema: ... github.com/devopsfaith/krakend-ratelimit/juju/router: maxRate: 100 clientMaxRate: 5 strategy: ip ... ...
Забележете как шлюзът прекъсва всички заявки, надвишаващи нашата квота:
$ docker compose restart gateway for i in {1..100}; do curl 'http://0.0.0.0:8080/auth/register' \ --request POST \ --header "Content-type: application/json" \ --include \ --data '{ "email": "d@d.os", "password": "pass" }'; done HTTP/1.1 201 Created HTTP/1.1 409 Conflict ... HTTP/1.1 429 Too Many Requests ... HTTP/1.1 429 Too Many Requests
Знайте кога да спрете
Тъй като доставчиците на пазара на API продължават да добавят функции, за да разграничат своите продукти, важно е да знаете кога да спрете да прехвърляте отговорността на ръба. Раздутите и свръхамбициозни портали са трудни за тестване и внедряване.
Освен че имате твърде много отговорности, други възможни притеснения включват:
- Единствена точка на повреда. Като единична входна точка за задния слой, трябва да гарантираме, че шлюзът е устойчив. Избягвайте отделни точки на отказ чрез „излишък“, „еластичност“ и „механизми за възстановяване при отказ“.
- Допълнителна услуга за поддръжка. Усилието за поддръжка на шлюза ще доведе ли до технически дълг? Какво означава това за отговорностите на екипа за разработка?
- Допълнителен мрежов скок. Шлюзът може да увеличи времето за реакция поради допълнителния мрежов преход към бекенда. Въпреки че това има по-малко въздействие от директните заявки от клиент към бекенд, остава изключително важно последователното тестване на натоварването на системата, гарантирайки, че отговаряме на нашите SLO с увереност.
Последният манифест на KrakenD
Обобщавайки
API шлюзовете осигуряват надежден интерфейс за клиентите и централна точка за управление на заявки и отговори.
В разпределена архитектура те могат да се използват за разтоварване на междусекторна функционалност, която иначе би трябвало да бъде репликирана. API шлюзът идва с много предимства, но също така добавя още един компонент за поддържане и оптимизиране за производителност и надеждност.
Благодаря за четенето.
Тази статия първоначално е публикувана на:https://portfo lio.fabijanbajo.com/api-gateways