Google Cloud Run の運用

先日 β 版としてリリースされた [Google Cloud Run](https://cloud.google.com/run/)。ずっと AWS Lambda で Docker コンテナを動かせたらいいのにと思い続けていた身にとっては、待ってましたと言わんばかりのサービスでした。

Google Cloud Platform 自体これまでまともに使ったことはありませんでしたが、[クイックスタート ドキュメント](https://cloud.google.com/run/docs/quickstarts)に従ってサンプルを実行したりして、Cloud Run はスムーズに使えるようになりました。

### Cloud Run のメリット

- HTTP サーバが起動する Docker コンテナイメージを Cloud Run にデプロイすれば URL を発行してくれる。
  - AWS API Gateway のような設定は必要ない。
- Cloud Run 用のライブラリを組み込む必要もなく、好きな言語で実装したサーバのコンテナさえ動けばいいので可搬性が高い。
  - AWS Lambda のようにハンドラー関数のシグニチャが決まっていたりしない。
- 使われた分(リクエストを処理した分)だけの課金体系。

あるサービスからの Webhook を受信して、CI 的な処理を実行する Kotlin Ktor フレームワークを用いたプログラム(Java プログラム)があったのですが、これを実際に Cloud Run で運用しています。
単なる Java プログラムであれば AWS Lambda でも動かせるのですが、外部コマンドを実行する必要もあり、そのコマンドを使える実行環境を AWS Lambda では用意することができず、これらのコマンドを配備した Docker コンテナで実行する必要がありました。

複数の Docker コンテナが必要なわけではないため Kubernates 的なアプローチは大袈裟で、また自前でサーバを用意して Docker 運用する手段もありますが、CI 的な用途だとアイドルタイムも多く、逆にデプロイが連続して発生することもあり、コストやスケールの面で FaaS やこの Cloud Run にメリットがありました。

以下に Cloud Run の設定や実行時にメモしたポイントをまとめます。

### コンテナイメージの登録

- [Google Cloud Run のドキュメント](https://cloud.google.com/run/docs/quickstarts/build-and-deploy#containerizing)では `gcloud builds submit` コマンドを使い、Google Cloud Build を通してイメージをビルドして Google Container Registry への登録を行っているが、手元の環境で docker コマンドを使って作成したイメージを `git push` で GCR に登録しても構わない。
- 要するに GCR にイメージが登録されていれば良い。

### コンテナイメージのデプロイ

設定コマンド: `gcloud beta run deploy --image gcr.io/[REPOSITORY]/[IMAGE]:[TAG]`

- `deploy` コマンドに後述の `--memory` や `--concurrency` のオプションも指定できる。

### メモリ割り当て

設定コマンド: `gcloud beta run services update [SERVICE] --memory [MEMORY]`

- コンテナへの割り当て可能メモリは 256MB〜2GB。
- プログラムからファイルを出力するとこのメモリ領域が使われてしまう。
  - 例えば /tmp ディレクトリにファイルを生成するようなプログラムを実行する場合、処理の終わりに生成したファイルを削除するようにしておかないと、同一コンテナインスタンスが使い回され続けるとメモリ不足エラーが生じる。
- メモリ割り当ては、コマンドで実行してもコンソールで選択できるメモリしか設定できない(?)
  - `gcloud beta run services update [SERVICE] --memory 1500M` のように割り当てメモリを 1.5GB に設定しようとしても、コンソールで表示される「割り当てられたメモリ」は 2GB になる。

### コンテナインスタンス

- HTTP リクエストに対してレスポンスを返した時点で Cloud Run でのコンテナ実行は終了したとみなされる。
- コンテナ実行が終了してしばらくするとコンテナインスタンスが終了する。
  - このようなログが出力される。 `Container terminated by the container manager on signal 9.`
- 先にレスポンスだけ返して、バックグラウンドスレッドで処理を継続しようとしてもできない。コンテナインスタンスが途中で終了してしまう。
- コンテナインスタンスが終了するタイミングは特に決まっていない様子。最後の実行から1分程後で終了することもあれば、10分程後のことも。

### 並行処理

設定コマンド: `gcloud beta run services update [SERVICE] --concurrency=[NUMBER]`

- 複数の HTTP リクエストが同時に来たときに、デフォルトでは最大 80 リクエストが 1 コンテナインスタンスに送られる。
- この `80` が設定できる上限値で、`1` が下限値。
- `1` に設定すると、同時に来たリクエストの分だけコンテナインスタンスが立ち上がる。
  - あくまでも同時リクエストの場合のことで、すべてのリクエストが新しく起動されたインスタンスで処理されるわけではない。あるインスタンスで処理が終わっていて、インスタンスが終了していなければ、そのインスタンスが使い回される。

### 環境変数

設定コマンド: `gcloud beta run services update [SERVICE] --update-env-vars ENV1=VAL1,ENV2=VAL2`

### ドメインマッピング

設定コマンド: `gcloud beta run domain-mappings create --service [SERVICE] --domain [DOMAIN]`

### Java のコンテナを実行する場合

- メモリの扱いに注意。 https://cloud.google.com/run/docs/issues#java
  - [Ktor のコンテナについてのドキュメント](https://ktor.io/servers/deploy/containers.html)に記載されている Dockerfile だと `-XX:MaxRAMFraction=2` になっているので、そのままだと最大ヒープメモリが 1GB になってしまう。今回 Cooud Run に最大の 2GB を割り当てていたので、`-XX:MaxRAMFraction=4` に設定した。
- 実行時間は遅い印象。まだ Java のコンテナしか使ったことがないので、言語が原因なのかは分かりません。デプロイ先のリージョンが今は US-Central1 だけなので、日本からのアクセスだとラウンドトリップタイムもあるせいだと思います。