go で ESPv2 を使用して Cloud Run 用の Endpoints gRPC を設定するチュートリアルをやってみた

はじめに

チュートリアルとして、「ESPv2 を使用して Cloud Run 用の Cloud Endpoints gRPC を設定する」をやってみた。
元の手順は、Python 用なので、go 言語用に手順を変更している。
実施は、原則ローカル環境で行う。
ただし、Windows 上で実施するため、一部 Cloud Shell を利用する。
Cloud Shell を利用すれば、vi や、git はもちろん、go や、protoc などもインストール済みなので、環境構築は不要となる。
このチュートリアルを実施している様子を YouTube に投稿している。

事前準備

以下はインストール済みとする。

  • gcloud
  • go
  • git
  • protoc

Google Cloud のプロジェクトは作成済みとする。
以下のように gcloud コマンドの設定をする。
gcloud config set project YOUR_PROJECT_ID
gcloud config set run/region asia-northeast1

このチュートリアルでは、以下の Google Cloud API を利用する。

  • Cloud Build API
  • Cloud Run API
  • Google Cloud Endpoints
  • Artifact Registry API
  • Service Control API
  • Service Management API

Google Cloud の API を有効化する。

gcloud services enable cloudbuild.googleapis.com run.googleapis.com endpoints.googleapis.com artifactregistry.googleapis.com servicecontrol.googleapis.com servicemanagement.googleapis.com

チュートリアル実施


Artifact Registry にリポジトリを作成する。

手順とは異なるが、Docker イメージの格納先を Artifact Registry とする。
gcloud artifacts repositories create --repository-format=docker --location=asia-northeast1 go-grpc-hello
作成されたリポジトリは、以下のような形式となる。
asia-northeast1-docker.pkg.dev/YOUR_PROJECT_ID/go-grpc-hello

チュートリアル用のソースを準備する。

以下の go 言語用の Compute Engine や Kubernetes 用のソースを流用する。
git clone --depth 1 https://github.com/GoogleCloudPlatform/golang-samples.git
cd golang-samples\endpoints\getting-started-grpc

gRPC バックエンドコンテナを Cloud Run にデプロイする。


gRPC バックエンド Docker イメージを作成する。

server/main.go ファイルを以下のように修正する。
Cloud Run の場合は、コンテナのポート(デフォルトは 8080)が、環境変数(PORT)として渡されるため、 gRPC サーバ側で、環境変数を読み取って、通信用ポートとして設定するようにソースを変更している。
index f976999..4ba5a9c 100644
--- a/endpoints/getting-started-grpc/server/main.go
+++ b/endpoints/getting-started-grpc/server/main.go
@@ -38,6 +38,7 @@ import (
 	"flag"
 	"log"
 	"net"
+	"os"

 	"google.golang.org/grpc"
 	"google.golang.org/grpc/examples/helloworld/helloworld"
@@ -63,7 +64,12 @@ func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloRe
 func main() {
 	flag.Parse()

-	lis, err := net.Listen("tcp", *addr)
+	port := os.Getenv("PORT")
+	addrPort := *addr
+	if port != "" {
+		addrPort = ":"+port
+	}
+	lis, err := net.Listen("tcp", addrPort)
 	if err != nil {
 		log.Fatalf("failed to listen: %v", err)
 	}
go.mod および go.sum をコピーする。
copy ..\go.* .
Dockerfile を以下のように変更する。
ソースビルド時に、module を利用する手順に変更している。
FROM golang:alpine

RUN apk update \
 && apk add git

COPY . /go/src/app

WORKDIR /go/src/app
RUN go install ./server

ENTRYPOINT ["/go/bin/server"]
リポジトリに Docker イメージの登録を実行する。YOUR_PROJECT_ID をプロジェクトIDに変更すること。
gcloud builds submit --tag asia-northeast1-docker.pkg.dev/YOUR_PROJECT_ID/go-grpc-hello/backend:1.0 .

gRPC バックエンドコンテナをデプロイする。

以下を実行すると、「Allow unauthenticated invocations to [go-grpc-hello-backend] (y/N)?」と問われるので、N のままで続行する。
gcloud run deploy go-grpc-hello-backend --image="asia-northeast1-docker.pkg.dev/YOUR_PROJECT_ID/go-grpc-hello/backend:1.0" --use-http2
gRPC バックエンドのサービス URL をコピーしておく。以下のような行が、サービス URL となる。
Service URL: https://go-grpc-hello-backend-tm4rajtw7a-an.a.run.app

仮のイメージで、Extensible Service Proxy V2(ESPv2) 用のコンテナを Cloud Run にデプロイする。

利用するイメージは、仮のイメージであり、後で差し替える前提となる。 自身の Cloud Run サービス URL が、コンテナイメージの作成や、Endpoints の設定などで必要となるため、事前に仮のイメージでサービスを登録する必要がある。
gcloud run deploy go-grpc-hello --image="gcr.io/cloudrun/hello" --allow-unauthenticated --platform managed
作成した Cloud Run のサービス URL をコピーしておく。以下のような行が、サービス URL となる。
Service URL: https://go-grpc-hello-tm4rajtw7a-an.a.run.app

Endpoints サービスを作成する。


protobuf の定義ファイル(out.pb)を生成する。

protoc --include_imports --include_source_info --descriptor_set_out out.pb helloworld/helloworld.proto

Endpoints サービス定義ファイル(api_config.yaml)を変更する。

Endpoints サービス定義ファイル(api_config.yaml)を以下のように変更する。
「name: hellogrpc.endpoints.YOUR_PROJECT_ID.cloud.goog」行の値を、 サービスホスト(サービス URL の「http://」を除いた後半部分すべて)を設定する。
例:go-grpc-hello-tm4rajtw7a-an.a.run.app
末尾行に以下を追加する。 address の値は、「grpcs://」+gRPC バックエンド Cloud Run サービスホストに変更すること。
backend:
  rules:
  - selector: "*"
    address: grpcs://go-grpc-hello-backend-tm4rajtw7a-an.a.run.app
最終的な差分は、以下のようになる。値は、環境によって変わるので注意。
index 81b4b25..f7be921 100644
--- a/endpoints/getting-started-grpc/api_config.yaml
+++ b/endpoints/getting-started-grpc/api_config.yaml
@@ -22,9 +22,13 @@ type: google.api.Service
 config_version: 3

 # Name of the service configuration.
-name: hellogrpc.endpoints.YOUR_PROJECT_ID.cloud.goog
+name: go-grpc-hello-tm4rajtw7a-an.a.run.app

 # API title to appear in the user interface (Google Cloud Console).
 title: Hello gRPC API
 apis:
 - name: helloworld.Greeter
+backend:
+  rules:
+  - selector: "*"
+    address: grpcs://go-grpc-hello-backend-tm4rajtw7a-an.a.run.app

Endpoints サービスをデプロイする。

gcloud endpoints services deploy out.pb api_config.yaml
Endpoints サービスの「設定 ID」(ここでは、2022-12-19r0)をコピーしておく。 以下は、表示例。
Service Configuration [2022-12-19r0] uploaded for service [go-grpc-hello-tm4rajtw7a-an.a.run.app]

Extensible Service Proxy V2(ESPv2) コンテナを Cloud Run にデプロイする。


新たに Extensible Service Proxy V2(ESPv2) Docker イメージを作成する。

ESP インスタンスは、動作に必要な情報を取得するため、起動時に幾つかの API を呼び出す必要がある。 Cloud Run のような、インスタンスが永続しないサーバーレス構成では、このような API 呼び出しが頻繁に行われることになる。 起動時に呼び出す API で取得する情報は、変更することがない情報のため、Docker イメージ作成時に埋め込むようにする。
この作業には、bash が必要となるため、Cloud Shell を利用して実施する。 起動する手順は、以下を参照。
curl -LO https://github.com/GoogleCloudPlatform/esp-v2/raw/master/docker/serverless/gcloud_build_image
chmod +x gcloud_build_image
以下のコマンドを実行すると、ブラウザ側に「Cloud Shell の承認」ダイアログが開くので、「承認」をクリックする。 コマンドで「Would you like to enable and retry (this will take a few minutes)? (y/N)?」と表示されるので、「y」を入力して Enter を押す。 場合によっては、API の有効化に時間がかかる場合もある。 「-p」オプションは、必須となる。
./gcloud_build_image -g <Artifact Registry にリポジトリ>  -s <ESPv2 Cloud Run サービスホスト> -c <Endpoints サービスの設定 ID> -p YOUR_PROJECT_ID
コマンド実行が完了すると、Artifact Registry のリポジトリに、以下のイメージが作成される。
asia-northeast1-docker.pkg.dev/YOUR_PROJECT_ID/go-grpc-hello/endpoints-runtime-serverless:ESP_VERSION-<ESPv2 Cloud Run サービスホスト>-<Endpoints サービスの設定 ID>

ESPv2 コンテナをデプロイする。

コマンドの実行は、ローカルで行う。 このチュートリアルでは、意味は無いが、「--use-http2」オプションによって、gRPC サービスでストリーミングプロトコルを利用できるようにし、CORS 関連のオプションも設定している。
gcloud run deploy go-grpc-hello --image="asia-northeast1-docker.pkg.dev/YOUR_PROJECT_ID/go-grpc-hello/endpoints-runtime-serverless:ESP_VERSION-<Cloud Run サービスホスト>-<Endpoints サービスの設定 ID>" --allow-unauthenticated --use-http2 --set-env-vars=ESPv2_ARGS=--cors_preset=basic
CORS のオリジン指定など、詳細なオプションは、以下を参照。

クライアントで接続する。


API キーを作成する。

Endpoints に作成したサービスにアクセスするために、API キーが必要となる。
Google Cloud コンソールの「API とサービス」→「認証情報」画面で、「+認証情報を作成」ボタンを押下し、「API キー」を選択する。 表示された API キーをコピーしておく。

クライアントを修正する。

クライアント(client\main.go)を以下の差分のように修正する。
Cloud Run の場合は、接続プロトコルが HTTPS となるため、 クライアントの接続方法を変更する必要がある。
index 605106e..70084be 100644
--- a/endpoints/getting-started-grpc/client/main.go
+++ b/endpoints/getting-started-grpc/client/main.go
@@ -39,7 +39,10 @@ import (
 	"fmt"
 	"io/ioutil"
 	"log"
+	"crypto/tls"
+	"crypto/x509"

+	"google.golang.org/grpc/credentials"
 	"golang.org/x/oauth2/google"
 	"google.golang.org/grpc"
 	pb "google.golang.org/grpc/examples/helloworld/helloworld"
@@ -59,8 +62,15 @@ var (
 func main() {
 	flag.Parse()

+	systemRoots, err := x509.SystemCertPool()
+	if err != nil {
+		log.Fatal(err)
+	}
+	cred := credentials.NewTLS(&tls.Config{
+		RootCAs: systemRoots,
+	})
 	// Set up a connection to the server.
-	conn, err := grpc.Dial(*addr, grpc.WithInsecure())
+	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(cred))
 	if err != nil {
 		log.Fatalf("did not connect: %v", err)
 	}

クライアントで接続する。

「--api-key」オプションの値には、作成しておいた API キーを指定する。
「--addr」オプションの値には、ESPv2 コンテナのサービス URLのホスト名(https:// を除いた後半すべて)+ ":443" を設定する。例:go-grpc-hello-tm4rajtw7a-an.a.run.app:443
go run client/main.go --api-key=AIza... --addr=<ESPv2 サービスホスト名>:443 FOOBAR
以下のように表示されれば成功。
2022/12/20 17:12:06 Using API key: AIzaxxxxxxxxxxxxxxxxxxxxxx
2022/12/20 17:12:08 Greeting: Hello FOOBAR

参考資料