最小限のダウンタイムでApp Engine・Cloud Runを別プロジェクトに移行した話 #Developer&Designer Advent Calendar 2024

Developer&Designer Advent Calendar 2024

はじめに

 クライアントサービス開発部 HR forecasterエンジニアリンググループの熊切です。

 私たちのチームではdodaの膨大なマーケットデータを用いて採用活動を支援するHR forecasterの開発を行っております。 https://hr-forecaster.jp/

 今回はGoogle CloudのサーバレスサービスであるApp Engine・Cloud Runを最小限のダウンタイムで別のGoogle Cloudプロジェクトに移行したお話しをしたいと思います。スムーズな環境移行を行いたい方にご参考になれば幸いです。

HR forecasterの開発体制

 現在HR forecasterの開発はフロントエンドとバックエンドの2つのグループに分かれて開発しています。それぞれ別部署に所属しており日々デイリースクラムやチャットツールを使用し密にコミュニケーションを取りながら開発しています。  インフラはGoogle Cloud上で作成されており、管理はバックエンドチームがメインで行っています。

HR forecasterチーム体制

 近年サービスのグロースに伴い、開発メンバーがフロントエンドチームを中心に増えていきました。ここでバックエンドチームとのメンバーバランスに偏りが生まれてしまい、バックエンドチームの負担が増加する形となりました。またアカウント管理の都合上、他部署にあるGoogle Cloudへのアクセスが難しくフロントエンドチームはなかなかフォローに入れないという課題が生まれました。

 そこで今回フロントエンドに関わるリソースだけでも、フロントエンドチームで管理しやすい別環境のGoogle Cloudに移行しバックエンドチームの負担軽減を目指しました。

移行概要

 移行前と今回構築した移行後の概略比較は以下の通りとなります。

環境移行比較

 バックエンドのAPIサーバーはCloud Run functionsを使用しています。一方フロントエンドはマイクロサービス化されており、App EngineやCloud Runを使用しています。  移行ではバックエンドチームが管理していたApp EngineやCloud Runをフロントエンドチーム管理のGoogle Cloudプロジェクトを新たに作成した上で移行しました。これまでバックエンドチーム管理では同一のCloud Load Balancingに全てのマイクロサービスが紐づいていましたが、今回の移行に伴い フロントエンドチーム管理のプロジェクトにもCloud Load Balancingを作成しフロントエンドのマイクロサービスを紐付ける構成にしました。またそれぞれの環境のCloud Load Balancingには同一ルールのCloud Armorを設定しました。こちらの同期方法については後述します。

 今回のポイントはフロントエンド環境に必要なものを予め準備した上でDNSを切り替えることによりユーザーに対してダウンタイムなしで移行できる点です。DNS切り替え時には同じソースコードをそれぞれの環境にデプロイしておき、時間経過で全てのリクエストがフロントエンド環境に届く仕組みです。最後にバックエンド環境に不要になったフロントエンドリソースを削除することで完了となります。

移行手順

 具体的な手順は以下のようになります。こちらでは各項目の概要について記します。 1. Terraformを用いてリソースを定義 1. SSL証明書を作成 1. フロントエンドのデプロイ先を切り替え・動作確認 1. DNSのAレコードを切り替え

Terraformを用いてリソースを定義

  フロントエンド環境は今回新たに作成するため今後の管理コストを考え、Terraformを用いたコード管理をしました。

Cloud Load Balancingの定義

 Cloud Load Balancingは細かいモジュールの組み合わせで成り立っており、Terraformで作成する際には全て宣言し組み合わせる必要があります。簡単に作成できるよう予めmodule化されているものもあります。参考

 しかし今回後述するSSL証明書の設定や多くのマイクロサービスを柔軟に管理するなどの観点から独自でCloud Load Balancingのモジュールを構築することにしました。ポイントはlocalsを変更すれば自動的に関連するリソースを作成するようにした点です。従来のモジュールは拡張性を重視するため外部でモジュールを作成し指定しなければならないものが多くありました。それを今回独自に作成することで必要な設定のみをlocalsに持つことができ、作成する際にはCloud Load Balancingのファイルのみ宣言すれば良い形にしました。

locals {
  ...
  backends = {
    ...
    "マイクロサービス1" = {
      service_name  = "サービス名1"
      type          = "APP_ENGINE"
    },
    "マイクロサービス2" = {
      service_name  = "サービス名2"
      type          = "CLOUD_RUN"
    },
    ...
  }
  host_rule_and_path_matcher = {
    "パスマッチャー1" = {
      default_service = "サービス名1"
      hosts           = "ホスト名"
      backends = {
        "マイクロサービス1" = {
          paths = ["/hoge", "/hoge/*"]
        }
        ...
      }
    },
    ...
  }
}

module "app_engine_neg" {
  for_each     = { for k, v in local.backends : k => v if v.type == "APP_ENGINE" }
  # モジュールに必要な定義を書く
}

module "cloud_run_neg" {
  for_each     = { for k, v in local.backends : k => v if v.type == "CLOUD_RUN" }
  # モジュールに必要な定義を書く
}

# その他Load Balancingに必要な定義を書く

Workflowsを用いたCloud Armorの同期方法

 開発環境やAdmin向けのサービスではセキュリティを加味しCloud ArmorによるIP制限を実施しています。元々バックエンド環境のIP登録・更新作業はSlackコマンドとWorkflowsを用いて自動化されていました。しかし対象となる環境がバックエンド環境とフロントエンド環境に増え、どちらの環境でも差異が出ないようにCloud Armorポリシーのルールを同期させる必要がありました。

 そこで今回は以下のようにチェーンさせることで同期を取るようにしました。

連携方法

 前述の通りSlackからバックエンド環境の連携はすでに実施済みですが、バックエンド環境からフロントエンド環境への連携をする必要がありました。そこでバックエンド環境のIP更新Workflowsのyamlに以下を追加しました。これでバックエンド環境のIP更新処理完了後にフロントエンド環境処理を連携できます。

# ...
- executeOnOtherEnvironments:
        for:
          value: external_project_id
          in: ${chain_environments}
          steps:
            - runWorkflow:
                try:
                  call: googleapis.workflowexecutions.v1.projects.locations.workflows.executions.run
                  args:
                    workflow_id: ${"IP更新ワークフローのID"}
                    location: ${location}
                    project_id: ${external_project_id}
                    argument:
                      ip: ${ip}
                      slackMemberID: ${slack_member_id}
                  result: result
                retry: ${http.default_retry}

 他環境のWorkflows実行にはサービスアカウントの権限付与が必要となります。バックエンド環境でワークフローを実行するサービスアカウントに対して、フロントエンド環境にてroles/workflows.invokerを付与しました。これでIP更新処理の同期が可能となりました。

SSL証明書を作成

 ここが今回の移行のポイントの一つになります。  現在Googleマネージド証明書(グローバル証明書)の発行は3種類存在します。公式ドキュメント

  • ロードバランサ認証を使用した Google マネージド証明書
  • DNS 認証を使用した Google マネージド証明書
  • Certificate Authority Service(CA Service)を使用した Google マネージド証明書

 最もメジャーなのはロードバランサ認証を使用した方法です。しかしこの方法ではCloud Load Balancingが作成された後に証明書のプロビジョニングが開始されるため、環境整備直後にダウンタイムが発生してしまいます。そのため今回はDNS認証を使用して証明書を作成しました。DNS認証は対象となるドメインのCNAMEを使用してGoogle側で認証を行う方式です。これはCloud Load Balancingが作成されていなくても、CNAMEさえ追加できれば独立して作成可能であるため、環境整備直後のダウンタイムは発生しません。

 予めSSL証明書を作成し、Terraform内で紐付けしておくことでDNS切り替え前にSSL証明書を使用した動作確認をする環境づくりを行いました。

 まず公式ドキュメントに従いDNS認証を用いた証明書作成を行います。ここではTerraformを使用せずgcloudコマンドにて作成しました。これは証明書マップ作成に時間がかかりTerraform側でapplyする時に該当する証明書マップは存在しないとエラーが出てしまったためです(もしかしたら現在では解消されているかもしれません)。ここでは証明書マップ・証明書マップエントリの作成までgcloudコマンドで実行します。

 Terraform側では作成された証明書を紐づける必要があります。設定では以下のように宣言することで行いました。作成された証明書マップを文字列指定することでTerraformをapplyした際に問題なく作成することができます。

resource "google_compute_target_https_proxy" "default" {
  name          = "名前"
  url_map       = module.url_map.self_link
  quic_override = "NONE"
  # 以下で設定
  certificate_map = "//certificatemanager.googleapis.com/projects/${var.project_id}/locations/global/certificateMaps/${var.project_id}-certificate-map"
}

フロントエンドのデプロイ先を切り替え・動作確認

 HR forecasterではGitHub ActionsとGoogle CloudのWorkload Identity連携を用いてデプロイを実施しています。そのためここではGitHub Actionsのworkflowファイル上にあるWorkload Identity連携のIDを書き換えるのみで対応が完了しました。切り替え後フロントエンド環境に対してデプロイすることで環境整備を完了することができました。

 この状態ではまだドメインにアクセスしても新しい環境へアクセスすることができないため、ローカルのhostsファイルを書き換えることで動作確認を行いました。

xxx.xxx.xxx.xxx(IPアドレス) 対象ドメイン名

DNSのAレコード切り替え

 これまでで動作確認は完了しているため、リリース作業としてはDNSのAレコードをフロントエンド環境のIPに切り替えるのみとなります。正しくリクエストが来ているかCloud Logging等を活用し確認した上で作業は終了となります。

最後に

 今回は組織としてより柔軟な体制にするためApp Engine・Cloud Runを別プロジェクトに移行するお話をしました。リリース前に動作確認が可能かつリリース時のダウンタイムも最小限に押さえた形で実現しました。今後もサービス成長を支援できるようなシステム構築をしていければと思います。  

熊切 俊夫 Toshio Kumakiri

プロダクト統括部 クライアントサービス開発部 HR_forecasterエンジニアリンググループ エンジニア

2023年4月新卒でパーソルキャリアに入社

※2024年12月現在の情報です。