ジョブスケジューラから対話なしで起動していた定期ジョブが、ある日 401 Invalid authentication credentials で失敗しました。資格情報そのものは何も変えていないのに、です。4回以上連続で成功していたジョブが、突然6秒で認証エラーを返して落ちる——いかにも「たまたまの一過性障害」に見える、いやらしい表面化のしかたでした。
原因を追っていくと、犯人は資格情報の中身ではなく、OAuth トークンの生存期間とジョブ発火時刻の衝突でした。本記事は、ヘッドレス(対話なし)で外部 AI CLI ツールを定期実行するときに潜む、この構造的な認証リスクの記録です。
事の発端:間欠的に401で落ちる定期ジョブ
問題のジョブは、OS 標準のジョブスケジューラが決まった時刻に CLI ツールを 対話なし(ヘッドレス) で起動する構成でした。普段は問題なく回っていたのですが、ある日の発火時に次のエラーで失敗しました。
1Failed to authenticate. API Error: 401 Invalid authentication credentials最初に目を引くのは「資格情報が無効」という文言です。しかしログを確認すると、認証情報そのものは何も変更していません。環境変数に API キーを直接埋めているわけでもなく、CLI ツールは OS の資格情報ストアに保存された OAuth トークン(サブスクリプションログインで得たトークン)を読んで認証していました。
つまり「正しいはずの資格情報を使っているのに、間欠的に 401 が返る」という状態でした。
なぜ問題か:トークンの生存期間と発火時刻が衝突する
調査の決め手は、資格情報ストアに保存されたトークンの 書き込み時刻 と 有効期限 でした。タイムラインを並べると、構造がはっきり見えてきました。
| 時刻 | 出来事 |
|---|---|
| 発火時刻 | スケジューラがジョブを起動 → ヘッドレス CLI が資格情報ストアの OAuth トークンを読む → すでに期限切れ → 401 で失敗 |
| 発火の少し後 | 同じユーザーが対話セッションを開いた(あるいはバックグラウンドのトークン更新が走った)→ トークンが再発行され、ストアに書き直される |
ポイントは2つあります。
ひとつめは、OAuth トークンには有効期限があるということです。今回のトークンは生存期間が比較的短く(数時間オーダー)、新しく発行されたトークンも数時間後に切れる設計でした。つまり、トークンは定期的にリフレッシュされ続けることを前提にしています。
ふたつめは、ヘッドレス実行はトークンを自分でリフレッシュできないということです。対話セッションであれば、トークンが切れていれば再ログインを促されたり、CLI がバックグラウンドで黙って更新してくれたりします。ところが、スケジューラから対話なしで起動されたプロセスには、その更新フローがありません。期限切れのトークンを読み、そのまま 401 を食らって終わります。
この2つが噛み合うと、「前回のトークンリフレッシュからジョブ発火までの間にトークンが切れていると、ジョブだけが認証に失敗する」という現象が起きます。資格情報は壊れていないのに、ジョブのタイミングだけが運悪く期限の谷間に落ちる、というわけです。
「一過性障害」に見えるのが厄介
このバグの厄介さは、単発の transient failure(一過性障害)に見える点にあります。
- ジョブは普段は成功している(連続成功の実績がある)
- 失敗は短時間(今回は6秒)で、認証ステップだけが落ちる
- その後、人が対話セッションを開けばトークンが更新され、次回のジョブは何事もなかったように成功する
これだと、運用者は「ネットワークの一時的な揺らぎかな」「再実行したら通ったから問題なし」と片付けてしまいがちです。しかし実態は、一過性ではなく 構造的なリスクです。トークンの生存期間より長い間ジョブだけが孤立して走り続ければ、また同じ谷間に落ちます。
「再実行したら直った」は、たまたま誰かが対話セッションを開いてトークンを更新したからにすぎません。根本原因(ヘッドレスがトークンを自力更新できない)は何も解決していないのです。
学び:ヘッドレス実行は外部認証のライフサイクルに構造依存する
このケースを普遍化すると、ひとつの設計原則になります。対話なしで外部サービスを叩く定期実行は、外部認証トークンのライフサイクルに構造的に依存している、ということです。
人が画面の前にいる対話実行では、トークンの期限切れは「再ログインしてください」というプロンプトで吸収されます。だから期限切れが致命傷になりにくい。一方、ヘッドレス実行にはその吸収機構がありません。トークンが切れた瞬間に発火すれば、即座に失敗します。
さらに、複数の定期ジョブが同じ認証情報を共有している場合、リスクは増幅します。あるジョブが期限の谷間に落ちるなら、同じトークンを使う別のジョブも同じ谷間で落ちうるからです。単発の障害に見えて、実は複数ジョブにまたがる共通の構造リスク、ということになります。
| 実行形態 | トークン期限切れへの耐性 | 理由 |
|---|---|---|
| 対話実行 | 高い | 再ログイン・バックグラウンド更新で吸収できる |
| ヘッドレス実行 | 低い | 更新フローを持たず、切れたら即失敗 |
運用Tips:preflight検証とリトライをベースラインに組み込む
この学びは、ヘッドレスで外部認証を使うジョブを設計するときの定石に落とせます。AI にこの種のジョブを組ませるときも、最初から次を要件に入れておくと安全です。
1つめ:トークンの preflight 検証を入れる。
本処理に入る前に、トークンの有効期限(expiresAt のような値)を読んで「いま使えるか」を確認します。期限切れが見えたら、本処理を走らせる前にエラーを上げる(あるいは更新を試みる)ようにすれば、「本処理の途中で 401」という分かりにくい失敗を、「事前チェックで弾いた」という分かりやすい失敗に変換できます。
2つめ:認証失敗時のリトライ/再認証を組み込む。
401 を「一過性かもしれないが致命的」として扱い、トークン更新を試みてからリトライする経路を持たせます。ヘッドレスがトークンを自力更新できないなら、せめて「切れていたら更新を試みる」一手をジョブ自身に持たせておくべきです。
3つめ:間欠的な認証失敗を「一過性」で片付けない。
ログに 401 が出たら、まず「資格情報ストアのトークン有効期限と、ジョブ発火時刻の関係」を疑います。連続成功の後の単発失敗・短時間での失敗・認証ステップでの失敗、という3つが揃ったら、ネットワーク揺らぎではなくトークン期限切れを第一容疑者にするのが近道です。
1## ヘッドレス定期ジョブの認証要件2
31. 本処理の前にトークンの有効期限を preflight 検証する4 (期限切れなら本処理に入る前に止める/更新を試みる)52. 401 を受けたら、トークン更新 → リトライの経路を持たせる63. 複数ジョブが同じ認証を共有するなら、トークン更新の7 責任を1箇所に集約する(各ジョブが孤立して切れないように)84. ログには「いつ・どのトークンで失敗したか」を残し、9 期限切れと発火時刻の衝突を後から追えるようにするまとめ:間欠的な401は「期限の谷間」を疑う
資格情報を何も変えていないのに、定期ジョブだけが間欠的に 401 で落ちる——このとき最初に疑うべきは、資格情報の中身ではなく、トークンの生存期間とジョブ発火時刻の関係です。
今回の学びをまとめます。
- ヘッドレス(対話なし)実行は、OAuth トークンを自力でリフレッシュできない
- トークンの生存期間より発火が遅れると、ジョブだけが期限の谷間に落ちて 401 になる
- これは一過性障害に見えて、実は構造的なリスク(複数ジョブが同じ認証を共有するとなお顕著)
- 対策は preflight 検証(事前にトークン有効期限を確認)と リトライ/再認証をベースラインに組み込むこと
「再実行したら通った」で安心してはいけません。それは誰かが対話セッションを開いてトークンを更新しただけかもしれず、根本原因は次の発火でまた牙を剥きます。ヘッドレス実行の認証は、最初から「トークンは切れる前提」で設計するのが、静かな間欠障害を防ぐ近道です。