Всем привет! Кратко обо мне: меня зовут Никита и я уже третий год работаю над тем, чтобы платформа СберМаркета становилась лучше день ото дня. Мой основной язык программирования — Go, но, учитывая специфику платформенной разработки, еще и bash.

В этой статье на примерах разберу, что мешает строить разработчикам надежные интеграции, попутно заглядывая в детали реализации нашей утилиты sbm-cli, шаблона микросервиса и CI/CD. Этот материал я написал в соавторстве с моим коллегой Эмилем Шарифуллиным.

В ходе статьи я еще не раз буду упоминать утилиту sbm-cli. Под спойлером оставляю ее краткое описание. Для большего погружения рекомендую ознакомиться с предыдущими статьями о PaaS и sbm-cli в частности. Раз. Два. Три.

Коротко о sbm-cli

sbm-cli — это утилита, разработанная командой платформы СберМаркета для автоматизации рутиных действий разработчиков.

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

Что вы узнаете из статьи?

Какую проблему мы решаем?

Мы в своей работе ориентируемся на метрику Time to Market: совокупное время, затраченное на разработку фичи от идеи до продакшена. За счет оптимизации процесса разработки и тестирования интеграций, мы помогаем снижать количество инцидентов. А это, в свою очередь, позволяет предоставлять более качественный сервис нашим пользователям и экономить деньги компании.

Мысленный эксперимент

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

Давайте договоримся называть сервис, предоставляющий свой API другим сервисам — провайдером. Сервис, который использует API — консумером.

Ситуация #1 Поломка обратной совместимости

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

Как предотвратить инцидент?

Нужно четко понимать отношения между сервисами. Не давать провайдеру ломать обратную совместимость, если хотя бы один сервис уже использует их API. Лучшее решение — проверка обратной совместимости на этапе CI/CD. А еще лучше — узнать это до этапа CI/CD, локально запустив команду.

Ситуация #2 Несогласованные контракты

Разработчик должен написать сервис-консумер. Требуется запросить персональную скидку для покупателя по идентификатору из внешнего сервиса и применить ее на этапе чекаута. Внешний сервис разрабатывает другая команда, поэтому наш герой, собрав в кулак свои софт скиллы, отправляется к разработчикам сервиса-провайдера за подробностями.

Они договариваются заранее о контракте и разрабатывают консумера и провайдера одновременно. И все вроде бы хорошо, тестирование прошло успешно. Но в какой-то момент выясняется, что сервис выдает скидки более 100%.

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

Как предотвратить инцидент?

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

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

Не басня, но притча
Не басня, но притча

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

Способ разработки API курильщика

В среднем, разработка интеграции происходит следующим образом:

  1. Разработчики сервиса-консумера и сервиса-провайдера согласовывают план взаимодействия. Утверждают схему и протоколы.

  2. Разработчик сервиса-провайдера описывает спецификацию API.

  3. Разработчик сервиса-провайдера пересылает спецификацию.

  4. Разработчики пишут и/или генерируют код сервера и клиента согласно спецификации.

  5. Сервисы начинают взаимодействовать на стейджинге или в проде.

  6. Спецификация изменяется разработчиком сервиса-провайдера.

  7. Тут идем на шаг 3 и так по кругу.

Не сложно заметить, что в текущей схеме есть сразу несколько возможностей для создания инцидента «на ровном месте»:

  • Отсутствует механизм «устаревания» спецификации. Нет гарантий, что разработчик поделился корректной и актуальной спецификацией.

  • Написание кода руками по спецификации мало того, что затратно, так еще и чревато появлением багов. Человеческий фактор никто не отменял.

  • Нет шага валидации контрактов при деплое приложения в стейджеое и продовое окружение.

Способ разработки API здорового человека

Вот так должен выглядеть оптимизированный процесс разработки интеграции.

Разработчик сервиса-провайдера:

  1. Описывает спецификацию.

  2. Запускает валидацию локально (проверятся синтаксис и обратная совместимость).

  3. Отправляет спецификацию в репозиторий.

  4. В CI/CD валидация выполняется идентично локальному запуску.

  5. Запускается генерацию сервера локально.

  6. Сервис наполняется логикой.

  7. Запускает деплой на стейдж.

Разработчик сервиса-консумера:

  1. Регистрирует в конфиге своего сервиса зависимость от стороннего сервиса и указывает актуальную ветку.

  2. С помощью одной команды скачивает актуальную (!) спецификацию и генерирует клиента.

  3. Реализует бизнес логику.

  4. Запускает валидацию локально (проверятся синтаксис и обратная совместимость).

  5. Отправляет спецификацию в репозиторий.

  6. В CI/CD валидация выполняется идентично локальному запуску.

  7. Происходит регистрация клиента.

  8. Запускается деплой на стейдж.

Какие плюсы можно увидеть, сравнивая эти два подхода?

Как минимум, коммуникация разработчиков значительно упрощается. Все, что им нужно обсудить это сроки реализации и сам контракт. Спецификация является отправной точкой, технические детали скрываются за интерфейс sbm-cli и CI/CD. Разработчики могут сконцентрироваться на логике приложения.

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

Что такое интеграция?

Разрабатывая приложения в клиент-серверной архитектуре, необходимо обеспечить надежный канал связи (интеграцию) между клиентом и сервером. Cap!

Для построения интеграции между частями распределенного приложения используют интерфейс программирования приложения (или API). API предоставляет доступ к «ручкам» приложения, абстрагируя сложность каждого отдельного компонента.**

Главная задача API — обеспечение работоспособности интеграции компонентов системы, удовлетворяющих описанному контракту или спецификации (это не одно и то же).

☝️ PaaS оперирует абстракциями высокого уровня, будь то Контракт или Спецификация. О них мы поговорим более подробно ниже, а пока, убедимся, что у нас нет разночтений в плане терминологии.

Интеграции вашего приложения почти наверняка организованы одним из следующих способов: REST, gRPC или Kafka (Nats, MQ, etc).

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

  • JSON-over-HTTP;

  • Protobuf-over-RPC;

  • JSON-over-Messaging-System.

На самом деле каждый метод являет собой «слоеный пирог» из нескольких протоколов, технологий, стандартов или фреймворков:

  • REST (или RESTful) это набор соглашений (или архитектурный стиль) проектирования API поверх протокола HTTP, в котором данные передаются в текстовом виде (JSON). Взаимодействие синхронно.

  • gRPC это фреймворк для вызова удаленных процедур или RPC (архитектурный стиль противопостовляемый RESTful), поверх протокола HTTP/2, данные передаются в бинарном формате. Взаимодействие может выполняться синхронно, или, в случае gRPC Streaming, асинхронно.

  • Messaging System (e. g. Kafka, RabbitMQ, etc) это шина данных, основанная на протоколе TCP. Шина данных не обязывает вас использовать какой-то конкретный формат данных. Вызовы выполняются асинхронно.

А теперь, чтобы стало понятнее, представим все это в виде таблицы.

Название

Протокол

Архитектурный стиль

Фреймворк

Формат данных

Асинхронность

REST

HTTP

REST

-

XML, JSON, etc

Нет

gRPC

HTTP/2

RPC

gRPC

Protobuf

Да (для gRPC Streaming)

SOAP

SOAP

SOAP

-

XML

Да

Messaging System

TCP

Messaging

-

XML, JSON, Protobuf

Да

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

Когда использовать синхронный метод, а когда асинхронный?

Ситуации, в которых следует выбрать синхронные интеграции:

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

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

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

Ситуации, в которых следует отдать предпочтение асинхронным интеграциям:

  1. Обработка долгих операций. Если ваши операции требуют значительного времени для выполнения, асинхронные интеграции могут быть более предпочтительными. Например, обработка больших объемов данных или выполнение длительных вычислений.

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

  3. Очереди сообщений. Использование асинхронных сообщений в виде очередей (например, RabbitMQ, Apache Kafka) может быть полезным для разделения компонентов системы и обеспечения более гибкой и масштабируемой архитектуры.

  4. Повышенная масштабируемость. Асинхронные интеграции могут облегчить масштабирование системы, так как они позволяют более гибко управлять нагрузкой.

Когда использовать gRPC, а когда REST?

Для ответа на этот вопрос разберем основные плюсы каждого.

Основные плюсы REST:

  1. Простота и универсальность.

  2. Легкость в понимании и использовании. REST API легко понимать и использовать, особенно с использованием стандартных инструментов, таких как браузеры.

  3. Совместимость с ограниченными сетевыми условиями. REST легко адаптируется к различным условиям сети и обычно лучше себя ведет в случае ограниченной пропускной способности или ненадежной сети.

Основные плюсы gRPC:

  1. Производительность. gRPC обеспечивает высокую производительность за счет использования протокола HTTP/2 и бинарного формата данных Protocol Buffers.

  2. Сильная типизация. Позволяет строить более надежные интеграции.

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

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

Что такое контракт?

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

Можно, конечно, описать контракт с помощью натурального языка, но чаще используется специализированная нотация (или Schema Definition Language, SDL), позволяющая свести количество вариантов трактовки к минимуму.

Примерами языков для описания контрактов могут служить Protobuf и OpenAPI.

Каждый из них имеет свои плюсы и минусы. Каждый имеет личный «зоопарк» в виде генераторов, валидаторов и прочих инструментов автоматизации.

☝️ Это обсуждение выходит далеко за рамки текущей статьи, думаю, мы вернемся к нему в рамках рассказа про контрактное тестирование в PaaS.

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

Для большего погружения советую прочитать статью Яна Робинсона, в которой сопоставляются два этих понятия в контексте контрактных тестов в микросервисной архитектуре.

Когда все точки над i расставлены, договоримся в рамках сегодняшнего рассказа читать «контракт», подразумевать «спецификация». Главное — контракт позволяет описать интеграцию. На основе этого описания можно построить целую экосистему инструментов, дополняющих друг друга. Облегчающих разработку надежных интеграций как минимум. Позволяющих избегать инцидентов как максимум.

Code First vs API First

Подход Code First предполагает, что разработка API начинается с написания кода и реализации бизнес-логики, а затем автоматически генерируется спецификация API на основе этого кода.

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

Что первично? Контракт или код?

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

А спецификация никогда не будет отражать контракт на 100% (если баг пока не нашли, не значит, что его нет).

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

Однако, генерация спецификации из кода имеет ряд существенных недостатков, таких как:

  • отсутствие возможности разрабатывать консумера и провайдера параллельно

  • процесс изменения контракта сложнее и чреват большим количеством багов

  • сложнее автоматизировать процесс разработки и тестирования интеграций

Забегая вперед, для себя мы выбрали API First подход, ставший де-факто стандартом индустрии. Но обо всем по порядку.

Под спойлером можно ознакомиться с примерами описания API простого сервиса с помощью OpenAPI и Protobuf.

Примеры спецификаций
  • Protobuf

syntax = "proto3";

package greeter;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
  • OpenAPI

openapi: 3.0.0
info:
  title: Greeter Service
  version: 1.0.0
paths:
  /greet:
    post:
      summary: Greet a user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string

☝️ У нас в планах большая статья про внутренний стандарт для OpenAPI. Инструменты генерации и валидации OpenAPI. А еще, про портал с документацией для внешних потребителей API СберМаркет. Не пропустите!

Как платформа может помочь разработчикам?

Ну наконец-то :)

- мотивация
- интересы бизнеса
- определения
- теоретизирование
- блабла


-> вы находитесь здесь


- как изменился воркфлоу разработчиков?
- подробности реализации

Выбираем способы интеграций

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

Если кратко, то:

  • внутренние/внешние

  • синхронные/асинхронные

Внутренний ресерч показал следующий набор юзкейсов:

  1. бэкенд для фронтенда и мобильных приложений;

  2. бэкенд для внешних потребителей;

  3. внутренний бэкенд;

    • синхронный;

    • асинхронный.

Для первых двух сценариев выбираем REST. Для третьего: gRPC и Kafka.

Почему именно Kafka для асинхронного внутреннего бэкенда?

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

Почему важно «на берегу» договориться о наборе методов интеграции компонентов распределенной системы:

  • концентрация на поддержке узкого круга технологий позволяет писать более надежные библиотеки;

  • всегда можно найти эксперта в рамках домена или всей компании.

В качестве формата данных для Kafka выбрали Protobuf за экономичность и сильную типизацию. И даже написали инструмент для разработки и дебага — Protokaf.

Где будем хранить описание интеграций?

Стандартизация начинается с шаблона приложения. Неважно какой язык программирования выбран, в PaaS проекте всегда будут директории:

  • configs (с манифестом приложения);

  • api (с набором контрактов сервера);

    • grpc (описание синхронных внутренних “ручек” в формате Protobuf);

    • events (описание структур данных для общений через Kafka в формате Protobuf);

    • openapi (описание синхронных внутренних “ручек” в формате OpenAPI);

  • deps (с набором контрактов сервиса-провайдера);

    • <service-provider-name>;

      • grpc;

      • events;

      • openapi.

Воркфлоу сервиса-консумера

Вспомнить, каким должен быть идеальный воркфлоу разработчика сервиса-консумера (мы обсудили его в самом начале статьи). Если успели подзабыть, добро пожаловать под спойлер.

Воркфлоу разработчика сервиса-консумера

Разработчик сервиса-консумера:

  1. Регистрирует в конфиге своего сервиса зависимость от стороннего сервиса и указывает актуальную ветку.

  2. С помощью одной команды скачивает актуальную (!) спецификацию и генерирует клиента.

  3. Реализует бизнес логику.

  4. Запускает валидацию локально (проверятся синтаксис и обратная совместимость).

  5. Отправляет спецификацию в репозиторий.

  6. В CI/CD валидация выполняется идентично локальному запуску.

  7. Происходит регистрация клиента.

  8. Запускается деплой на стейдж.

А теперь пройдемся по каждому шагу (кроме написания бизнес логики) и прочувствуем разницу.

Как добавлять провайдеров в сервис-консумер?

В манифесте приложения (app.toml).

В секции dependencies описываем все сервисы-провайдеры. Указываем тип интеграции (grpc|openapi|events) и ветку с нужной версией котрактов.

[dependencies]
  [[dependencies.services]]
    name = "alpha"
    branch = "master"
    grpc = ["system"]
    repository = "https://gitlab/paas/alpha

Платформа не была бы платформой, если бы не написала линтер на определение расхождения реально существующих зависимостей и зависимостей объявленных в манифесте ????.

Линтер доступен для запуска как в локальном окружении с помощью sbm-cli, так и в Gitlab CI/CD.

sbm-cli dependency check
✘ The declared and downloaded dependencies have a difference

Issues:
- The openapi contract `greeter` from service `bravo` should be declared

What to do:
- To fix obvious errors visit https://wiki...
- To download dependencies automatically run `sbm-cli dependencies upgrade --manifest configs/app.toml`
- To declare PaaS dependencies, fill in the `dependencies` section in configs/app.toml
Полный интерфейс команды кладу под спойлер
sbm-cli dependency check -h
Check difference between declared dependencies from the app.toml/values.yaml and downloaded contracts in the deps directory.

Examples:
  sbm-cli dependency check

The <manifest_filename.toml> is a service manifest filename (default "configs/app.toml")

Usage:
  sbm-cli dependency check [flags]

Flags:
  -h, --help                   help for check

Global Flags:
      --env-file string   Path to .env file. Equivalent to 'source ./path/to/.env; sbm-cli command run'
      --manifest string   Service manifest filename (default "configs/app.toml")
  -s, --silent            Silent mode
  -v, --verbose           Verbose mode

Ну не руками же добавлять зависимости и их контракты в самом деле?

Конечно. Команда dependency add сделает это за разработчика. Зарегистрирует зависимость в манифесте приложения, скачает из репозитория нужные контракты (с учетом указанной ветки сервиса-провайдера) в папку deps.

И, по желанию, сгенерирует клиента на их основе.

Но об этом чуточку позже.

$ sbm-cli dependency add https://gitlab/paas/alpha --grpc=system
✔ Added, updated and generated code `alpha` service dependency

Changes:
- Downloaded contract `system`, service `alpha`, version `master`
- Changed pkg/clients/alpha/grpc/system

Next steps:
- To use gRPC-clients https://wiki/...
- To use OpenAPI-clients https://wiki/...
Полный интерфейс команды кладу под спойлер
sbm-cli dependency add -h
Add dependency to the manifest, check availability contracts, download contract and generate code

Examples:
  sbm-cli dependency add https://gitlab.com/paas/baugi --grpc=baugi
  sbm-cli dependency add https://gitlab.com/paas/baugi --grpc=baugi --till-add
  sbm-cli dependency add https://gitlab.com/paas/baugi --grpc=baugi --till-upgrade
  sbm-cli dependency add /full/local/path/to/service --grpc=baugi
  sbm-cli dependency add ../service --grpc=baugi

The <repository> argument is the url of repository

Usage:
  sbm-cli dependency add [<repository>] [flags]

Flags:
      --branch string         Specify branch of dependency (default "master")
      --events stringArray    Add events contract to dependency manifest
      --grpc stringArray      Add grpc contract to dependency manifest
  -h, --help                  help for add
      --openapi stringArray   Add open-api contract to dependency manifest
      --till-add              Add behavior that only allows you to add a dependency (mutually exclusive with --upgrade)
      --till-upgrade          Upgrade behavior that only allows you to add and download a dependency (mutually exclusive with --add)

Global Flags:
      --env-file string   Path to .env file. Equivalent to 'source ./path/to/.env; sbm-cli command run'
      --manifest string   Service manifest filename (default "configs/app.toml")
  -s, --silent            Silent mode
  -v, --verbose           Verbose mode

А что делать если провайдер опубликовал новую версию контракта?

Запустить команду dependency upgrade.

sbm-cli dependency upgrade
✔ Service contracts have been updated and regenerated

Changes:
- Downloaded contract `events`, service `bravo`, version `master`
- Downloaded contract `dummy`, service `ctrlz`, version `master`
- Changed pkg/clients/ctrlz/grpc/dummy
- Changed pkg/clients/mincer/grpc/events

Next steps:
- To add contracts run ` sbm-cli dependency add `
- To add contracts of services manually visit https://wiki...
Полный интерфейс команды кладу под спойлер
sbm-cli dependency upgrade -h
Upgrade service dependency contracts.

Examples:
  sbm-cli dependency upgrade
  sbm-cli dependency upgrade mainservice

The <name> argument is the name of registered service

Usage:
  sbm-cli dependency upgrade [<name>] [flags]

Flags:
      --download   Download contracts without re-generation
  -h, --help       help for upgrade

Global Flags:
      --env-file string   Path to .env file. Equivalent to 'source ./path/to/.env; sbm-cli command run'
      --manifest string   Service manifest filename (default "configs/app.toml")
  -s, --silent            Silent mode
  -v, --verbose           Verbose mode

А теперь к самому интересному.

Как сгенерировать код?

Для этого у нас есть команда, не побоюсь этого слова, — комбайн (codegen).

В данный момент она работает с сервисами на Go и Python. В планах еще Ruby и JS.

Она умеет генерировать следующее:

  • код сервера (gRPC, OpenAPI);

  • код клиента (gRPC, OpenAPI);

  • код стабов — функции эндпоитов, которые остается наполнить бизнес логикой (gRPC, OpenAPI);

  • код Kafka консумеров (events);

  • документацию в виде сервера (OpenAPI).

Так выглядит ее полный интерфейс:

sbm-cli codegen -h
Generate service code.

Examples:
  sbm-cli codegen # the same as --servers --server-stubs --clients
  sbm-cli codegen --servers --server-stubs --clients
  sbm-cli codegen --clients
  sbm-cli codegen --servers --swagger
  sbm-cli codegen --consumer-stub <service_name> --consumer-catalog internal/<service_name>/consumers

Usage:
  sbm-cli codegen [flags]

Flags:
      --clients                   Generate clients
      --consumer-catalog string   Kafka consumer output path (Required flag: --consumer-stub)
      --consumer-stub string      Generate kafka consumer with name
  -h, --help                      help for codegen
      --server-stubs              Generate server stubs
      --servers                   Generate servers
      --swagger                   Add swagger JSON to servers

Global Flags:
      --env-file string   Path to .env file. Equivalent to 'source ./path/to/.env; sbm-cli command run'
      --manifest string   Service manifest filename (default "configs/app.toml")
  -s, --silent            Silent mode
  -v, --verbose           Verbose mode

Так можно сгенерировать клиентов:

sbm-cli codegen --clients
✔ Clients generated successfully

Changes:
- Changed pkg/clients/ctrlz/grpc/dummy
- Changed pkg/clients/bravo/grpc/events

Next steps:
- To use gRPC-clients https://wiki...
- To use OpenAPI-clients https://wiki...
- Check your project changes and commit to git repository.
- To get details run command with --verbose option

Воркфлоу сервиса-провайдера

Вспомнить target state воркфлоу разработчика сервиса-провайдера можно под спойлером ниже.

Воркфлоу разработчика сервиса-провайдера

Разработчик сервиса-провайдера:

  1. Описывает спецификацию.

  2. Запускает валидацию локально (проверятся синтаксис и обратная совместимость).

  3. Отправляет спецификацию в репозиторий.

  4. В CI/CD валидация выполняется идентично локальному запуску.

  5. Запускается генерацию сервера локально.

  6. Сервис наполняется логикой.

  7. Запускает деплой на стейдж.

Как валидировать контракты?

Допустим, что контракт уже готов. Следующий шаг — проверить его корректность прежде чем, он попадет в репозиторий.

Для этого у нас есть команда contracts verify. Для Protobuf и OpenAPI.

sbm-cli contract verify --type protobuf
✘ Contracts have issues

Issues:
- 1. protobuf, breaking change, "api/grpc/enums.proto", type: FILE_NO_DELETE, message: file api/grpc/enums.proto was removed
- 2. protobuf, breaking change, "system.proto", type: FIELD_SAME_TYPE, message: Field "3" on message "EntityByIDResponse" changed type from "enums.Status" to "string".

What to do:
- To get help about versioning gRPC visit https://wiki/x/...
- To get more info about OpenAPI and REST API Standard's linting, visit https://wiki/x/...
sbm-cli contract verify --type openapi
✘ Contracts have issues

Issues:
- 1. OpenAPI, breaking changes, "api/openapi/system.yaml", contract has 1 breaking changes
  error [api-path-removed-without-deprecation] в API GET /entities/permissions
        api path удалён без процедуры deprecation
- 2. OpenAPI, sm-status-codes-get, "api/openapi/system.yaml", (131:14)
  Disallowed status code for the GET method.; Path: paths./entities/teapot.get.responses.418

What to do:
- To get help about versioning gRPC visit https://wiki/x/...
- To get more info about OpenAPI and REST API Standard's linting, visit https://wiki/x/...

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

Полный интерфейс команды кладу под спойлер
sbm-cli contracts verify -h
Validate local contracts and verify them for breaking changes with master branch in gitlab.

Examples:
  sbm-cli contract verify

Usage:
  sbm-cli contract verify [flags]

Flags:
      --allow-no-contracts          Do not finish with error if there are no contracts found
  -h, --help                        help for verify
      --type strings                Contract type to verify. Possible values: protobuf, openapi, api-products-openapi. By default all contracts will be verified. Required if verification type is specified
      --verification-type strings   Verification type. Possible values for protobuf (grpc and events) contracts: breaking-changes; for OpenAPI contracts: spec, breaking-changes.

Global Flags:
      --env-file string   Path to .env file. Equivalent to 'source ./path/to/.env; sbm-cli command run'
      --manifest string   Service manifest filename (default "configs/app.toml")
  -s, --silent            Silent mode
  -v, --verbose           Verbose mode

В PaaS помимо валидации Protobuf и OpenAPI спецификаций есть еще отдельный тип проверок — api-products-openapi. Вкратце, этот тип валидации позволяет убедиться, что API сервисов-провайдеров для внешних систем (не СберМаркет) соответсвуют внутренним стандартам платформы. Об этом мы тоже готовим отдельный объемный материал.

Как сгенерировать код?

Здесь ничего нового по отношению к воркфлоу разработчика сервиса-консумера. Команда codegen.

sbm-cli codegen --servers
✔ Servers generated successfully

Changes:
- Changed pkg/server/events/system
- Changed pkg/server/grpc/system
- Changed pkg/server/openapi/system

Next steps:
- To add gRPC-server https://wiki/x/...
- To check gRPC-service available locally run `grpcurl --plaintext localhost:3009 list`
- To add HTTP-server based on OpenAPI https://wiki/x/...
- To use gRPC-encoded kafka events https://wiki/x/...
- Check your project changes and commit to git repository.
- To get details run command with --verbose option

Детали реализации

Вся работа с контрактами происходит из утилиты sbm-cli. Мы пишем скрипты доставки необходимых зависимостей на компьютеры пользователей. Регулярно проверяем и обновляем зависимости. Все это помогает быстро настроить окружение для работы с контрактами и контролировать устаревание зависимостей в локальном окружении разработчиков.

В sbm-cli встроен механизм проверки появления более свежих версий. Утилита обновляется автоматически при появлении нового тега в репозитории. sbm-cli содержит следующие зависимости для работы со спецификациями. Некоторые из них мы обсудим далее.

Название

Тип контракта

Тип инструмента

Описание

github.com/getkin/kin-openapi

OpenAPI

Библиотека

Модель OpenAPI спецификации и методы для работы с ней

github.com/deepmap/oapi-codegen

OpenAPI

Библиотека

Инструмента для генерации кода

github.com/tufin/oasdiff

OpenAPI

Библиотека

Инструмент для выявления ломающих изменений

protoc-gen-openapiv2

OpenAPI

Утилита

Плагин для protoc. Генерирует OpenAPI на основе Protobuf

github.com/jhump/protoreflect

Protobuf

Библиотека

Модель Protobuf и методы для работы с ней

github.com/protocolbuffers/protobuf

Protobuf

Утилита

Компилятор (protoc)

github.com/golang/protobuf

Protobuf

Утилита

Плагин для protoc. Генерирует модели Go из Protobuf

github.com/grpc/grpc-go

Protobuf

Утилита

Плагин для protoc. Генерирует Go сервер из Protobuf

github.com/grpc-ecosystem/grpc-gateway

Protobuf

Утилита

Плагин для protoc. Генерирует REST reverse proxy для Go сервера

grpc-go-stubs

Protobuf

Утилита

Внутренний плагин для protoc. Генерирует функции, которые назначаются эндпоинтам (или стабы)

buf.build

Protobuf

Утилита

Утилита для валидации Protobuf. Используется для проверки обратной совместимости

Генерация кода на основе контрактов

Зачем нужна генерация кода мы уже выяснили, осталось обсудить реализацию для OpenAPI и Protobuf.

В СберМаркет генерация выполняется с помощью команды codegen.

sbm-cli codegen

Что происходит после вызова команды? Рекурсивно вычитываются пользовательские и библиотечные контракты.

Далее происходит генерация серверов, клиентов, документации, etc.

  1. Для OpenAPI хранятся кастомные шаблоны для каждого языка программирования, сгенерировать код из шаблонов помогает библиотека oapi-codegen.

  2. Для Protobuf мы используем proto compiler. Компилятор занимается созданием дескрипторов на основе описанных proto-файлов.

Компилятор занимается созданием дескрипторов на основе описанных proto-файлов.

Дескрипторы контрактов в Protocol Buffers представляют собой структуры данных, описывающие структуру сообщений и сервисов, определенных в этих файлах.

Компилятор Protocol Buffers (после построения дескрипторов из proto-файлов) позволяет динамически создавать или изменять сущности определенные в proto-файлах. Также компилятор имеет систему плагинов, которые позволяют генерировать исходный код для разных языков программирования. Для проектов на Go мы используем следующий набор плагинов:

  • protoc-gen-go-grpc;

  • protoc-gen-go;

  • protoc-gen-grpc-gateway;

  • protoc-gen-openapiv2;

  • protoc-gen-grpc-go-stubs.

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

Если вам понадобится написать собственный плагин для protoc, советую обратить внимание на эту статью.

Если перед генерацией кода из proto-файлов нужно их видоизменить — может пригодиться библиотека github.com/jhump/protoreflect. Она позволяет загружать дескрипторы из proto-файлов без использования компилятора. А затем видоизменять и экспортировать их.

После вызова команды codegen для всех контрактов сервиса, в дереве проекта появятся сгенерированные файлы на языке проекта, в котором вызывается команда codegen.

$ tree pkg/server
pkg/server
├── events
│   └── system
│       └── system.pb.go
├── grpc
│   └── system
│       └── system.pb.go
└── openapi
    └── system
        ├── server.go
        ├── spec.go
        └── type.go

Валидация контрактов

Валидация контрактов происходит в два этапа:

  • проверка синтаксической корректности контрактов

  • проверка обратной совместимости контрактов

Что такое обратная совместимость контрактов и как ей управлять в своей статье рассказывает Саша Сусиков. Рекомендую обратить внимание.

Валидация синтаксиса

sbm-cli contracts verify --verification-type spec

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

Тут все просто. Если контракт синтаксически некорректен, по нему не получится сгенерировать код сервера и клиента. Для проверки контрактов на синтаксическую корректность сообществом разработано множество утилит.

Как известно, OpenAPI часто критикуют за «излишнюю» гибкость. Именно поэтому для OpenAPI стандарта у нас в компании приняты более строгие правила.

Например, проверяется, что разработчик указал один из разрешенных кодов ответа, регистр заголовков и параметров. Более того, проверяется содержание некоторых полей.И, если с точки зрения OpenAPI спецификация корректна, далеко не факт, что она удовлетворяет всем правилам принятым в PaaS.

Пример некорректного кода ответа с точки зрения платформы СберМаркета:

responses:
  "418":
    description: "Returns I am a Teapot"
    content:
      text/plain:
        schema:
          type: string
        example: |
          I am a Teapot

Валидация обратной совместимости

Что такое обратно несовместимые изменения? Это изменения, которые делают новую версию контракта несовместимой с предыдущими версиями.

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

Вот несколько примеров обратно несовместимых изменений, которые могут возникнуть при изменении контрактов (на примере Protobuf):

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

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

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

  4. Изменение порядка полей Если изменяется порядок полей в сообщении, это может привести к ошибкам в разборе данных, так как структура данных не будет соответствовать ожиданиям старых программ.

  5. Изменение формата данных Если вы меняете формат данных, например, переходите от целых чисел к числам с плавающей запятой или изменяете кодировку, это также может вызвать несовместимость.

  6. Изменение семантики Изменение семантики сообщений или служб может привести к тому, что старые программы будут неправильно взаимодействовать с новыми данными.

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

  • добавление новых полей;

  • поддержка устаревших полей;

  • использование необязательных полей.

Важно внимательно следить за совместимостью при внесении изменений в контракты, чтобы избежать проблем при обновлении версий программ.

В СберМаркет разработчик может проверить контракт на наличие ломающих изменений с помощью команды contracts verify.

sbm-cli contracts verify --verification-type breaking-changes

Для проверки обратной совместимости контрактов мы используем утилиту Buf.

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

CI/CD

CI/CD является склеивающим слоем, без которого вся экосистема не смогла бы функционировать должным образом.

Допустим, что разработчики договорились между собой и написали корректную интеграцию. Далеко не факт, что перед отправкой в репозиторий не произойдет изменение кода или контракта. Что естественно повлечет за собой инцидент, простой, грустные лица NOC инженеров. Оно нам не надо.

Именно поэтому мы запускаем множество линтеров в каждом пайплайне.

Проверяем все, что описано выше:

  • регистрацию зависимостей;

  • синтаксические ошибки;

  • ломающие изменения;

  • да даже расширения OpenAPI файлов (придумал же кто-то и .yml и .yaml одновременно).

При этом, важно отметить, что все проверки легко воспроизводятся локально. Это означает, что разработчикам необязательно запускать пайплайн, чтобы понять, что знание Protobuf синтаксиса у них хромает (например).

Huginn (PaaS-портал разработчика)

Ну и как «вишенка на торте». Декларирование отношений сервисов через манифест приложения (секция dependecies) позволяет нам отображать интерактивную карту взаимодействия сервисов.

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

Заключение

Я описал, каким образом мы помогаем продуктовым сервисам, живущим в платформе СберМаркета, взаимодействовать с другими сервисами и избавляем разработчиков от необходимости проводить рутинные действия вручную, и тем самым уменьшаем Time to Market.

Резюмируя, сформулирую принципы, которые смогут помочь вам взять под контроль работу с интеграциями:

  • Используйте принцип API-first. В связке с хорошим набором инструментов он позволит очень сильно упростить разработку межсервисного взаимодействия и позволит автоматизировать рутинные действия. Результат —  снижение количества инцидентов и повышение качества сервиса.

  • Договоритесь, как хранить контракты в своей компании. Определите базовый шаблон сервиса и храните контракты единообразно. Это позволит вам, даже не имея самописных утилит автоматизации, написать простые инструкции по работе с контрактами и создать типовые баш скрипты для вызова валидации или кодогенерации. А еще заметно снизит когнитивную нагрузку при переходе от проекта к проекту.

  • Используйте кодогенерацию. Она избавит вас от ошибок, возникающих от невнимательности во время реализации контракта в коде.

  • Проводите валидацию контрактов. Ошибка, найденная во время разработки, гораздо дешевле, чем ошибка, найденная во время инцидента на проде.

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

Но! Когда на противоположной чаше весов стабильность и пользовательский опыт наших клиентов, приходится делать над собой усилия и совершенствоваться. Главное, предоставить инженерам удобное альтернативное решение, с чем, на мой взгляд, мы успешно справляемся.

Надеюсь, статья будет вам полезна. Возможно, кто-то даже вдохновится при реализации собственной платформы. Спасибо за ваше внимание и до встречи! Буду рад ответить на вопросы в комментариях.

Tech-команда СберМаркета ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на  YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.

Комментарии (5)


  1. aceofspades88
    24.11.2023 18:24

    Не сложно заметить, что в текущей схеме есть сразу несколько возможностей для создания инцидента «на ровном месте»:

    • Отсутствует механизм «устаревания» спецификации. Нет гарантий, что разработчик поделился корректной и актуальной спецификацией.

    • Написание кода руками по спецификации мало того, что затратно, так еще и чревато появлением багов. Человеческий фактор никто не отменял.

    • Нет шага валидации контрактов при деплое приложения в стейджеое и продовое окружение.

    1. но ведь и в вашем варианте нет гарантий

    2. никто руками их и не пишет, просто генерируют командами из "коробки"

    3. логичнее их валидировать при генерации тогда?


    1. nikitych1 Автор
      24.11.2023 18:24

      Добрый вечер! В обновленном воркфлоу на каждый коммит запускается валидация в CI/CD. Поэтому контракты всегда в актуальном состоянии. Иначе разработчик просто не может смержить изменения в мастер ветку. И тем более задеплоиться в прод.


  1. lesha108
    24.11.2023 18:24

    Честно говоря, я надеялся, что в ИТ сбера люди слышали, что такое процессы разработки софта. Управление требованиями, изменениями, конфигурациями уж десятки лет используются, всякие стандарты на эту тему написаны. А тут опять про софт-скиллс история, команды не договорились, не сообщили и т.п. Очередное "открытие" контрактного программирования, TDD, валидации, QA и т.п.


    1. nikitych1 Автор
      24.11.2023 18:24

      Здравствуйте! You live — you learn, как говорится ;)


  1. SkU4
    24.11.2023 18:24

    Очередная статья про sbm-cli, когда уже open source сделаете?