Как работать с API‑Gateway, API‑Composition, KrakenD, FastAPI

Микросервисы на пальцах: API‑Gateway, API‑Composition, KrakenD, FastAPI

Тирекс
Тирекс Самый зубастый автор
8 февраля 2025

В этом материале рассмотрим API-Gateway, BFF и композицию API — три базовых шаблона, которые можно встретить почти в каждой системе с микросервисами.

Изображение записи

Количество современных приложений, построенных на микросервисной архитектуре, очень велико. А сами микросервисы стали уже больше обыденностью, чем какой-то редкостью. Наша задача — понять, для чего они нужны, какие проблемы решают и создают. Пройдемся по теории и развернем реальный пример на инфраструктуре.

Надпись API-Gateway  между условными элементами сети.

Архитектура

Смоделируем проблему. Предположим, что у нас есть маркетплейс, на главной странице которого расположено огромное количество компонентов:

  • информация по авторизованному пользователю;
  • компонент со списком товаров;
  • корзина;
  • личные скидки и персональные предложения;
  • каталог со списком категорий, фильтров и еще огромное множество компонентов.

Маркетплейс построен на микросервисной архитектуре. В данном случае за бизнес-логику и поставку данных клиенту отвечает отдельный сервис:

  • профиль клиента,
  • сервис каталога,
  • сервис корзины,
  • сервис дискаунта,
  • сервис товаров.
Схема взаимодействия клиента с различными сервисами.

Каждый из сервисов имеет изолированную по бизнес-домену зону ответственности и поставляет/обрабатывает те ресурсы, за которые отвечает, предоставляя клиенту внешние API.

Какие проблемы

1. Для отображения одной страницы клиенту часто требуется получить данные сразу из нескольких микросервисов. Случается, что одновременно нужны:

  • информация о пользователе из сервиса профиля: имя, аватар, текущие настройки;
  • список товаров из сервиса каталога;
  • персональные скидки из сервиса дискаунта;
  • корзина пользователя из сервиса корзины, чтобы показать добавленные товары.
Схема взаимодействия клиента с различными сервисами.

Без дополнительной обработки клиент вынужден отправить четыре отдельных запроса (или даже больше) по разным адресам, дождаться ответа, а затем объединить данные. В реальных приложениях запросов может быть десятки — например, для подгрузки рекомендаций, рейтингов или похожих товаров.

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

2.  Потребности в данных у клиентов разные, к тому же сильно зависят от вида используемого устройства.

Схема взаимодействия клиента с различными сервисами.

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

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

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

Мы могли бы на стороне конкретного сервиса сделать две версии API — каждая для конкретного клиента. Однако данный процесс очень трудоемкий и может повлечь за собой переписывание бизнес-логики.

3. Динамическое расположение сервисов привносит свои особенности. Микросервисы обычно развертываются в облачных средах, где адреса сервисов динамически изменяются — например, сервис корзины может быть развернут на разных хостах и портах при масштабировании.

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

Однако клиенты не должны вручную отслеживать изменения в расположении сервисов или их количества. Это создает проблему, если клиент напрямую взаимодействует с каждым из них. — требуется дополнительная логика. В таком случае чувствуется, что явно не хватает единой фиксированной точки входа.

Схема взаимодействия клиента с различными сервисами.

Решение

В решении вышеописанных проблем нам поможет API-Gateway (API-шлюз). Что он из себя представляет? Как правило, это еще один сервис в нашей архитектуре. Есть большое количество решений open source, которые предоставляют весь необходимый функционал. Однако нередко встречаются случаи, когда API-Gateway по тем или иным причинам оказывается самописным.

Добавим API-Gateway в нашу схему и посмотрим, какой профит можем извлечь.

Преимущества

1. Единая точка входа: клиенту не нужно знать адреса каждого из сервисов. У нас их может быть огромное количество, причем по несколько экземпляров на один сервис. А эта информация совершенно не нужна клиенту. Его задача — отправить запрос в один endpoint, а все остальные проблемы за него решит API-шлюз.

Схема взаимодействия клиента с различными сервисами.

2. Сокращение количества сетевых запросов от клиента. Выше упоминалось, что часто на одном экране клиенту необходимо отобразить ресурсы, которые принадлежат сразу нескольким сервисам. Мы можем реализовать взаимодействие таким образом, что по одному endpoint в API-Gateway будем получать данные сразу из нескольких сервисов. Как раз Gateway их объединит или выполнит агрегацию — такой подход называется API-Composition. В нем шлюз выступает в роли API-Composer и предоставляет готовый ответ клиенту. Таким образом, мы сократим затраты на сетевые запросы от клиента. Разумеется, у нас останутся запросы от Gateway к сервисам, но в рамках общей инфраструктуры это, как правило, будет быстрее.

3. На стороне API-Gateway мы можем реализовать BFF (Backend For Frontend). Это актуально, когда есть несколько типов клиентов, которым удобно работать с разными форматами ответа. Делать реализацию API для всех клиентов на стороне бэкенд-приложения не очень удобно, а вот на стороне API-Gateway вполне.

Схема взаимодействия клиента с различными сервисами.

4. Также мы можем вынести составляющую безопасности на сторону API-Gateway, который может проверять авторизацию пользователя и вытягивать данные из Payload JWT‑токена, чтобы не дублировать логику парсинга на каждом из сервисов.

Схема взаимодействия клиента с различными сервисами.

5. Кэширование тоже может лечь на плечи API-Gateway, однако необходимо корректно выбрать место, куда будет записываться содержимое кэша. По своему опыту отмечу, что такой кейс крайне редкий: обычно кэширование оставляют на стороне бэкенд-приложения.

6. Rate Limiter — ограничение количества запросов от пользователя в единицу времени. Это еще одно решение, которое можно вынести на API-шлюз. Таким образом, запрос от пользователя отобьется раньше, чем дойдет непосредственно до приложений, как и в случае с авторизацией.

Недостатки

1. Прежде всего, API-Gateway — дополнительный слой, который появляется в архитектуре. Это значит, что он влечет затраты на поддержку, ведение документации и требует наличие экспертизы. 

2. Может показаться, что API-Gateway, будучи единой точкой входа, является неким «бутылочным горлышком» (в плане ограниченного количества входящих запросов), а также единой точкой отказа. Это отчасти так, однако при проектировании нашей системы необходимо сначала позаботиться о горизонтальном масштабировании. То есть добавить инстансы и возможности отката на предыдущую версию кодовой базы, если есть потенциальные ошибки в реализации фичей. 

Стоит еще раз подчеркнуть: воплощение всех вышеперечисленных возможностей на стороне API-Gateway не является «серебряной пулей». Всегда отталкивайтесь от своих потребностей и ресурсов.

Подготовка

Теперь рассмотрим все это на практике. Развернем несколько сервисов и API-Gateway на демонстрационной инфраструктуре на реальном сервере. Мы не будем поднимать полноценный кластер Kubernetes на нескольких стендах — развернем все в Docker Compose. Он закроет все вопросы с обнаружением сервисов (ServiceDiscovery), а также позволит эмулировать межсервисное взаимодействие для pet-проектов или попросту попрактиковаться. Помним, что на реальных продуктах все же нужно поднимать полноценную инфраструктуру.

Что будем разворачивать

Схема взаимодействия используемых сервисов.

Сервисы:

  • users_service — микросервис пользователей;
  • delivery_service — микросервис доставки;

Микросервисы будут на FastApi, запустим их с помощью Uvicorn.

1. Прямо перед ними стоит API-Gateway. В нашем случае возьмем готовое решение KrakenD, возможностей которого нам вполне хватит. Он станет единой точкой входа в сервисы и будет проксировать в них запросы. Также реализуем пример паттерна API-composition для объединения ответов двух сервисов в один.

2. Далее располагается nginx — он может отдавать статику, то есть фронтенд-приложение или Swagger-бэкенда. На него можно накрутить SSL-сертификат для перевода приложения на HTTPS и многое другое. Реализуем его как обратный прокси-сервер, который будет перенаправлять запросы в Kraken и отдавать ответы.

3. Все это развернем на одном удаленном сервере.

Покупка, подключение и настройка сервера

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

Регистрируемся на платформе selectel.ru, авторизуемся и переходим в Облачную платформу. Нажимаем Создать сервер и переходим к конфигурированию.

Выбираем регион, например Москву, затем — образ сервера. Рекомендую самый легковесный Linux Debian 12, максимально «голый» — все необходимое добавим самостоятельно.

Аппаратную конфигурацию возьмем самую непритязательную: 1 vCPU и 2 ГБ RAM. Если есть интуитивное ощущение, что понадобится больше ресурсов, — заказываем более мощную машину. Так как образы Docker могут быть достаточно объемными, на дисковое пространство лучше не скупиться и взять не меньше 30 ГБ.

В сети выбираем один публичный статический IP‑адрес, чтобы приложение было доступно из интернета. Сразу добавим SSH‑ключ с нашего устройства. Если ключ на машине уже есть, то копируем его и добавляем — обычно он располагается в домашнем каталоге ~/.ssh/id_rsa.pub. Если ключа еще нет, то сгенерируем его командой ssh-keygen.

Сохраняем пароль пользователя root и нажимаем Создать сервер. Теперь в панели управления появился созданный сервер и его публичный IP‑адрес, а это значит, что мы можем подключиться к нему по SSH и настроить.


    ssh root@176.114.67.25

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


    sudo apt-get update
sudo apt-get install git

Воспользуемся официальной инструкцией Docker и для его установки.


    # Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Развертывание компонентов

Первый сервис

Вернемся к нашей инфраструктуре и начнем развертывание. По всем канонам микросервисной архитектуры, а также для изоляции команд и стека при разработке, создадим под каждый из сервисов свой репозиторий в GitLab.

Есть четыре пустых репозитория под:

  • nginx,
  • krakend,
  • users-service (FastApi),
  • delivery-service (FastApi).

Начнем с delivery-service — клонируем репозиторий с приложением. Создадим само приложение на FastAPI и добавим всего один endpoint "/health_check", который всегда возвращает «200 ОК». Такой endpoint необходимо иметь в каждом из сервисов, чтобы на уровне инфраструктуры можно было проверить их работоспособность.


    from fastapi import FastAPI
app = FastAPI()
@app.get("/health_check")
async def heath_check():
    return {"status": "Delivery service started."}

Создадим окружение, а из зависимостей поставим только FastAPI и Uvicorn, зафиксировав их в requirements.txt. Для управления зависимостями и виртуальными окружениями в Python можно использовать poetry:


    fastapi==0.115.4
uvicorn==0.32.0

Создадим Dockerfile для сборки и запуска нашего приложения:


    FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001", "--workers", "4"]

Давайте разберемся, что делает этот код.

  1. Берет образ Python 3.12.
  2. Создает рабочую директорию контейнера.
  3. Копирует файл зависимостей.
  4. Устанавливает зависимости.
  5. Копирует исходный код приложения.
  6.  Добавляет entrypoint — запуск нашего приложения через Uvicorn.

Создадим файл docker-compose.yaml для быстрого запуска:


    version: '3.8'
services:
  web:
    container_name: delivery_service
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8001:8001"
    environment:
      - ENV=dev
    restart: always
    networks:
      - app-network
networks:
  app-network:
    external: true

Давайте проанализируем, как работает этот код:

  1. Создает один веб‑сервис.
  2. Указывает container_name: delivery_service — именно этот container_name будет доменным именем внутри сети Docker при последующем обращении к нему из других контейнеров.
  3. Открывает 8001-й порт приложения наружу для проверки.
  4. Указывает Dockerfile, на основе которого будет собираться образ и параметр restart.
  5. Указывает docker-network, в которой должен жить данный сервис. Это обязательный шаг для межсервисного взаимодействия.

Теперь накидаем супертонкий CI/CD-пайплайн, который позволит автоматизировать развертывание приложения при вносимых изменениях. Будем вытягивать все из общих репозиториев, без пуша своего приложения в registry.

Создадим файл .gitlab-ci.yml и добавим всего один этап в пайплайн — deploy, в котором запустим compose:


    stages:
  - deploy
deploy:
  stage: deploy
  script:
    - echo "Deploying app to remote server..."
    - docker compose up -d --build

Теперь вернемся в GitLab и сделаем еще пару шагов для настройки CI/CD. Создадим runner, который будет работать на стенде и запускать джобы нашего пайплайна. Также добавим этот runner в репозиторий.

Открываем SettingsCI/CDNew project runner, ставим галочку Run untagged jobsCreate runner. Возвращаемся на сервер и запускаем runner на стенде без dind и прочего. Не забываем выдать ему права для доступа к Docker:


    curl -LJO "https://s3.dualstack.us-east-1.amazonaws.com/gitlab-runner-downloads/latest/deb/gitlab-runner_amd64.deb"
dpkg -i gitlab-runner_amd64.deb
gitlab-runner register   --url https://gitlab.com   --token glrt-t3_UN3rAqKQy3nySTdayMwc

Теперь выбираем Shell — вся сборка будет происходить прямо в оболочке.


    sudo usermod -aG docker gitlab-runner
sudo systemctl restart gitlab-runner

Открываем GitLab и видим, что runner зарегистрирован. Попробуем пушнуть приложение и проверить, что все развернулось. Если зайти в репозиторий, то увидим, что сборка началась. Однако во время сборки стрельнула ошибка: Docker не увидел нашу сеть. Зайдем на стенд и создадим ее:


    docker network create app-network

Перезапускаем пайплайн — и все работает!

Зайдем на стенд и посмотрим активные контейнеры, а также попробуем достучаться до нашего приложения по сети:


    http://176.114.67.25:8001/docs

Все отлично!

Второй сервис

По такой же схеме развернем второй сервис. Для начала клонируем репозиторий, скопируем в него все файлы из предыдущего сервиса и немного изменим их. В Dockerfile возьмем другой порт, так как 8001-й уже занят другим сервисом:


    FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

В docker-compose поменяем тот самый container_name и прокинутый наружу порт:


    version: '3.8'
services:
  web:
    container_name: users_service
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - ENV=production
    restart: always
    networks:
      - app-network
networks:
  app-network:
    external: true

Зайдем в GitLabCI/CD и выберем уже созданный runner для этого контейнера. Пушнем изменения и проверим, что пайплайн завершен успешно. Зайдем на стенд по порту 8000 и проверим, запущено ли приложение. Для этого вводим аррес http://176.114.67.25:8000/docs, например, в браузер. Видим, что все работает отлично.

KrakenD

Теперь развернем желанный API‑шлюз, который вполне себе может быть и самописным, но мы возьмем готовое решение с KrakenD.

1. Клонируем репозиторий с KrakenD и добавляем немного конфигов. Файл kraken.json задает настройки нашего шлюза. В нем указываем два простых прокси endpoint для healthcheck. В массиве endpoints добавляем /delivery/health_check — путь, который виден снаружи и проксирует запросы в delivery_service/health_check. Важный параметр — HOST. В нем указываем адрес application server с учетом домена внутри сети Docker, тот самый container_name. Такой же прокси добавим и для сервиса users:


    {
  "version": 3,
  "name": "My API Gateway",
  "port": 8080,
  "cache_ttl": "300s",
  "endpoints": [
    {
      "endpoint": "/delivery/health_check/",
      "method": "GET",
      "backend": [
        {
          "url_pattern": "/health_check/",
          "host": [ "http://delivery_service:8001" ]
        }
      ]
    },
    {
      "endpoint": "/users/health_check/",
      "method": "GET",
      "backend": [
        {
          "url_pattern": "/health_check/",
          "host": [ "http://users_service:8000" ]
        }
      ]
    }
  ]
}

Сделаем дефолтный Dockerfile для запуска KrakenD из конфига:


    FROM devopsfaith/krakend:latest
COPY krakend.json /etc/krakend/config/krakend.json
CMD [ "run", "-c", "/etc/krakend/config/krakend.json" ]

В файле docker-compose.yaml для запуска укажем container_name, а также наши созданные network и Dockerfile для сборки, после чего прокинем порт наружу:


    version: '3.8'
services:
  krakend:
    container_name: krakend
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    networks:
      - app-network
networks:
  app-network:
    external: true

Файл .gitlab-ci.yml остается без изменений, как в предыдущих сервисах.

Зайдем в GitLab, выберем runner для KrakenD и пушнем все в репозиторий. Попробуем постучаться на сервер по порту 8080 и двум endpoint прокси:


    <http://176.114.67.25:8080/delivery/health_check/>
<http://176.114.67.25:8080/users/health_check/>

Все работает. Теперь вернемся в application server и закроем открытые порты для внешнего мира, чтобы в них мог стучаться только KrakenD. Открываем docker-compose в сервисах и удаляем блок с открытыми портами, после чего деплоим. Теперь по портам 8000 и 8001 мы достучаться до приложений не можем, все идет через единую точку входа — KrakenD.

Остался nginx

В листинге ниже делаем следующее. Клонируем и аналогично добавляем Dockerfile, compose и конфиг для CI. Далее создадим файл src/nginx.conf, в котором определим конфигурацию nginx-сервера. Из основного — слушаем порт 80, который браузер подставляет по умолчанию, и делаем один прокси location до KrakenD. Все запросы, начинающиеся с /api-gateway/ будут проксироваться в proxy_pass <http://krakend:8080>; где krakend — тот самый container_name внутри compose KrakenD. C помощью такой регулярки обрежем префикс api-gateway, чтобы запросы в KrakenD шли по нужным URL:


    server {
    listen 80;
    server_name 5.35.4.197;
    charset utf-8;
    location / {
        default_type text/plain;
        return 200 'Тестовый запуск 2';
    }
    # Проксирование запросов к FastAPI на /users
    location /api-gateway/ {
        rewrite ^/api-gateway/(.*)$ /$1 break;
        proxy_pass <http://krakend:8080>;
        # Настройки заголовков проксирования
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Напишем Dockerfile для нашего образа: берем nginx, копируем конфиг, открываем порт и запускаем:


    FROM nginx:alpine
# Копируем конфиг Nginx
COPY src/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Напишем docker-compose, в котором будет один сервис nginx c открытым 80-м портом, подключенным к нашей сетке. Опять зайдем в GitLabCI/CD и добавим runner для данного сервиса. После пушнем изменения и проверим прошел ли пайплайн.

Теперь закроем порт KrakenD, чтобы единой точкой входа во все приложения был nginx. Удаляем ports в docker-compose krakend и деплоим. Адрес http://176.114.67.25:8080 больше недоступен — все идет через http://176.114.67.25/api-gateway.

API-composition

Добавим два метода в наши микросервисы и объединим их при помощи API-GateWay в один.


    # users
@app.get("/users/{user_id}/")
async def get_user_data():
    return {"first_name": "Max.", "last_name": "Iglin"}
# delivery
@app.get("/users/{user_id}/")
async def get_user_deliveries():
    return {
        "deliveries": [{"id": 1, "date": "2024-01-01"}, {"id": 2, "date": "2024-12-01"}]
    }

Задеплоим и добавим в конфиг KrakenD endpoint для композиции API.


    {
  "endpoint": "/user-delivery/{user}/",
  "method": "GET",
  "backend": [
    {
      "url_pattern": "/users/{user}/",
      "host": ["http://users_service:8000"]
    },
    {
      "url_pattern": "/users/{user}/",
      "host": ["http://delivery_service:8001"]
    }
  ]
}

По endpoint http://176.114.67.25/api-gateway/user-delivery/1/ получаем данные из двух сервисов. 

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