Apache APISIX と gRPC-Web の統合

Fei Han

January 25, 2022

Ecosystem

gRPC Web 紹介

Googleによって最初に開発されたgRPCは、HTTP/2上に実装された高性能なリモートプロシージャコールフレームワークです。しかし、ブラウザが直接HTTP/2を公開していないため、WebアプリケーションはgRPCを直接使用できません。gRPC Webはこの問題を解決する標準化されたプロトコルです。

最初のgRPC-web実装は2018年にJavaScriptライブラリとしてリリースされ、WebアプリケーションがgRPCサービスと直接通信できるようになりました。その原理は、HTTP/1.1およびHTTP/2と互換性のあるエンドツーエンドのgRPCパイプラインを作成することです。ブラウザは通常のHTTPリクエストを送信し、ブラウザとサーバーの間に位置するgRPC-Webプロキシがリクエストとレスポンスを変換します。gRPCと同様に、gRPC WebはWebクライアントとバックエンドのgRPCサービスの間で事前に定義された契約を使用します。メッセージのシリアライズとエンコードにはProtocol Buffersが使用されます。

gRPC-webの動作原理

gRPC Webを使用すると、ユーザーはブラウザまたはNodeクライアントを使用してバックエンドのgRPCアプリケーションを直接呼び出すことができます。ただし、ブラウザ側でgRPCサービスを呼び出すためにgRPC-Webを使用するにはいくつかの制限があります。

  • クライアント側ストリーミングおよび双方向ストリーミング呼び出しはサポートされていません。
  • クロスドメインでgRPCサービスを呼び出すには、サーバー側でCORSを設定する必要があります。
  • gRPCサーバー側はgRPC-Webをサポートするように設定するか、ブラウザとサーバーの間の呼び出しを変換するためのサードパーティサービスエージェントが利用可能である必要があります。

Apache APISIX gRPC Web プロキシ

Apache APISIXは、プラグインを通じてgRPC Webプロトコルのプロキシをサポートしています。grpc-webプラグインでは、gRPC WebがgRPC Serverと通信する際のプロトコル変換とデータコーデックの作業が行われ、その通信プロセスは以下の通りです。

gRPC Webクライアント -> Apache APISIX(プロトコル変換 & データコーデック) -> gRPCサーバー

以下は、gRPC Webクライアントを構築し、Apache APISIXを通じてgRPC Webリクエストをプロキシする方法を示す完全な例です。以下の例では、GoをgRPCサーバーのハンドラとして、NodeをgRPC Webクライアントのリクエスタとして使用します。

Protocol Bufferの設定

最初に、Protocol Bufferコンパイラと関連プラグインをインストールします。

  1. protocproto-grpc-*をインストールします。

    Protocol Bufferコンパイラprotocと、.protoファイルからGo、JavaScript、およびgRPC Webインターフェースコードを生成するためのprotoc-gen-goおよびprotoc-gen-grpc-webプラグインをシステムにインストールする必要があります。

    以下のスクリプトを実行して上記のコンポーネントをインストールしてください。

    #!/usr/bin/env bash
    
    set -ex
    
    PROTOBUF_VERSION="3.19.0"
    wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    unzip protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    mv bin/protoc /usr/local/bin/protoc
    mv include/google /usr/local/include/
    chmod +x /usr/local/bin/protoc
    
    PROTO_GO_PLUGIN_VER="1.2.0"
    wget https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v${PROTO_GO_PLUGIN_VER}/protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz
    tar -zxvf protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz
    mv protoc-gen-go-grpc /usr/local/bin/protoc-gen-go
    chmod +x /usr/local/bin/protoc-gen-go
    
    PROTO_JS_PLUGIN_VER="1.3.0"
    wget https://github.com/grpc/grpc-web/releases/download/${PROTO_JS_PLUGIN_VER}/protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64
    mv protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64 /usr/local/bin/protoc-gen-grpc-web
    chmod +x /usr/local/bin/protoc-gen-grpc-web
    
  2. SayHelloの例となるprotoファイルを作成します。

    // a6/echo.proto
    
    syntax = "proto3";
    
    package a6;
    
    option go_package = "./;a6";
    
    message EchoRequest {
    string message = 1;
    }
    
    message EchoResponse {
    string message = 1;
    }
    
    service EchoService {
    rpc Echo(EchoRequest) returns (EchoResponse);
    }
    

サーバー側アプリケーションの設定

  1. サーバー側のGo生メッセージとサービス/クライアントスタブを生成します。

    protoc -I./a6 echo.proto --go_out=plugins=grpc:./a6
    
  2. サーバー側のハンドラインターフェースを実装します。

    // a6/echo.impl.go
    
    package a6
    
    import (
    "errors"
    "golang.org/x/net/context"
    )
    
    type EchoServiceImpl struct {
    }
    
    func (esi *EchoServiceImpl) Echo(ctx context.Context, in *EchoRequest) (*EchoResponse, error) {
    if len(in.Message) <= 0 {
        return nil, errors.New("message invalid")
    }
    return &EchoResponse{Message: "response: " + in.Message}, nil
    }
    
  3. サーバー側アプリケーションの実行エントリファイル。

    // server.go
    package main
    
    import (
    "fmt"
    "log"
    "net"
    
    "apisix.apache.org/example/a6"
    "google.golang.org/grpc"
    )
    
    func main() {
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 50001))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    
    grpcServer := grpc.NewServer()
    a6.RegisterEchoServiceServer(grpcServer, &a6.EchoServiceImpl{})
    
    if err = grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %s", err)
    }
    }
    
  4. サーバー側サービスをコンパイルして起動します。

    go build -o grpc-server server.go
    ./grpc-server
    

クライアントプログラムの設定

  1. クライアント側のprotoコードを生成します。

    gRPC WebのJavaScript用に、クライアント側のJavaScript生メッセージ、サービス/クライアントスタブ、およびインターフェースコードを生成します。

    gRPC Webのprotoプラグインは、2つのモードのコード生成を提供します。

    1. mode=grpcwebtext: デフォルトの生成コードは、ペイロードをgrpc-web-text形式で送信します。

    • Content-type: application/grpc-web-text

    • ペイロードはbase64エンコードを使用

    • 単項およびサーバーストリーミング呼び出しをサポート

    1. mode=grpcweb: ペイロードをバイナリprotobuf形式で送信します。

    • Content-type: application/grpc-web+proto
    • ペイロードはバイナリprotobuf形式
    • 現在は単項呼び出しのみサポート
    protoc -I=./a6 echo.proto --js_out=import_style=commonjs:./a6 --grpc-web_out=import_style=commonjs,mode=grpcweb:./a6
    
  2. クライアント側の依存関係をインストールします。

    npm i grpc-web
    npm i google-protobuf
    
  3. クライアント側のエントリファイルを実行します。

    // client.js
    const {EchoRequest} = require('./a6/echo_pb');
    const {EchoServiceClient} = require('./a6/echo_grpc_web_pb');
    // Apache APISIXのエントリに接続
    let echoService = new EchoServiceClient('http://127.0.0.1:9080');
    
    let request = new EchoRequest();
    request.setMessage("hello")
    
    echoService.echo(request, {}, function (err, response) {
       if (err) {
            console.log(err.code);
            console.log(err.message);
        } else {
            console.log(response.getMessage());
        }
    });
    
  4. 最終的なプロジェクト構造

    $ tree .
    ├── a6
    │   ├── echo.impl.go
    │   ├── echo.pb.go
    │   ├── echo.proto
    │   ├── echo_grpc_web_pb.js
    │   └── echo_pb.js
    ├── client.js
    ├── server.go
    ├── go.mod
    ├── go.sum
    ├── package.json
    └── package-lock.json
    

上記の手順を完了すると、gRPCサーバーサーバーアプリケーションとgRPC Webクライアントアプリケーションを設定し、サーバーアプリケーションを起動しました。サーバーアプリケーションはポート50001でリクエストを受信します。

Apache APISIXの設定

次に、Apache APISIXのルーティングプラグイン設定でgrpc-webプラグインを有効にして、gRPC Webリクエストをプロキシします。

  1. grpc-webプロキシプラグインを有効にします。

    ルーティングはプレフィックスマッチングを使用する必要があります(例:/*または/grpc/example/*)。gRPC WebクライアントはURIにprotoで宣言されたパッケージ名、サービスインターフェース名、メソッド名などを渡すため(例:/path/a6.EchoService/Echo)、絶対マッチを使用するとプラグインがURIからproto情報を抽出できなくなります

    $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    {
        "uri":"/*", // プレフィックスマッチングモード
        "plugins":{
            "grpc-web":{} // gRPC Webプラグインを有効化
        },
        "upstream":{
            "scheme":"grpc",
            "type":"roundrobin",
            "nodes":{
                "127.0.0.1:50001":1 // gRPCサーバーのリスンアドレスとポート
            }
        }
    }'
    
  2. gRPC Webプロキシリクエストを検証します。

    Nodeからclient.jsを実行して、gRPC WebプロトコルリクエストをApache APISIXに送信できます。

    上記のクライアント側とサーバー側の処理ロジックはそれぞれ、クライアントがサーバーにhelloというメッセージを送信し、サーバーがメッセージを受信してresponse: helloと応答するものです。実行結果は以下の通りです。

    $ node client.js
    response: hello
    
  3. grpc-webプロキシプラグインを無効にします。

    ルーティングプラグイン設定からgrpc-web属性を削除するだけです。

    $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    {
        "uri":"/*",
        "plugins":{
        },
        "upstream":{
            "scheme":"grpc",
            "type":"roundrobin",
            "nodes":{
                "127.0.0.1:50001":1
            }
        }
    }'
    

まとめ

この記事では、Apache APISIXでgrpc-webを使用するための実践的な経験を紹介しました。

GitHub Discussionsでディスカッションを開始したり、メーリングリストを通じてコミュニケーションを取ることを自由に行ってください。

Tags: