Привет! Меня зовут Данил, я бэкенд разработчик.
На последнем проекте мне выпала удача разрабатывать микросервисную архитектуру в условиях широкого стэка технологий и языков, требующих стандартизации. Это и натолкнуло меня написать статью, в которой я бы хотел предложить способ автоматизации рутинной работы в 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
В заключение, если вы хотите значительно сократить время, затрачиваемое командами на интеграцию сервисов, стоит потратить время на реализацию собственного решения по автоматизации на начальном этапе. Это не только ускорит работу, но и упростит внедрение новых разработчиков и команд.
Комментарии (2)
ptr128
10.12.2024 07:57Мы просто собираем контракты в пакеты, публикуемые в локальный репозиторий вместе с утилитой на C#, позволяющей на основании proto и конфигурационного файла в json генерировать исходный код на нескольких языках для разных случаев. Вплоть до генерации метаданных для MS SQL и PostgreSQL.
Пакеты собираются под .NET, но возможности MSBuild позволяют использовать их в любых языках.
Необходимость именно пакетов обусловлена тем, что в одной ветке git репозитория контрактов могут содержаться контракты не того сочетания версий, которое требуется конкретному микросервису. А через пакеты легко привязываются контракты разных версий.
dos
Не смотрели buf ? Там и сборка контрактов хорошо организована, есть контроль совместимости версий контрактов и в конце концов есть линтинг.