Что, почему и как используют шлюзы 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