python

Прокладываем тропинки до микросервисов

  • пятница, 3 июня 2022 г. в 00:38:54
https://habr.com/ru/company/otus/blog/669342/
  • Блог компании OTUS
  • Python
  • DevOps
  • Микросервисы


Одна из наиболее важных задач при разделении системы на микросервисы - обеспечить надежный механизм их репликации и обнаружения и создать набор правил для маршрутизации входящих запросов к соответствующим контейнерам или сетевым узлам. Идеальная система также должна уметь отслеживать состояние доступности и исключать недоступные реплики из маршрутизации. В этой статье мы поговорим об использовании маршрутизатора 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 и выполнить аналогичные настройки через веб-интерфейс:

Интерфейс Konga
Интерфейс Konga

Запустим теперь наш сервис для маршрутизации и выполним привязку его методов к префиксу /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 можно динамически изменять привязку маршрутов к экземплярам сервисов, а также решать типичные задачи контроля доступа и получения метрик по времени выполнения запросов.


Всех, кому интересна тема микросервисов, хочу пригласить на бесплатный урок по теме: "Авторизация и аутентификация в микросервисной архитектуре". Регистрация доступна по ссылке ниже.