Привет! Меня зовут Данил, я бэкенд разработчик.
На последнем проекте мне выпала удача разрабатывать микросервисную архитектуру в условиях широкого стэка технологий и языков, требующих стандартизации. Это и натолкнуло меня написать статью, в которой я бы хотел предложить способ автоматизации рутинной работы в 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): ...
При публикации пакетов, отличия в значительной степени определяются тем, какой репозиторий хранения кода, и какая система управления зависимостей кодом взята за стандарт для конкретного языка программирования.
-
Golang. В этом языке его создатели позаботились о простоте использования зависимостей в Go Modules. Для публикации пакета вам нужен публичный репозиторий Github или Gitlab, где создание версионных тэгов полностью соответствует подходу Go Modules
-
Python. В Python стандартом для управления зависимостями является использование PyPI (Python Package Index). Для публикации пакета разработчики часто используют такие инструменты, как setuptools или poetry для подготовки пакета и twine для его загрузки. Если требуется хранение пакетов в частном хранилище, применяются решения вроде Nexus или Artifactory.
-
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/
Добавить комментарий