gRPC Federationを使ったBFF開発の実態

この記事は、Merpay & Mercoin Advent Calendar 2024 の記事です。

こんにちは、メルペイ BackendエンジニアのSakabeと申します。
私の所属するKYCチームでは、主に本人確認に関するマイクロサービスの開発を行っています。

現在、メルペイの各チームが共通で使用する大規模なBFF(Backend For Frontend)がその巨大さゆえに管理や拡張が難しくなっています。この問題に対応するため、各チームごとにBFFを切り出す取り組みを進めています。KYCチームではこの対応として、こちらの記事で紹介されているgRPC Federationの仕組みを活用し、効率的にBFFサービスの立ち上げを行いました(gRPC FederationのOSSリポジトリはこちら)。

今回の記事では、実際のBFFサービスの構築に使われたprotoファイルと実際に使用したgRPC Federationのoptionを紹介することでgRPC Federationを用いた開発を紹介します。また、開発中に感じたことを説明します。

gRPC Federationを採用した背景

KYCチームがBFFを新規作成する際、gRPC Federationを採用した背景について説明します。巨大なBFFをKYCチーム専用のBFFに切り出すにあたり、gRPC Federationを活用する方法と、ゼロからBFFを構築する方法がありました。その中でgRPC Federationを選んだ理由は以下の2点です。
1点目は、少ない手間でBFFが構築できることです。具体的には、Protoファイルにオプションを記述するだけで、BFFのコードを自動生成することが可能です。これにより、シンプルなロジックについては、ゼロからBFFを構築するよりも開発コストが低いと判断しました。
2点目は、社内に基盤が作られていることです。他のチームでも実際に使われているため、デプロイまでのフローが整備されており迅速なリリースが可能と判断しました。
これらの理由から、KYCチームではBFFを新規作成する手段としてgRPC Federationを採用しました。

gRPC Federationを使用した実際のBFF開発

KYCチームではgRPC Federationを利用して新規のBFFを開発しました。今回は、マイクロサービスに新規実装したエンドポイントをそのまま中継するだけのBFFを作成します。

今回はその様子を以下の順番で紹介していきます。

  1. ベースとなる新規サービスのProtocol Buffers定義の作成
  2. gRPC Federationのoption設定
  3. 生成されるGoファイル

1. ベースとなる新規サービスのProtocol Buffers定義の作成

今回新たに公開するマイクロサービスのProtocol Buffersの定義(抜粋)です。リクエストは空でレスポンスでは2つの要素を返しています。

service KYCMicroService {
    rpc GetConfirmation(GetConfirmationRequest) returns (GetConfirmationResponse) {
    };
}

message GetConfirmationRequest {}

message GetConfirmationResponse {
    bool need_confirmation               = 1;
    ConfirmationEvent confirmation_event = 2;
}

2. gRPC Federationのoption設定

このマイクロサービスを中継するためにBFFを作成します。gRPC Federationを使用するため、実際のGoのコードを書く必要はなく、protoファイルにgRPC Federationのoptionを記述しBFFのコードの自動生成を行います。

マイクロサービスのprotoファイルをコピーし、以下のような追記を行いました。

service MerpaySharedKYCAPI {
    option (.grpc.federation.service) = {
    };
    rpc GetConfirmation(GetConfirmationRequest) returns (GetConfirmationResponse) {
    };
}

message GetConfirmationRequest {}

message GetConfirmationResponse {
    option (.grpc.federation.message) = {
        def{
            call {method : "mercari.path.api.v1.KYC/GetConfirmation"} 
            autobind : true
        }
    };
    bool need_confirmation               = 1;
    ConfirmationEvent confirmation_event = 2;
}

まず、serviceにgRPC Federationを利用するためのoptionを記述します。

option (.grpc.federation.service) = {};

次にmessageのResponseを定義するためのoptionを記述します。

option (.grpc.federation.message) = {
    def[ {
        call { method : "mercari.path.api.v1.KYC/GetConfirmation" }
        autobind : true
        }
    ]
};

今回使用したのは、(grpc.federation.message).def.call(grpc.federation.message).def.autobindです。
(grpc.federation.message).def.callは、gRPCメソッドを呼び出し、レスポンスを変数に代入します。ただし今回は後述する(grpc.federation.message).def.autobindによる自動的な代入を使用しているため、gRPCのレスポンスを代入する変数を省略しています。
(grpc.federation.message).def.autobindは、callのレスポンスのmessageと自身のfieldの名前と型が一致する場合にfieldの値を自動的に代入します。

逆に一致しない場合は、以下の例のようにレスポンスのfieldを明示的に指定する必要があります。call の結果を res に代入し、res の field である foo の結果を need_confirmation 変数に代入しています。

option (.grpc.federation.message) = {
    def[ {
        name: "res"
        call { method : "mercari.path.api.v1.KYC/GetConfirmation" }
    }
    ]
    def { name: "need_confirmation" by: "res.foo"}
    ...
};

つまり、callとautobindのoptionの記述によって、KYCのマイクロサービスのgRPCエンドポイントをcallした結果がfieldに自動的に代入されます。

3. 生成されるGoファイル

grpc-federation-generatorコマンドを実行することでprotoファイルからコードを生成することができます。

上記のprotoファイルから、以下のようなGoのコード(疑似コード)が生成されます。

func (s *MerpaySharedKYCAPI) getConfirmationResponse(ctx context.Context, req *GetConfirmationRequest) (*GetConfirmationResponse, error) {
    // 新しいレスポンスを作成
    res := &GetConfirmationResponse{}

    // APIへリクエストを送信し、結果を取得
    apiResponse, err := s.client.GetConfirmation(ctx, &ExternalAPIRequest{})
    if err != nil {
        logError(ctx, err)
        return nil, err
    }

    // 必要な情報を取得しレスポンスへ設定
    res.NeedConfirmation = apiResponse.NeedConfirmation

    // 追加の情報を変換しレスポンスに追加
    confirmationEvent, err := s.convertEvent(apiResponse.ConfirmationEvent)
    if err != nil {
        logError(ctx, err)
        return nil, err
    }
    res.ConfirmationEvent = confirmationEvent

    // 処理が成功した場合、最終レスポンスを返す
    return res, nil
}

gRPC Federationを使用してBFFの開発した気づき

今回、gRPC Federationを用いてBFFを開発する中で、以下の4つの点でさまざまな発見や気づきを得ました。

  1. 短期間でのBFF構築の可能性
  2. 高い表現の自由度
  3. 技術導入時のコスト評価の重要性
  4. 実行順の依存関係の明確化

以下、それぞれのポイントについて詳しく掘り下げていきます。

  • 短期間でのBFF構築の可能性

1点目は、短期間でのBFF構築が可能であることです。
BFFのprotoの実装は1日で終了しました。今回のようなマイクロサービスのAPIをプロキシするだけのBFFであれば、optionを10行程度追加するだけでBFFを生成できるのは大きなメリットだと感じています。gRPC Federationを選ぶ理由として定型作業の簡素化があげられており、今回のようなマイクロサービスのAPIを中継するだけのBFFの実装はまさに定型作業といえます。gRPC Federationではprotocol buffers上でメッセージの対応関係を記述することで、型変換処理を全て自動生成しています。
また、私自身はGo言語に関する専門的な知識を持っていませんが、gRPC Federationによってコードが自動生成されることで、Goにおける最新のベストプラクティスを享受し、ハイパフォーマンスなコードを利用できます。もし同じ品質のものを開発する場合、もっと時間がかかると思います。その点で、gRPC Federationを使えばGoの初心者であっても短期間で高品質の成果を得られます。
 さらに、社内ではgRPC Federationを利用したBFFをモノレポで運用しており、そのレポジトリではBFFを迅速にデプロイする環境が整えられています。

  • 高い表現の自由度

2点目は、表現の自由度が高いということです。
現在はCEL(Common Expression Language)のサポートが行われています。proto上で計算を行ったり、計算結果を元にAPIの呼び出しの分岐を書いたりすることも可能です。また、protoからGoのコードを呼び出すことができるため、他のライブラリを使うなどの用途で一部の処理を切り出すこともできます。
このようにgRPC Federationのoptionでできないことを柔軟に実装することが可能です。

  • 技術導入時のコスト評価の重要性

3点目は、新しい技術導入時のコストとメリットを評価する必要があるということです。
新しい技術を導入する際には、そのコストに対してメリットが上回るかを慎重に評価する必要があります。普段はGo言語で開発しており、Goでも十分に開発が可能です。しかし、新しい技術がそれを上回るメリットを提供するかどうかを判断することが求められます。
私自身は、gRPC Federationのオプションに対する生成コードをある程度想像できていますが、チームメンバーは一から学ぶ必要があり、そのためコードレビューが難しくなる場合があります。また、新しいメンバーが加わった際に、BFFに少しの修正を加えるにしても時間がかかることが多いです。そのため、gRPC Federationを導入した際には、チーム内で的確な開発サイクルやリリースフローをしっかりと周知する必要があると感じています。
これはgRPC Federationに限らず、どのような新しい技術を導入する時にも一般的に言える課題であり、gRPC Federation固有の問題ではないことを明記しておきたいです。

  • 実行順の依存関係の明確化

4点目は、実行順の依存関係を明確にする必要があることです。
依存関係を設定しない場合、処理は並列で行われ、最適化された状態で自動的に呼び出されます。これにより、開発者が自らパフォーマンスチューニングを行わなくても済むという利点があります。
しかし、特定の順序での実行が求められる場合は、依存関係を設定する必要があります。例えば、先に実行したいmessageを次のmessageで使用することで、依存関係を明示し、処理を直列に実行させることが可能です。Goのコードであれば、処理を単に上から順に書いていくことで明確な順序を保てます。しかし、これは依存関係のない処理同士が無駄に順番待ちをする可能性もあります。
依存関係を使って実行順序を制御し始めると、並列に実行されるのか、直列に実行されるのかがわかりにくくなる可能性があります。そのため、単純に上から実行されるようなGoのコードであっても、複雑なprotoファイルになることがあります。これを避けるためには、BFF層に複雑なロジックを書かないことが重要です。
optionを駆使すれば複雑なprotoファイルを実装することも可能ですが、その前にロジックを理解し、それをマイクロサービスで処理することが求められます。私個人としては、ロジックを持っているBFFをマイグレーションする際、そのコードを深く理解し、protoに落とし込みやすくする工夫をしています。

まとめ

今回は、gRPC Federationを用いたBFFの開発について紹介しました。KYCチームでは今回のgRPC Federationによる新規BFF開発を足掛かりとして、既存のBFFマイグレーションを行う予定です。もしgRPC Federationに興味のある方は、実際に手を動かして試してみてください!

次の記事はhibagonさんです。引き続きお楽しみください!

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加