目的
システムの規模が大きくなってくると、モノリシックな構成からマイクロサービス化を検討し始めます。その際にサービス間の連携はどのようにするでしょうか。
チームやネットワークインフラが同じであれば、普段はローカルに環境を用意して開発を行い、プロダクションではプライベートに閉じた通信を行えば問題ないかもしれません。
しかし、インフラ構成を分けたかったり手軽に開発環境を用意したい場合であれば、今回の記事で紹介するGoogle Cloud Endpoints(以下、Cloud Endpoints)は使いやすいと思います。
この記事では、通信のプロトコルにはgRPC with HTTP/2を使った上で、Cloud Endpointsを用いてpublicなエンドポイントをセキュアに提供する方法をご紹介します。
なお、サンプルのprotoファイルや構成はこちらの公式ドキュメントをベースにしていますが、本番向けや自動化するには不親切な設定がいくつかあるので加筆しています。
環境
- Mac OS: Big Sur
- gRPC Client: Kotlin
- GKE: 1.21.5
新しいEndpointを作成する
まず、新しいEndpointの設定を行いましょう。
# reserve IP Address for assigning to our Endpoint
gcloud compute addresses create <IP_NAME> \
--global \
--ip-version IPV4
# generate Descriptor file
# (protocコマンドはもしなければbrewなどでインストールしてください。)
protoc --descriptor_set_out=api_descriptor.pb \
--include_imports \
--include_source_info \
bookstore.proto
# create Endpoint
gcloud endpoints services deploy api_descriptor.pb api_config.yaml --project <project id>
# The configuration schema is defined by service.proto file
# https://github.com/googleapis/googleapis/blob/master/google/api/service.proto
type: google.api.Service
config_version: 3
name: bookstore.endpoints.<project id>.cloud.goog
endpoints:
- name: bookstore.endpoints.<project id>.cloud.goog
target: <reserved ip address>
title: Bookstore gRPC API
apis:
- name: endpoints.examples.bookstore.Bookstore
usage:
rules:
# ListShelves methods can be called without an API Key.
- selector: endpoints.examples.bookstore.Bookstore.ListShelves
allow_unregistered_calls: true
endpintを作成するときの設定ファイルとなるapi_config.yamlですが、以下のように記述します。
-
endpoints.name
はendpointのドメインとなり、<service name>.endpoints.<project id>.cloud.goog
という命名規則になる様子 -
target
を設定することで、上記ドメインへGoogle側でDNSのAレコードを作成してくれます。- このIP Addressは後ほどIngressに設定します。
-
apis.name
はprotoファイルのpackage名が入ります。 -
usage.rules
はAPIの権限管理をすることが出来ます。- ここではサンプルを引用したのでAPI key無しのアクセスも設定されていますが、全て許可しないのが一般的ではないでしょうか。
(Serviceの有効化 ServiceAccountの権限設定)
これらは基本的に必要ありませんが、もし下記APIがenableになっていなければ設定します。
gcloud services enable servicemanagement.googleapis.com
gcloud services enable servicecontrol.googleapis.com
gcloud services enable endpoints.googleapis.com
また、GKEがAutopilotモードの場合、k8sのServiceAccountにGCEデフォルトのService Accountの権限が入っていないため、別途Service Accountの追加と権限付与が必要です。
gcloud iam service-accounts create esp-v2-sa --display-name="SA for Cloud Endpoints SideCar Container" --project <project id>
gcloud iam service-accounts keys create ~/sa-private-key.json \
--iam-account=esp-v2-sa@<project id>.iam.gserviceaccount.com
gcloud endpoints services add-iam-policy-binding bookstore.endpoints.<project id>.cloud.goog \
--member serviceAccount:esp-v2-sa@<project id>.iam.gserviceaccount.com \
--role roles/servicemanagement.serviceController
gcloud projects add-iam-policy-binding <project id> \
--member serviceAccount:esp-v2-sa@<project id>.iam.gserviceaccount.com \
--role roles/cloudtrace.agent
k8sのDeploymentの設定
apiVersion: apps/v1
kind: Deployment
metadata:
name: esp-grpc-bookstore
spec:
replicas: 1
selector:
matchLabels:
app: esp-grpc-bookstore
template:
metadata:
labels:
app: esp-grpc-bookstore
spec:
volumes:
- name: esp-ssl
secret:
secretName: esp-ssl
- name: google-cloud-key-esp2
secret:
secretName: esp2-sa-secret
containers:
- name: esp
image: gcr.io/endpoints-release/endpoints-runtime:2
args: [
"--listener_port=9000",
"--service=bookstore.endpoints.<project id>.cloud.goog",
"--rollout_strategy=managed",
"--backend=grpc://127.0.0.1:8000",
"--healthz=/",
"--service_account_key=/etc/sa/key.json",
"--ssl_server_cert_path=/etc/esp/ssl",
]
ports:
- containerPort: 9000
volumeMounts:
- mountPath: /etc/esp/ssl
name: esp-ssl
readOnly: true
- mountPath: /etc/sa
name: google-cloud-key-esp2
readOnly: true
- name: bookstore
image: gcr.io/endpointsv2/python-grpc-bookstore-server:1
ports:
- containerPort: 8000
ここで、
-
gcr.io/endpoints-release/endpoints-runtime:2
(ESP v2)をSideCar Containerとして用意しています。- これがProxyの役割を果たし、
gRPC Backend(port:8000) -> ESP V2(port:8000 -> 9000) -> External
という流れで繋がっています。 - ログを見る限り、ESP V2は中でEnvoyが動いているようです。公式によるとここのオーバーヘッドは1ms以下だそうなのでパフォーマンスを気にすることはなさそうです。
- これがProxyの役割を果たし、
-
rollout_strategy=managed
を設定すると、endpointのdeployが行われると自動で各SideCarの設定を反映してくれます(5分間隔?) -
esp-ssl
secretは以下のように自己署名証明書を作成します。- 後述しますが、これはHTTP/2を有効にするためにつけているもので、
Ingress <-> Deployment
間の内部の通信に使われるものなので特に問題はないです。
- 後述しますが、これはHTTP/2を有効にするためにつけているもので、
# create self-signed cert file with any duration
openssl req -x509 -nodes -days 4650 -newkey rsa:2048 \
-keyout ./server.key -out ./server.crt
kubectl create secret generic line-service-esp-ssl --from-file=server.crt=./server.crt --from-file=server.key=./server.key
k8sのServiceの設定
apiVersion: v1
kind: Service
metadata:
name: esp-grpc-bookstore
annotations:
service.alpha.kubernetes.io/app-protocols: '{"esp-grpc-bookstore":"HTTP2"}'
cloud.google.com/neg: '{"ingress": true, "exposed_ports": {"443":{}}}'
cloud.google.com/backend-config: '{"default": "esp-grpc-bookstore"}'
spec:
ports:
# Port that accepts gRPC and JSON/HTTP2 requests over TLS.
- port: 443
targetPort: 9000
protocol: TCP
name: esp-grpc-bookstore
selector:
app: esp-grpc-bookstore
type: ClusterIP
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: esp-grpc-bookstore
spec:
healthCheck:
type: HTTP2
requestPath: /
port: 9000
ここで、
-
service.alpha.kubernetes.io/app-protocols
の箇所で、今回のportへHTTP/2のprotocolを有効にしています。 -
cloud.google.com/neg
はNEG(Network Endpoint Group)と呼ばれるGCPの通信経路最適化の機能です。無くても動くとは思います。 -
cloud.google.com/backend-config
は、backend-config.yaml
を反映する設定です。- ここではHealthCheckの設定を入れていて、Ingressとつないだ際にLBのリクエストを流していいかの判断に使われます。
- Deployment側のreadinessProbeとはおそらく少し役割が異なっていて、死活監視ではなくServiceを経由してのHTTP/2の疎通確認のためかと思います。
k8s Ingressの設定
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: esp-grpc-bookstore
annotations:
# kubernetes.io/ingress.allow-http: "false"
kubernetes.io/ingress.global-static-ip-name: <IP_NAME>
networking.gke.io/managed-certificates: bookstore-grpc-certificate
spec:
defaultBackend:
service:
name: esp-grpc-bookstore
port:
number: 443
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: bookstore-grpc-certificate
spec:
domains:
- bookstore.endpoints.<project id>.cloud.goog
ここで、
-
kubernetes.io/ingress.allow-http
は後述するManaged SSLが取得出来たら無効にしても問題ないです。 -
networking.gke.io/managed-certificates
には上記に記載しているManagedCertificate
のnameを入れてください。- ManagedCertificateは、Google ManagedのSSL証明書を用意してくれるResourceです。
- dominsに設定しているドメインへのIngressを通じてのhttpの疎通が成功(ドキュメントが見つかりませんでしたが、おそらく
path=/
が200を返す)するとActiveになります。 - これがあるためにESPコンテナで
--healthz=/
を設定していますが、証明書が取れたあとであれば変えても構いません。
-
kubernetes.io/ingress.allow-http
は、Managed証明書を取得するためにhttpを許可しています。- 証明書が取れた後であればコメントを外してHTTP portを閉じても構いません。
Clientからアクセス出来るか確認
ここまででサーバーサイドの設定は全て完了です。最後にgRPCクライアントからの接続を試します。
まずAPI Keyを作成します。GCPの管理画面から作成しましょう。
API KeyのAPI restrictionsにて、作成したBookStore APIのみ許可します。
取得したAPI Keyを用いたClientのサンプル(Kotlin)がこちらです。
import com.google.endpoints.examples.bookstore.BookstoreGrpcKt
import com.google.endpoints.examples.bookstore.ListBooksRequest
import io.grpc.ClientInterceptors
import io.grpc.ManagedChannelBuilder
import io.grpc.Metadata
import io.grpc.stub.MetadataUtils
suspend fun callGrpc() {
val metadata = Metadata()
metadata.put(Metadata.Key.of("x-api-key",Metadata.ASCII_STRING_MARSHALLER), "<API key>")
val channel = ClientInterceptors.intercept(
ManagedChannelBuilder.forAddress("bookstore.endpoints.<project id>.cloud.goog", 443)
.useTransportSecurity()
.build(),
MetadataUtils.newAttachHeadersInterceptor(metadata)
)
val stub = BookstoreGrpcKt.BookstoreCoroutineStub(channel)
val res = stub.ListBooks(
ListBooksRequest.newBuilder()
.shelf(1)
.build()
)
println("message: ${res}")
}
プロダクションレディか?
さて、今回のインフラ構成において、end-to-endの通信の流れは以下のようになっています。
Client(gRPC) ->
cloud endpoints domain DNS(ookstore.endpoints.<project id>.cloud.goog) ->
k8s ingress -> k8s service ->
ESP SideCar Container (Envoy) ->
gRPC backend
このうち、Clientからk8s ingressまではGoogle Managed SSLが使われ、ingressからESP Containerまでは自己署名証明書が使われる形になります。公式ドキュメントにあるように、podをport-forwardしてgRPCを繋ごうとすると自己証明書のCAファイルが求められます。
ちなみに、HTTP/2が有効になっていることはブラウザのExtensionやチェックサイトで確認できます。LBやProxyを噛ませた場合の接続のtimeoutやStreamingに影響が出るかなどは調べきれてないので後日確認したいと思います。
次に応答速度については、今回のbootStoreの例だと、end-to-endのレイテンシはリージョンが同じであればおよそ100ms以内で返ってきます。Cloud Endpoints特有のインフラはESP Container部分のみで、上述の通り1ms以下で動くとされているため、サービス間通信に使用しても普通は問題ないでしょう。
また運用面においては、protoファイルに変更があった場合には以下の手順で更新を行います。
- protocでdescriptorファイルを生成
-
gcloud endpoints services deploy
で最新に反映 - ESP Contianerは自動で設定を反映
- gRPC Backendも、最新のImageをデプロイすることで反映
他、SSL証明書もManagedなものと長く期間を設定した自己署名のものしかないので、手間もコストもかからず運用出来ます。