GitHub Actionsの定時実行が、3時間遅れました

山田 蓮

大分を拠点に、全国でWebサイト制作・業務ツール開発・Webマーケティングを手がけるフリーランスです。

担当しているサイトを毎日自動で確認する仕組みを、作り直しました。お問い合わせフォームが送れるか、全ページが正しく表示されるかを、決まった時刻に確かめるものです。

全ページの確認は、GitHub Actions の定時実行(schedule)で、毎朝8時17分と13時17分に動かす設定にしました。ところが作った翌朝、8時17分の回が記録にありません。調べると、実際に動いたのは11時8分でした。約3時間の遅れです。

この記事では、その遅れが仕様どおりのものだったこと、サーバーから起動を指示する形に変えた経緯、そして監視そのものが止まっても気づける作りについてまとめます。

お客様のお名前やサイト名は伏せています。構成は、GitHub Actions(非公開リポジトリ・Ubuntu 24.04・Node.js 22・Puppeteer)、エックスサーバー(共用サーバー)の cron と PHP、外部の見張りサービス Healthchecks.io(無料プラン)です。

何を、どこで確認しているのか

確認すること動かす場所いつ
お問い合わせフォーム(実際に送信し、メールが届くまで)エックスサーバーの cron毎日 9:07
全ページ(本物のブラウザで、パソコンとスマホの表示)GitHub Actions毎日 8:17・13:17
上の2つが最後まで終わったかHealthchecks.io(見張り役)常時

全ページの確認には、本物のブラウザでページを開く必要があります。共用サーバーではブラウザを動かせないので、ここだけ GitHub Actions を使っています。2つの確認を別の場所で動かしているのは、片方の不調でもう片方が止まらないようにするためです。

手元のパソコンでは動かさないことにしました。閉じていたりスリープしていたりする日があるので、「毎日確認します」と約束する場所には向きません。

定時実行は、約3時間遅れて動きました

8時17分は、もともと毎時0分を外した時刻です。GitHub の公式の説明でも、毎時0分は混みやすいので、ずらした時刻にするよう勧められています。それでも3時間遅れました。

これは不具合ではなく、説明どおりの動きです。GitHub のドキュメントには、次のように書かれています。

GitHub Actions のワークフローの実行によって高い負荷がかかっている間、schedule イベントが遅延する可能性があります。 高負荷の時間帯には、毎時の開始時点が含まれます。 負荷が十分に高い場合、キューに登録されたジョブの一部が削除される可能性があります。

出典:GitHub Docs「ワークフローをトリガーするイベント」(schedule)

つまり、遅れるだけでなくその回が実行されないこともある、という前提の機能です。時刻は既定で UTC として扱われる点にも注意が要ります。日本時間の朝8時17分は、UTC では前日の23時17分です。

定時のバックアップやレポートの作成なら、数時間の遅れは問題にならないかもしれません。ただ、「毎朝この時刻に確認しています」とお客様に約束する監視には、遅れも抜けも困ります。

もうひとつ気づいたこと

この日、毎朝のまとめは全ページの確認を「正常」と表示していました。前日に手動で動かした回が成功していたからです。「最後に成功した時刻」だけを見ていると、今日の回が動いていなくても正常に見えます。

確認する側は「いつ成功したか」まで見て、予定の回が動いたかを見分ける必要があります。とはいえ、まとめの判定を細かくしても、予定の回が抜ける原因は残ります。そこで、そもそも予定の回が抜けない形に変えることにしました。

サーバーから「今すぐ動いて」と指示する形に変えました

GitHub Actions には、定時実行のほかに、外から指示して動かす仕組み(workflow_dispatch)があります。REST API の POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches を呼ぶと、その場で実行が始まります。

そこで GitHub の定時実行をやめ、エックスサーバーの cron が8時17分と13時17分に、この API で起動を指示する形にしました。重い処理は GitHub、時刻を守る役はサーバー、と分けた形です。

切り替えてからの2回は、どちらも指定した時刻の3秒後に始まっています。

鍵は、できることを最小にします

API を呼ぶには、GitHub の鍵(fine-grained personal access token)が要ります。鍵には、できることを絞って持たせました。

万一この鍵が漏れても、できるのは「監視を1回余計に動かす」ことだけです。サイトにもサーバーにも触れられません。鍵の期限が切れたり取り消されたりしたときは、起動の指示が失敗した時点で見張り役に失敗を報告し、知らせが届くようにしています。

二重には動かしません

指示を出す前に、直近25分以内に始まった実行がないかを API で確かめています。手動で動かした直後などに、同じ確認が二重に走らないようにするためです。ワークフローの側にも concurrency を設定し、同時に2つは動かない形にしています。

別の監視では、GitHub の定時実行を残したまま、予定の約18分後にサーバーが様子を見て、動いていなかったときだけ代わりに指示する形にしました。定時実行を活かしつつ、抜けた回だけを取り戻す使い方です。

監視が止まったら、気づける形にしました

時刻どおりに動かすことと同じくらい大事なのが、監視そのものが止まったときに気づけることです。

「異常があったときだけ知らせる」作りでは、監視が止まると何も届きません。そして何も届かない状態は、正常なときと見分けがつきません。目覚まし時計が壊れても、時計自身は「鳴らなかった」と言えないのと同じです。気づくには、「起きてこない」ことに気づく別の誰かが要ります。

その役を、外部のサービスに任せました。Healthchecks.io は、決まった間隔で合図(ping)が届くのを待ち、届いているあいだは黙っています。予定の時刻と猶予の時間を過ぎても届かなければ、異常として知らせます。無料のプランで20件まで見張れるので、この規模なら費用はかかりません。

見張り方は、確認ごとに変えています。フォームの確認は「毎日9時7分」という予定そのものを登録し、1時間を過ぎても報告がなければ知らせます。全ページの確認は1日2回なので、「丸1日(猶予6時間)報告がない」ことを見ています。数時間の遅れまでは見張り役では拾わず、時刻の正確さはサーバーの cron に任せる、という分担です。

あわせて、次の決まりを作りに入れています。

  • 成功のたびに報告します。「異常のときだけ」に戻すと、止まったことに気づけなくなります
  • 「確認できなかった」も異常として扱います。設定ファイルが読めないなどの理由で確認を飛ばしたまま、正常として終わらせません
  • 1回の確認に上限時間を付けます。フォームは15分、全ページは40分。固まっても次の回を妨げません。全ページの確認は、1回目が失敗したら1回だけやり直します

わざと壊して、知らせが届くのを確かめました

知らせの仕組みは、本当に知らせが届くのを見るまで「できた」と言えません。そこで、故障をわざと起こして確かめました。

起こした故障結果
フォームの確認が固まった場合(報告が来ない)約1分で見張り役が異常と判定し、メールが届きました
フォームから送ったメールが届かなかった場合見張り役に失敗が報告され、試験用の知らせも直接届きました
全ページの確認が固まった場合上限時間で打ち切り、1回やり直し、再び打ち切って失敗を報告しました。メールも届きました
無効な鍵で起動を指示した場合「鍵の期限切れか取り消し」と見分けて、失敗として扱いました

試験の知らせには件名に【試験】と付けて、本物の異常と取り違えないようにしています。

まとめ

  • GitHub Actions の定時実行は、負荷しだいで遅れたり、実行されなかったりします。公式にそう書かれています
  • 時刻を守る必要があるなら、サーバーの cron から workflow_dispatch で起動を指示する方法があります
  • 起動用の鍵は、対象のリポジトリと権限を最小に絞ります
  • 「異常のときだけ知らせる」監視は、止まっても気づけません。外に見張り役を置き、成功のたびに報告します
  • 知らせの仕組みは、わざと壊して、知らせが届くのを見てから完成とします

自動の確認は、作った日に動いていても、動き続ける保証はありません。毎日の確認を約束するなら、「確認が動いたかどうか」まで確認できる形にしておく必要がありました。

サイトの監視や、止まっても気づける仕組みを作りたい方は、お問い合わせからご相談ください。

参考資料

SHARE