はじめに
gRPCでAPIを実装するにあたって、忘れがちだけれども必要な処理を追加していきます。
API実装は前回までの記事で行ったので、気になる方は以下をご覧ください。
Error handling
サーバ側とクライアント側にError handlingの処理を追加します。
まず、サーバ側にリクエストの値が0より小さい場合にstatus.Errorf()
でエラーを返すようにします。
クライアントが無効な引数を指定(InvalidArgument)したこととして、ステータスコード3(HTTPだと400)を返します。
func (*server) SquareRoot(ctx context.Context, req *calculatorpb.SquareRootRequest) (*calculatorpb.SquareRootResponse, error) {
fmt.Println("Received SquareRoot RPC")
number := req.GetNumber()
if number < 0 {
return nil, status.Errorf(
codes.InvalidArgument,
fmt.Sprintf("Received a negative number: %v", number),
)
}
return &calculatorpb.SquareRootResponse{
NumberRoot: math.Sqrt(float64(number)),
}, nil
}
また、クライアント側では、レスポンスエラーの場合の処理を追加します。
status.FromError(err)
で取得したrespErr
からエラーメッセージやコードを出力します。
func doErrorCall(c calculatorpb.CalculatorServiceClient, n int32) {
res, err := c.SquareRoot(context.Background(), &calculatorpb.SquareRootRequest{Number: n})
if err != nil {
respErr, ok := status.FromError(err)
if ok {
// actual error from gRPC (user error)
fmt.Printf("Error message from server: %v\n", respErr.Message())
fmt.Println(respErr.Code())
if respErr.Code() == codes.InvalidArgument {
fmt.Println("We probably sent a negative number!")
return
}
} else {
log.Fatalf("Big Error calling SquareRoot: %v", err)
return
}
}
fmt.Printf("Result of square root of %v: %v\n", n, res.GetNumberRoot())
}
Deadline
gRPCクライアントがどれくらいの間RPCの完結を待つかどうかをDeadlineで設定することができます。
サーバ側には、Deadlineを超えているかどうかのチェックと超えていた場合のキャンセル処理を追加してあげる必要があります。
gRPCのドキュメントでは、すべてのクライアントのRPCコールにDeadlineを設定することを推奨しています。
サーバ側ではクライアントがリクエストをキャンセルしたかどうかをチェックして、3秒後にレスポンスを返すようにします。
func (*server) GreetWithDeadline(ctx context.Context, req *greetpb.GreetWithDeadlineRequest) (*greetpb.GreetWithDeadlineResponse, error) {
fmt.Printf("GreetWithDeadline function was invoked with %v\n", req)
for i := 0; i < 3; i++ {
if ctx.Err() == context.DeadlineExceeded {
// the client canceled the request
fmt.Println("The client canceled the request!")
return nil, status.Error(codes.Canceled, "the client canceled the request")
}
time.Sleep(1 * time.Second)
}
firstName := req.GetGreeting().GetFirstName()
result := "Hello " + firstName
res := &greetpb.GreetWithDeadlineResponse{
Result: result,
}
return res, nil
}
そして、クライアント側ではif err != nil
以下にDeadlineを超えたときの出力を記述します。
func doUnaryWithDeadline(c greetpb.GreetServiceClient, timeout time.Duration) {
fmt.Println("Starting to do a UnaryWithDeadline RPC...")
req := &greetpb.GreetWithDeadlineRequest{
Greeting: &greetpb.Greeting{
FirstName: "Katsumi",
LastName: "Yamada",
},
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
res, err := c.GreetWithDeadline(ctx, req)
if err != nil {
statusErr, ok := status.FromError(err)
if ok {
if statusErr.Code() == codes.DeadlineExceeded {
fmt.Println("Timeout was hit! Deadline was exceeded")
} else {
fmt.Printf("unexpected error: %v", statusErr)
}
} else {
log.Fatalf("error while calling GreetWithDeadline RPC: %v", err)
}
return
}
log.Printf("Response from GreetWithDeadline: %v", res.Result)
}
SSL
gRPCではSSLによるセキュアな通信が推奨されています。
クライアント-サーバ間のデータのやりとりの流れは以下のようになります。
- クライアントがサーバにSSL/TLS通信をリクエスト
- サーバがクライアントにサーバ証明書、中間CA証明書(公開鍵)、暗号スイート(暗号方式の設定)などを送信
- 公開鍵暗号基盤を用いてクライアント内でドメイン認証 → RSA, ECDSA
- 鍵交換用のアルゴリズムを使って共通鍵の素を交換しお互いに共通鍵を生成 → DHE, ECDHE
- サーバが共通鍵で暗号化したデータを送信 → AES, ChaCha20
- クライアントが暗号化されたデータを共通鍵で復号
もっと詳細を知りたい方は以前書いた記事をご覧ください。
実装
まずは以下の証明書やその鍵(公開鍵、秘密鍵)を生成します。
- ca.key: CAの秘密鍵
- ca.crt: CA中間証明書
- server.key: サーバの秘密鍵
- server.csr: サーバの公開鍵
- server.crt: サーバ証明書(CAの秘密鍵によって暗号化されたサーバの公開鍵)
- server.pem: server.keyをgRPC用にフォーマットしたもの
# コモンネーム(SSLサーバ証明書の登録情報の一つで、その証明書が有効なサーバのドメイン名やIPアドレス)を設定する
SERVER_CN=localhost
# Step 1: ca.keyとca.crtを生成する
openssl genrsa -passout pass:1111 -des3 -out ca.key 4096
openssl req -passin pass:1111 -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=${SERVER_CN}"
# Step 2: server.keyを生成する
openssl genrsa -passout pass:1111 -des3 -out server.key 4096
# Step 3: server.csrを生成する
openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/CN=${SERVER_CN}" -config ssl.cnf
# Step 4: server.crtを生成する(CAでserver.csrに署名する)
openssl x509 -req -passin pass:1111 -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt -extensions req_ext -extfile ssl.cnf
# Step 5: server.keyをserver.pemに変換する
openssl pkcs8 -topk8 -nocrypt -passin pass:1111 -in server.key -out server.pem
サーバ側の設定を行います。
生成したserver.crtとserver.pemをcreds, sslErr := credentials.NewServerTLSFromFile(certFile, keyFile)
のように埋め込みます。
func main() {
fmt.Println("Hello world")
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
opts := []grpc.ServerOption{}
tls := true
if tls {
certFile := "ssl/server.crt"
keyFile := "ssl/server.pem"
creds, sslErr := credentials.NewServerTLSFromFile(certFile, keyFile)
if sslErr != nil {
log.Fatalf("Failed loading certificates: %v", sslErr)
return
}
opts = append(opts, grpc.Creds(creds))
}
s := grpc.NewServer(opts...)
greetpb.RegisterGreetServiceServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
クライアント側ではca.crt(CA中間証明書)を使用します。
このようにして、サーバから取得したサーバ証明書がCAの秘密鍵で署名されていることを中間CA証明書で検証します。
func main() {
fmt.Println("Hello I'm a client")
tls := true
opts := grpc.WithInsecure()
if tls {
certFile := "ssl/ca.crt"
creds, sslErr := credentials.NewClientTLSFromFile(certFile, "")
if sslErr != nil {
log.Fatalf("Error while loading CA trust certificate: %v", sslErr)
return
}
opts = grpc.WithTransportCredentials(creds)
}
cc, err := grpc.Dial("localhost:50051", opts)
if err != nil {
log.Fatalf("could not connect: %v", err)
}
defer cc.Close()
c := greetpb.NewGreetServiceClient(cc)
doUnary(c)
}
参考資料