Что, почему и как используют шлюзы API

Шлюз API - это компонент, который направляет трафик на серверную часть и отделяет клиентов от контрактов API. Он инкапсулирует сложную архитектуру приложения, объединяя ее с единым интерфейсом API. Помимо инкапсуляции и обратного проксирования, они также могут переносить сквозные проблемы с отдельных служб, такие как аутентификация, ограничение скорости и ведение журнала запросов.

Кризис идентичности

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

Чтобы избежать путаницы, в этой статье предполагается, что каждое решение несет следующие основные обязанности:

  • Сервисная сетка. Выделенная сетевая инфраструктура, которая накладывается на ваши сервисы, разгружая функции межсервисного взаимодействия, такие как механизмы шифрования, наблюдения и устойчивости.
  • Шлюз API. Компонент, который обеспечивает целостную абстракцию всей архитектуры приложения, одновременно разгружая пограничные функции от имени отдельных служб.

Теперь мы рассмотрим «почему» API-шлюзов, а позже рассмотрим пример кода.

Развитие - это Сопровождение

Хорошо известно, что большая часть стоимости программного обеспечения связана с его текущим обслуживанием. Все улучшения и исправления после доставки считаются работами по техническому обслуживанию - поддержание систем в рабочем состоянии, анализ сбоев, адаптация к различным платформам, исправление ошибок и погашение технического долга. Мы должны разрабатывать системы, которые могут легко адаптироваться к меняющимся требованиям, делая режим обслуживания менее болезненным.

Всегда кодируйте так, как будто парень, который в конечном итоге поддерживает ваш код, будет жестоким психопатом, который знает, где вы живете - Джон Вудс

Изменения неизбежны

Меняются бизнес-приоритеты, меняются базовые платформы, меняются законодательные и нормативные требования, меняются ваши пользователи! Неслучайно большинство принципов дизайна сосредоточено на том, чтобы легче изменить. Будь то изоляция проблем между модулями (разъединение), маскировка сложных взаимодействий за простыми фасадами или просто не повторение самих себя (СУХОЙ); Мы должны стремиться к развивающимся системам, которые позволяют легко адаптировать их в будущем.

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

Обслуживание API

По мере того, как организации переходят от монолитной к более распределенной архитектуре или архитектуре без совместного использования, где сервисы являются самоуправляемыми, облачными или бессерверными функциями, операционная сложность возрастает, и становится все больше и больше предлагать надежный интерфейс API. испытывающий. Эта сложность замедляет работу проектной группы, что еще больше увеличивает стоимость обслуживания.

В управлении API прямая связь между клиентом и сервисом имеет тенденцию к увеличению сложности API и может иметь непредвиденные последствия:

  • Тесная связь. Клиентские приложения напрямую зависят от постоянно меняющихся контрактов API серверных служб.
  • Дублирование знаний. Каждая открытая служба реализует свои собственные пограничные функции, такие как завершение SSL и ограничение скорости.
  • Много циклов приема-передачи. Чрезмерное количество циклов приема-передачи по сети из-за сложных потоков композиции API может снизить производительность.
  • Увеличенная поверхность для атак. Сейчас открыто гораздо больше портов, доступно больше служб, а аутентификация также стала проблемой распределенного доступа.

Улучшение ремонтопригодности не обязательно означает снижение функциональности; это также может означать снижение сложности.

Фасад для серверной части

Правильно спроектированные абстракции могут скрыть множество деталей реализации за простыми фасадами. Шлюзы API обеспечивают эту абстракцию, инкапсулируя сложные серверные архитектуры, предоставляя согласованный и удобный для клиентов интерфейс API.

Чтобы поддерживать сложность API (распределенных) систем на управляемом уровне, шлюзы API помогают:

  • Отделение клиентов от внутренних контрактов. Маршруты API управляются с помощью отдельной конфигурации маршрутизации запросов, что обеспечивает согласованность клиентского интерфейса.
  • Объединение сквозных проблем на одном уровне. Шлюзы сокращают дублирование и упрощают каждую службу за счет централизации ответственности за критически важные пограничные функции.
  • Агрегирование данных по сервисам. Теперь мы можем применять состав API «на стороне сервера», отправляя один клиентский запрос нескольким внутренним службам и отвечая агрегированной полезной нагрузкой.
  • Скрытие внутренних сервисов от внешнего мира. Открытие только шлюза уменьшает поверхность для сетевых атак и позволяет централизованно управлять безопасностью API.

Декларативные API-шлюзы с KrakenD

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

Некоторые из популярных (с открытым исходным кодом) API-шлюзов, доступных сегодня, включают:

Я выбрал KrakenD по следующим причинам:

  • Простота. Все, что вам нужно, - это образ докера с одним файлом конфигурации.
  • Отсутствие состояния и неизменяемость. Отсутствие состояния, неизменность и независимость от окружающих рабочих нагрузок упрощает обслуживание и снижает взаимосвязь.
  • Производительность. Имея дополнительный сетевой переход, через который должен проходить каждый запрос, вы хотите, чтобы он выполнялся быстро. KrakenD построен с учетом производительности (~ 18 000 запросов в секунду).

Он придерживается большинства двенадцатифакторных практик приложений, что делает его идеальным кандидатом для контейнерных сред.

Проэкт

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

Заявка:

  • Услуга корзины. Служба REST, написанная на GO, которая управляет тележками для покупок для зарегистрированных клиентов.
  • Служба идентификации. Служба REST, написанная на Typescript, которая управляет учетными записями клиентов и выдает веб-токены JSON (JWT).
  • Шлюз. API-шлюз KrakenD (Community Edition), который обрабатывает маршрутизацию запросов, авторизацию, проверку полезной нагрузки и ограничение скорости.

Я расширил уровень хранения для этой статьи, чтобы он не зависел от 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 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НастройкиKubernetes и нажмите Включить Kubernetes.

Установите Skaffold и Helm и разверните все 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

Здесь мы указываем «идентификатор ключа» и разрешаем 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