Spring BootでgRPCする

この記事は [エムスリー Advent Calendar 2017](https://qiita.com/advent-calendar/2017/m3) の3日目の記事です。

自分の関わっているシステムのリニューアルにあたり、マイクロサービスっぽい構成を目指すことになりまして、現在Spring Boot + gRPC (+ Kotlin)でサーバを書きはじめています。

この記事では、Spring BootでgRPCを扱う場合のHello World的な話と、実際にアプリケーションを作りこんでいくにあたって大体必要になりそうな認証・エラーハンドリングといった話をまとめました。

現在 サーバがKotlin & Spring Boot / クライアントが主にRailsという構成で実装を進めている関係上、 サンプルコードがJavaだったりKotlinだったりRubyだったりしますが、予めご了承ください。

# Spring BootでgRPC

## Spring BootでgRPCサーバを立ち上げる
実は `grpc-java` を使っていると、以下のライブラリを使えば特に難しい設定をすることなく gRPCサーバを立てることができます。

[LogNet/grpc-spring-boot-starter](https://github.com/LogNet/grpc-spring-boot-starter)

```java
    @GRpcService
    public static class GreeterService extends  GreeterGrpc.GreeterImplBase{
        @Override
        public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver<GreeterOuterClass.HelloReply> responseObserver) {
            final GreeterOuterClass.HelloReply.Builder replyBuilder = GreeterOuterClass.HelloReply.newBuilder().setMessage("Hello " + request.getName());
            responseObserver.onNext(replyBuilder.build());
            responseObserver.onCompleted();
        }
    }
```

gRPCの実装は `@GRpcService` というアノテーションをつけるだけでOK。

この状態でSpring Bootを立ち上げると…

```bash
$ ./gradlew bootRun
# 中略
2017-12-03 10:59:16.744  INFO 41958 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 9090 (http)
2017-12-03 10:59:16.747  INFO 41958 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : Starting gRPC Server ...
2017-12-03 10:59:16.875  INFO 41958 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : 'app.grpc.GreeterService' service has been registered.
2017-12-03 10:59:17.408  INFO 41958 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : gRPC Server started, listening on port 6565.
2017-12-03 10:59:17.413  INFO 41958 --- [           main] app.AppApplicationKt                     : Started AppApplicationKt in 9.786 seconds (JVM running for 10.686)
```

Spring Bootのサーバと一緒に、gRPCのサーバが異なるポートで立ち上がっているのがわかります。

gRPCに関連するコードを変更しても、Spring Boot側のコードを触っているときと同じく、きちんと再コンパイルしてくれますし、今のところSpring Bootを普通に使っている時と開発体験としては何も変わりません。

## Inteceptorを使う
gRPCを使うにあたっては、Interceptorがキモになります。これは名前のとおり、リクエストを `Intercept` して、何らかの処理をはさみこむものです。

これも [LogNet/grpc-spring-boot-starter](https://github.com/LogNet/grpc-spring-boot-starter) を使えば簡単に実装できます。

```Java
// 特定のgRPCのサービスにInterceptorを指定する
@GRpcService(interceptors = { LogInterceptor.class })
public  class GreeterService extends  GreeterGrpc.GreeterImplBase{
    // ommited
}
```

```java
// 全てのサービスにこのInterceptorを指定する
@GRpcGlobalInterceptor
public  class MyInterceptor implements ServerInterceptor{
    // ommited
}
```

```java
// 指定した順番でインターセプターをかませる。
@GRpcGlobalInterceptor
@Order(10)
public  class A implements ServerInterceptor{
    // will be called before B
}

@GRpcGlobalInterceptor
@Order(20)
public  class B implements ServerInterceptor{
    // will be called after A
}
```

アプリケーション共通で実施したい処理は、 この `Interceptor` を使うことで実現できます。

# 実際にアプリケーションを作っていくときのアレコレ
ここまではHello World的な内容でした。ここからは実際にアプリケーションを作っていくときに考えなくてはいけない認証・エラーハンドリングといったところをまとめていきます。


## 認証
gRPCのドキュメントには [Authentication](https://grpc.io/docs/guides/auth.html) の項がありますが、これは実際のところgRPCを使ってきたクライアントが正しいクライアントか？という用途に使われているような気がします。

リクエストをしてきたのが誰かという認証を行う場合には、 `Metadata` という仕組みを使ってユーザーのアクセストークンなどをリクエストに付与し、それをサーバ側で検証する形で認証を行う場合が多いようです。

例えばRubyのgRPCクライアントから `Metadata` を使ってアクセストークンを送る場合はこんな感じになります。

```ruby
# 現在のプロジェクトではRubyを使っているので、サンプルはRubyで書いていますが、
# 他の言語でも同じような感じでセットできるはず。
stub = Helloworld::Greeter::Stub.new('localhost:50051', :this_channel_is_insecure)
req =  Helloworld::Greeter::HelloRequest.new(name: "suusan2go")
stub.say_hello(req, { metadata: {authorization: "76ea9743-bef9-4b1f-b116-3076ea51a1" } })
```

これをサーバ側で処理すればいいわけですが、呼び出す側で毎回これを検証するのはDRYではありません。これには `Interceptor` を使います。
以下は `Interceptor` でクライアント側でセットした認証情報をサーバ側で取り出す場合のサンプルコードです。

```kotlin
// 現在のプロジェクトではKotlinを使っているので、Kotlinで書いていますが、Javaでもおんなじ感じになるはず。
// エラー処理やなんやらは端折ってます。
class AuthenticationInterceptor: ServerInterceptor {
    override fun <ReqT : Any?, RespT : Any?> interceptCall(call: ServerCall<ReqT, RespT>?, headers: Metadata?,
                                                           next: ServerCallHandler<ReqT, RespT>?): ServerCall.Listener<ReqT> {
          val token: String = headers?.let {
              it.get(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER))
          } ?: ""
          // 何か認証処理

          return next?.startCall(call, headers)!!
    }
}
```

### Spring Securityを使いたい
`Spring` で認証といえば `Spring Security` です。gRPCは使っている場合、当然ではありますが `Spring Security` の機能をそのまま使うことはできません。

`Spring Security` と `gRPC` を統合している良い例として以下を見つけました。
https://github.com/revinate/grpc-spring-security-demo

以下はコードの抜粋です。
gRPCの各メソッドに対して、 通常 `controller` につけるような `@PreAuthorize("hasRole('USER')")` というアノテーションを付けても、動作するようになっています。

```java
    @Override
    @PreAuthorize("hasRole('USER')")
    public void fibonacci(FibonacciRequest request, StreamObserver<FibonacciResponse> responseObserver) {
        if (request.getValue() < 0) {
            responseObserver.onError(Status.INVALID_ARGUMENT.withDescription("Number cannot be negative").asRuntimeException());
            return;
        }

        FibonacciResponse response = FibonacciResponse.newBuilder()
                .setValue(numberService.fibonacci(request.getValue()))
                .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
```

これらは通常Spring Securityが自動でやってくれていることを `Interceptor` を使って自前で実装することで実現されています。作者(会社？)のブログに詳しい解説があるのでそちらを見てみるとよいでしょう。

https://eng.revinate.com/2017/11/07/grpc-spring-security.html

## エラーハンドリング
gRPCを使う場合には、メソッドが実行されるのは実際にはリモートのサーバになります。
ですのでサーバ側で何か例外が起きた場合には、それがどのような例外なのかきちんとクライアントに伝えてあげる必要があります。

例えばRubyクライアントからgRPCを使ってリモートのサーバでなにか例外が起きた場合、何もケアしていないとこんな感じになります。

```ruby
# 現在のプロジェクトではRubyを使っているので、サンプルはRubyで書いていますが、
# 他の言語でも同じような感じになるはず
> stub.say_hello(req)
GRPC::Unknown: 2:
```

これでは中々厳しいですね。 `grpc-java` では適切なステータスコードを定義して、 `call.close` を呼んであげることで、クライアントに適切な例外、及びメッセージを返せるようになります。
上述した認証の例に、例外処理を足してみると以下のような感じになります。

```kotlin
class AuthenticationInterceptor: ServerInterceptor {
    override fun <ReqT : Any?, RespT : Any?> interceptCall(call: ServerCall<ReqT, RespT>?, headers: Metadata?,
                                                           next: ServerCallHandler<ReqT, RespT>?): ServerCall.Listener<ReqT> {
        try {
            val token: String = headers?.let {
                it.get(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER))
            } ?: ""
            // 何か認証処理
            return next?.startCall(call, headers)!!
        } catch(e: InvalidTokenException) {
            call?.close(Status.fromCode(Status.UNAUTHENTICATED.code).withDescription(e.message), Metadata())
            throw e
        }
    }
}
```

こうするとクライアント側では、適切な例外クラスで適切なエラーメッセージを受け取れるようになります。

```ruby
> stub.say_hello(req, { metadata: {authorization: "76ea9743-bef9-4b1f-b116-3076ea51a1" } })
GRPC::Unauthenticated: 16:Invalid access token: 76ea9743-bef9-4b1f-b116-3076ea51a1
```

当然毎回これをやり続けるのは冗長です。gRPCサーバで横断的にキャッチしたい例外がある場合には、 `Interceptor` を使って実装するとよいでしょう。

`Interceptor` でのエラーハンドリングについては、 `nsoushi` さんのブログがとても参考になりました。  `grpc-java` のリポジトリにも参考になりそうな実装があるので見てみると良さそうです。

* https://github.com/grpc/grpc-java/blob/2b1eee90e5bd7f5ad905e34f73f2040d6c9a3568/core/src/main/java/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.java
* http://blog.soushi.me/entry/2017/08/18/234615

# まとめ
gRPCをSpring Bootから扱う方法についてまとめました。最初はなかなか取っつきにくい部分もあるgRPCですが、 `Interceptor` や `Metadata` といったところを理解できるとアプリケーション的にやりたいこと(今回紹介した以外にもロギングなど)は大体実現できる感じがしました。

gRPCの動くサンプル(本当に簡単なやつですが)は以下にまとまっているので、参考になれば幸いです。
https://github.com/suusan2go/nuxtjs-auth-with-spring/tree/master/spring-backend

# 参考資料
- [平日インプット週末アウトプットぶろぐ](http://blog.soushi.me/)
    - grpc-javaについての知見を沢山公開されていて、とても参考になりました
- [Securing Java gRPC services with Spring Security](https://eng.revinate.com/2017/11/07/grpc-spring-security.html)
    - Spring Security + gRPC について実践的な内容が詳しく記載されています
- [gRPC and REST with gRPC in practice](https://speakerdeck.com/kazegusuri/grpc-and-rest-with-grpc-in-practice)
    - gRPCについてざっくり理解するにあたり、大変参考になりました