Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

GKE から Cloud Run に戻した話

はじめに

こんにちは、SRE のあんどう(@Andoobomber)です。

突然ですが、2024 年に「Kubernetes 初学者が担当した GKE 移行プロセスの全貌」という記事を書きました。Cloud Run で動いていた弊社のプロダクトを GKE に移行した、というお話です。

あれから約 2 年。今度はそのをやりました。

つまり、GKE で動かしていたプロダクトを、もう一度 Cloud Run に戻しました。前回 GKE に持っていった本人が、今度は GKE を Cloud Run にしてるわけで、自分でも「何をやっているんだ」と思わなくもないのですが、これには理由があります。

私たち Gaudiy はスタートアップで、組織や事業のフェーズが進むごとに、インフラに求めるものも変わります。2024 年の GKE 移行も、今回の Cloud Run 回帰も、それぞれのフェーズで「いま最も合う構成」を選び続けてきた結果 です。後ろから見るとジグザグに見えるかもしれませんが、その時々で意図して選び直してきた話です。

この記事では、

  • なぜ Cloud Run に戻すことにしたのか
  • どうやって移行したのか
  • 何をやめて、何に置き換えたのか(Argo CD → GitHub Actions など)
  • 途中で詰まったポイントと、そこから得た学び

を、書いていきます。

Google Cloud でアーキテクチャ選定に迷っている方、「Cloud Run と GKE どっちにするか」を社内で議論している方、ベンチャー企業のリアルなインフラ判断が気になる方の、参考になれば嬉しいです。

TL;DR

経緯をひと息でまとめると、

Cloud Run(〜2023)→ GKE(2024〜)→ Cloud Run(2026〜)

という、ぐるっと一周してきた構成です。戻した理由は大きく 2 つあります。

  1. 事業フェーズの変化: Tech Base 中心の探索フェーズから、ビジネス中心のフェーズへシフトしたことに伴うコスト最適化。
  2. Cloud Run 側の機能拡充: GPU インスタンス対応や Direct VPC Egress の GA など、2024 年当時 GKE を選ぶ理由になっていた要素が、Cloud Run でも実現できるようになった。

主要コンポーネントの差分は次の通りです。

レイヤー Before(GKE) After(Cloud Run)
ランタイム GKE Standard Cloud Run
マニフェスト Kustomize Cloud Run Deploy YAML
CD Argo CD GitHub Actions
公開系ロードバランサ Gateway API(GKE Gateway Controller) Cloud Load Balancing + Serverless NEG
サービス間通信 Cluster 内 DNS / Service Direct VPC Egress + ID Token 認証
定期実行 CronJob Cloud Scheduler → Cloud Run
シークレット External Secrets Operator Secret Manager(Cloud Run ネイティブ統合)
監視 Datadog(OpenTelemetry Collector 経由) Datadog(変更なし)

Before/After アーキテクチャ図

ざっくり言えば、「Kubernetes のエコシステムを使っていたのを、Google Cloud のマネージドサービスに換えた」という構図になります。


なぜ GKE から Cloud Run に戻すのか

そもそもなぜ GKE に行ったのか

詳しい経緯は前回の記事に書いていますが、ざっくり 3 行でいうと、

  • 当時の Cloud Run には GPU インスタンスがなく、AI ワークロード・画像生成(Stable Diffusion)を動かすには GKE が必須だった
  • Datadog Agent / OpenTelemetry Collector のような常駐型コンポーネントを置きたかった
  • クラウドネイティブやAIなどの技術スタックを試験して、エンジニア組織として最新技術を持って組織を引っ張る方針だった。

この選択をしたことにより、SRE / クラウドネイティブ領域の知見は組織としてかなり積み上がりました。GKE 移行を経て OpenTelemetry Collector の導入や AI Workload の使用が可能となり、エンジニアのスキルアップに大きく繋がりました。

では、何が変わったのか

主に 事業フェーズの変化 と、Cloud Run 側の機能拡充 の 2 つです。

1. 事業フェーズの変化

プロダクトのフェーズが、新しい技術を試しながら作る「探索フェーズ」から、すでに見えているビジネスをきちんと回す「スケールフェーズ」に進みました。

  • 探索フェーズ: 自由度・拡張性・最新技術の取り込みやすさ
  • スケールフェーズ: 運用負荷の低さ・コスト効率・安定性

これに合わせて、インフラをもう一度見直しました。

2. Cloud Run 側の機能拡充

私たちが GKE に移った 2024 年当時、Cloud Run には足りないピースがいくつかありました。それが、この 2 年で次々に埋まりました。特に決定打になったのは次の 2 つです。

  • GPU インスタンス対応: Cloud Run でも GPU が使えるようになり、AI ワークロードのために GKE を残す理由がなくなりました。
  • Direct VPC Egress の GA: かつての Serverless VPC Connector を介した通信ではなく、Cloud Run のインスタンスから VPC 経由で Memorystore Redis などのリソースへ直接アクセスできるようになりました。Cloud Run 同士の Internal 通信自体は Connector 時代から可能でしたが、Connector インスタンスを経由する分のコストとレイテンシが乗っていた ところを、Direct VPC Egress ではゼロにできます。複数プロダクト・Google Cloud Projects を抱える我々にとって、コスト面とレイテンシ面の両方で移行を後押しする要因でした。

加えて、min-instances や CPU always-allocated、Cloud Run jobs、Cloud Run worker pools、Pub/Sub Push との統合など、「常駐型に近い使い方」が現実的になっており、今まで GKE 上の Deployment / CronJob としていたワークロードも Cloud Run に寄せられるようになりました。

まとめると、事業フェーズが進み、Cloud Run も進化したため、アーキテクチャを刷新した。 ということです。

ここから先は実際にどう戻したかの話に入っていきます。


アーキテクチャ Before / After

TL;DR の差分表で全体像は示しましたが、ここでは図を交えてもう少し詳しく見ていきます。図は 「処理パス」「サービス間通信」「CD」「非同期処理」 の 4 つの観点で並べました。

1. クライアント → LB → ランタイムの処理パス

処理パス Before/After 図

Before(GKE)

Client
  → Cloud Load Balancing (Global HTTP(S) LB)
  → Network Endpoint Group (GKE Gateway controller 経由)
  → GKE Gateway API
  → アプリケーション Pod

After(Cloud Run)

Client
  → Cloud Load Balancing (Global HTTP(S) LB)
  → Serverless NEG
  → アプリケーション (Cloud Run service)

GKE Gateway Controller が Cloud Load Balancing 上のリソース(NEG / Backend Service / URL Map …)を裏で組み立ててくれていた構成から、Serverless NEG を使った構成に変わりました。Cloud Load Balancing 自体は変わっていません。

2. サービス間通信(VPC / Direct VPC Egress / PSA)

ネットワーク Before/After 図

Before

  • GKE 内のサービス間通信は Kubernetes Service / ClusterDNS で完結。
  • Memorystore Redis は VPC ピアリング(DIRECT_PEERING モード) で GKE Pod から接続。
  • 認証は基本的に内部 IP ベース or mTLS。

After

  • Cloud Run の各サービスは Direct VPC Egress で VPC に直接接続。
  • Memorystore Redis は VPC ピアリング(PRIVATE_SERVICE_ACCESS モード、新規インスタンス) で接続。
  • サービス間通信は Direct VPC Egress で VPC を経由し、呼び出し側のクライアントで OIDC ID Token を生成 → roles/run.invoker を持つ呼び出し先 Cloud Run service を叩く構成(ingress: internal で受ける)。

3. デプロイパイプライン(CD)

CD パイプライン Before/After 図

Before(GKE / GitOps)

GitHub PR merge
  → CI (build / test / image push to Artifact Registry)
  → Kustomize manifest を Git へ commit
  → Argo CD が同期
  → GKE クラスタに反映

After(Cloud Run / GitHub Actions)

GitHub PR merge
  → GitHub Actions ワークフロー
    - build / test / image push
    - Cloud Run Deploy YAML を render
    - `gcloud run services replace` で適用
  → Cloud Run revision として反映

GitOps の良さ(Git を Source of Truth にできる、差分レビューがしやすい)は手放しましたが、 「PR 1 本ですべて完結する」「Argo CD と Kustomize の二重学習コストが消える」 という2点はかなりのメリットだと思います。

4. Pub/Sub / Cloud Tasks / Scheduler の処理経路

私たちのアプリケーションは内部的に gRPC で実装しているのですが、Pub/Sub Push / Cloud Tasks / Cloud Scheduler のような Google Cloud のマネージドコンポーネントは、ターゲットを HTTP でしか叩けません。そこで、HTTP → gRPC のプロトコル変換を担う層として、ESPv2(Extensible Service Proxy V2 / Cloud Endpoints)を挟んでいます。

非同期経路 Before/After 図

Before

  • Pub/Sub: Pub/Sub Push が中心。Google Cloud 側 → Internal LB → ESPv2(独立 Pod / Deployment)→ アプリ Pod(gRPC) という経路で届く構成。
  • Cloud Tasks / Cloud Scheduler: 同じく Internal LB → ESPv2 → アプリ Pod の経路。
  • 定期実行: Kubernetes CronJob。CronJob は ESPv2 を介さず 直接アプリ Pod に gRPC で到達(Cluster 内なので HTTP→gRPC 変換が不要)。

After

  • Pub/Sub: Pub/Sub Push サブスクリプション → ESPv2(Cloud Run service)→ アプリ(Cloud Run service, gRPC)。OIDC 付き。
  • Cloud Tasks 経由の即時実行: Cloud Tasks → ESPv2 → アプリ。
  • 定期実行: Cloud Scheduler → ESPv2 → アプリ。

ESPv2 とアプリの役割(HTTP→gRPC 変換)は変わらず、置き場が Pod / Deployment から Cloud Run service に変わり、Internal LB を挟む必要が無くなりました(Cloud Run の Serverless NEG が直接 ESPv2 service にルーティングしてくれるため)。CronJob は Cloud Scheduler に置き換え。とはいえ Cloud Run 上で ESPv2 を動かすにはそれなりの作法があり(HTTP ヘッダ周りの設定や audience の取り扱いなど)、移行に苦戦した箇所でもあります。

このレイヤーは、地味ながら一番「Cloud Run らしさ」を活かせた箇所でもあります。GKE 上で常時起動していた Deployment / CronJob 群を、リクエスト単位の従量課金に寄せられたからです。


移行プロセス

dev → test/prod → GKE 解体の 3 フェーズ で動きました。

Phase 1: dev 環境の Cloud Run 化

dev に Cloud Run を新規構築し、GKE と並走させながら必要な作法を一通り洗い出しました。

  1. Terraform で Cloud Run 用ネットワークを敷く: サブネット、Cloud Router、Cloud NAT、PSA レンジ、必要な Firewall ルール。Memorystore Redis に届かせるための PSA は PRIVATE_SERVICE_ACCESS で構成。
  2. GitHub Actions ワークフローを用意する: image build → Cloud Run Deploy YAML render → gcloud run services replace までを 1 本のワークフローで回す。
  3. Cloud Run Deploy YAML を書く: Kustomize で生成していた manifest の Cloud Run 版。サービスアカウント、環境変数、VPC アクセス(Direct VPC Egress)、Concurrency、min/max instances などを揃える。
  4. サービス間通信を ID Token 認証に切り替える: Cluster 内 DNS で叩いていた呼び出しを、roles/run.invoker を持つ SA から OIDC ID Token を付けて叩く方式へ。Go であれば idtoken.NewClient を使う、といった呼び出し側の改修もここで合わせて入れる。
  5. ESPv2 を Cloud Run service として切り出す: GKE 時代は独立した Pod / Deployment として動いていた ESPv2 を、Cloud Run service として再定義する。Cloud Run 用の Endpoints 設定(audience が GKE 用と別になる、HTTP ヘッダ周りの設定など)を整える。GKE 時代に必要だった Internal LB は、Cloud Run の Serverless NEG が代わりに繋いでくれるので不要になる。
  6. dev で並走させる: GKE 側を生かしたまま Cloud Run を立てて、後段の動作確認を回す。

dev の構築が作業量やハマりポイントが多く一番大変で、次の test/prod フェーズではIaCのメリットを活かして、楽に構築できました。

Phase 2: test / prod の Cloud Run 化と LB 切替

dev で固めた構成を まず test 環境に展開 し、サービスオーナーとの動作確認・ステークホルダー調整を挟んだうえで、prod に切り替えました。

  1. test 環境に Cloud Run 構成を展開

    • dev で確立した Terraform / GitHub Actions ワークフロー / Cloud Run Deploy YAML を test 用に複製・調整。
    • Cloud Run を立てつつ、LB は当面 GKE 側に向けたままにし、いつでも比較できる状態を維持。
  2. test 環境で各サービスのエンジニアと一緒に動作確認

    • 移行は私たちインフラチームだけで完結する話ではないので、各サービスのエンジニアと並走しながら、Cloud Run 側の挙動を実機で確認していきました。
    • 機能・性能・ログ・メトリクスの差分を、Synthetics / E2E / 個別の手動チェックを組み合わせて洗い出し、見つかった違和感はその場で Cloud Run 構成側にフィードバック。
    • 「サービスごとのオーナーが Cloud Run 側で OK を出している」という状態を、prod 切替の前提条件に置きました。
  3. prod 環境でも「社員専用の確認用 LB」経由で事前に動作確認

    • 私たちのサービスには 社内専用アプリ(社内テナント) があり、ここから 本番環境のまま動作確認 ができます。これが今回の移行で本当に助けられました。
    • ただし、本番ユーザーのトラフィックが流れている 本番 LB はあくまで GKE 側に向けたままにしておきたい。そこで、Cloud Run 側を本番環境で叩くための 社員しかアクセスできない確認用 LB を別途用意 しました。経路としては
      • 社員アカウント → 確認用 LB(社員限定アクセス制御 / 本番ユーザーは到達不可)→ 本番 Cloud Run
      • 一般ユーザー → 本番 LB(GKE 向けのまま)→ 本番 GKE
    • という形で、本番ユーザーが流れる経路と確認経路が 完全に分離 されています。確認用 LB はアクセス制御で社員アカウントに限定しており、エンドユーザーが誤ってこの経路を踏むことはありません。
    • この経路で、社内専用アプリから本番 Cloud Run 側を直接叩いて、実 prod のデータ・依存サービスと組み合わせた挙動を確認しました。test では拾いきれない、本番固有の構成差分もここで潰せています。
    • Create / Update / Delete 系の操作は 他テナントに影響が出ないよう細心の注意を払って、Read 系と限定的な書き込みに絞って実施しました。
    • 「test だけでなく prod 環境でも事前に Cloud Run 側の動作確認ができた」 のが、このフェーズで一番効いたポイントです。
  4. prod 切り替えのスケジューリング調整

    • prod のカットオーバーは、技術的に問題なくても「いつやるか」がそのまま事業リスクに直結します。リリースフリーズ期間や、他チームのキャンペーン・運用イベントを避けるため、プロダクトマネージャー / 開発リード / カスタマーサポート といったステークホルダーと、切替日時を事前にすり合わせました。
    • 合わせて、当日の オンコール体制 / ロールバック条件 / 責任者 も明文化して合意。「何かあったらどう判断して、誰が踏むか」を当日の判断材料にしないことで、本番作業の負荷をかなり下げられました。
  5. LB を Serverless NEG に切替

    • Terraform 側に use_gke_backends = true/false という 1 行のフラグを用意し、
      • true: LB の backend は GKE NEG(スタンドアロン)
      • false: LB の backend は Serverless NEG(Cloud Run)
    • を切り替えられるようにしました。万一の不具合時に、Terraform 側で 1 行戻すだけで GKE 構成に戻せる 安全策を入れたうえでカットオーバー。前段の test / prod 両方での事前確認が効いていたため、カナリアリリースは挟まずに一発で Cloud Run 側へ置換しています。「ぱっと戻せる保険」と「事前にちゃんと触れた事実」の両方が揃っていたからこそ取れた判断 で、これは精神衛生上もとても助かりました。

Phase 3: GKE の解体

LB が Cloud Run に切り替わり、過去のトラフィックが完全に Cloud Run 側に乗っていることを確認したうえで、GKE 側の構成を畳みました。

  • アプリケーションサービスの Manifest と Argo CD ApplicationSet を削除
  • GKE 用の Pub/Sub / Scheduler モジュール(Terraform)を削除
  • GKE クラスタは Platform 系(cert-manager、external-dns、Datadog 周辺)と AI 系の一部のみ残す

合計で数百ファイル単位の YAML が消えることになり、Before / After のスケール感としても象徴的な作業でした。

期間感

ざっくりした期間感はこんな具合です。

フェーズ 期間目安
Phase 1(dev 移行) 約3週間
Phase 2(test / prod 移行 + LB 切替) 約2週間
Phase 3(GKE 解体) 2日

動作確認 / 品質保証

カットオーバーで一番怖いのは「気付かないうちに何かが壊れている」ことです。今回はそれを防ぐために、E2E テストDatadog Synthetics を 2 段構えで使いました。

E2E テスト(Playwright)

  • フロントエンドからの主要ユーザーシナリオ(ログイン、コミュニティ閲覧、投稿、購入処理等)を一通り通す。
  • カットオーバー前 / 後にそれぞれ実行し、差分が出ないかを比較。

E2E は以前からずっと運用してきており、シナリオが十分に積み上がって充実していたので、移行のために改めて E2E を整備する必要がなく、既存のテスト資産をそのまま今回の検証に活用できたのが大きかったです。

Datadog Synthetics

  • Browser Test でフロントエンドの主要画面の死活と性能を定期監視。
  • カットオーバー時のリアルタイムなダッシュボードとしても使う。

特に LB 切替の瞬間は、Synthetics の成功率グラフを横に並べながら作業しました。「数値が正常に推移していること」を視覚的に確認しながら進められる のは、本番カットオーバーの心の余裕としてかなり大きかったです。

異常を検知した場合は、Phase 2 で説明した Terraform 1 行 (use_gke_backends) のロールバック を即実行できる体制で臨みました。実際にロールバックを発動するような事態はありませんでしたが、「踏める保険を持っている」ことが移行の判断と速度に効いていたと思います。


詰まったポイントと学び

移行を進めるなかで「思わずハマったポイント」をピックアップしてご紹介します。

サービス間認証は roles/run.invoker + ID Token に切り替え

GKE にいた頃は、サービス間通信は Cluster 内部の DNS で名前解決して、そのまま叩けば動いていました。社内ネットワーク内の話なので、認証もシンプル。

Cloud Run になると、これが 「Direct VPC Egress で VPC を経由しつつ、roles/run.invoker を持つ SA から OIDC ID Token を付けて呼び出す」 に変わります。経路としては、

呼び出し元 Cloud Run service
  → Direct VPC Egress(VPC の serverless サブネットに egress)
  → VPC ネットワーク経由で呼び出し先 Cloud Run service の URL を解決
  → 呼び出し先 Cloud Run service(ingress: internal で受ける)
  • 呼び出される側: ingress を internal にして、VPC 経由のトラフィックのみ受け入れる。roles/run.invoker を呼び出し元の SA に対して付与
  • 呼び出す側: gRPC / HTTP クライアントで Google ID Token を取得し、Authorization: Bearer ... を付ける(audience は呼び出し先サービスの URL)。Direct VPC Egress 経由なので外部インターネットには出ない。
  • 参考: Cloud Run から Cloud Run へのプライベートアクセス(G-gen Tech Blog)

実際にやったことは、大きく次の 2 つです。

1. Terraform で google_cloud_run_service_iam_member を呼び出し関係ごとにすべて用意

「誰が誰を呼ぶか」の組み合わせに対応する形で、roles/run.invoker 付与を Terraform リソースとして明示的に書きました(example)。

resource "google_cloud_run_service_iam_member" "invoker" {
  service  = google_cloud_run_v2_service.target.name
  location = google_cloud_run_v2_service.target.location
  role     = "roles/run.invoker"
  member   = "serviceAccount:${google_service_account.caller.email}"
}

権限付与の状態を Terraform 上で一元管理できるので、レビュー時に「このサービスから呼べる相手」が明示的に追えるのも副次的なメリットでした。

2. gRPC クライアント側で ID Token を付与する共通ヘルパーを用意

呼び出し側のコードでは、ランタイム環境を環境変数で判定して、Cloud Run(Services / Jobs)のときだけ ID Token を付与する grpc.DialOption を返すヘルパーを共通化しました(example)。

// Cloud Run Services (K_SERVICE) または Cloud Run Jobs (CLOUD_RUN_JOB) では
// ID Token を付与し、それ以外(GKE / ローカル)では insecure で接続する。
func credentialOption(address string) grpc.DialOption {
    if os.Getenv("K_SERVICE") != "" || os.Getenv("CLOUD_RUN_JOB") != "" {
        creds, err := newIDTokenCredentials(context.Background(), address)
        if err != nil {
            // ID Token 取得失敗時は insecure にフォールバック
            return grpc.WithTransportCredentials(insecure.NewCredentials())
        }
        return creds
    }
    return grpc.WithTransportCredentials(insecure.NewCredentials())
}

ポイントは、Cloud Run Services だけでなく Cloud Run Jobs(CLOUD_RUN_JOB 環境変数)も同じヘルパーで扱える ようにしている点です。Job からも同じやり方でサービス間呼び出しができるので、バッチ処理や定期実行を Cloud Run Jobs に寄せた際も、認証まわりに特別な分岐を入れずに済みました。

各サービスからはこのヘルパーを呼ぶだけで済むので、「サービスごとに ID Token の付け方を考えなくていい」 状態を作れました。Cloud Run でも GKE でもローカルでも同じインターフェースで動かせるので、移行期の並走も楽になります。

Memorystore Redis は Shared VPC では PRIVATE_SERVICE_ACCESS モード必須(DIRECT_PEERING は選べない)

GKE 時代は Memorystore Redis を DIRECT_PEERING モード で作成していました。ノードプールが同じ VPC にあれば直接ルーティングできるモードです。

Cloud Run 移行で、このまま既存 Redis に接続しようとしたところ繋がらない。Memorystore for Redis は Shared VPC ネットワークで作成する場合 PRIVATE_SERVICE_ACCESS モードのみサポートされ、DIRECT_PEERING は選択できません

docs.cloud.google.com

私たちの構成は Shared VPCなので、GKE 時代に DIRECT_PEERING で作っていたインスタンスは、この Shared VPC 構成では到達経路が成立しませんでした。

しかも、connect_modeインスタンス作成後に変更できません。そのため、

  1. 新しい Redis インスタンスを PRIVATE_SERVICE_ACCESS モード で作成
  2. データを乗せ替え(キャッシュ用途なので意識的な移行は最小限)
  3. DIRECT_PEERING インスタンスを削除

という手順で Redis ごと立て直しました。

revision の "暗黙の再利用" で deploy が進まない

gcloud run services replace は、設定が前回と完全に一致していると 新しい revision を作らずに既存 revision をそのまま使い回す 挙動をします。普段は嬉しいのですが、たとえば「直前の revision が起動失敗で Ready=False のまま残っている」ような状況で、同じ image + 同じ YAML を再投入すると、

  • 新しい revision が作られず
  • 失敗した古い revision が「現行」のまま

という、"何度デプロイしても直らない" 現象になります。CI のログだけ見ていると Successfully deployed のように見えるのに、実際のサービスは壊れている、という状態です。

回避策はシンプルで、Manifest の "何かを変えて" 新 revision を強制的に切らせることです。具体的には、

  • image tag を必ず一意にする(コミット SHA を使う)
  • どうしても同じ image の場合は、Annotations やラベルにビルド時刻などを入れて差分を作る

のいずれかをデプロイパイプライン側で担保しておくと、この罠を踏まずに済みます。


何が変わったか(運用面)

技術スタックの差分はすでに TL;DR とアーキ図で見ていただきましたが、それを 「日々の運用がどう変わったか」 という視点で改めて整理します。

マニフェストの軽量化(Kustomize → Cloud Run YAML)

GKE 時代は、サービスごとに base/overlays/{dev,test,prod}/ を持ち、Kustomize で環境差分を patch していく構成でした。柔軟である一方、

  • patch の入れ子で「結局どの値が効いているのか」が追いづらい
  • 新しいサービスを足すときに、テンプレ+4 環境分の overlay を毎回作る必要がある
  • Kustomize 自体の挙動を理解しないとレビューが通せない

といった、地味な認知コストがありました。

Cloud Run 化したことで、サービスごとの manifest は 環境ごとに 1 ファイルcloudrun_deploy.yaml)に集約されました。差分は GitHub Actions 側のレンダリングで埋め込み、適用は gcloud run services replace 1 コマンド。Kubernetes 特有のリソース(Deployment / Service / HPA / PodDisruptionBudget …)を 1 サービスごとに揃える必要もなくなり、新サービス追加の心理的ハードルがぐっと下がりました。

CD のシンプル化(Argo CD → GitHub Actions)

GKE 時代は GitOps パターンに乗っており、image を build / push したあと、別リポジトリの manifest を更新する PR を出して、Argo CD が同期するのを待つ、という流れでした。「Git が Source of Truth」の世界として一貫性は高いものの、

  • リポジトリをまたいだ PR レビューになる
  • Argo CD の sync が詰まったときにデバッグ先が増える
  • ロールバックが「manifest を書き戻す PR を出す」になる

というトレードオフがありました。

GitHub Actions に寄せた今は、1 つの PR をマージすると、その PR の CI が直接 Cloud Run へ反映してくれる シンプルさになりました。ロールバックも gcloud run services update-traffic --to-revisions=... で対応できます。

Datadog の設定はそのまま

GKE 時代に整えた Datadog のダッシュボード・Monitor・Synthetics は、ランタイムが Cloud Run になっても そのまま使えました。理由はシンプルで、アプリ側の計装は OpenTelemetry SDK でやっていて、Cloud Run でも同じ Collector / Datadog 連携が成立するからです。

「観測の仕組みを抽象化しておくと、ランタイム差し替えのコストが下がる」というのは OpenTelemetry Collector 導入の記事 でも書いた話ですが、今回の移行で改めて実感しました。


コスト削減効果

具体的な金額は伏せますが、結論からいうと インフラコストを大幅に削減することができました。GKE クラスタ自体の固定コスト、Daemonset / sidecar として常駐していた周辺コンポーネントのコスト、Pub/Sub サブスクライバとして常時起動させていた Deployment のコストなど、「使っていないときも止まらないもの」を Cloud Run の従量課金モデルに寄せられたのが大きかったです。

事業フェーズに合った正しい削減ができ、結果としてプロダクトの体力も維持できる、というのが今回の移行の大きな成果のひとつでした。


今後

すべてのワークロードを Cloud Run に統一したわけではなく、GKE には AI 領域 が残っています。具体的には、

  • AI 系: 推論で GPU(NVIDIA A100)を使うワークロード
  • Platform 系: その AI 系を動かすための Platform。cert-manager、external-dns、内部用の常駐コントローラなど、クラスタ全体の前提を支えるもの

AI 系を Cloud Run に寄せていない理由は、運用ポリシーというよりも 現時点(2026/05)での Cloud Run GPU の制約 によるものです。

  • Cloud Run の GPU は asia-northeast1(Tokyo)でまだ提供されていない
    • 現時点(2026/05)で Cloud Run GPU が提供されているのは asia-southeast1(Singapore)、europe-west1 / europe-west4、us-east4 などの一部リージョンに限られており、私たちの本番リージョンである asia-northeast1(Tokyo)はまだ対応していません
  • 使っている A100 が Cloud Run GPU のラインナップに無い
    • Cloud Run でサポートされている GPU は NVIDIA L4NVIDIA RTX PRO 6000 Blackwell の 2 種類で、私たちが本番推論で使っているNVIDIA A100 は Cloud Run のラインナップには現状含まれていません。

つまり、AI 系を Cloud Run に寄せようとすると 「Tokyo を諦めて Singapore にする」かつ「A100 を諦めて L4 などに変える」 という大きな構成変更が同時に必要になります。これは性能・コスト・レイテンシ(クロスリージョン化の影響)・法務など評価すべき軸が多く、現在検討中です。GKE 上であれば、Tokyo リージョンの A100 ノードプールでそのまま動かせるので、無理に急がず、Cloud Run 側に寄せるメリットが上回るかをじっくり見極めている段階です。

それまでは 「全員 Cloud Run」ではなく、Cloud Run と GKE を適材適所で使い分ける という現実解で運用していきます。今回の移行で得た「ぱっと戻せる設計の効きどころ」を頭に置きながら、また数年後に何かのきっかけで構成を見直すことになっても身軽に動けるように環境を整えておきたいと思っています。


まとめ

ここまで読んでいただき、ありがとうございました。

今回の件で学びとしては、インフラ選定は事業フェーズと要件に合わせて見直していく。でした。 GKE → Cloud Run も、その逆も、どちらが正解という話ではなく、「いまの自分たちにとっての最適解」を選び続ける作業です。そして、その選び直しを軽くするには、ぱっと戻せる設計と、ランタイムに依存しない観測の仕組み(OpenTelemetry / Datadog)を、最初に仕込んでおくことが効きます。

Special Thanks

今回の移行は、自分ひとりで進めたものではありません。

  • 一緒に PR をレビューしてくれた SRE / インフラチーム
  • 各サービスのオーナーとして、Cloud Run 化の影響範囲を一緒に確認してくれた開発チーム
  • 移行までのスケジュールを調整してくださったプロダクトマネージャー

のみなさん、ありがとうございました。

一緒に働きませんか

Gaudiy ではエンジニアを募集しています。Cloud Run, GKE など、この領域に知見のある方や、Gaudiyでのプロダクト開発に興味のある方がいたらぜひお話ししましょう!

gaudiy.com

special.gaudiy.com