App Engine から Cloud Run への移行

2024-03-21

はじめに

はじめまして。Belong で Backend Engineer をしている atsushi です。
弊社では数年前からサーバーを App Engine から Cloud Run へ移行する取り組みを行っており、2023 年末にほぼ全てのサービスの Cloud Run 移行が完了しました。 本記事ではその備忘録として、私が移行作業を担当したサービスを例に、App Engine から Cloud Run へ移行した背景と対応内容について紹介します。

また、今回の移行作業では、サービス間の連携やセキュリティなど、さまざまな技術的トピックが取り上げられます。 App Engine と Cloud Run の比較から始め、ロードバランサーへの接続、Service Account の作成と権限付与、Dockerfile の作成、CI/CD の改善や Blue/Green デプロイメント戦略の導入など、移行作業における効果的なアプローチについて簡単な例を交えて説明します。

App Engine と Cloud Run

まずはじめに App Engine と Cloud Run についての概要について触れていきます。

App Engine は、Google Cloud のフルマネージドの PaaS(Platform as a Service)で、アプリケーションのデプロイメントとスケーリングを容易に行えます。自動スケーリング機能により、トラフィックの変動に対応できますが、特定のランタイムやサービスに依存しており、カスタマイズの自由度には限界があります。

一方、Cloud Run は、コンテナを使用したサービスのデプロイメントに特化したマネージドサービスです。Docker コンテナを使用するため、開発者は好きなプログラミング言語やフレームワークを使用し、アプリケーションをカスタマイズすることができます。

なぜ移行するのか

App Engine から Cloud Run への移行は、以下に挙げられる理由により検討されました。

柔軟性と流用性を向上させたい

Cloud Run では、Docker コンテナを使用するため、アプリケーションの環境やランタイムを自由にカスタマイズできます。また、他のクラウドプロバイダーやオンプレミス環境への移行も容易になります。これにより、アプリケーションの要件に応じて最適な環境を構築し、共有資産の流用やシステム全体の統一性を実現できます。

コストを抑えたい

Cloud Run はリクエストに応じて自動的にスケールアウト・インし、コスト効率が高いというメリットがあります。今回移行を行うサービスは通常時のリクエスト数が 1 日平均 200-300 程度とそこまで多くないため、常に起動しているサービスに対して課金される場合と比べて、リクエスト数による従量課金のメリットを享受できます。 一方、App Engine も従量課金を採用していますが、最後のリクエスト処理から 15 分後にインスタンスが終了するため、その間に新たなリクエストがない場合でも課金が発生します。この点において、Cloud Run は App Engine と比較して多少ですがメリットがあります。

ランタイムの制約を受けたくない

App Engine では最新のバージョンに必ずしも対応しているわけではないので、新しい機能を入れたい場合でもランタイム側が対応するまで待つ必要があります。 一方、Cloud Run は、App Engine のような特定のランタイムに依存せず、Docker コンテナを使用するため、新たなバージョンへと適宜アップデートを行うことが可能であり、ランタイムのアップデートやサポート期限の心配がありません。その結果、開発プロセスがスムーズになり、メンテナンスコスト削減が期待できます。

移行前の状況

移行前のシステムアーキテクチャは以下の通りです。

Image

移行対象となったサービスは、Google Pub/Sub や Google Sheets などの外部サービスと密接に連携する、Go 言語で開発されたバックエンドアプリケーションでした。このアプリケーションの運用では、自動スケーリング、トラフィック管理、セキュリティ設定など、各サービス間で運用ポリシーが異なり、統一された運用管理が難しくなっていました。これにより、効率的なシステム管理と迅速な問題解決が妨げられていました。

当時のシステムアーキテクチャでは、ほとんどの関連サービスが既に Cloud Run に移行しており、対象のサービスのみが App Engine 上に残されている状態でした。このため、一貫性のある運用とシステム全体の最適化を実現するために、今回のサービスを Cloud Run に移行することが決定されました。

移行は、既存のロードバランサーのバックエンドサービスに新しい Cloud Run サービスを追加する形で行われました。このアプローチにより、シームレスなトランジションと、移行中のサービスのダウンタイムの最小化を目指しました。

移行内容

実際に移行する際の手順を紹介します。あらかじめ以下のような関連サービスについての前提知識があることが望ましいです。

  • Google Cloud Platform (GCP) のマネージドサービスである Cloud Run 、Cloud Load Balancer 、Service Account についての基本的な理解
  • Service Account や Secret Manager を使用したアクセス制御や認証についての基本的な理解
  • Terraform をはじめとするインフラストラクチャのコード化やリソースの定義方法についての基本的な理解
  • Dockerfile を使用したコンテナのビルドやイメージの管理についての基本的な理解
  • 自動化されたビルド、テスト、デプロイのプロセスについての基本的な理解

Google Cloud Platform のリソースの作成や設定は、Terraform を使用して例示しています。

Cloud Run の作成

はじめに Cloud Run サービスを作成します。

resource "google_cloud_run_service" "example_service" {
  name     = "example_service"
  location = "asia-northeast1"
}

次に、この Cloud Run サービスをロードバランサーに接続して、トラフィックをルーティングする設定を行います。

ロードバランサーへのアタッチ

Image

Cloud Load balancer は主に 4 つの要素で構成されています。

  • Forwarding rule : フロントエンド(IP アドレスやポートなど)とバックエンド サービスを関連付けます。
  • Target HTTP(S) proxy : ロードバランサーが受け取ったリクエストを処理し、バックエンド サービスにルーティングするために使用します。また、SSL 証明書をロードバランサーに関連付けます。
  • URL Map : 特定のパスまたはホスト名に基づいてトラフィックを処理する方法を指定し、それに基づいて適切なバックエンド サービスにリクエストをルーティングします。
  • Backend service : ロードバランサーのバックエンドとして機能し、リクエストを実際のサービス(Cloud Run を使用する場合は Serverless NEG)に転送します。要件に応じて IAP 認証を設定することができます。

Serverless NEG は、Cloud Run や Cloud Functions などのサーバーレスサービスをロードバランサーに接続するための特別なエンドポイントグループです。

既存のドメインに新しいサービスを追加する場合、通常は新しいサービスに対する URL Map ルーティングを追加し、そのサービスを既存のロードバランサーの Backend service として指定するだけで済みます。しかし、今回の場合は新しいドメインを既存のロードバランサーに紐づける必要があったため、トラフィックを受け取るための新しい Forwarding Rule を作成し、トラフィックを転送するための新しい Target HTTP(S) Proxy を作成するという追加のステップが必要でした。

以下は実際に行なったロードバランサー設定のサンプルです。

IP アドレスと SSL 証明書

ロードバランサーに紐付ける IP アドレスを取得します。

resource "google_compute_global_address" "example_service" {
  name         = "example-service"
  address_type = "EXTERNAL"
}

次に、SSL 証明書の DNS 認証を行います。このリソースは、指定されたドメイン(ここでは "example.com")に対して SSL/TLS 証明書を取得および管理します。

resource "google_certificate_manager_dns_authorization" "example_service" {
  name        = "example-service-dns-authorization"
  description = "Certificates for Load Balancer of example service"
  domain      = "example.com"
}

DNS 認証を使用して証明書を取得し、指定されたドメインに対する SSL/TLS 証明書を作成します。

resource "google_certificate_manager_certificate" "example_service" {
  name = "example-service-certificate-example-service"
  managed {
    dns_authorizations = [google_certificate_manager_dns_authorization.example_service.id]
    domains            = ["example.com"]
  }
}

SSL 証明書のマッピングを定義します。このリソースは、SSL 証明書と特定のホスト名(ここでは "example.com")の関連付けを行います。

resource "google_certificate_manager_certificate_map" "example_service" {
  name = "example-service-certificate-map"
}

最後に、SSL 証明書のエントリを作成します。このリソースは、SSL 証明書のマッピングにおける各エントリを定義し、特定のドメイン名に対する証明書の適用を行います。

resource "google_certificate_manager_certificate_map_entry" "example_service" {
  name         = "certificate-map-entry-example-service"
  map          = google_certificate_manager_certificate_map.example_service.name
  certificates = [google_certificate_manager_certificate.example_service.id]
  hostname     = "example.com"
}

Forwarding Rule

新しい Forwarding Rule を作成して、Cloud Run へのトラフィックを受け入れるポートやプロトコルを指定します。この Forwarding Rule は、ロードバランサーが受け取ったトラフィックを適切な Target HTTP(S) Proxy に転送します。

resource "google_compute_global_forwarding_rule" "lb_frontend_https_for_example_service" {
  ip_address            = google_compute_global_address.example_service.address
  ip_protocol           = "TCP"
  load_balancing_scheme = "EXTERNAL"
  name                  = "lb-frontend-https-for-example-service"
  port_range            = "443"
  target                = google_compute_target_https_proxy.lb_target_proxy_for_example_service.id
}

Target HTTP(S) proxy

新しい Target HTTP(S) Proxy を作成して、Cloud Run にトラフィックを転送する設定を構成します。これには、SSL 証明書やその他のセキュリティ設定を含めることができます。

resource "google_compute_target_https_proxy" "lb_target_proxy_for_example_service" {
  name            = "lb-target-proxy-for-example-service"
  quic_override   = "NONE"
  url_map         = google_compute_url_map.example_service.id
  certificate_map = "//certificatemanager.googleapis.com/${google_certificate_manager_certificate_map.example_service.id}"
}

URL Map

既存の URL Map を編集して、Cloud Run へのルートパスまたは特定のパスにトラフィックを転送するように設定を更新します。

resource "google_compute_url_map" "lb" {
  name = "lb"

  # host_rules of other services
  # ....

  host_rule {
    hosts        = ["example.com"]
    path_matcher = "path-matcher-example-service"
  }

  path_matcher {
    default_service = google_compute_backend_service.lb_backend_for_example_service.id
    name            = "path-matcher-example-service"
  }
}

Backend service

新しい Backend Service を作成して、Cloud Run のエンドポイントを指定します。これにより、ロードバランサーがトラフィックを Cloud Run に転送する準備が整います。今回は使用しませんでしたが、必要に応じて IAP 認証1を有効化することでセキュリティの向上を図ることができます。

resource "google_compute_backend_service" "lb_backend_for_example_service" {
  connection_draining_timeout_sec = 0
  load_balancing_scheme           = "EXTERNAL"
  name                            = "lb-backend-for-example-service"
  protocol                        = "HTTPS"

  backend {
    balancing_mode  = "UTILIZATION"
    capacity_scaler = 0
    group           = google_compute_region_network_endpoint_group.neg_group_example_service.id
    max_utilization = 0
  }
}

Serverless NEG

新しい Serverless NEG をバックエンドとして追加します。これにより、ロードバランサーがサーバーレスサービスにトラフィックを転送できるようになります。

resource "google_compute_region_network_endpoint_group" "neg_group_example_service" {
  name                  = "neg-group-example-service"
  network_endpoint_type = "SERVERLESS"
  region                = "asia-northeast1"

  cloud_run {
    service = google_cloud_run_service.example_service.name
  }
}

Service Account の作成と権限付与

サービスを作成しただけではサービス間の連携を行うことができません。関連する Service Account を作成し、必要に応じて権限を付与し、安全なサービス連携を行えるようにする必要があります。

resource "google_service_account" "cloud_run_service_account" {
  account_id   = "cloud-run-service-account"
  display_name = "Cloud Run Service Account"
  description  = "Example cloud run account"
}

resource "google_project_iam_member" "cloud_run_service_account" {
  project = "project"
  role    = "roles/secretmanager.secretAccessor"
  member  = "serviceAccount:${google_service_account.cloud_run_service_account.email}"
}

上記で例示した Service Account は  Cloud Run サービスにアタッチする用のものであり、Cloud Run サービスに関連するリソースへのアクセス権限を持ちます。 今回の場合はデプロイ時に Secret Manager から認証情報を取得するため、Secret Manager へのアクセス権限(roles/secretmanager.secretAccessor)を付与しました。

参考までに上記以外に今回追加した Service Account の詳細についても軽く触れておきます。

  • CI/CD パイプラインにおいてビルドやデプロイなどの自動化プロセスを実行する用 : CI ツールから GCP リソースへのアクセス権限を持ちます
  • Terraform の Cloud Run サービス管理用 : Terraform が GCP へのアクセス権限を持ちます
  • 関連サービスから Cloud Run サービスを実行する用 : 関連サービス が GCP へのアクセス権限を持ちます。他のマイクロサービスやバッチジョブなどが Cloud Run サービスを呼び出す際などに使用されます

Dockerfile の作成

App Engine 環境から Cloud Run への移行にはアプリケーションをコンテナ化することが必要です。Dockerfile を作成します。

# ビルドステージとして、golang イメージを使用
FROM golang:1.20 as builder

# コンテナ内での作業ディレクトリを設定
WORKDIR /app

# ローカルのコードをコンテナイメージにコピー
COPY . ./

# アプリケーションをビルド
RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -v -o example-service main.go

# 本番用の軽量な Alpine イメージを使用
FROM alpine:3
RUN apk add --no-cache ca-certificates tzdata

# ビルドステージからバイナリをコピー
COPY --from=builder /app/example-service /example-service

# コンテナ起動時にウェブサービスを実行
CMD ["/example-service"]

CI の改善および修正

CI フローの改善を行います。App Engine から Cloud Run への移行に伴い、以下のステップが必要となります。

  • Google Artifact Registry にコンテナイメージをビルドしてプッシュ
  • 各環境へのデプロイ
  • トラフィックの移行

Google Artifact Registry へのコンテナイメージのビルドとプッシュ、およびトラフィックの移行はサービスごとに共通したプロセスですので、詳細には触れません。2 今回は、サービスごとに設定が必要となる Cloud Run サービスデプロイに焦点を当てます。CI の config ファイルには、Cloud Run へのデプロイに必要なステップとコマンドを追記します。 以下はそのサンプルです。

gcloud run deploy example-service \
    --image image \
    --region asia-northeast1 \
    --platform managed \
    --service-account service-account \
    --min-instances min-instances \
    --no-traffic true \
    --no-allow-unauthenticated true \
    --tag tag-name \
    --set-env-vars ENV_VALUE=env_value \
    --set-secrets=ENV_SECRET=env-secret:latest

今回指定した Cloud Run デプロイオプション3のうち、特に注意して設定したオプションは以下の 3 つです。

  • no-trafic : デフォルトではデプロイ時にトラフィック移行が行われますが、今回は明示的にトラフィック移行をオフに設定しました。これにより、トラフィック移行を別のジョブで行うことができ、トラフィックの移行時に必要な追加の承認フローやテストを組み込むことができます。これにより、現行サービスへの影響を最小限に抑えることができます。
  • no-allow-unauthenticated : 認証されていないユーザーからのリクエストを拒否するように Cloud Run に指示します。セキュリティ上のリスクを最小限に抑えるために、認証されていないユーザーからのアクセスを防ぐことが重要です。
  • set-secret : Secret Manager に登録されている認証情報を自動で取得し、環境変数に設定します。アプリケーション側で認証情報を独自に取得する必要がなくなり、セキュリティ上のリスクを軽減できます。

その他

あわせて移行対応に必要なアプリケーションおよび関連サービスの修正を行いました。

  • サービスを呼び出す Cloud Pub/Sub のトピックを新規作成し、Push エンドポイントを Cloud Run のエンドポイントに設定
  • Google Sheets に Cloud Run サービスにアタッチした Service Account を「編集者」で追加

デプロイ作業

移行では、DNS 切り替えを利用した Blue/Green デプロイメント戦略4を採用し、リスクを最小限に抑えつつ、スムーズな移行を実現しました。 この戦略では、App Engine と Cloud Run の両方の環境を用意し、同じドメインでそれぞれに設定された DNS レコードを切り替えることで、ダウンタイムゼロで環境の切り替えを行いました。DNS の切り替えには約 1 時間ほどかかりましたが、問題なくサービスの移行を達成することができました。

移行後の状況

移行後のシステムアーキテクチャは以下の通りです。

Image

移行完了後、すべての関連サービスが Cloud Run に統合され、一元的なロードバランサーを介して管理されるようになりました。この変更により、システム全体の運用効率が向上し、リソースの利用が最適化されました。

終わりに

本記事では、App Engine から Cloud Run への適切な移行プロセスについて紹介しました。採用した手法や具体的な手順は、移行プロジェクトや環境によって異なる場合がありますが、ここで紹介した内容はその一例に過ぎません。各々のプロジェクトに適したアプローチを見つける手助けとなれば幸いです。

弊社では、この記事のような技術的な試みを日々行っています。新しい技術を積極的に取り入れ、プロダクトを共に成長させていくことができるメンバーを募集しています。 本記事で Belong に興味を持っていただけたらぜひエンジニアリングチーム紹介ページをご覧ください。カジュアル面談も行っておりますので、お気軽にお申し込みください。

Footnotes

  1. Identity-Aware Proxy の概要

  2. Belong では、プラットフォームエンジニアリングの一環として、CI/CD 環境の整備が推進されており、具体的には GitHub Actions や CircleCI を用いたビルドやデプロイのプラクティスの策定と運用、および共通利用される ActionOrbs の構築が行われています。コンテナイメージのビルドやプッシュ、トラフィックの移行などのジョブに関しては、すでに全社的に使用される共通の資産が存在していたため、今回の移行作業ではそちらを参照するだけで事足りたという背景があります

  3. gcloud run deploy | Google Cloud CLI Documentation

  4. Blue/Green デプロイ パターン