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 を使用して SSL / TLS 接続の終端処理を行い、gRPC トラフィックを適切な Kubernetes Service にルーティングします。Kubernetes Ingress などの他のアプリケーション レイヤのソリューションとは異なり、Envoy を使用すると、次のようないくつものカスタマイズ オプションを直接利用できます。

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

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

費用

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

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

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

始める前に

  1. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

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

    [プロジェクトの選択] ページに移動

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

  4. Cloud Build, Container Registry, and Container Analysis API を有効にします。

    API を有効にする

アーキテクチャ

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

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

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

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

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

  • SSL/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 オブジェクト

環境を初期化する

このセクションでは、チュートリアルで使用する環境変数を設定します。

  1. Cloud Shell を開きます。

    Cloud Shell に移動

    このチュートリアルでは、コマンドの実行はすべて Cloud Shell で行います。

  2. Cloud Shell で現在のプロジェクト ID を表示します。

    gcloud config list --format 'value(core.project)'
    
  3. コマンドから選択したプロジェクトの ID が返されない場合は、プロジェクトが使用されるように Cloud Shell を構成します。project-id はプロジェクトの名前に置き換えます。

    gcloud config set project project-id
    
  4. このチュートリアルで使用するリージョンとゾーンの環境変数を定義します。

    REGION=us-central1
    ZONE=$REGION-b
    

    このチュートリアルでは、us-central1 リージョンと us-central1-b ゾーンを使用します。ただし、リージョンとゾーンは必要に応じて変更できます。

GKE クラスタを作成する

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

    gcloud container clusters create grpc-cluster --zone $ZONE
    
  2. クラスタ内の動作中のノードを一覧表示して、kubectl コンテキストが設定されていることを確認します。

    kubectl get nodes -o 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 をデプロイします。どちらのサービスも、コンテンツのリクエスト フィールド内の文字列を取得する単項メソッドを公開します。echo-grpc のレスポンスではコンテンツを変更せずに返し、reverse-grpc のレスポンスではコンテンツの文字列を逆にして返します。

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

    git clone https://github.com/GoogleCloudPlatform/grpc-gke-nlb-tutorial
    cd grpc-gke-nlb-tutorial
    
  2. Cloud Build を使用して、Echo と Reverse の gRPC サービス用にコンテナ イメージを作成し、それらを Container Registry に格納します。

    gcloud builds submit -t gcr.io/$GOOGLE_CLOUD_PROJECT/echo-grpc echo-grpc
    
    gcloud builds submit -t gcr.io/$GOOGLE_CLOUD_PROJECT/reverse-grpc reverse-grpc
    
  3. Container Registry にイメージが存在することを確認します。

    gcloud container images list --repository gcr.io/$GOOGLE_CLOUD_PROJECT
    

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

    NAME
    gcr.io/grpc-gke-nlb-tutorial/echo-grpc
    gcr.io/grpc-gke-nlb-tutorial/reverse-grpc
    
  4. echo-grpcreverse-grpc 用に Kubernetes Deployment を作成します。

    sed s/GOOGLE_CLOUD_PROJECT/$GOOGLE_CLOUD_PROJECT/ \
        k8s/echo-deployment.yaml | kubectl apply -f -
    
    sed s/GOOGLE_CLOUD_PROJECT/$GOOGLE_CLOUD_PROJECT/ \
        k8s/reverse-deployment.yaml | kubectl apply -f -
    
  5. 各 Deployment で 2 つの Pod が利用可能であることを確認します。

    kubectl get deployments
    

    出力は次のようになります。どちらの Deployment でも、DESIREDCURRENTUP-TO-DATEAVAILABLE の値が 2 になっているはずです。

    NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
    echo-grpc      2         2         2            2           1m
    reverse-grpc   2         2         2            2           1m
    
  6. echo-grpcreverse-grpc 用に Kubernetes ヘッドレス Service を作成します。以下のコマンドはクラスタの DNS サービスに DNS A レコードを作成しますが、仮想 IP アドレスは割り振りません。

    kubectl apply -f k8s/echo-service.yaml
    kubectl apply -f k8s/reverse-service.yaml
    
  7. echo-grpcreverse-grpc が Kubernetes Services として存在することを確認します。

    kubectl get services
    

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

    NAME           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
    echo-grpc      ClusterIP   None         <none>        8081/TCP   35s
    kubernetes     ClusterIP   10.0.0.1     <none>        443/TCP    47m
    reverse-grpc   ClusterIP   None         <none>        8082/TCP   21s
    

ネットワーク負荷分散を設定する

  1. クラスタ内に LoadBalancer タイプの Kubernetes Service を作成します。

    kubectl apply -f k8s/envoy-service.yaml
    

    このコマンドはネットワーク負荷分散に必要なリソースをプロビジョニングし、エフェメラル パブリック IP アドレスを割り振ります。パブリック IP アドレスが割り振られるまで数分かかることがあります。

  2. 次のコマンドを実行して、envoy サービスの EXTERNAL-IP の値が <pending> からパブリック IP アドレスに変わるまで待機します。

    kubectl get services envoy --watch
    
  3. Control+C キーを押すと、待機を終了します。

自己署名 SSL / TLS 証明書を作成する

Envoy は SSL/TLS 接続の終端処理を行う際に証明書と鍵を使用します。まずは、自己署名 SSL/TLS 証明書を作成します。

  1. 環境変数を作成し、前のセクションで作成した Envoy サービスのパブリック IP アドレスを格納します。

    EXTERNAL_IP=$(kubectl get service envoy -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    
  2. 自己署名 SSL / TLS 証明書と鍵を作成します。

    openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
        -keyout privkey.pem -out cert.pem -subj "/CN=$EXTERNAL_IP"
    
  3. envoy-certs という名前の Kubernetes TLS Secret を作成し、自己署名 SSL / TLS 証明書と鍵を格納します。

    kubectl create secret tls envoy-certs \
        --key privkey.pem --cert cert.pem \
        --dry-run -o yaml | kubectl apply -f -
    

Envoy をデプロイする

  1. Envoy の構成ファイル(envoy.yaml)を保存するための Kubernetes ConfigMap を作成します。

    kubectl apply -f k8s/envoy-configmap.yaml
    
  2. Envoy 用の Kubernetes Deployment を作成します。

    kubectl apply -f k8s/envoy-deployment.yaml
    
  3. 2 つの envoy Pod が実行されていることを確認します。

    kubectl get deployment envoy
    

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

    NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
    envoy     2         2         2            2           1m
    

これで、gRPC サービスをテストする準備が整いました。

gRPC サービスをテストする

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

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

    go get github.com/fullstorydev/grpcurl
    go install github.com/fullstorydev/grpcurl/cmd/grpcurl
    
  2. Echo gRPC サービスにリクエストを送信します。

    grpcurl -d '{"content": "echo"}' -proto echo-grpc/api/echo.proto \
        -insecure -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, 27 Feb 2019 04:40:19 GMT
    hostname: echo-grpc-5c4f59c578-wcsvr
    server: envoy
    x-envoy-upstream-service-time: 0
    
    Response contents:
    {
      "content": "echo"
    }
    
    Response trailers received:
    (empty)
    Sent 1 request and received 1 response
    

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

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

    grpcurl -d '{"content": "reverse"}' -proto reverse-grpc/api/reverse.proto \
        -insecure -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, 27 Feb 2019 04:45:56 GMT
    hostname: reverse-grpc-74cdc4849f-tvsfb
        server: envoy
    x-envoy-upstream-service-time: 2
    
    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) 負荷分散の使用には、ネットワーク負荷分散と比べていくつかの利点があります。たとえば、マネージド型の SSL / TLS 証明書、Cloud CDN や IAP などの他の Google Cloud プロダクトとの統合などが挙げられます。

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

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

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

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

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

  • AmbassadorContour を使用できます。これらは Kubernetes Ingress コントローラを提供し、Envoy をベースにしています。

  • Voyager を使用できます。Voyager は、HAProxy をベースにした Kubernetes Ingress コントローラです。

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

クリーンアップ

現在のチュートリアルが終了したら、Google Cloud で作成したリソースをクリーンアップして、今後料金が発生しないようにします。次のセクションで、リソースを削除または無効にする方法を説明します。

プロジェクトの削除

  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. Google Kubernetes Engine クラスタを削除します。

    gcloud container clusters delete grpc-cluster --zone $ZONE --quiet  --async
    

次のステップ