Эффективное создание и деплой gRPC API с помощью GitHub Actions и Packages для проекта на Kotlin и React

—

от автора

В этом посте я покажу, как с помощью GitHub Actions легко реализовать генерацию и публикацию gRPC API пакетов в GitHub Packages, в реестрах Apache Maven и npm. Если вы хотите освоить GitHub Packages для своих проектов и научиться генерировать gRPC API для сервисов на Kotlin/Java и gRPC-web клиентов — добро пожаловать под кат.

Введение

Во время подготовки к докладу JPoint у меня возникла идея создать тестовый стенд, включающий веб-клиент и бэкенд-приложение. Этот стенд позволил бы наглядно демонстрировать эффективность различных стратегий выполнения SQL-запросов с пагинацией. Для взаимодействия между клиентом и бэкендом я решил использовать gRPC. Мне показалась интересной такая реализация взаимодействия между сервером и веб-клиентом, а также генерация и публикация gRPC API пакетов с помощью GitHub Actions и GitHub Packages. Поэтому я решил поделиться этим опытом с читателями Хабра.

Выбор технологического стека и особенности

Для реализации я использовал Kotlin, однако все представленные здесь примеры также могут быть адаптированы для Java.

Я выбрал Spring и Kotlin для бэкенда, а также React с TypeScript для клиентской части, что облегчило реализацию нужного функционала. Выбранный для взаимодействия gRPC — это открытый фреймворк Google, который позволяет вызывать удаленные процедуры (RPC) между клиентом и сервером, используя Protocol Buffers как язык описания интерфейса. gRPC имеет ряд преимуществ:

  • высокая производительность благодаря использованию бинарного протокола HTTP/2;

  • строгая типизация Protocol Buffers, которая отлично подходит для дизайна API и снижает вероятность ошибок при взаимодействии сервисов;

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

Для клиентской части, работающей в браузере, я использовал gRPC-web, поскольку браузеры не поддерживают обычный gRPC. gRPC-web позволяет взаимодействовать с обычными gRPC-сервисами из браузера. Пока что gRPC-web поддерживает только два режима взаимодействия: унарные вызовы и server-side стриминг.

Автоматизация

Мне показалось неудобным вручную создавать и подключать сгенерированные gRPC API пакеты. Поэтому я решил автоматизировать этот процесс через GitHub Actions и GitHub Packages. Дополнительно, чтобы гарантировать обратную совместимость изменений в API и следование официальному style guide от Google для .proto файлов, я внедрил проверки с помощью protolock и protolint.

О выборе GitHub Packages

Привлекательность использования GitHub Packages совместно с GitHub Actions, по моему мнению, заключается в следующем:

  • простота настройки и деплоя пакетов в GitHub Packages в сравнении с Maven Central;

  • централизация всех ресурсов проекта (код, CI/CD пайплайны, пакеты) на GitHub, что упрощает управление проектом;

  • бесплатный тариф, включающий приватное хранение репозиториев и пакетов, а также их деплой.

Все эти преимущества делают GitHub Actions и Packages хорошим решением, особенно для Pet-проектов. Тем не менее есть и недостаток: в отличие от Maven Central, для скачивания опубликованных пакетов требуется GitHub-аккаунт и токен с правами на чтение пакетов. Как получить этот токен, я опишу ниже. 

Реализация

Для начала создадим Gradle-проект с использованием Kotlin DSL и добавим .proto файлы.

Proto-файл API проекта:

syntax = "proto3";  package com.arvgord.api.grpc.bankdemo.v1;  import "bankdemo/v1/messages/client_list_item.proto"; import "bankdemo/v1/messages/extracting_strategy.proto"; import "bankdemo/v1/messages/page_request.proto"; import "google/protobuf/wrappers.proto";  // Get client list request message GetClientListRequest {  // Current page  PageRequest page_request = 1;  // Extracting strategy  ExtractingStrategy extracting_strategy = 2; }  // Get client list response message GetClientListResponse {  // Clients  repeated ClientListItem clients = 1;  // Total number of clients  google.protobuf.Int64Value total_clients = 2;  // Total number of pages  google.protobuf.Int32Value total_pages = 3; }  // Service BankDemo service BankDemo {  // Get client list  rpc GetClientList(GetClientListRequest) returns (GetClientListResponse); }

Пример .proto файла сообщения PageRequest:

syntax = "proto3";  package com.arvgord.api.grpc.bankdemo.v1;  import "google/protobuf/wrappers.proto";  // Page message PageRequest {  // Number of clients on page  google.protobuf.Int32Value page = 1;  // Page size  google.protobuf.Int32Value size = 2; }

Настройка зависимостей проекта

Для управления версиями плагинов и библиотек проекта добавим в корень проекта файл gradle.properties с версиями зависимостей:

kotlinVersion=1.9.10 protobufPluginVersion=0.9.4 protobufKotlinVersion=3.24.4 grpcProtobufVersion=1.58.0 grpcKotlinVersion=1.4.0

Настроим файл settings.gradle.kts, в котором укажем название проекта:

rootProject.name = "bank-demo-api"

А также настроим менеджмент плагинов:

pluginManagement {    val kotlinVersion: String by settings    val protobufPluginVersion: String by settings    plugins {        kotlin("jvm") version kotlinVersion        id("com.google.protobuf") version protobufPluginVersion    }    repositories {        gradlePluginPortal()    } }

Версии плагинов, которые определены в settings.gradle.kts с помощью переменных, указанных в gradle.properties, автоматически используются в build.gradle.kts. Что избавляет от необходимости указывать их вручную.

Настроим файл build.gradle.kts. Добавим плагины:

plugins {    kotlin("jvm")    id("com.google.protobuf")    id("maven-publish") }

Эти плагины необходимы для компиляции проекта, .proto файлов и публикации пакетов в Apache Maven registry GitHub.

Добавим группу и версию библиотеки API, которые будут необходимы для публикации пакета:

group = "com.arvgord" version = "0.0.1"

Название проекта name для публикации пакета будет взято из файла settings.gradle.kts. В итоге после публикации пакет будет выглядеть так: com.arvgord:bank-demo-api:0.0.1.

Укажем зависимости проекта, необходимые для добавления поддержки gRPC и Protocol Buffers для Kotlin:

dependencies {    implementation("io.grpc:grpc-kotlin-stub:${property("grpcKotlinVersion")}")    implementation("io.grpc:grpc-protobuf:${property("grpcProtobufVersion")}")    implementation("com.google.protobuf:protobuf-kotlin:${property("protobufKotlinVersion")}") }

Настройка protobuf плагина

Настроим плагин protobuf, чтобы генерировать код на основе .proto файлов:

protobuf {    protoc {        artifact = "com.google.protobuf:protoc:${property("protobufKotlinVersion")}"    }    plugins {        id("grpc") {            artifact = "io.grpc:protoc-gen-grpc-java:${property("grpcProtobufVersion")}"        }        id("grpckt") {            artifact = "io.grpc:protoc-gen-grpc-kotlin:${property("grpcKotlinVersion")}:jdk8@jar"        }        id("protoc-gen-js") {            path = projectDir.path.plus("/tools/protoc-gen-js-3.21.2-linux-x86_64")        }        id("protoc-gen-grpc-web") {            path = projectDir.path.plus("/tools/protoc-gen-grpc-web-1.4.2-linux-x86_64")        }    }    generateProtoTasks {        all().forEach {            it.plugins {                id("grpc")                id("grpckt")                id("protoc-gen-js") {                    option("import_style=commonjs,binary")                }                id("protoc-gen-grpc-web") {                    option("import_style=commonjs+dts,mode=grpcweb")                }            }            it.builtins {                id("kotlin")            }        }    } }

Рассмотрим секцию plugins: 

  • Плагины grpc и grpckt используются для генерации Java-кода, необходимого для сериализации/десериализации данных, а также создания серверного и клиентского gRPC кода на Kotlin. 

  • Плагин protoc-gen-js необходим для генерации JavaScript кода на основе .proto файлов для сериализации/десериализации данных. Необходимо загрузить плагин и указать его расположение.

  • Плагин protoc-gen-grpc-web позволяет генерировать код вызывающий gRPC-сервисы из веб-приложений. Этот плагин также необходимо загрузить и указать путь к расположению.

В секции generateProtoTasks определим задачи для генерации кода на основе .proto файлов. protoc-gen-grpc-web плагин позволяет генерировать как JS, так и TypeScript код. Так как мне необходимо было генерировать TypeScript код для вызова gRPC сервисов в options protoc-gen-js и protoc-gen-grpc-web, я использовал настройки import_style=commonjs,binary и import_style=commonjs+dts,mode=grpcweb. Вы можете использовать другие настройки.

Настройка публикации Maven-артефакта

Файл build.gradle.kts имеет следующие настройки:

publishing {    repositories {        maven {            name = "GitHubPackages"            url = uri("https://maven.pkg.github.com/arvgord/bank-demo-api")            credentials {                username = System.getenv("GITHUB_ACTOR")                password = System.getenv("GITHUB_TOKEN")            }        }    }    publications {        create<MavenPublication>("maven") {            from(components["kotlin"])        }    } }

В URL репозитория (https://maven.pkg.github.com/OWNER/REPOSITORY), куда планируется опубликовать пакет, необходимо заменить OWNER на имя вашего аккаунта на GitHub и REPOSITORY на имя вашего репозитория. В качестве username и password используются переменные среды GITHUB_ACTOR и GITHUB_TOKEN. Они будут автоматически подставлены при выполнении в GitHub Actions.

Настройка публикации npm-пакета

Для публикации npm-пакета я решил использовать отдельную директорию npm_package в корне проекта, содержащую только файл package.json с конфигурацией публикации. В build.gradle.kts необходимо добавить задачу для копирования сгенерированных TypeScript и JS файлов в директорию npm_package:

tasks.register<Copy>("buildAndCopy") {    from(        projectDir.path.plus("/build/generated/source/proto/main/protoc-gen-js"),        projectDir.path.plus("/build/generated/source/proto/main/protoc-gen-grpc-web")    )    into(projectDir.path.plus("/npm_package/")) }

Далее приступим к настройке файла package.json, содержащего конфигурацию для публикации npm-пакета:

{  "name": "@arvgord/bank-demo-api",  "version": "0.0.1",  "description": "Generated typescript files for gRPC-web bank-demo-client application",  "repository": {    "type": "git",    "url": "https://github.com/arvgord/bank-demo-api.git"  },  "dependencies": {    "grpc-web": "^1.4.2",    "google-protobuf": "^3.21.2"  } }

В этом файле:

  • name определяет пространство имен и уникальное имя пакета;

  • version указывает текущую версию пакета;

  • description предоставляет краткое описание содержимого и предназначения пакета;

  • repository указывает местоположение репозитория пакета;

  • dependencies содержит список зависимостей, необходимых для работы пакета.

Настройка Action для публикации в GitHub Packages

Для файлов GitHub Actions необходимо в корне проекта создать директории .github/workflows.

Создадим файл конфигурации для публикации пакетов publish_packages.yml в директории .github/workflows:

name: Publish bank-demo-api packages on:   workflow_dispatch:  jobs:  publish:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-java@v3        with:          java-version: '8'          distribution: 'corretto'      - name: Build packages        run: ./gradlew buildAndCopy      - name: Publish Kotlin gRPC API        run: ./gradlew publish        env:          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}      - uses: actions/setup-node@v3        with:          node-version: '20.x'          registry-url: 'https://npm.pkg.github.com'          scope: '@arvgord'      - name: Publish bank-demo-client gRPC API        run: |          cd ./npm_package          npm i          npm publish        env:          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Разберем содержимое этого файла:

  • workflow_dispatch: позволяет запускать workflow вручную из интерфейса GitHub в разделе Actions.

  • actions/checkout@v4: клонирование кода репозитория.

  • setup-java@v3: установка Java 8 версии.

  • ./gradlew buildAndCopy: сборка пакетов.

  • ./gradlew publish: с помощью команды происходит публикация Kotlin пакета в GitHub Apache Maven registry. Для аутентификации используется GITHUB_TOKEN. GITHUB_ACTOR подставляется в build.gradle.kts автоматически т.к. является стандартной переменной окружения.

  • actions/setup-node@v3: настройка окружения Node.js версии 20.x для последующей публикации npm пакета.

  • Publish bank-demo-client gRPC API: происходит переход в директорию npm_package, установка зависимостей и публикация npm-пакета с использованием NODE_AUTH_TOKEN.

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

  1. В файле build.gradle.kts необходимо обновить значение version.

  2. В файле package.json также обновить значение version.

Ручной запуск публикации пакета из раздела Actions

Ручной запуск публикации пакета из раздела Actions
После успешной сборки новые пакеты появятся в разделе Packages

После успешной сборки новые пакеты появятся в разделе Packages

Настройка прокси

В самом начале я упоминал о ключевой особенности: gRPC-web клиенты не способны напрямую связываться с обычными gRPC-сервисами. Чтобы обеспечить взаимодействие, требуется проксирование. В проекте я применяю envoy прокси. Настройки были реализованы на основе примера, доступного в репозитории gRPC-web и выглядят следующим образом:

admin:  access_log_path: /tmp/admin_access.log  address:    socket_address: { address: 0.0.0.0, port_value: 9901 }   static_resources:  listeners:    - name: listener_0      address:        socket_address: { address: 0.0.0.0, port_value: 8080 }      filter_chains:        - filters:            - name: envoy.filters.network.http_connection_manager              typed_config:                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager                codec_type: auto                stat_prefix: ingress_http                route_config:                  name: local_route                  virtual_hosts:                    - name: local_service                      domains: ["*"]                      routes:                        - match: { prefix: "/" }                          route:                            cluster: echo_service                            timeout: 0s                            max_stream_duration:                              grpc_timeout_header_max: 0s                      cors:                        allow_origin_string_match:                          - prefix: "*"                        allow_methods: GET, PUT, DELETE, POST                        allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout                        max_age: "1728000"                        expose_headers: custom-header-1,grpc-status,grpc-message                http_filters:                  - name: envoy.filters.http.grpc_web                    typed_config:                      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb                  - name: envoy.filters.http.cors                    typed_config:                      "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors                  - name: envoy.filters.http.router                    typed_config:                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router  clusters:    - name: echo_service      connect_timeout: 0.25s      type: logical_dns      http2_protocol_options: {}      lb_policy: round_robin      load_assignment:        cluster_name: cluster_0        endpoints:          - lb_endpoints:              - endpoint:                  address:                    socket_address:                      address: 172.17.0.1                      port_value: 6565

Подключение API

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

Настроим подключение к GitHub Apache maven registry на бэкенде для проекта, который будет использовать опубликованный пакет:

repositories {    mavenCentral()    maven {        url = uri("https://maven.pkg.github.com/arvgord/bank-demo-api")        credentials {            username = project.findProperty("gpr.user") as String? ?: ("GITHUB_ACTOR")            password = project.findProperty("gpr.key") as String? ?: ("GITHUB_TOKEN")        }    } }

В URL https://maven.pkg.github.com/OWNER/REPOSITORY необходимо заменить OWNER на имя аккаунта на GitHub и REPOSITORY на имя репозитория, откуда планируете скачивать пакет. В системных переменных необходимо задать токен на чтение GITHUB_TOKEN.

Вот как происходит вызов gRPC API на бэкенде подключенного пакета:

package com.arvgord.bankdemoserver.controller.grpc.cartesianissue.v1  import com.arvgord.api.grpc.bankdemo.v1.BankDemoGrpcKt import com.arvgord.api.grpc.bankdemo.v1.BankDemoOuterClass.GetClientListRequest import com.arvgord.api.grpc.bankdemo.v1.BankDemoOuterClass.GetClientListResponse import io.grpc.Status import io.grpc.StatusException import org.lognet.springboot.grpc.GRpcService import com.arvgord.bankdemoserver.controller.grpc.cartesianissue.v1.adapter.BankDemoAdapter  @GRpcService class BankDemoCartesianIssueController(    private val adapter: BankDemoAdapter ) : BankDemoGrpcKt.BankDemoCoroutineImplBase() {       override suspend fun getClientList(request: GetClientListRequest): GetClientListResponse =        try {            adapter.getClientList(request)        } catch (e: Exception) {            throw StatusException(Status.INTERNAL.withDescription(e.message))        } }

Для подключения к npm GitHub registry необходимо:

  1. Выполнить команду npm login —registry=https://npm.pkg.github.com.

  2. Ввести имя аккаунта на GitHub и GITHUB_TOKEN на чтение пакетов.

  3. Выполнить npm i в вашем проекте.

Так выглядит вызов gRPC API на React-клиенте:

import {useEffect, useState} from 'react'; import {GetClientListRequest, GetClientListResponse} from "@arvgord/bank-demo-api/bankdemo/v1/api/business/bank_demo_pb"; import {PageRequest} from "@arvgord/bank-demo-api/bankdemo/v1/messages/page_request_pb"; import {Int32Value} from "google-protobuf/google/protobuf/wrappers_pb"; import {ExtractingStrategy} from "@arvgord/bank-demo-api/bankdemo/v1/messages/extracting_strategy_pb"; import {BankDemoPromiseClient} from "@arvgord/bank-demo-api/bankdemo/v1/api/business/bank_demo_grpc_web_pb";  export function useGetList(page: number, size: number, strategy: ExtractingStrategy) {    const [response, setResponse] = useState(new GetClientListResponse().toObject())    const [error, setError] = useState()     useEffect(() => {        if (!page && !size && !strategy) return        const service = new BankDemoPromiseClient('http://localhost:8080', null, null)        const request = new GetClientListRequest()        const pageRequest = new PageRequest()        pageRequest.setPage(new Int32Value().setValue(page))        pageRequest.setSize(new Int32Value().setValue(size))        request.setPageRequest(pageRequest)        request.setExtractingStrategy(strategy)        service.getClientList(request, {})            .then(result => result.toObject())            .then(setResponse)            .catch(setError)    }, [page, size, strategy]);     return {        response,        error    }; }

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

Исходный код описанных примеров вы найдете в проекте на GitHub, как и пример подключения API к клиенту и бэкенду.

Заключение

gRPC — это мощный фреймворк для создания эффективных и надежных API. На основе .proto файлов вы можете одновременно генерировать как серверный код, так и код для веб-клиентов. Генерация и публикация gRPC API пакетов значительно упрощается с использованием GitHub Actions и GitHub Packages, что и было продемонстрировано в этом посте.


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


Комментарии

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

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