Cloud Endpoints + ESPv2のデプロイをgit hub actionsで自動化する

Cloud Runで動く[自分用API](https://github.com/tktkc72/gkeeper)を保護するため、Cloud Endpointsを[手順](https://cloud.google.com/endpoints/docs/openapi/get-started-cloud-run#configure_esp)を参考にデプロイしてみた

CloudRunで作ったAPIが保護できるだけでなく、APIの実行状況監視（実行回数、レスポンスタイム、エラー回数）もCloudEndpointsの画面でできるようになってよき

この手順は用意された[スクリプト](https://github.com/GoogleCloudPlatform/esp-v2/blob/master/docker/serverless/gcloud_build_image)でESPv2コンテナをビルドし、
手動でCloud Runにデプロイしなければならないので、
これをGitHubActionsで自動化した

要は[スクリプト](https://github.com/GoogleCloudPlatform/esp-v2/blob/master/docker/serverless/gcloud_build_image)の中身でやっていることをGitHub Actionsにした

openapiの中身には触れない
gcloudのコマンドも一通り打てる環境になっていることが前提

## 自分のAPIサーバコンテナをCloudRunにデプロイ

保護したいAPIエンドポイントを持つサーバコンテナをCloudRunにデプロイする
払い出されたエンドポイントURLは後ほど用いる

`--no-allow-unauthenticated` オプションをつけて未認証の呼び出しを許可しない設定とすること

## ESPv2コンテナをCloudRunにデプロイ

[手順](https://cloud.google.com/endpoints/docs/openapi/get-started-cloud-run#configure_esp)を参考にESPv2コンテナをとりあえずデプロイ

これがゲートウェイになり、APIを保護する
払い出されたエンドポイントURLのホスト名は後ほど用いる

## Cloud Endpointsをデプロイ

Cloud EndpointsをopenapiのドキュメントをデプロイするGitHubActionsを準備する
下記コマンドを打てば良い

`gcloud endpoints services deploy openapi.yaml --project ${GCP_PROJECT}
`

### openapi.yaml

openapi.yamlの中身は以下

【ENDPOINTS_HOST】には[ESPv2コンテナをCloudRunにデプロイ](#ESPv2コンテナをCloudRunにデプロイ)で払い出されたURLのホスト名、
【BACKEND_ADDRESS】には[自分のAPIサーバコンテナをCloudRunにデプロイ](#自分のAPIサーバコンテナをCloudRunにデプロイ)で払い出されたエンドポイントURLを指定

【apiの定義】にはopenapiの書式でAPIの仕様を記載する

securityDefinitionsとsecurityを下記の通り定義するとAPIキーによる保護ができるようになる


``` yaml
swagger: '2.0'
info:
  title: [サービス名]
  description: description
  version: 1.0.0
host: 【ENDPOINTS_HOST】
schemes:
  - https
produces:
  - application/json
x-google-backend:
  address: 【BACKEND_ADDRESS】
  protocol: h2
security:
  - api_key: []
paths:
  【apiの定義】
securityDefinitions:
  api_key:
    type: "apiKey"
    name: "key"
    in: "query"
```

## ESPv2コンテナ再ビルド/デプロイ

ESPv2コンテナに上記CloudEndpointsの設定を載せて再ビルドし、CloudRunに再デプロイする

[スクリプト](https://github.com/GoogleCloudPlatform/esp-v2/blob/master/docker/serverless/gcloud_build_image)を確認するとESPv2にはCloudEndpointsの設定情報の入った`service.json`が組み込まれていないといけない
この設定情報を取得するにはCloudEndpointsのConfigIDがいるので、[Cloud Endpointsをデプロイ](#Cloud Endpointsをデプロイ)でデプロイしたCloudEndpointsの最新ConfigIDをまず取得する
取得したConfigIDをもとに`service.json`を取得してESPv2コンテナに組み込んでビルド
できたコンテナをCloudRunにデプロイする

こちらは`--allow-unauthenticated`オプションをつけて未認証の呼び出しを許可する

上記をやるGitHubActionsが以下

``` yaml
update-gateway-cloud-run:
    runs-on: ubuntu-latest
    needs: update-cloud-endpoints
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v1
      - name: GCP Authenticate
        uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
        with:
          version: '290.0.1'
          service_account_key: ${{ secrets.GCP_CI_SA_KEY }}
          project_id: ${{ secrets.GCP_PROJECT }}
      - name: get latest config id
        run: |-
          echo "::set-env name=CONFIG_ID::$(gcloud endpoints configs list \
            --service=${ENDPOINTS_HOST} \
            --sort-by='~CONFIG_ID' \
            --limit=1 \
            | tail -1 | cut -d' ' -f1)"
      - name: get service.json
        run: |-
          curl --fail -o "service.json" -H "Authorization: Bearer $(gcloud auth print-access-token)" \
          "https://servicemanagement.googleapis.com/v1/services/${ENDPOINTS_HOST}/configs/${CONFIG_ID}?view=FULL"
      - name: Build a docker image
        run:
          docker build -t asia.gcr.io/${GCP_PROJECT}/${GATEWAY_NAME}:${GITHUB_SHA::7} -f endpoints/Dockerfile .
      - name: Configure docker to use the gcloud cli
        run: gcloud auth configure-docker --quiet
      - name: Push the docker image
        run:
          docker push asia.gcr.io/${GCP_PROJECT}/${GATEWAY_NAME}:${GITHUB_SHA::7}
      - name: deploy
        run: |-
          gcloud run deploy "${GATEWAY_NAME}" \
            --quiet \
            --allow-unauthenticated \
            --region "${GCP_REGION}" \
            --image "asia.gcr.io/${GCP_PROJECT}/${GATEWAY_NAME}:${GITHUB_SHA::7}" \
            --project "${GCP_PROJECT}" \
            --platform "managed" \
            --service-account "${GCP_SA_EMAIL}"
```

Dockerfileは[スクリプト](https://github.com/GoogleCloudPlatform/esp-v2/blob/master/docker/serverless/gcloud_build_image)で作っているものをそのままファイル化

## 必要権限の付与

作成されたCloudEndpointsはGCP上でサービスとして管理される（`gcloud services list`ででてくる）
このサービスに対する下記権限を、ESPv2のCloudRunが動作するサービスアカウントに付与する必要がある
過不足あるかも

- roles/servicemanagement.serviceController
    - `gcloud endpoints services add-iam-policy-binding $(SERVICE_NAME) --member=serviceAccount:$(GCP_SA_EMAIL) --role=roles/servicemanagement.serviceController`
- roles/servicemanagement.configEditor
    - `gcloud endpoints services add-iam-policy-binding $(SERVICE_NAME) --member=serviceAccount:$(GCP_SA_EMAIL) --role=roles/servicemanagement.configEditor`



またESPv2CloudRunのサービスアカウントに自分のAPIサーバコンテナCloudRunの起動元権限を付与する必要がある

```
gcloud run services add-iam-policy-binding 自分のAPIサーバコンテナCloudRun名 \
		--member "serviceAccount:$(GCP_SA_EMAIL)" \
		--role "roles/run.invoker" \
		--platform managed \
		--project $(GCP_PROJECT) \
		--region $(GCP_REGION)
```

## APIキーの発行

APIキーはGCP上で管理される
ESPv2CloudRunが動作するGCPプロジェクトで「APIとサービス」-「認証情報」から「認証情報の作成」で作成できる

キーを制限することで、APIキーを利用できるサービスを作成したCloudEndpointsのサービスに限定したり、
APIキー利用元（ソースIPアドレス等）を制限できる

## APIアクセス

発行されたAPIキーを`?key=APIキー`の形で付与して、ESPv2コンテナ CloudRunのエンドポイントに対してAPIを呼び出すと成功
つけないと失敗する

``` shell
$ curl -s -o /dev/null -w "%{http_code}" https://[CloudRunエンドポイント]/api/health?key=[APIキー]
200
```

``` shell
$ curl -s -o /dev/null -w "%{http_code}" https://[CloudRunエンドポイント]/api/health
401
```

## CloudEndpoints

APIの情報がCloudEndpointsの画面で見れるようになる

<img width="1855" alt="スクリーンショット 2020-07-19 12.01.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254745/6f64bc11-4c20-6446-2cf8-08f748a3bee3.png">

## まとめ

~~CloudRunのエンドポイント保護はCloudEndpoints + ESPv2で~~ [Google Cloud Api Gateway](https://cloud.google.com/blog/ja/products/serverless/google-cloud-api-gateway-is-now-available-in-public-beta)が出てきてオワコンかも
ESPv2のCloud Runを建てなくてもいいっぽいのでこっちに移行したいと考えている

詳しいコードは[実際作ったやつ](https://github.com/tktkc72/gkeeper)参照