gRPC ServerをKotlinで動かす

KotlinのロゴとgRPCのロゴ

gRPC ServerをKotlin+SpringBootで動かすサンプルを作った。

gorou-178/grpc-springboot-example - GitHub

簡単に作成できると思っていたが、調べて出てくる情報は古くライブラリの依存関係も変わっており動かなかった。 また、Gradleのprotocを利用するのではなくBuf CLIを利用したかった。そのようなサンプルもほぼなかったため作成した。

Buf CLI

Bufが作ったProtocol Bufferの開発を支援するツール。lintや破壊的変更検知、コード生成もできる。

Buf - The Buf CLI

protocコマンドでのコード生成はわりと難しかった。しかし、Buf CLIは、buf.gen.yml でコード生成の設定ができ、buf generate でコード生成できる手軽さがうれしい。

Kotlin Codeの生成

以下buf.gen.yml設定でKotlin Codeの生成ができた。Kotlin+gRPCは、Javaのコードも必要なのがわかりにくかった。

version: v1
plugins:
  - plugin: buf.build/grpc/java
    out: src/main/java
  - plugin: buf.build/grpc/kotlin
    out: src/main/kotlin
  - plugin: buf.build/protocolbuffers/java:v25.1
    out: src/main/java
  - plugin: buf.build/protocolbuffers/kotlin:v25.1
    out: src/main/kotlin

もうひとつ厄介だったのが、protocolbuffersプラグインにて破壊的変更があったこと。

Protocol Buffers v26.0 の破壊的変更 - Qiita

gradleのprotobuf-javaのversionと、protocのversionに依存関係があり、protoc v26以降はprotobuf-java v4.26以降を利用する必要がある。 それぞれ最新を利用していたがエラーでうまく動かない。根本原因について調べ切れていないが、protobuf-java v3.25にして、protoc v25系にしたところ問題なく動作した。ここにとてつもなく時間を溶かした。

最新版で動作する環境は、今後調べて動かせるようにしたい。

gRPCの破壊的変更の検知

Protocol Bufferの変更に破壊的な変更がある場合に指摘してくれる。 比較対象は、ローカルのGitやGitリポジトリなど指定可能。常にチェックしておけば安心して作業できそう。

マネージモード

Java(Kotlin)でProtocol Buffer利用する際、パッケージ名などをoptionで指定する必要がある。このときパッケージ名は文字列であるため、完全修飾名のようにすべて記載する必要がある。 そのため、複数のProtocol Bufferファイルを利用している場合、パッケージ名の記述が冗長になる。

Buf CLIのマネージモードを利用すると、java_package_prefixの設定ができるなど、Protocol Bufferファイルへの依存と記述量を減らせる。割とこれが便利だと思っている。

Buf - Managed mode

runnのシナリオテスト導入

runnコマンドでgRPCのテストを書いてみたかったため導入。

k1LoW/runn - GitHub

シナリオファイルを1から書くのは面倒だが、and-runオプション利用すると、リクエスト・レスポンス内容でシナリオを作成してくれるのでとても便利。

runn new --and-run --grpc-no-tls --out test-runn.yml \                     
    -- grpcurl -d '{"name": "test"}' localhost:9090 GreeterService/SayHello  
$ cat test-runn.yml
desc: Generated by `runn new`                                                                                                
runners:                                                                     
  greq: grpc://localhost:9090                                                
steps:                                                                        
- greq:                                                                      
    GreeterService/SayHello:                                                 
      message:                                                               
        name: abc                                                            
  test: |                                                                    
    current.res.headers["content-type"][0] == "application/grpc"             
    && current.res.headers["grpc-accept-encoding"][0] == "gzip"              
    && compare(current.res.message, {"message":"Hello abc"})                 
    && current.res.status == 0

protovalidateの導入

protovalidateというものがあることを、runnコマンドのドキュメントを読んでいて知った。Protocol Bufferにvalidationを書け、gRPC Code Generateされ、gRPC Server側でバリデーション処理も行ってくれるプラグイン。

このプラグインは、BSR(Buf Schema Registry: docker hubのようなもの)に登録されているため、プラグイン名の指定で利用できる。 buf.ymldepsを追加するだけ。

version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT
  allow_comment_ignores: true
deps:
  - buf.build/bufbuild/protovalidate

Protocol Bufferは以下のように記述する。重要なのはimport文。

syntax = "proto3";

option java_package = "com.example.grpc.server.greeter";
option java_outer_classname = "Greeter";

import "buf/validate/validate.proto";

message HelloRequest {
  string name = 1 [(buf.validate.field).string = {
    pattern: "^[a-zA-Z0-9 !,]+$",
    min_len: 4,
    max_len: 16,
  }];
}

このままbuf generateするとimport "buf/validate/validate.proto";でエラーになってしまう。 depsを追加した場合は、buf dep updateを行った上でbuf generateする必要がある。

protovalidateのgRPC Server側の実装

gradleの場合build.buf:protovalidateを依存関係に追加の上、以下のように実装することで、Protocol Bufferに記載したバリデーションチェックしてくれる。 Validator().validate()isSuccesstrueなら成功とみなす。エラーメッセージは、violationsに配列で返ってくる。

    override fun sayHello(request: Greeter.HelloRequest, responseObserver: StreamObserver<Greeter.HelloReply>) {
        val reply = Greeter.HelloReply.newBuilder().setMessage("Hello, ${request.name}!").build()
        try {
            val result = Validator().validate(reply)
            if (result.isSuccess) {
                responseObserver.onNext(reply)
                responseObserver.onCompleted()
            } else {
                responseObserver.onError(
                    StatusRuntimeException(
                        Status.INVALID_ARGUMENT.withDescription(createMessage(result.violations))
                    )
                )
            }
        } catch (e: Exception) {
            responseObserver.onError(
                StatusRuntimeException(
                    createInvalidArgumentStatus(e.message ?: "Unknown error")
                )
            )
        }
    }

    private fun createMessage(violations: List<Violation>): String {
        val sb = StringBuilder()
        sb.append("Validation failed:\n")
        violations.forEach {
            sb.append("property: ").append(it.fieldPath).append("\n")
                .append("  message: ").append(it.message).append("\n")
        }
        return sb.toString()
    }

バリデーションルール含めて、runnでシナリオテストの作成ができれば変更時の安心感が高まりそう。

まとめ

Kotlin+SpringBootにてgRPC Serverのサンプルを作成した。 今までは、protocを使ってたがBuf CLIという便利なツールが出てきたため、Buf CLIを利用するフローを模索してみた。

Protocol Bufferベースで、シナリオテストが行えるrunnコマンドとBuf CLIを導入することで、

これらのチェックが簡単に行える。個人的には満足。 今後、CIに組み込むところをやってみようと思う。

See Also