Прокладываем тропинки до микросервисов
- пятница, 3 июня 2022 г. в 00:38:54
Одна из наиболее важных задач при разделении системы на микросервисы - обеспечить надежный механизм их репликации и обнаружения и создать набор правил для маршрутизации входящих запросов к соответствующим контейнерам или сетевым узлам. Идеальная система также должна уметь отслеживать состояние доступности и исключать недоступные реплики из маршрутизации. В этой статье мы поговорим об использовании маршрутизатора Kong, который принимает на себя не только задачи умной маршрутизации, но и возможности по протоколированию и трансформации запросов, контролю доступа, мониторингу запросов, а также может быть расширен с использованием плагинов.
Маршрутизатор Kong Gateway существует в свободном и коммерческом варианте, может быть запущен как самостоятельное приложение (например, через docker-контейнер kong), либо установлен в Kubernetes (в этом случае он представляется как специализированный Ingress-контроллер). Для управления Kong можно использовать как запросы к API, так и веб-интерфейсы Kong Manager или Konga. Многие расширения предустановлены в официальном контейнере, но они также могут быть найдены в Kong Plugin Hub. Мы с вами рассмотрим установку и настройку на примере простого приложения, состоящего из трех микросервисов (один из которых представляет публичный интерфейс без авторизации, для одного мы будем задавать сложные правила маршрутизации, а третий будет требовать обязательное использование токена для доступа к точкам подключения).
Для начала составим топологию нашей системы и выделим микросервисы:
сервис поиска местоположения и расширенной информации о городе по стране и названию (используется приложение Chercheville на Elixir , доступное на Docker Hub);
сервис для получения информации из профиля авторизованного пользователя;
сервис для выполнения авторизации и получения токена.
Каждый сервис будет иметь собственную базу данных (или сторонний источник данных) и все взаимодействие между системами будет выполняться через шлюз API, который мы и будем настраивать с использованием Kong API Gateway.
Kong использует для сохранения своей конфигурации базу данных PostgreSQL, либо может быть запущен с определением сервисов и привязок к json/yaml-файле. Мы будем рассматривать вариант запуска с базой данных. Для координации запуска будем использовать Docker Compose, который можно найти в официальном репозитории: https://github.com/Kong/docker-kong (путь /compose).
docker compose --profile database up -d
После выполнения первоначальных миграций базы данных запускается основной процесс, который начинает прослушать несколько TCP-портов:
8000/8443 - HTTP/HTTPS-трафик для маршрутизации к серверам;
8001/8444 - HTTP/HTTPS порты для отправки административных запросов (также используется веб-интерфейсом Konga для управления ресурсами);
8002/8445 - HTTP/HTTPS подключение к Kong Manager (веб-интерфейс), должен быть дополнительно разрешен и опубликован через docker-compose.yaml.
Перед настройкой маршрутов нужно разобраться с используемыми терминами в Kong:
Service - конфигурация backend-сервиса (протокол http/https, адрес хоста, порт, количество попыток подключения и таймауты, идентификатор клиентского сертификата при использовании https). К конфигурации сервиса могут быть подключены плагины для аудита/трансформации запросов, а также определены допустимые потребители (consumer).
Route - правило маршрутизации входящих запросов, включает в себя перечисления связанных сервисов (может быть больше одного для отказоустойчивости) и группу фильтров (для http-протоколов) или набор правил для tcp-stream маршрутизации. К каждому маршруту может быть также добавлен плагин для обработки запросов.
название домена (Hosts);
префиксы пути (Paths);
HTTP-методы (Methods);
протоколы (http / https);
проверка доменов для tcp-stream маршрутизации (SNI);
источники запроса для tcp-stream (sources);
получатели запроса для tcp-stream (destinations).
Certificate - зарегистрированные клиентские сертификаты для подключения к https backend-сервисам (для каждого формируется уникальный id, который используется при конфигурации сервиса).
Upstream - правило пересылки http/https трафика на один или несколько сервисов. После создания и настройки upstream появляется возможность добавить targets (ip/dns-адреса целевых серверов, порты и значение веса, которое влияет на вероятность выбора сервера при использовании хэш-правила none), а также Alert для отправки уведомлений администраторам по почте или в Slack при недоступности целевого сервера:
Hash on - метод хэширования для выбора целевого сервиса из пула (может быть consumer, ip, cookie, header, none). В случае none используется последовательный выбор сервисов для равномерного распределения;
Hash fallback - альтернативный метод хэширования, если основной не вернул результат (например, отсутствовал cookie или заголовок);
Active Health Check - конфигурация проверки доступности (http-метод, путь, коды успешных и неуспешных ответов, количество попыток проверки, интервал проверки);
Consumer - зарегистрированные авторизованные пользователи сервисов и метод их авторизации. Для Consumer определяется набор Credentials - это может быть Basic Auth (имя и пароль), API Key, Hash-based message authentication code (HMAC) - имя и секрет, OAuth2 (id и секрет приложения, redirect uri), JSON Web Token (JWT).
Для управления ресурсами можно использовать http-запросы к порту администрирования 8001 (или 8444 для https-запросов):
http://ip:8001/services
- просмотр списка/регистрация нового сервиса;
http://ip:8001/routes
- просмотр/создание маршрута;
http://ip:8001/consumers
- управление авторизованными пользователями;
http://ip:8001/upstreams
- изменение правил проксирования с проверкой доступности (upstream);
http://ip:8001/certificates
- управление клиентскими сертификатами;
http://ip:8001
- просмотр информации о запущенном экземпляре kong.
Для удобства доступа можно создать маршрут к admin api через основной порт (8000):
curl -X POST http://127.0.0.1:8001/services \
--data name=admin-api \
--data host=127.0.0.1 \
--data port=8001
curl -X POST http://127.0.0.1:8001/services/admin-api/routes \
--data paths[]=/admin-api
И далее будет возможно получить доступ к сервису администрирования через http://localhost:8000/admin-api/. Наличие API для администрирования позволяет выполнить саморегистрацию сервиса и маршрута при запуске, но сейчас у нас доступ к api выполняется без авторизации, что нельзя назвать безопасным решением. Создадим новый consumer с APIKey и назначим его на доступ к маршруту api:
curl -i -X POST \
--url http://localhost:8001/services/admin-api/plugins/ \
--data 'name=key-auth'
curl -i -X POST \
--url http://localhost:8000/admin-api/consumers/ \
--data "username=admin"
curl -i -X POST \
--url http://localhost:8000/admin-api/consumers/admin/key-auth/ \
--data 'key=6Y6fKCsFWWWSRMXGyUWs8g9q'
Теперь доступ к API будет происходить только с использованием заголовка apikey: 6Y6fKCsFWWWSRMXGyUWs8g9q.
Кроме плагинов авторизации, можно добавить следующие расширения:
session
- отслеживание сессии между запросами (чаще всего с использованием cookie);
bot detection
- обнаружение активности ботов;
cors
- управление политикой cors;
ip restriction
- ограничение доступа по белым и черным спискам;
acme
- интеграция с letsencrypt для автоматического обновления сертификатов;
rate limiting
- ограничение количества запросов в секунду;
response ratelimiting
- ограничение количества ответов в секунду;
request size limiting
- ограничение размера запроса;
proxy cache
- кэширование ответа;
pre function
- запустить lua-функцию перед обработкой запроса;
post function
- запустить lua-функцию после обработки запроса;
aws lambda
- запустить внешнюю функцию с AWS при обработке правила;
azure functions
- запустить Azure-функцию при обработке правила;
datadog
- отправить метрики запросов в datadog;
prometheus
- отправить метрики по правилам в prometheus;
zipkin
- отправка меток по обработке правил для распределенной трассировки zipkin;
request transformer
- изменение запроса перед передачей на upstream-сервер;
response transformer
- изменение ответа upstream-сервера;
correlation ID
- отслеживание связи в цепочке пересылок;
tcp/udp/http log
- отправка информации о запросе и ответе на сервер журналирования через tcp/udp/http;
file log
- запись информации о запросе/ответе в файл;
syslog
- отправка запроса и ответа в системный процесс syslog;
statsd
- отправка статистики по обработке запроса в statsd;
loggly
- пересылка информации о запросе/ответе в loggly.
При необходимости создания собственных расширений можно использовать Python Developent Kit.
Для управления также можно установить konga для настройки всех ресурсов через веб-интерфейс:
docker network create konga
docker rm -f konga-db
docker run -d --network=konga --name konga-db -v /data/mongo:/data/db mongo
docker rm -f konga2
docker run -d --network=konga --name konga -e NODE_ENV=production -e "TOKEN_STRING=fdoER#2sa" -e DB_ADAPTER=mongo -e DB_HOST=konga-db -e DB_DATABASE=konga --network=konga -p 443:1337 -v /data/konga:/data -e SSL_KEY_PATH=/data/cert.key -e SSL_CRT_PATH=/data/cert.crt pantsel/konga
При выполнении первого подключения нужно будет сконфигурировать адрес Kong API и далее можно будет подключиться к https и выполнить аналогичные настройки через веб-интерфейс:
Запустим теперь наш сервис для маршрутизации и выполним привязку его методов к префиксу /place. Для этого клонируем репозиторий и запустим docker compose up -d. После запуска будет необходимо выполнить импорт данных по странам:
docker exec -ti chercheville-app-1 sh
./bin/chercheville rpc 'ChercheVille.SeedData.import_data(["FR"])'
Для проверки доступности сервиса выполним запрос http://localhost:5000/cities/?q=Paris
. Теперь выполним связывание сервиса и маршрута через konga:
Здесь Host - внешний адрес сервера (альтернативно можно использовать DNS-имя во внутренней сети или через используемый Service Discovery, например Consul). Дальше необходимо настроить привязку сервиса к внешнему маршруту:
Strip Path позволяет трансформировать адрес и убрать префикс /places при пересылке запроса. Теперь сервис будем доступен через адрес http://localhost:8000/places/ и можно будет выполнить запрос между микросервисами или от внешнего клиента через API Gateway.
Следующим шагом выполним саморегистрацию сервиса авторизации в kong, который также будет управлять Consumer для доступа к сервису профиля:
import requests
import socket
from flask import Flask
gateway = "http://localhost:8000"
kong_api = f"{gateway}/admin-api"
headers = {
"apikey": "6Y6fKCsFWWWSRMXGyUWs8g9q"
}
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
# саморегистрация сервиса
requests.post(f"{kong_api}/services", headers=headers, data={
"name": "authorizer",
"url": "http://{local_ip}:10080"
})
requests.post(f"{kong_api}/services/authorizer/routes", data={
"paths[]=/auth"
})
app = Flask(__name__)
@app.route("/login")
def login():
# extract and check login/password
login = "login"
requests.post(f"{kong_api}/consumers", data={"username": login}) # регистрируем consumer
@app.route("/logout")
def logout():
requests.delete(f"{kong_api}/consumers/{login}")
app.run(port=10080)
При регистрации сервиса profile необходимо добавить плагин key-auth для проверки токена при обращении к адресам profile. При авторизации будет добавлен заголовок X-Consumer-ID для хранения идентификатора, который соответствует представленному токену.
import requests
import socket
import json
import flask
gateway = "http://localhost:8000"
kong_api = f"{gateway}/admin-api"
headers = {
"apikey": "6Y6fKCsFWWWSRMXGyUWs8g9q"
}
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
# саморегистрация сервиса
requests.post(f"{kong_api}/services", headers=headers, data={
"name": "profile",
"url": "http://{local_ip}:9080"
})
requests.post(f"{kong_api}/services/profile/routes", data={
"paths[]=/profile"
})
requests.post(f"{kong_api}/services/profile/plugins", data={
"name=key-auth"
})
app = flask.Flask(__name__)
@app.route("/profile")
def login():
consumers = json.loads(requests.get(f"{kong_api}/consumers"))
# определение имени (идентификатора) пользователя
# список содержит id (consumer id) и имя пользователя (name)
id = flask.request.headers.get("X-Consumer-ID")
username = None
for c in consumers:
if c["id"]==id:
username = c["username"]
if not username:
flask.abort(403, "Not authorized")
return f"Logged user: {username}"
app.run(port=9080)
Аналогично можно настроить удаление сервисов и маршрутов при завершении приложения. Таким образом с использованием API-шлюза Kong можно динамически изменять привязку маршрутов к экземплярам сервисов, а также решать типичные задачи контроля доступа и получения метрик по времени выполнения запросов.
Всех, кому интересна тема микросервисов, хочу пригласить на бесплатный урок по теме: "Авторизация и аутентификация в микросервисной архитектуре". Регистрация доступна по ссылке ниже.