GCEで動かしていたマイクロサービスをCloud Runに移した話

# はじめに

現在お仕事の一つとして研究室内システムのお守りをしているのですが、「これからいろいろなシステムを作っていきたいがアカウントがそれぞれのシステムにあるのは避けたい（つまりシングルサインオン）」というリクエストに対し、当時[『マイクロサービスアーキテクチャ』](https://www.oreilly.co.jp/books/9784873117607/)読んでてばっちり感化されていたので「じゃあマイクロサービスで作りましょう」とあまりDocker使ったこともないのにシステムを作り始めました・・・[^1]
なお、現時点ではまだ「いろいろなシステムを作る」ところまでは行っていません。今のところアクティブに動いているのはアカウント（認証）サービスだけです・・・

[^1]: システムのアーキテクチャどうするかとかは私に決定権というか、まあ研究室なので「とりあえず作ってみる」という感じなのです。

# そもそもなんでGKE使ってないの？

他の記事でもおわかりのように私は基本GCP派です。最近AWSに流れ気味な面もありますが。
で、Googleでコンテナと言ったら当然GKEですね。私もまあGKE使うことになるかな（k8sももちろん学習途上だったわけですが）と設定ファイル書いてデプロイとかもしてみたのですが最終的に「使わない」という判断をしました。理由は以下になります。

- 研究室レベルのシステム（アクティブユーザ30人ぐらい）を動かすには稼働費が高い
- 負荷分散とか落ちたら再起動とかの機能は魅力的だがどうにもオーバースペック（そこまで必要ない）

# 移行前の構成

行きついたのはタイトルにあるようにGCEにdocker-composeを入れて複数コンテナを動かすという運用でした。
少しシステム構成の細かい（個別事情の）話をしますが、GCEで動かしてた時の構成とCloud Runに移すにあたりどうしたかが話のポイントにもなりますのでご容赦ください。

従来のシステムが「Googleでログイン」[^2]する形式だったため、それを踏襲して新アカウントサービスでも「Google認証」を使うことにしました。その際に問題になるのが「OAuth 2.0のコールバックにIPアドレスは使えない」ということで、以下のように「AppEngineのドメインをコールバックにしてコンテナまで転送する」というアクロバティックな技を使いました。

[^2]: 研究室のメールアドレスが旧G Suiteで管理されてます。その管理者権限は私にはないので詳しくは知らないですけど。

1. AppEngineのドメインにアクセスする（ここはGoogleのSSL証明書）
2. AppEngineがGCEで動いているnginxコンテナに転送（ここのhttps通信はオレオレ証明書・・・）
3. nginx（リバースプロキシ）がパスに応じて個別のマイクロサービスに転送

その後、コンテナがオレオレ証明書だとブラウザ等から通信するのは支障があるということで、すべての通信はAppEngineにまずリクエストを送って転送されてコンテナに届くという形式にしました。[^3]

[^3]: この構成自体をネタに記事を書こうと思ったこともあったのですが、「誰の参考にもならなそう」なので没にしました(笑)

![flow-using-gce.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/11214b8f-4e75-4b29-9b92-c029b54bd906.png)


ともかくこのようなコンテナ群（nginx、アカウントサービス、後あまり使われてない試作サービス）をf1-microで動かしていました[^4]。なお、他にf1-microを動かしている別のプロジェクトがあるので無料枠は使えません。というわけで月に$7ぐらい（インスタンス稼働費＋ストレージ代＋今年7月以降IPアドレス代）がかかってました。私の財布に・・・

[^4]: 「使用率が高いからスケールアップしろ」とコンソールに警告は出ていましたが一応まだメモリの余裕はありました。

# 移行の検討

お金が（私の財布に）かかるのをどうにかしたいということで、サーバレスにすれば「固定費」はかからなくなるのではとCloud Functionsを使うことを考えましたが、すでに作っているコンテナの複数のAPIをばらばらにするのも面倒だなとためらっていました。

そんなときに今回の本題、[Cloud Run](https://cloud.google.com/run)を見つけました。説明読んでいる感じ、「動いてるだけ課金」になっており要望にマッチしそうです。無料枠もあるし。
問題は「コードが実行されている間のみ料金が発生」というのが「リクエストが来たのでコンテナが起動して、その後しばらく動いている時間」なのか、「リクエストの処理時間」なのかがわからないということですが、例によって「まあいいや、とりあえず使ってみよ」と設定してみました。

# 移行に伴う修正

Cloud Run化するにあたり一番の問題は「マイクロサービス間の通信どうすんの？」ということでした。
そもそも作り始めたときに「Dockerって別のコンテナとどう通信するの？」という状態だったので、以下のように必要な時に通信先のコンテナを「ディスカバリー」する仕組みを作っていました（結局コンテナ名を指定すればdocker-composeがうまいことやってくれるということがわかり単にプロトコル・名前・ポート番号を返すようになってます）

```py:discovery.py移行前版
def get_endpoint(name):
    endpoint = {
        'account': 'http://account-service:5001'
    }

    return endpoint[name]
```

Cloud Runでサービスを作ってみたところ以下のようにURLが割り当てられることがわかったので、

![cloudrun-url-sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/cb703db9-20e9-66a5-41bd-d154fcf41ddb.jpeg)

先の「ディスカバリーの仕組み」をハードコーディングではなく環境変数から通信先を取るように変更し、

```py:discovery.py移行後版
import os

ACCOUNT_SERVICE_ENDPOINT = os.getenv('ACCOUNT_SERVICE_ENDPOINT', 'http://account-service:5001')

def get_endpoint(name):
    endpoint = {
        'account': ACCOUNT_SERVICE_ENDPOINT
    }

    return endpoint[name]
```

「サービスにアクセスする側のサービス」に環境変数として設定するようにしました。

![cloudrun-envvar.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/f4e94065-308a-a225-91ed-ee0998e3554b.jpeg)

またURLが割り当てられるのでOAuth 2.0のコールバックも直接アカウントサービスを指定することができます（上の方はとりあえず残してある移行前のコールバック先です）。どのコールバックに返すかの指定については本番環境と開発（実験）環境を切り替えやすいようにあらかじめ環境変数で設定できるようにしてあったので同様にアカウントサービスの環境変数として設定しました。

![oauth-callback.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/d430edab-81b2-dfa7-d3e6-d85967de01d8.jpeg)

まとめると、中間管理職のAppEngineとnginxは解雇され、個々のマイクロサービスに直接アクセスするようになりました。

![flow-using-cloudrun.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/f0de55a2-a63f-4d73-3300-40bf29a263c4.png)

「Cloud Runに立てたサービス」間の通信速度については「同じリージョンであればミリ秒」で行われるようで特にクライアント画面開いて気になるほど遅いということはありません。

# 移行により得られたメリットとデメリット

## 安くなった！

これが一番大事です。安くなりました。まだ動かし始めて一週間ですが課金は発生していません。

![cloudrun-cost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/4a729366-384f-fb60-04cc-a930b06cf0a1.jpeg)

Cloud Runの無料枠は以下のようになっています。

- 180,000 vCPU秒
- 360,000 GiB秒
- 200万リクエスト

それぞれ5倍しても超えることはありませんね。
ちなみに現在律義に「認証されているか毎回確認」しているのですが他のサービスも本格稼働してきたら「1時間ぐらいはまあ許容ってことでキャッシュ」するようにしないと無料枠を超えてしまいそうですね。

## ログが見やすくなった！

何かうまく動いていないという場合、これまではGCEにログインして`docker-compose logs`する必要がありましたがCloud Runでは管理画面でログが見れるので、いつどのパスにリクエストが来て結果がどうだったかということが素早く確認することができるようになりました。

## 継続的デプロイ！

Cloud Runで動かすコンテナの元は以下のどちらかを選択できます。

- Container Registryに置いてあるイメージ
- Cloud Buildを使ってSource Repositoriesにpushされたら継続的デプロイ[^5]

[^5]: GitHubも使えるらしいですが試したことありません。

元からSource Repositoriesにコードは置いていたので継続的デプロイの設定にしました。GCEで動かしてたころから「pushしたらイメージがビルドされるようにしたいな」と思っていましたが実現できました。

ちなみに、現在「リポジトリに全部のサービスが入っている（さらにクライアント側も入れている）」ので無関係の部分を修正してpushしてもビルドされてしまいます。ここは作成されたCloud Build側で「含まれるファイルフィルタ」をちゃんと設定すれば「サービスに関係する箇所が変更された場合のみビルド＆デプロイ」されるようにできそうですが未検証です。

## フィッシングサイトっぽい・・・

先ほどのURL例では全部黒塗りしていましたが、URLにはランダムな文字列が割り当てられます。こんなURL見せられて疑わずにログインしようとする人は逆にITリテラシーを疑いますね。事前に「ある日突然こんなログイン画面になるかもしれないけどフィッシングサイトじゃないから」とアナウンスしたうえで切り替えました(笑)

![oauth-screen.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/f29a628a-47e3-69a2-2ad6-ee439c94cef5.jpeg)

# その他の課題

GCEで動かしていたものをそのままお引越ししただけなのでいろいろと「Cloud Runのお作法」に従っていない部分があります。

- Cloud Runは環境変数PORTで待ち受けることを推奨しているが既存のイメージはポート番号が決め打ちなのでCloud Run側でそのポートを使うように設定している。
- アカウントサービスがユーザに発行するトークンはメモリ上に保存されているので「リクエスト来ないしコンテナ落とす」とトークンが消えてしまう。なお「Googleログイン」は一回OKすれば先のフィッシングっぽい画面は再表示されずにリダイレクトされていくので「トークンが消えていて実は新しいトークンが割り当てられていた」ということはユーザは気づかない。
- 上の「トークンをメモリ保存」とも関連して、現在はコンテナの最大起動数を1にしている。初めの方に書いたように負荷分散が必要なほどのユーザ数ではないが複数インスタンスにスケールできるようにするにはJWTなどの「ユーザ側にセッション情報を持たせる」仕組みに変更する必要がある。

# おわりに

以上、今回はGKE使うほどじゃないからとGCEで動かしていたマイクロサービスをCloud Runに移行したお話でした。ポイントは以下になります。

- サービスにURLが割り当てられるので通信先はそのURLを環境変数で設定しておけばよい
- 無料枠が使える！
- ログが見やすくなる！
- 継続的デプロイができる！
- （一応）動かしてたコンテナをそのまま移行できる