この記事は エムスリー Advent Calendar 2017 の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
@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を立ち上げると…
$ ./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 を使えば簡単に実装できます。
// 特定のgRPCのサービスにInterceptorを指定する
@GRpcService(interceptors = { LogInterceptor.class })
public class GreeterService extends GreeterGrpc.GreeterImplBase{
// ommited
}
// 全てのサービスにこのInterceptorを指定する
@GRpcGlobalInterceptor
public class MyInterceptor implements ServerInterceptor{
// ommited
}
// 指定した順番でインターセプターをかませる。
@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 の項がありますが、これは実際のところgRPCを使ってきたクライアントが正しいクライアントか?という用途に使われているような気がします。
リクエストをしてきたのが誰かという認証を行う場合には、 Metadata
という仕組みを使ってユーザーのアクセストークンなどをリクエストに付与し、それをサーバ側で検証する形で認証を行う場合が多いようです。
例えばRubyのgRPCクライアントから Metadata
を使ってアクセストークンを送る場合はこんな感じになります。
# 現在のプロジェクトでは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で書いていますが、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')")
というアノテーションを付けても、動作するようになっています。
@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
を使って自前で実装することで実現されています。作者(会社?)のブログに詳しい解説があるのでそちらを見てみるとよいでしょう。
エラーハンドリング
gRPCを使う場合には、メソッドが実行されるのは実際にはリモートのサーバになります。
ですのでサーバ側で何か例外が起きた場合には、それがどのような例外なのかきちんとクライアントに伝えてあげる必要があります。
例えばRubyクライアントからgRPCを使ってリモートのサーバでなにか例外が起きた場合、何もケアしていないとこんな感じになります。
# 現在のプロジェクトではRubyを使っているので、サンプルはRubyで書いていますが、
# 他の言語でも同じような感じになるはず
> stub.say_hello(req)
GRPC::Unknown: 2:
これでは中々厳しいですね。 grpc-java
では適切なステータスコードを定義して、 call.close
を呼んであげることで、クライアントに適切な例外、及びメッセージを返せるようになります。
上述した認証の例に、例外処理を足してみると以下のような感じになります。
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
}
}
}
こうするとクライアント側では、適切な例外クラスで適切なエラーメッセージを受け取れるようになります。
> 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
参考資料
-
平日インプット週末アウトプットぶろぐ
- grpc-javaについての知見を沢山公開されていて、とても参考になりました
-
Securing Java gRPC services with Spring Security
- Spring Security + gRPC について実践的な内容が詳しく記載されています
-
gRPC and REST with gRPC in practice
- gRPCについてざっくり理解するにあたり、大変参考になりました