grpc-gateway を実運用で使うための追加実装
見出し画像

grpc-gateway を実運用で使うための追加実装

電通デジタルでバックエンドの開発をしている平沼です。

Dentsu Digital Advent Calendar 2020 の 18 日目の記事になります。前回の記事は「Micro Frontends 導入の覚書」でした。

弊社では、社内 / グループ会社向けのデジタル広告運用実績管理システムのバックエンドサービスに gRPC を利用しています。また Web などから HTTP によるアクセスができるように、 gRPC から HTTP に変換して API を提供する grpc-ecosystem/grpc-gateway も利用しています。

grpc-gateway を利用するとき、 README.md 通りの使い方ではサービス運用上困ることがあります。今回はそのうち下記 3 点を取り上げて対応方法を紹介します。

・grpc-gateway サーバ自身のヘルスチェックをしたい
・認証情報をバックエンドサービスに引き継ぎたい
・アクセスログを出力したい

grpc-gateway サーバ自身のヘルスチェックをしたい

私たちのサービスでは、 grpc-gateway サーバの前に Application Load Balancer(ALB) が配置されています。そのため、 grpc-gateway サーバ自身が ALB のヘルスチェックに応答する必要があり、エンドポイントを追加しました。

以下の実装では /sample/health にアクセスすると 200 OK のステータスコードが返ります。grpc-gateway の自動生成スタブの実装を参考に作成しています。

func registerHealthCheckEndpoint(mux *runtime.ServeMux) {
    // version は実装上 1 で固定
	const version = 1
    // パスに使う要素
	pool := []string{"sample", "health"}
    // パス作成のための操作と pool のインデックスの組
	ops := []int{int(utilities.OpLitPush), 0, int(utilities.OpLitPush), 1}
    // パス作成
    pat := runtime.MustPattern(runtime.NewPattern(version, ops, pool, ""))
    // ルーティングの設定
	mux.Handle("GET", pat, func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
        // ヘルスチェックの処理
		w.WriteHeader(http.StatusOK)
	})
}

エンドポイントの作り方が複雑に見えますが、ヘルスチェックとバックエンドサービスへのゲートウェイを同じ grpc-gateway サーバに共存させることができました。

この実装は grpc-gateway のバージョンが v1.x を想定していますが、v2.x では HandlePath() メソッドが追加されパスに文字列が使えるようになるため、簡単に作れるようになります。

認証情報をバックエンドサービスに引き継ぎたい

私たちが AWS で Web アプリケーションを構築する場合、ALB と Cognito を使い、その後ろにバックエンドサービスを配置する構成にしています。 Cognito の認証を経て、 ALB が HTTP のヘッダに認証情報を付与します。その後、バックエンドサービスで認証情報からユーザ情報を取得して利用しています。

grpc-gateway では x-* などの独自 HTTP ヘッダは自動的には引き継がれません。 Grpc-Metadata- を付けてから送るか、NewServeMux のオプションで対応する必要があります。今回はサーバサイドでバックエンドサービスに引き継ぐメタデータを選別するために、NewServeMux のオプションで対応する方法での実装を採用しました。

具体的な実装は以下になります。以下の実装は x-* から始まる認証情報に関係するヘッダをバックエンドサービスに引き継いでいます。

// runtime.WithIncomingHeaderMatcher に matcher を指定する
func matcher(key string) (string, bool) {
    if strings.HasPrefix(strings.ToLower(key), "x-") {
        return key, true
    }
    return "", false
}

grpc-gateway ではバックエンドサービスを登録するさいに、 runtime.NewServeMux() でマルチプレクサを作成します。 NewServeMux() には、ヘッダを引き継がせるか判断するための runtime.WithIncomingHeaderMatcher() というオプションがあります。 matcher は http の各ヘッダに対して評価され、ヘッダ名と true を返すことでバックエンドサービスへ引き継ぎます。

アクセスログを出力したい

アクセスログが取得できると、トラフィックの分析やトラブルシューティングなどに役立ちます。 アクセスログは、 grpc.DialOption にインターセプターを追加することで出力できます。

// grpc.WithUnaryInterceptor に loggingInterceptor を指定します。 Client Streaming RPC の場合は grpc.WithStreamInterceptor を利用します。
func loggingInterceptor() grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		// トレース ID やリクエスト内容の取得
		var traceID, oidcData string
		if md, ok := metadata.FromOutgoingContext(ctx); ok {
			if values := md.Get("x-amzn-trace-id"); len(values) > 0 {
				traceID = values[0]
			} else if values := md.Get("x-amzn-oidc-data"); len(values) > 0 {
				oidcData = values[0]
			}
		}
        // リクエストを json に変換
		jsonStr, errCnv := convertToJSONString(req)
		if errCnv != nil {
			logrus.WithFields(logrus.Fields{"error": errCnv}).Error("request cannot marshal")
		}
        // method には /[proto で定義した package].[サービス]/[RPC メソッド] が入ります
		logrus.WithFields(logrus.Fields{"traceID": traceID, "oidc-data": oidcData, "method": method, "req": jsonStr}).Info("call api")
		startTime := time.Now()

		// バックエンドサービスの呼び出し
		err := invoker(ctx, method, req, reply, cc, opts...)
		// 返り値やエラー内容の取得
		execTime := time.Now().Sub(startTime)
		code := grpc_logging.DefaultErrorToCode(err)
		level := grpc_logrus.DefaultCodeToLevel(code)
        // バックエンドサービスからのレスポンスを json に変換
		jsonStr, errCnv = convertToJSONString(reply)
		if errCnv != nil {
			logrus.WithFields(logrus.Fields{"error": errCnv}).Error("reply cannot marshal")
		}
		logrus.WithFields(logrus.Fields{"traceID": traceID, "method": method, "code": code.String(), "level": level, "reply": jsonStr, "time": execTime}).Info("response from api")
		return err
	}
}

まとめ

grpc-gateway を利用する上で困った以下の 3 点について解説しました。

・grpc-gateway サーバ自身のヘルスチェックをしたい
・認証情報をバックエンドサービスに引き継ぎたい
・アクセスログを出力したい

grpc-gateway を利用されている方、これから利用される方の参考になれば幸いです。

次回は「ADH API を効率的に呼び出すために開発した Hooks の紹介」です。よろしくお願いいたします。