🔖

Google Cloudのメンテナンスモードをインフラレイヤで実現する:Cloud Armor + URL Mapsの IaC 活用術

に公開

はじめに

これは株式会社TimeTree Advent Calendar 2025の7日目の記事です。
(去年と同じ7日目に記事を投稿していたことに気づきました。)

私は普段、SREチームの一員として弊社サービスの信頼性向上を目指し日々業務に取り組んでいます。

さて、弊社で提供しているTimeTreeというサービスは2025年1月にAWSからGoogle Cloudへと移行しました
移行元のAWSと移行先のGoogle Cloudで同じTimeTreeというサービスを提供していますが、利用しているクラウドベンダーが提供するサービスや機能は必ずしも全くの同一ではありません。

一例として、AWSではALBに対して指定したレスポンスコードと固定レスポンスを返す仕組みによって、TimeTreeサービスをメンテナンスモードに入れることが出来ましたが、Google Cloudではこの内容を検討していた当時(2025年4月ごろ)には同等の機能はありませんでした。

そこで、Google Cloud環境下でもなるべくAWSと同じような操作感でメンテナンスモードを実装できないかを検討してみました。

前提

弊社TimeTreeのアプリケーションは以下のような構成となっており、インフラやアプリはそれぞれのリポジトリで管理され、デプロイフローなども独立しています。

  • プログラミング言語 / フレームワークなど
    • Ruby on Rails
    • Webサーバーとしてnginxを採用し、Cloud Runのサイドカーコンテナとして動作
  • インフラ(Google Cloud)
    • アプリケーションはCloud Run上で稼働
    • Cloud Runの動作のために、Cloud LoadBalancingなど動作に必要なサービスを利用
  • メンテナンスモードのアプリケーション側の実装
    • 特定の文字列とレスポンスコードを受け取ることでユーザーの画面ではメンテナンス画面に切り替わります。
    • この機能はすでにWebやネイティブアプリの各プラットフォームで実装されています。

インフラ・アプリケーションリポジトリとアプリの構成については以下の概略図を記載します。

※ これ以外にも使っているリソースや言語等はありますが、今回の話の本筋ではないため割愛します。

必要要件

まず、メンテナンスモードを実装するための機能要件として以下を満たす必要があります。

  1. 特定のHTTPステータスレスポンスを返せること
  2. 特定の固定レスポンスを返せること

一方で、非機能要件として以下を満たす必要があると考えました。
3. terraformで完結できること(アプリケーションのデプロイと切り離されていること)
4. 開発者等の社内メンバーがメンテナンス終了前に本番環境でテストできること
特定のユーザーかどうかはIPアドレスで判定します。

先の通り、アプリケーションはnginx + Ruby on Railsで稼働しており、例えば今回の用途のためにnginx側の設定変更を行うことで実現自体はできそうでした。
当初はCloud LoadBalancingに目的の機能がなかったため、nginx側の設定変更で方針を固めようとしていましたが、すでにこの時点で以下の問題点が見えていました。

  1. nginxの設定変更はアプリのデプロイに依存する
    • アプリ単体でメンテナンスモードにできるという利点はあるものの、アプリのリリース回数が増えてしまいます。
      • 単純計算でメンテナンスモード有効化、 アプリデプロイ、 メンテナンスモード無効化の合計3回
    • また、メンテナンスに入れるということは大規模な変更であることが想像され、ロールバックなども考えると出来る限りアプリのデプロイは「該当の変更のみ」をリリースしたい意図があります。
  2. nginxのconfigファイルのメンテナンスをどうするか
    • 年に1回あるかないかのメンテナンスのためにnginxのconfigファイルの運用まで思いを馳せるのは厳しいものがあります。
    • nginxのconfigをまずは整理する、など改善するポイントはあるかもしれませんが、いつメンテナンスモードが必要となるかわかりません。
    • 整理それ自体は必要ですが、本実装時でのスコープ範囲外としました。

これらを踏まえ、複数チーム・複数リポジトリで運営されているサービスを安定的にメンテナンスモードとする実装が必要だろうと考えました。

実装内容

概要

まず最初に実装内容から記載します。
実装した内容は、非常にシンプルになっています。

  1. 固定レスポンスを返すためのCloud Storageにメンテナンス用固定レスポンスのファイルを配置し、Backend Bucketとする
  2. 特定のIPアドレス以外は502を返すCloud Armorポリシーを作成する
  3. URL MapsでBackendから502が返ってきた場合、1. のBackend Bucketにルーティングするようカスタムエラーレスポンスを構成する
  4. Backend ServiceにCloud Armorポリシーをアタッチすることで、特定IP以外は502を返し、URL Mapsにより503がアクセス元に返る

こうすることで、既存のアプリに手を加えることなくメンテナンスモードに入れることが出来る状態となりました。

概略図ですが、緑の部分をterraformでメンテナンス時に動的に付与することでメンテナンスモードへの移行を実現しています。

なお、レスポンスコード502と503を使って実現している理由は以下のとおりです。

ちなみに、実際に試してみるとBackend ServiceへのCloud Armor適用からURL Mapsが更新されるまでの一瞬の間、502エラーが返る現象を確認しました。
どうやらURL MapsとBackend Serviceの依存関係による反映待ち時間が原因だと考えています。

Cloud ArmorポリシーをアタッチするBackend Serviceの数が少ない場合は影響がないかもしれませんが、Backend Serviceが多い場合は URLMapsを変更するだけのリリースを先行する必要があります。
ここは今後の改善ポイントです。

コード例

具体的なコードとしてはざっくり書くと以下のような感じです。
※ かなり端折って書いているので動かない可能性が高いです。もしご利用するのであればご自身の環境に合わせて書いてください。

#
# Cloud LoadBalancingとBackend Storageの設定
#

# Backend Storage用のCloud Storageはすでに作成済みとします
resource "google_compute_backend_bucket" "this" {
  name                    = "maintenance"
  description             = "for maintenance"
  bucket_name             = "YOUR_BUCKET_NAME"
}

# For Backend Storage
resource "google_compute_url_map" "this" {
  provider = google-beta
  project  = "YOUR_PROJECT_NAME"
  name     = "load-balancer"

  default_service = "YOUR_DEFAULT_BACKEND_SERVICE_NAME"

# 実際にバックエンドから502が返ってくる可能性を考慮して、メンテ時に default_custom_error_response_policy を構成する
  dynamic "default_custom_error_response_policy" {
    for_each = local.maintenance_mode ? [1] : []
    content {
      error_response_rule {
        # バックエンドから match_response_codesで指定したレスポンスコードが返ってきたら、 override_response_codeのレスポンスコードで上書きする
        match_response_codes   = "502"
        override_response_code = "503"
        path                   = "/server_down.json"
        }
      error_service = google_compute_backend_bucket.this.link
    }
  }
# ...以下略
#
# Cloud ArmorとBackend Serviceの設定
#

# Cloud Armor
resource "google_compute_security_policy" "maintenance" {
  count = local.maintenance_mode ? 1 : 0

  name        = "maintenance"
  description = "Maintenance"
  type        = "CLOUD_ARMOR"

  # VPNアクセス許可(優先度1)
  rule {
    action      = "allow"
    priority    = "1"
    description = "Allow access"

    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = ["YOUR_IP_RANGES"]
      }
    }
  }

  # その他は502エラー(優先度10)
  rule {
    action      = "deny(502)"
    priority    = "10"
    description = "Return 502 for all requests"
    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = ["*"]
      }
    }
  }

  # デフォルトルールは省略

# Backend Service
resource "google_compute_backend_service" "service_a" {
  project               = "YOUR_PROJECT_NAME"
  name                  = "service-a"
  description           = "Service A Backend Service"
  security_policy       = local.maintenance_mode ? google_compute_security_policy.maintenance.name : null
  # 以下省略

これらのコードを記載し、 locals { maintenance_mode = true } をlocal変数を管理するファイル等に記載するとメンテナンスモードに入る、というのがコード上の実装です。

実装に至るまで

さて、このメンテナンスモード対応についてはGoogle Cloud移行後、安定的に運用ができるようになってきた2025年2月〜3月ごろから少しずつ検討を進めていました。
一方で、2025年4月ごろにChatGPT-4 / ChatGPT-4oモデルやDeep Research機能が出てきたことで「なるほど?使ってみるか?」となり、自分で検討した内容とChatGPTが提案した内容のどちらが良さそうか、というの検討していたのでその内容を記載します。

自分で検討した内容

ChatGPTと壁打ちをする前に自前で検討したメンテナンスモードを実装するための方針は以下の2パターンでした。

  1. nginxのconfigで、各locationごとに503と固定レスポンスを返す設定を埋め込み、メンテナンス前にアプリケーションと一緒にデプロイする
  2. Cloud LoadBalancingでメンテナンスモードであることを示す特殊ヘッダを埋め込み、nginxが特殊ヘッダを受け取ったら固定レスポンスを返す

これらの方法は、先に上げた課題点である「nginxの設定変更はアプリのデプロイに依存する」や「nginxのconfigファイルのメンテナンスをどうするか」がクリアにならないため、運用に乗せるために一工夫必要だなと感じていました。

一方で、2. で実装を進める場合は、「nginxのconfigファイルのメンテナンスをどうするか」という運用面さえクリアすれば、インフラレイヤのみでメンテナンスモードを実装できることが分かっていました。

このあたりを実装している最中にChatGPT-4oモデルの利用が出来るようになったため、ここからはChatGPTを使い壁打ちを進めました。

ChatGPTと検討した内容

以下のプロンプトを記載し、Deep Researchを実施したところ、ほぼ初手で今回の実装プランが返ってきました。

Google Cloudでロードバランサを利用している。 
今までは、AWSのALBを利用しており、ALBから固定レスポンス(503)を返すように設定変更することでシステムのメンテナンスに突入するようにしたい。 
要件としては以下の通り
- 特定のIPアドレスの場合は503ではなく、普通にアクセスできるようにしたい
- インフラの変更のみで対応したい
前提としては以下のとおり
- nginx + railsアプリケーションをCloud Runで稼働させている
- nginxにて固定レスポンスを返しても良いが、リリースに時間がかかるため、インフラレイヤのみで変更する
現時点の案は、ロードバランサで特定のヘッダーを付与された場合( X-MAINTENANCE-NOW=true ) に、nginx側で503を返すようにできれば良いと考えているが、他に良い案が無いか調べて欲しい

ここから、例えばCloud Armorを利用しないで目的を達成できないか?などいくつかのやり取りを経てはいました。
しかし、提案された内容でPoCを実装し試してみたところ、上手くいきそうなことが分かったので現在はこの案を採用しています。

もちろん、会話をしていく中でこちらの要件を無視して「固定レスポンスを返す専用のBackend Serviceを作成し、terraformでURL Mapsを更新しましょう」などと言われることもありましたが・・・

ここまで書いておいて恐縮ですが、この記事の執筆時点では本記事で実装したメンテナンスモード機能はまだ使われていません。
そのため、検証段階で試してみた感想にはなりますが、インフラレイヤだけでメンテナンスモードを実現できるようになったのは、運用コストの観点から考えてもChatGPTに提案してもらった内容は良かったと思っています。

ChatGPTとの協業を終えて

さて、ここまでで実装や実装に向けてどのようにLLMと協業してきたか、について記載したので「LLMとの協業を終えてどうだったか」についての当時を思い出して感想を述べていきます。

本実装を進める前のChatGPTとの壁打ちを開始する前は、「ベンダー固有の機能を扱った検討・実装は苦手だろう」と高をくくっていました。
ところが、提案された内容を脳内で考えてみると「あれ?うまく行きそうじゃね?」と頭をよぎり、実際にPoCを実装してみるとほぼ期待通りに動いてしまったのです。
Google Cloudの公式ドキュメントを読み込めばLLMに頼らずとも今回の内容は実装できたのですが、すでに自分が思いついていた実装があるため、どうしてもそちらに思考の比重が偏ってしまいます。

その思考を断ち切ってくれた提案をしてくれた瞬間は「うお、マジかよ!最高じゃん!」と思いましたが、それと同時に自分自身の存在価値とは?とも思うようになりました。
また、コードの実装もChatGPTやCursorなどのLLMが生成しており、ますます「自分の存在意義とは・・・?一体・・・???」のようになったのを覚えています。
「AIが人類の仕事を奪う」のような記事はいくつも目を通してきましたが、いざ自分がそれに近い体験をしたというインパクトはなかなかのものがありました・・・

ただ、当然ながら課題を掘り下げて実装していくのは現時点では人間がやらなければ進みませんし、決断を下すのも人間です。
打ちひしがれはしたものの、四の五の言わずにLLMを使いこなしていこうと決意しました。

おわりに

最後の方はLLMとの協業で感じた不安点を述べる形にはなってしまいましたが、本記事の内容を実装後の今もLLMを使い倒していますし、使わないという選択肢は私の中にはありません。

今回の経験で、「何を実現したいのか」を考え、自分の考えとLLMが提案してくる内容のどれが一番良いものになるか、を検討できたのはとても有意義でしたし、今後もそのような使い方をしていくと思います。
また、自分の思考を実現するドライバーとしてLLMが存在することで「必要とする人に価値が届けられるのであれば過程はなんでも良い」という考えになり、それを補佐してくれるのがLLMであり、手を動かす速度を加速させてくれるのもLLMなのだと思えた体験はとても良かったです。

なお、本記事でご紹介したメンテナンスモードを実装するには他にもいくつか方法はありますが、参考の一助となれば幸いです。

最後までご覧いただき、ありがとうございました。

TimeTree Tech Blog

Discussion