Envoy プロキシを使用して GKE 上で gRPC サービスの負荷分散を行う

このチュートリアルでは、ネットワーク負荷分散Envoy プロキシを使用して、Google Kubernetes Engine(GKE)にデプロイした複数の gRPC サービスを 1 つの外部 IP アドレスで公開する方法について説明します。また、Envoy が gRPC 用に提供している高度な機能の一部についても取り上げます。

はじめに

gRPC は、HTTP/2 をベースとした、言語に依存しないオープンソースの RPC フレームワークです。プロトコル バッファを使用し、効率のよいデータ転送と高速なシリアル化を実現します。Google 内部の RPC フレームワークである Stubby からアイデアを得て、gRPC はマイクロサービス間やモバイル クライアントと API 間の低レイテンシ通信を可能にしています。

gRPC は HTTP/2 で動作するため、効率的なバイナリ エンコード、単一接続上でのリクエストとレスポンスの多重化、自動フロー制御といった、HTTP/1.1 より優れた点がいくつかあります。また、gRPC には負荷分散のオプションもいくつか用意されています。このチュートリアルでは、モバイル クライアントや、サービス プロバイダの信頼境界の外部で実行されているクライアントなど、クライアントを信頼できない状況に焦点を当てています。また、gRPC が提供する負荷分散オプションのうち、プロキシベースの負荷分散を使用します。

このチュートリアルでは、TYPE=LoadBalancer の Kubernetes Service をデプロイし、Google Cloud 上でトランスポート レイヤ(レイヤ 4)のネットワーク負荷分散として公開します。このサービスはパブリック IP アドレスを 1 つ提供し、構成されているバックエンドに TCP 接続を直接渡します。このチュートリアルでのバックエンドは、Envoy インスタンスの Kubernetes Deployment です。

Envoy は、高度な機能を数多く提供するオープンソースのアプリケーション レイヤ(レイヤ 7)プロキシです。このチュートリアルでは、Envoy を使用して TLS 接続の終端処理を行い、gRPC トラフィックを適切な Kubernetes Service に転送します。Kubernetes Ingress などの他のアプリケーション レイヤのソリューションとは異なり、Envoy を使用すると、次のようないくつものカスタマイズ オプションを直接利用できます。

  • サービス ディスカバリ
  • 負荷分散アルゴリズム
  • リクエストやレスポンスの変換(たとえば JSON や gRPC-Web などへの変換)
  • JWT トークンを検証することによるリクエストの認証
  • gRPC ヘルスチェック

ネットワーク負荷分散と Envoy を組み合わせることで、1 つのエンドポイント(外部 IP アドレス)をセットアップし、GKE クラスタ内で実行されている一連の Envoy インスタンスにトラフィックを転送できます。これらのインスタンスはアプリケーション レイヤの情報を使用して、クラスタ内で実行されているさまざまな gRPC サービスに対するリクエストをプロキシ処理します。Envoy インスタンスは、クラスタ DNS を使用して gRPC の受信リクエストを識別し、各サービスの正常に実行されている Pod に負荷分散します。つまりトラフィックは、クライアントからの TCP 接続単位ではなく、RPC リクエスト単位でポッドに負荷分散されます。

アーキテクチャ

このチュートリアルでは、Google Kubernetes Engine(GKE)クラスタに 2 つの gRPC サービス、echo-grpcreverse-grpc をデプロイし、パブリック IP アドレスでインターネットに公開します。次の図は、これら 2 つのサービスを 1 つのエンドポイントを通して公開するためのアーキテクチャを示しています。

1 つのエンドポイントを通して「echo-grpc」と「reverse-grpc」を公開するためのアーキテクチャ

ネットワーク負荷分散がインターネットから(たとえば、モバイル クライアントや社外のサービス ユーザーから)受信リクエストを受け入れます。ネットワーク負荷分散は次のタスクを実行します。

  • 受信接続をプール内のノードへ負荷分散します。トラフィックは、クラスタ内のすべてのノードに公開されている envoy Kubernetes Service に転送されます。Kubernetes ネットワーク プロキシは、Envoy を実行しているポッドにこれらの接続を転送します。
  • クラスタ内のノードに対して HTTP ヘルスチェックを実行します。

Envoy は次のタスクを実行します。

  • TLS 接続を終端します。
  • 内部クラスタの DNS サービスを照会して、gRPC サービスを実行しているポッドを検出します。
  • トラフィックを gRPC サービスの各ポッドにルーティング、負荷分散します。
  • gRPC ヘルスチェック プロトコルに従って gRPC サービスのヘルスチェックを実行します。
  • ネットワーク負荷分散を使用して、ヘルスチェックのためにエンドポイントを公開します。

gRPC サービス(echo-grpcreverse-grpc)は、Kubernetes のヘッドレス Service として公開されます。つまり、clusterIP アドレスが割り振られず、Kubernetes ネットワーク プロキシによってトラフィックが Pod に負荷分散されないということです。代わりに、クラスタの DNS サービス内に、Pod の IP アドレスが含まれた DNS A レコードが作成されます。Envoy はこの DNS エントリからポッドの IP アドレスを検出し、Envoy に構成されているポリシーに従ってそれらの IP アドレス間で負荷分散を行います。

次の図は、このチュートリアルに含まれる Kubernetes オブジェクトを示しています。

サービス、YAML ファイル、DNS A レコード、シークレット、Pod、プロキシ エントリなど、このチュートリアルで使用する Kubernetes オブジェクト

料金

このチュートリアルでは、課金対象である次の Google Cloud コンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。 新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

このチュートリアルを終了した後、作成したリソースを削除すると、それ以上の請求は発生しません。詳細については、クリーンアップをご覧ください。

始める前に

  1. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  2. Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  3. Cloud Console で、Cloud Shell をアクティブにします。

    Cloud Shell をアクティブにする

環境の準備

  1. Cloud Shell で、このチュートリアルで使用する Cloud プロジェクトを設定します。

    gcloud config set project PROJECT_ID
    

    PROJECT_ID をクラウド プロジェクト ID に置き換えます。このコマンドを実行すると、Cloud Shell は、プロジェクト ID を含むエクスポートされた GOOGLE_CLOUD_PROJECT という環境変数を作成します。

  2. Google Kubernetes Engine API を有効にします。

    gcloud services enable container.googleapis.com
    

GKE クラスタの作成

  1. gRPC サービスを実行するための GKE クラスタを作成します。

    gcloud container clusters create grpc-cluster \
        --enable-ip-alias \
        --release-channel regular \
        --scopes cloud-platform \
        --workload-pool $GOOGLE_CLOUD_PROJECT.svc.id.goog \
        --zone us-central1-f
    

    このチュートリアルでは、us-central1-f ゾーンを使用します。別のゾーンやリージョンも使用できます。

  2. クラスタ内のノードを一覧表示して、kubectl コンテキストが設定されていることを確認します。

    kubectl get nodes --output=name
    

    出力は次のようになります。

    node/gke-grpc-cluster-default-pool-c9a3c791-1kpt
    node/gke-grpc-cluster-default-pool-c9a3c791-qn92
    node/gke-grpc-cluster-default-pool-c9a3c791-wf2h
    

gRPC サービスのデプロイ

1 つのロードバランサの背後にある複数の gRPC サービスにトラフィックを転送するために、2 つのサンプル gRPC サービス、echo-grpcreverse-grpc をデプロイします。どちらのサービスも、content リクエスト フィールド内の文字列を取得する単項メソッドを公開します。echo-grpc のレスポンスではコンテンツを変更せずに返し、reverse-grpc のレスポンスではコンテンツの文字列を逆にして返します。

  1. これらの gRPC サービスが含まれているリポジトリのクローンを作成し、リポジトリ ディレクトリに切り替えます。

    git clone https://github.com/GoogleCloudPlatform/grpc-gke-nlb-tutorial.git ~/grpc-gke-nlb-tutorial
    
    cd ~/grpc-gke-nlb-tutorial
    
  2. 自己署名 TLS 証明書と秘密鍵を作成します。

    openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 365 \
        -keyout privkey.pem -out cert.pem -extensions san \
        -config \
        <(echo "[req]";
          echo distinguished_name=req;
          echo "[san]";
          echo subjectAltName=DNS:grpc.example.com
         ) \
        -subj '/CN=grpc.example.com'
    
  3. envoy-certs という名前の Kubernetes Secret を作成し、自己署名 TLS 証明書と秘密鍵を格納します。

    kubectl create secret tls envoy-certs --key=privkey.pem --cert=cert.pem \
        --dry-run=client --output=yaml | kubectl apply --filename -
    

    この TLS 証明書と秘密鍵は、Envoy が TLS 接続の終端時に使用します。

  4. Skaffold を使用して、サンプルアプリ echo-grpcreverse-grpc のコンテナ イメージを作成し、そのイメージを Container Registry に公開した後、Envoy と両方のサンプルアプリを GKE にデプロイします。

    skaffold run --default-repo=gcr.io/$GOOGLE_CLOUD_PROJECT
    

    Skaffold は、Google のオープンソースで、アプリケーションをコンテナとしてビルド、push、デプロイするためのワークフローを自動化します。

  5. 各デプロイで 2 つの Pod の準備ができていることを確認します。

    kubectl get deployments
    

    出力は次のようになります。すべての Deployment で、READY の値は 2/2 になっている必要があります。

    NAME           READY   UP-TO-DATE   AVAILABLE   AGE
    echo-grpc      2/2     2            2           1m
    envoy          2/2     2            2           1m
    reverse-grpc   2/2     2            2           1m
    
  6. echo-grpcenvoyreverse-grpc が Kubernetes Services として存在することを確認します。

    kubectl get services --selector=app.kubernetes.io/managed-by=skaffold
    

    出力は次のようになります。echo-grpcreverse-grpc はどちらも TYPE=ClusterIPCLUSTER-IP=None になります。

    NAME           TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)         AGE
    echo-grpc      ClusterIP      None          <none>           8081/TCP        2m
    envoy          LoadBalancer   10.40.2.203   203.0.113.1      443:31516/TCP   2m
    reverse-grpc   ClusterIP      None          <none>           8082/TCP        2m
    

gRPC サービスをテストする

サービスをテストするには、grpcurl コマンドライン ツールを使用します。

  1. Cloud Shell で、grpcurl をインストールします。

    go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
    
  2. envoy Kubernetes Service の外部 IP アドレスを取得して、環境変数に保存します。

    EXTERNAL_IP=$(kubectl get service envoy \
        --output=jsonpath='{.status.loadBalancer.ingress[0].ip}')
    
  3. echo-grpc サンプルアプリにリクエストを送信します。

    grpcurl -d '{"content": "echo"}' -proto echo-grpc/api/echo.proto \
        -authority grpc.example.com -cacert cert.pem -v \
        $EXTERNAL_IP:443 api.Echo/Echo
    

    出力は次のようになります。

    Resolved method descriptor:
    rpc Echo ( .api.EchoRequest ) returns ( .api.EchoResponse );
    
    Request metadata to send:
    (empty)
    
    Response headers received:
    content-type: application/grpc
    date: Wed, 02 Jun 2021 07:18:22 GMT
    hostname: echo-grpc-75947768c9-jkdcw
    server: envoy
    x-envoy-upstream-service-time: 3
    
    Response contents:
    {
      "content": "echo"
    }
    
    Response trailers received:
    (empty)
    Sent 1 request and received 1 response
    

    hostname レスポンス ヘッダーには、リクエストを処理した echo-grpc Pod の名前が表示されます。このコマンドを数回繰り返すと、hostname レスポンス ヘッダーには echo-grpc Pod の名前に対応した 2 つの異なる値が表示されます。

  4. Reverse gRPC サービスでも同じ動作を確認します。

    grpcurl -d '{"content": "reverse"}' -proto reverse-grpc/api/reverse.proto \
        -authority grpc.example.com -cacert cert.pem -v \
        $EXTERNAL_IP:443 api.Reverse/Reverse
    

    出力は次のようになります。

    Resolved method descriptor:
    rpc Reverse ( .api.ReverseRequest ) returns ( .api.ReverseResponse );
    
    Request metadata to send:
    (empty)
    
    Response headers received:
    content-type: application/grpc
    date: Wed, 02 Jun 2021 07:20:15 GMT
    hostname: reverse-grpc-5c9b974f54-wlfwt
    server: envoy
    x-envoy-upstream-service-time: 1
    
    Response contents:
    {
      "content": "esrever"
    }
    
    Response trailers received:
    (empty)
    Sent 1 request and received 1 response
    

トラブルシューティング

このチュートリアルで問題が発生した場合は、次のドキュメントを確認することをおすすめします。

Envoy 管理インターフェースを使用して、Envoy の構成に関する問題を診断することもできます。

  1. 管理インターフェースを開くには、Cloud Shell からいずれかの Envoy Pod の admin ポートへのポート転送を設定します。

    kubectl port-forward \
        $(kubectl get pods -o name | grep envoy | head -n1) 8080:8090
    
  2. コンソールに次の出力が表示されるまで待ちます。

    Forwarding from 127.0.0.1:8080 -> 8090
    
  3. Cloud Shell で [ウェブでプレビュー] ボタンをクリックし、[プレビューのポート: 8080] を選択します。新しいブラウザ ウィンドウが開き、管理インターフェースが表示されます。

    プレビューが選択された状態の Envoy 管理インターフェース

  4. 完了したら Cloud Shell に戻り、Control+C キーを押してポート転送を終了します。

gRPC トラフィックをルーティングするための代替方法

このソリューションは、ご使用の環境に合うようにさまざまな方法で変更できます。

代替のアプリケーション レイヤ ロードバランサ

Envoy が提供するアプリケーション レイヤの機能は、他の負荷分散ソリューションでも提供されている場合があります。

  • Kubernetes Ingress オブジェクトを使用して HTTP(S) 負荷分散を構成し、ネットワーク負荷分散と Envoy の代わりに使用できます。HTTP(S) 負荷分散の使用には、ネットワーク負荷分散と比べていくつかの利点があります。たとえば、マネージド型の TLS 証明書、Cloud CDN や IAP などの他の Google Cloud プロダクトとの統合などが挙げられます。

    以下のいずれにも対応する必要がない場合は、HTTP(S) 負荷分散を使用することをおすすめします。

    • gRPC ヘルスチェック
    • 負荷分散アルゴリズムに対するきめ細かな制御
    • 50 以上のサービスの公開

    HTTP(S) 負荷分散とサンプル gRPC サービスのデプロイについては、Google Kubernetes Engine の Ingress に関するドキュメントと、GitHub の GKE gRPC Ingress LoadBalancing チュートリアルをご覧ください。

  • Anthos Service Mesh や Istio を使用する場合は、それらの機能を使用して gRPC トラフィックの転送と負荷分散を行えます。このチュートリアルのアーキテクチャと同様、Anthos Service Mesh と Istio はどちらも、Envoy のバックエンドを持つネットワーク負荷分散としてデプロイされる Ingress ゲートウェイを備えています。主な違いは、Envoy プロキシが Istio のトラフィック ルーティング オブジェクトを通じて構成されることです。このチュートリアルのサンプル サービスを Anthos Service Mesh や Istio サービス メッシュ内で転送できるようにするには、Kubernetes Service マニフェスト(echo-service.yamlreverse-service.yaml)から clusterIP: None の行を削除する必要があります。これは、サービス ディスカバリと負荷分散の機能は、Envoy の同様の機能ではなく、Anthos Service Mesh または Istio の機能を使用することを意味します。Anthos Service Mesh か Istio をすでに使用している場合は、Ingress ゲートウェイを使用して gRPC サービスに転送することをおすすめします。

  • Envoy の代わりに NGINX を Deployment として使用できます。または、NGINX Ingress Controller for Kubernetes を使用することもできます。Envoy では gRPC ヘルスチェック プロトコルのサポートなどの高度な gRPC 機能が提供されるため、このチュートリアルでは Envoy を使用しています。

VPC 内部のネットワーク接続

GKE クラスタの外部にサービスを公開したいが、VPC ネットワーク内に限定したい場合は、ネットワーク負荷分散の代わりに内部 TCP / UDP 負荷分散を使用します。これを行うには、envoy-service.yaml マニフェストにアノテーション cloud.google.com/load-balancer-type: "Internal" を追加します。

Envoy の Deployment と DaemonSet

このチュートリアルでは、Envoy は Kubernetes Deployment として構成されます。この構成では、Deployment マニフェストの replica 設定によって Envoy の Pod 数が決定されます。ロードバランサが Envoy Pod を実行していないノードに受信リクエストを転送すると、Kubernetes ネットワーク プロキシはそのリクエストを Envoy Pod を実行しているノードに転送します。

DaemonSet は、Envoy の Deployment に代わるものです。DaemonSet では、Envoy Pod は GKE クラスタ内のすべてのノードで実行されます。この方法を使用すると、大規模なクラスタ(多数の Envoy Pod)においてはリソースの使用量が多くなりますが、Envoy Pod を実行しているノードに受信リクエストが必ず到達します。Envoy Pod に到達させるためにノード間でリクエストが転送されることがないため、結果としてクラスタ内のネットワーク トラフィックが減少し、平均レイテンシが小さくなります。

クリーンアップ

チュートリアルが終了したら、作成したリソースをクリーンアップして、割り当ての使用を停止し、課金されないようにできます。次のセクションで、リソースを削除または無効にする方法を説明します。

プロジェクトの削除

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

リソースの削除

このチュートリアルで使用した Google Cloud プロジェクトを残しておく場合は、個々のリソースを削除します。

  1. Cloud Shell で、ローカル Git リポジトリのクローンを削除します。

    cd ; rm -rf ~/grpc-gke-nlb-tutorial
    
  2. Container Registry 内のイメージを削除します。

    gcloud container images list-tags gcr.io/$GOOGLE_CLOUD_PROJECT/echo-grpc \
        --format 'value(digest)' | xargs -I {} gcloud container images delete \
        --force-delete-tags --quiet gcr.io/$GOOGLE_CLOUD_PROJECT/echo-grpc@sha256:{}
    
    gcloud container images list-tags gcr.io/$GOOGLE_CLOUD_PROJECT/reverse-grpc \
        --format 'value(digest)' | xargs -I {} gcloud container images delete \
        --force-delete-tags --quiet gcr.io/$GOOGLE_CLOUD_PROJECT/reverse-grpc@sha256:{}
    
  3. GKE クラスタを削除します。

    gcloud container clusters delete grpc-cluster --zone us-central1-f --async
    

次のステップ