Какво, защо и как на 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, отидете на DockerPreferencesKubernetes и след това щракнете върху Активиране на 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