無停止でGAEからCloud Runへの移行を成功させた話
はじめに
サイトの構成
- GCP上に構築されている
- 月間7億PV
- 売り上げの大半が広告なので広告が止まると大打撃
- DNS→LB→CDN→GAEというシンプルな構成
- LBにGAEがアタッチされてるので、エンドポイントを変えるだけでGAEからCloud Runに移行できる
Cloud runにするための下準備
GAEの時とほぼ同じデプロイフローを準備
GAEの時とデプロイフローが変わると他のメンバーの負荷や思わぬバグや歯車の再開発が起きると感じたので、GAEの時とほぼ同じデプロイフローを用意しました。
GAEの頃のデプロイフロー
- GitHub Actions上でAngularをビルド
- ビルドフォルダと必要なファイルだけをGCPにアップロード
- GAEがなんかいい感じにデプロイしてくれる(ブラックボックス)
- リリース前にE2Eを回して成功したら自動的にトラフィックを流す
Cloud run用に作った新しいデプロイフロー
- GitHub Actions上でAngularをビルド
- ビルドフォルダと必要なファイルだけをGCPにアップロード
- Cloud Buildでイメージを作成
- Cloud runのリビジョンを作成
- リリース前にE2Eを回して成功したら自動的にトラフィックを流す
Docker化
Cloud Runはコンテナベースのサービスなので、まずはDockerで動作するようにする必要がありました。
と言っても難しいことはなく、Firebaseを使っていたので本当に「起動するだけ」のDockerfileを作成しました。
GitHub ActionsでビルドしたファイルをDockerイメージに詰め込むだけです。
最終的に以下のようなDockerfileを作成しました:
FROM node:20
WORKDIR /usr/src/app
COPY . .
RUN npm ci
CMD ["npm", "run", "start"]
LB移行
GCPの新しいLBにはカナリアリリース機能がありますが、利用していたクラシックLBでは未対応でした。そのため、まずLBの移行から始めました。
- インフラチームと相談しながら、新旧両方のLBの設定を完全に同一にしました。
- 設定ミスによるサービス全停止のリスクを軽減するため、万が一大規模障害が発生しても、すぐに旧LBにトラフィックを戻すことができるようにDNSのTTLを30秒に変更しました。
- Aレコードの向き先を新しいLBのIPに変更しました。
当日のDNS設定変更作業は、サービス全体に影響を与える可能性があったため、非常に緊張感のある作業となりました。しかし、無事に障害を起こすことなく、ダウンタイム0でLB移行を完了できました。
Cloud Run移行
LBでカナリアリリース
GAEからCloud Runへいきなり100%移行は危険を伴うため、LBの機能を使ってカナリアリリースを実施しました。以下は使用したLBの設定例です:
name: path-matcher-3
defaultRouteAction:
weightedBackendServices:
- backendService: projects/prcmnovel-tokyo-prod/global/backendServices/novel-prcm-jp
weight: 90
- backendService: projects/prcmnovel-tokyo-prod/global/backendServices/novel-prcm-jp-cloudrun
weight: 10
この設定により、weightの部分を調整して少しずつトラフィックを流すことができました。最初は適切なスペックが分からなかったため、トラフィックを徐々に増やしながらスペックのチューニングを行いました。
Cloud runでハマった落とし穴
リビジョンタグの問題
GAEには各バージョンに対して固有のURLが生成され、そのURLにアクセスすることでトラフィック設定を無視して特定バージョンにアクセスできる機能がありました。Cloud Runではこれに相当する機能としてリビジョンタグがあります。
しかし、リビジョンタグの問題点として、minインスタンスが1以上に設定されている場合、トラフィックが流れていなくてもインスタンスが常に起動し続けることが分かりました。さらに、CPUを常に割り当てる設定にしていたため、使用されていないインスタンスが多数起動し続けるという事態が発生しました(無駄なリビジョンタグが5個、最小インスタンスが3、計15台もの無駄なインスタンスが起動)。
この問題に対処するため、E2Eテストの成功・失敗にかかわらず、使用済みのタグを自動的に削除する処理をデプロイフローに組み込みました。
CDNの課題
Cloud runに移行した際にGAEのnginxが担当していたキャッシュがなくなってしまい、ピーク時に通常の10倍以上のリクエストが発生してしまいました。この問題に対処するため、CDNのキャッシュ設定を見直しました。
しかしながらCloud CDNでデフォルトのURLパスだけをキャッシュのキーにしていたため、Etagが異なるバージョンのリソースをキャッシュしてしまい、古いバージョンのリソースが返されるという問題が発生しました。
古いバージョンが返されると、クラインアントから古いJSファイルがリクエストされますが、リビジョンはすでに切り替わっているため、新しいJSファイルが返されず、ページが正常に表示されないという問題が発生しました。
この問題の解決策として、CDNのキャッシュキーをURLだけでなく、URL+Etagにすることで、古いバージョンを持つユーザーにも適切にキャッシュを返せるようになりました。
衝撃の料金
移行の結果、1日のサーバーコストが2万円→7千円にまで削減されました。これは、色々なところで言われてますが、GAEと違い、Cloud Runの課金方式がリクエスト単位であるため、非常にコスト効率が良いことを示しています。
まとめ
Cloud runへの移行はGCP歴が浅い僕には壁に当たることが何度もありましたが、この大きな仕事を任せいてくれたマネージャー、ご協力いただいたインフラチーム、開発チームの皆様に感謝しています。また、この移行を通じて、GCPのサービスをより深く理解でき、今後の開発にも生かせると感じています。
Discussion