こんにちは、SCSKの齋藤です。
本記事では、Cloud Schedulerのリトライ設定(retry_config)について、実際に運用で問題に遭遇してから解消するまでの話をご紹介します。
はじめに
Cloud SchedulerでCloud Run Jobsを定期実行するパイプラインを運用していたところ、「なぜかジョブが実行されていない日がある」という問題に遭遇しました。ジョブ自体のコードにはバグがなく、手動実行すると問題なく動く。なのにスケジュール実行だけが時々失敗している。
Cloud Loggingで調べてみると、Cloud SchedulerからCloud Run APIへのHTTPリクエストが5xxエラーで失敗しており、リトライされずにそのまま終わっていることがわかりました。デフォルトではリトライ回数が0(リトライなし)なので、1回の一時的なエラーでジョブ実行が丸ごとスキップされてしまうのです。
retry_config を設定するだけでこの問題は解消されたのですが、そもそもデフォルトがリトライなしであることを知るまでに時間がかかりました。この記事では、Cloud Schedulerの仕組みとretry_configの設定方法、そして運用上の注意点を紹介します。
Cloud Schedulerとは
Cloud Schedulerは、Google Cloudが提供するフルマネージドのジョブスケジューラサービスです。cron形式でスケジュールを定義し、指定した時刻にHTTPリクエスト、Pub/Subメッセージ、App Engineリクエストを発行できます。いわゆるcronジョブのマネージド版です。
Cloud Schedulerの主な特徴として、完全マネージドでインフラ管理が不要なこと、cron式に加えてタイムゾーンの指定が可能なこと、HTTPターゲットではOAuthやOIDCによる認証付きリクエストを送信できること、そしてリトライ設定(retry_config)やデッドレター設定が可能なことが挙げられます。
従来のLinuxサーバー上のcronと比較すると、サーバーの死活監視が不要で可用性が高い点が大きなメリットです。また、実行履歴がCloud Loggingに記録されるため、「実行されたか」「成功したか」の確認が容易です。料金も1ジョブあたり月額$0.10と非常に安価で、定期バッチのスケジューリングに最適なサービスです。
Cloud Run Jobsとの連携では、Cloud Run Admin APIのジョブ実行エンドポイントにHTTP POSTリクエストを送信する形でジョブをトリガーします。このHTTPリクエスト自体が一時的なエラーで失敗する可能性があり、ここにretry_configが関わってきます。
問題: デフォルトではリトライ0回
Cloud Schedulerのretry_configのデフォルト値は retry_count = 0 です。つまりHTTPリクエストが1回失敗したら、それで終わりです。次のスケジュール実行タイミングまでジョブは起動しません。
Cloud Run APIは通常は安定していますが、一時的な過負荷や内部メンテナンスにより5xxエラーが返ることがあります。頻度は低いですが、毎日実行するジョブを半年運用していると数回は発生する確率です。毎日1回のバッチであれば、1回の失敗で丸1日分のデータが欠損することになります。
この仕様は公式ドキュメントに記載されていますが、明確に「デフォルトはリトライなし」と目立つ形では書かれていないため、見落としがちです。実際に実行漏れが発生してCloud Loggingでログを確認し、HTTPステータスが503だったことから初めてリトライの必要性に気づきました。
retry_config を設定するだけでこの問題は解消されました。以下に設定前後の比較をまとめます。
| 項目 | 設定前(デフォルト) | 設定後 |
|---|---|---|
| retry_count | 0(リトライなし) | 3 |
| 1回の失敗での影響 | ジョブ実行が丸ごとスキップ | リトライで復旧の可能性 |
| 実行漏れ頻度 | 月数回発生 | ほぼゼロ |
| 設定の手間 | — | Terraformで数行追加するだけ |
retry_configの設定
Terraformでは google_cloud_scheduler_job リソースの retry_config ブロックで設定します。
resource "google_cloud_scheduler_job" "trigger" {
name = "trigger-my-job"
schedule = "0 9 * * *"
time_zone = "Asia/Tokyo"
http_target {
http_method = "POST"
uri = "https://..."
oauth_token {
service_account_email = var.sa_email
}
}
retry_config {
retry_count = 3
min_backoff_duration = "5s"
max_backoff_duration = "30s"
}
attempt_deadline = "60s"
}
設定項目は以下の3つです。
retry_count はリトライの最大回数です。初回の試行に加えてこの回数分リトライするので、retry_count = 3 であれば最大4回試行されます。多すぎると外部APIへの負荷が心配ですが、Cloud Run API宛てであれば3回程度が適切です。
min_backoff_duration と max_backoff_duration はリトライ間の待ち時間の範囲です。Exponential Backoffで待ち時間が増えていき、min_backoffからmax_backoffの範囲内に収まります。短すぎるとリトライが集中しますが、Cloud Run API宛てであれば数秒〜10秒程度で十分です。
attempt_deadlineの重要性
retry_config とは別に attempt_deadline というパラメータがあります。これは1回の試行におけるHTTPレスポンスの待ち時間(タイムアウト)です。デフォルトは30秒です。
Cloud Run APIがジョブ実行リクエストを受理するまでに30秒以上かかるケースは稀ですが、API側が高負荷の場合に発生し得ます。タイムアウトするとリトライが走りますが、ジョブ自体はAPI側で受理されて実行開始している可能性があり、結果的にジョブが重複実行されるリスクがあります。
この問題を避けるためにattempt_deadlineは余裕を持って設定(60秒程度)し、かつジョブ側を冪等に設計しておくのが安全です。
重要な注意点として、attempt_deadlineは「ジョブの実行完了を待つ時間」ではありません。Cloud Run APIがリクエストを受理して「ジョブを開始しました」というレスポンスを返すまでの時間です。ジョブ自体が10分かかっても、受理のレスポンスが数秒で返れば問題ありません。最初はこの違いを理解しておらず、ジョブの実行時間に合わせてattempt_deadlineを長くしようとしていました。
工夫したところ・ハマったところ
Terraformモジュールのデフォルト値で標準化
新しいジョブを追加するたびにretry_configの設定を書くのは手間ですし、書き忘れのリスクもあります。そこでTerraformモジュールの変数にデフォルト値としてリトライ設定を入れておく設計にしました。明示的に指定しない限りリトライが有効になるため、「入れ忘れ」を仕組みとして防止できます。IaCのメリットを活かした運用上の工夫です。
Cloud Loggingでの失敗原因の特定
問題が発生したとき、Cloud Loggingでスケジューラの実行ログを確認するのが最初のステップです。リソースタイプでフィルタし、severity WARNING以上でフィルタすることで、失敗した実行を素早く特定できます。ログにはHTTPステータスコードやレスポンスボディが記録されるため、失敗原因の特定に役立ちます。
リトライとジョブの冪等性
リトライが有効になると、同じジョブが複数回実行される可能性が出てきます。attempt_deadlineタイムアウト → リトライ → でも最初の実行は実は成功していた、というケースです。このため、ジョブ側は必ず冪等に設計しておく必要があります。データの取り込みであればupsert方式(DELETE + APPEND)にしておけば、何回実行されても結果が同じになります。
まとめ
– Cloud Schedulerのデフォルトはリトライ0回。一時的なエラーでジョブ実行が丸ごとスキップされる
– retry_configで3回程度のリトライとExponential Backoffを設定するだけで、実行漏れが大幅に減る
– attempt_deadlineは「ジョブ実行リクエストの受理待ち時間」であって「ジョブ実行時間」ではないことに注意
– ジョブ側は冪等に設計しておく。リトライによる重複実行に備える
– Terraformモジュールのデフォルト値でリトライを標準化し、設定忘れを仕組みとして防止する
– 設定は数行で済むので、Cloud Schedulerを使っているなら必ず入れておくべき

