Способ организации gRPC контрактов и их автоматизация для микросервисов

от автора

Привет! Меня зовут Данил, я бэкенд разработчик.

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

Что затронуто в данной статье:

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

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

  • генерацию gRPC кода под нужные языка

  • генерацию автодокументации

  • публикация сгенерированных пакетов

Решение

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

  • Contracts-first спецификация сервисов

  • Однозначный способ объявления клиентов под все используемые языки

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

Итак, при contracts-first подоходе встает вопрос, как и где хранить сами .proto файлы. Можно предложить несколько вариантов:

  • единый монорепозиторий под контракты

  • репозиторий контрактов под каждый сервис

  • копирование .proto файлов в каждый сервис

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

Структура проекта следующая — директория с файлами-спецификациями сервисов .proto, разделенные по доменам — назовем их контексты:

proto/ domain_a/ v1/     service_a.proto         version.txt domain_b/ v1/ service_b.proto version.txt

Пример .proto спецификации сервиса:

syntax = "proto3";  package sample_service.foo;  import "google/api/annotations.proto"; import "google/protobuf/wrappers.proto";  // FooService is an example RPC service. service FooService {   // CreateFoo is an example method.   rpc CreateFoo(CreateFooRequest) returns (CreateFooResponse) {     option (google.api.http) = {       post: "/create-foo"       body: "*"     };   }   // GetFoo is an example getter method.   rpc GetFoo(GetFooRequest) returns (GetFooResponse) {     option (google.api.http) = {       get: "/get-foo/{id}"     };   } }   // version.txt: // 1.0.0

Текущий подход позволяет:

  • Делать обратно совместимые изменения контрактов.

  • Придерживаться SemVer семантики

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

Структура проекта. Скрипты на python выбраны из-за его простоты использования:

make.py scripts/ builders/     go.py python.py java.py ...     publishers/         go.py python.py java.py ...

make.py — входная точка скриптов автоматизации.

Usage:   make.py [command] [options]  Commands:   format          Форматирует proto-контракты.   lint            Проверяет контракты на ошибки стиля и валидность.   build           Собирает контракты в указанных целях и контекстах.   publish         Публикует собранные контракты.  Options:   -s, --source    Определяет, для каких языков генерировать код контрактов.   -d, --domain   Определяет, для каких контекстов генерировать код контрактов.   -r, --release   Используется при релизе контракта.

builders и publisher — скрипты для сборки и публикации пакетов

Под каждый язык, нужна своя реализация BaseBuilder:

class Builder(ABC):     @abstractmethod     def pre_build(self):          ...      @abstractmethod     def post_build(self):          ...      @abstractmethod      def build_domain(self, domain: Domain):          ...

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

В основу реализации build_domain берется какой-то из инструментов сборки — например, prototool или buf , и команды по сборке.

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

BasePublisher:

class Publisher(ABC):     @abstractmethod     def publish(self): ...

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

  1. Golang. В этом языке его создатели позаботились о простоте использования зависимостей в Go Modules. Для публикации пакета вам нужен публичный репозиторий Github или Gitlab, где создание версионных тэгов полностью соответствует подходу Go Modules

  2. Python. В Python стандартом для управления зависимостями является использование PyPI (Python Package Index). Для публикации пакета разработчики часто используют такие инструменты, как setuptools или poetry для подготовки пакета и twine для его загрузки. Если требуется хранение пакетов в частном хранилище, применяются решения вроде Nexus или Artifactory.

  3. Java. Управление зависимостями и публикация в Java осуществляется с помощью Maven или Gradle. Основное хранилище для Java-библиотек — Maven Central, а для внутренних проектов часто используются те же Nexus или Artifactory.

Пример CI/CD

.gitlab-ci.yml

workflow:   rules:     - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'       variables:         MAKE_FLAGS: "--release"  stages:   - lint   - build   - test   - publish    lint:   stage: lint   script:     - python3 make.py lint  build:   stage: build   script:     - python3 make.py --source ${SOURCE} --debug   artifacts:     paths:       - generated/proto/${SOURCE}     expire_in: 1 day  publish:   stage: publish   script:     - python3 make.py publish --source ${SOURCE} --debug   needs:     - build

Github Actions

name: CI/CD Pipeline  on:   push:     branches:       - main   pull_request:     branches:       - "**"  jobs:   lint:     steps:       - Install dependencies       - Lint proto files:         script: python3 make.py lint  build:   needs: lint   steps:     - Install dependencies     - Build proto files:       script: python3 make.py build --debug     - Upload artifacts:       path: generated/proto/${{ github.event.repository.name }}  publish:   needs: build   steps:     - Install dependencies     - Publish proto files:       script: python3 make.py publish --debug

Визуализация текущей схемы пайплайна

Визуализация текущей схемы пайплайна

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

Версионирование

Диаграмма иллюстрирует процесс обновления версий контрактов:

  • из ветки master формируется релизная версия (например, 1.0.0 → 1.0.1),

  • из веток для разработки фичей — альфа-версии с временными метками

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

Пример использования зависимостей

Golang

Установка пакета

go get gitlab.yourdomain.com/contracts/generated/go/<context_name>@latest

Так будет выглядеть go.mod файл:

module yourproject  go 1.23  require (     gitlab.yourdomain.com/contracts/generated/go/<context_name> v1.2.3 )

Python

Для Python зависимости указываются в файле pyproject.toml (при использовании Poetry) или requirements.txt.

pyproject.toml

[tool.poetry.dependencies] python = "^3.10" <context-name> = { version = "1.2.3", source = "https://nexus.yourdomain.com/repository/pypi/simple/" }

requirements.txt

<context-name>==1.2.3  --extra-index-url https://nexus.yourdomain.com/repository/pypi/simple/

Java

Java использует Gradle для управления зависимостями. Пример для Gradle (build.gradle):

dependencies {     implementation 'com.yourdomain.contracts.generated:<context-name>:1.2.3' }  repositories {     maven {         url "https://nexus.yourdomain.com/repository/maven-releases/"     } }

Выводы

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

Было предложено решение по автоматизации рутинной работы, упрощении жизни разработчиков и в том числе для самодокументации.

Здесь также есть пространство для улучшения, например, автоматическое обновление зависимостей с помощью Dependabot, или добавление контрактных тестов в отдельную стадию CI/CD

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


ссылка на оригинал статьи https://habr.com/ru/articles/864918/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *