GCPでREST APIを提供するなら、Cloud Endpointsを使わない選択肢はほとんどあり得ません、かどうかは知りませんが、筆者は利用しなかったことがありません。

Cloud EndpointsはそのバックエンドとしてAppEngine, Kubernetes Engine, Cloud Runといったサービスが利用できますが、先日リリースされたGKE Autopilotでの利用を想定して素振りをするのがこの記事の目的です。

今ここまで読んでパッとやり方のイメージができない人, Workload Identityという言葉を理解していない人, Cloud Endpointsを使ったことがない人の役に立つかもしれません。

ポイントは、AutopilotでCloud Endpointsを利用するにはWorkload Identityを設定することが必須となる、ということだけです。

準備

Autopilotクラスタを作成する

https://cloud.google.com/kubernetes-engine/docs/how-to/creating-an-autopilot-cluster?hl=ja#create_an_autopilot_cluster

サービスアカウントを1つ作成して役割を付与する

esp-v2という名前でGCPのサービスアカウントを作成します。
付与する役割は、Cloud Trace AgentService Controllerです。

サンプルコードのダウンロード

今回はGCPが提供してくれているサンプルコードの一部を改変して利用します。

openapi.yamlの書き換え

ダウンロードしたopenapi.yamlの中のYOUR_PROJECT_IDの部分を任意のIDに変更してください。
https://github.com/GoogleCloudPlatform/golang-samples/blob/d06287ee79c9082cb0b4e55b4aa685bf67926059/endpoints/getting-started/openapi.yaml#L21

kubernetesのマニフェストファイルを用意する

サンプルコードのマニフェストファイルは記述が古かったので、大部分を書き直しました。
YOUR_PROJECT_IDの部分を読み替えて参考にしてください。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: esp-echo
spec:
  backend:
    serviceName: esp-echo
    servicePort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: esp-echo
  annotations:
    cloud.google.com/neg: '{"ingress": true}'
    cloud.google.com/backend-config: '{"default": "esp-echo"}'
spec:
  ports:
  - port: 80
    targetPort: 8081
    protocol: TCP
    name: http
  selector:
    app: esp-echo
  type: ClusterIP
---
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: esp-echo
spec:
  healthCheck:
    type: HTTP
    requestPath: /healthz
    port: 8081
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: esp-echo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: esp-echo
  template:
    metadata:
      labels:
        app: esp-echo
    spec:
      serviceAccountName: esp-v2 # ここがあとで重要
      containers:
      # [START esp]
      - name: esp
        image: gcr.io/endpoints-release/endpoints-runtime:2
        args: [
          "--listener_port", "8081",
          "--backend", "127.0.0.1:8080",
          "--service", "echo-api.endpoints.YOUR_PROJECT_ID.cloud.goog",
          "--rollout_strategy", "managed",
          "--healthz=/healthz",
        ]
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8081
      # [END esp]
        ports:
          - containerPort: 8081
      - name: echo
        image: gcr.io/google-samples/echo-go:1.0
        ports:
          - containerPort: 8080

デプロイ

Cloud Endpoints Service

修正したopenapi.yamlを利用して次のようにします。

$ gcloud endpoints services deploy openapi.yaml

Waiting for async operation...

確認

$ gcloud endpoints services list
NAME                                                     TITLE
echo-api.endpoints.YOUR_PROJECT_ID.cloud.goog

Workload Identity

Workload Identityとは、KubernetesのService Account(KSA)とGCPのService Account(GSA)を結びつけ、Kubernetesクラスタ内で実行されるPodにGSAの役割を付与するものです。
Autopilotクラスタでは、デフォルトでWorkload Identityが有効化されています。

Cloud Endpointsでは、Extensive Service Proxy(ESP)と呼ばれるものが自分たちのコンテナの前段にProxyとして配置され、リクエストカウント, エラー率, レイテンシ, 認可などの処理を行ってくれます。
Extensive Service Proxyがあるから、Cloud Endpointsが下画像のように各指標をダッシュボードで表示できるのです。

この指標を計測するために、準備で作成したサービスアカウントに必要な役割を付与しました。
つまり、任意のKSAがGSA esp-v2として振る舞えるようにします。

Workload Identityを利用するには大きく3つのステップが必要です
– KSAの作成
– GSAにKSAのバインディング
– KSAにアノテーションを追加

これらを実行していきます。
今回はKSAもesp-v2という名前でdefault namespaceに作成することにします。

$ kubectl create serviceaccount --namespace default esp-v2

更に、KSAがGSAの権限を利用できるようにバインディングをします。

$ gcloud iam service-accounts add-iam-policy-binding \
        --role roles/iam.workloadIdentityUser \
        --member "serviceAccount:YOUR_PROJECT_ID.svc.id.goog[default/esp-v2]" \
        esp-v2@YOUR_PROJECT_ID.iam.gserviceaccount.com

[default/esp-v2]を一般化すると [namespace/KSA]です。
今回はdefault namespaceの esp-v2というKSAなので上記のようになります。

最後にKSAにアノテーションを追加します。

$ kubectl annotate serviceaccount \
  --namespace default esp-v2 \
  iam.gke.io/gcp-service-account=esp-v2@YOUR_PROJECT_ID.iam.gserviceaccount.com

ここで、kubernetesのマニフェストファイルに1度戻りましょう。Deployment中の次の行を探してください
serviceAccountName: esp-v2
これは、このDeploymentで起動するPodにKSAesp-v2が割り当てられることを意味しています。
つまり、これらのPodはGSAesp-v2の権限を持つことになるわけです。
Autopilotでは、NodeのService Accountが利用できないため、必ずこのようにWorkload Identityを利用する必要があります。

アプリケーション

あとはKubernetesのマニフェストファイルをデプロイすれば、リクエストがESPを通してアプリケーションコンテナに到達できます。

$ kubectl apply -f k8s/esp_echo_http.yaml

IngressのExternal IPを確認したら、次のようにリクエストを送ってみましょう。

$ kubectl get ing
NAME       CLASS    HOSTS   ADDRESS         PORTS   AGE
esp-echo   <none>   *       35.201.76.254   80      129m
$ curl --request POST \
           --header "content-type:application/json" \
           --data '{"message":"hello world"}' \
           "http://35.201.76.254:80/echo"

{"message":"UNAUTHENTICATED:Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.","code":401}

するとこのように401エラーが返ってくるはずです。
これは、サンプルのopenapi.yamlが、APIキーをパラメータとして要求するためです。
上記はESPが不適切なリクエストに対して返しているレスポンスです。

GCPのCredentialsのページでAPIキーを発行して次のように付与すると、正常なレスポンスが得られます。

$ curl --request POST \
     --header "content-type:application/json" \
     --data '{"message":"hello world"}' \
     "http://35.201.76.254:80/echo?key=YOUR_API_KEY"
{"message":"hello world"}

このように、Workload Identityさえ使えば今までのGKEでやっていたことと同じようにCloud Endpointsを利用することができました。めでたしめでたし。