ある朝、個人開発で回している複数サイトの夜間ジョブのうち一本だけがログに 401 を残して止まっていました。原因は単純で、以前ワークスペースを整理したときに古いキーを削除し、片方のジョブだけ差し替え忘れていた、というものでした。手で消したキーは、いつ、どのジョブが使っていたのかを後から思い出しにくいものです。
Console で APIキー・Admin APIキーに有効期限を設定できるようになりました。プリセット・任意の日付・無期限から選べ、7日以上のキーは失効前にメールで知らせてくれます。無人でモデルに処理を委ねている私自身の立場からすると、これは「鍵がいつか静かに切れる」という不安を、「この日に切れると分かっている」という予定に変えてくれる変更です。ただし、期限付きのキーは設計を誤ると、予定どおりに夜間ジョブを止めてしまいます。失効を事故ではなく予定として扱うための運用を、順を追ってまとめていきます。
有効期限は「いつか切れる不安」を「切れる日付」に変える
これまで APIキーは、自分か管理者が明示的に削除しない限り生き続けました。安心なようでいて、実際には「どのキーが古いのか」「もう使っていないキーが放置されていないか」を人間の記憶に頼る状態でした。放置された古いキーは、漏れたときの被害を大きくします。
有効期限を付けると、この関係が反転します。キーは決めた日付で必ず失効し、7日以上先に設定したキーは期限が近づくとメールで通知が届きます。つまり「気づいたら切れていた」ではなく「あと7日で切れると事前に分かる」状態になります。
一方で、無人運用では通知だけに頼るのは危ういという現実もあります。通知メールを見落とせば、期限当日にジョブが 401 で止まります。失効を安全に扱うには、通知を受け取る仕組みと、切れる前に鍵を入れ替える仕組みの両方が必要です。
無人運用でありがちな失敗は「単一キーの一発差し替え」
いちばん壊れやすいのは、本番で使っているキーを1本だけ持ち、期限が来たら新しいキーを作って古いキーを消す、という一発差し替えです。この方式には切れ目があります。古いキーを消してから新しいキーを環境変数へ反映するまでの数分間、あるいは反映を忘れた区間で、ジョブは認証に失敗します。
夜間の自動実行では、この数分の切れ目が「その日の処理がまるごと落ちる」ことに直結します。しかも失敗は静かで、翌朝ログを見るまで気づけません。認証エラーそのものの切り分けはClaude API 認証エラー 401 Invalid API Key の原因と解決方法で扱っていますが、有効期限の運用では「そもそも切れ目を作らない」ことが主眼になります。
二重鍵の重ね替えで切れ目をなくす
切れ目をなくす基本は、常に有効なキーを2本持つことです。片方が現役、もう片方が次の世代の準備、という形にします。
手順はこうです。まず現役キー(key-A)が期限の7日前になり通知が届いたら、新しいキー(key-B)を発行します。key-B にも同じように有効期限を付けます。次に、環境変数やシークレットストアの参照先を key-B へ切り替え、夜間ジョブを1回走らせて 200 が返ることを確認します。ここまでで key-A はまだ生きているので、切り替えに失敗しても key-A へ戻せます。最後に、key-B での稼働を1〜2日見届けてから key-A を無効化します。
この「重ねてから外す」順序が肝心です。先に古いキーを消してしまうと、二重鍵の意味がなくなります。スコープを絞ったローテーション設計そのものは漏洩しても請求が膨らむ前に止める — 無人パイプラインのAPIキー被害半径設計でも触れています。有効期限は、そのローテーションに「いつやるか」という締め切りを与えてくれる仕組みだと捉えると腑に落ちます。
Console の通知を待たず、ジョブ自身に失効を見張らせる
通知メールは便利ですが、受け取るのは人間です。人間が見落とす前提で、夜間ジョブ自身に「自分が使っているキーの残り日数」を確認させておくと安心です。
やり方は素朴で、キーを発行したときに、そのキーのラベルと有効期限日をローカルの台帳へ書いておき、毎晩ジョブの先頭で残り日数を計算するだけです。Console の API はキーの秘密値を後から返しませんが、期限日は自分で記録できます。
import json
from datetime import date, datetime
from pathlib import Path
LEDGER = Path.home() / ".claude_key_ledger.json"
WARN_DAYS = 10 # Console の7日通知より前に自分でも気づくための余白
def check_key_expiry(active_label: str) -> None:
"""夜間ジョブの先頭で呼ぶ。残り日数が閾値を切ったら警告を出す。"""
if not LEDGER.exists():
print("[key-check] 台帳がありません。キー発行時に expires_at を記録してください")
return
ledger = json.loads(LEDGER.read_text())
entry = ledger.get(active_label)
if not entry:
print(f"[key-check] ラベル {active_label} が台帳に未登録です")
return
expires = datetime.strptime(entry["expires_at"], "%Y-%m-%d").date()
remaining = (expires - date.today()).days
if remaining < 0:
raise SystemExit(f"[key-check] {active_label} は {abs(remaining)} 日前に失効しています。停止します")
if remaining <= WARN_DAYS:
print(f"[key-check] 警告: {active_label} はあと {remaining} 日で失効します。次世代キーを準備してください")
else:
print(f"[key-check] {active_label} は残り {remaining} 日。問題ありません")
# 台帳の例(キー発行時に追記する)
# {
# "prod-nightly-2026-07": {"expires_at": "2026-10-10", "note": "夜間4サイト用"}
# }
if __name__ == "__main__":
check_key_expiry("prod-nightly-2026-07")このスクリプトを夜間ジョブの最初のステップに置くと、Console のメール通知を見落としても、ログに残り日数が毎晩記録されます。閾値(WARN_DAYS)を Console の7日通知より前に置いているのは、人間が動ける余白を自分の側でも確保するためです。すでに失効していれば処理を止め、盲目的に 401 を量産しないようにしています。
どの期限を選ぶか — 用途別の目安
有効期限の長さは、キーの用途と差し替えの手間で決めます。短くするほど漏洩時の生存期間は縮みますが、重ね替えの頻度は上がります。私の場合の目安を挙げておきます。
| 用途 | 期限の目安 | 考え方 |
|---|---|---|
| 本番の夜間バッチ | 60〜90日 | 四半期に一度の重ね替えなら手が回る。通知+台帳で二重に監視する |
| 検証・実験用 | 7〜30日 | 使い終わったら放置されがち。短くして自動的に片づける |
| CI・一時的な自動化 | 可能なら無期限を避ける | キーそのものをやめて鍵なし方式に寄せる選択肢がある |
| 手元の対話・下書き用 | 30〜60日 | 個人利用でも定期的に入れ替える習慣をつける |
CI や一時的な自動化については、そもそも長命の静的キーを持たない方向も検討に値します。発行のたびに短命の資格情報へ寄せる考え方はClaude API の静的キーをやめる — Workload Identity Federation で CI と本番を鍵なしに切り替えるにまとめています。有効期限は静的キーを使い続ける場合の安全弁、鍵なし方式はキーそのものを減らす一手、と役割が分かれます。
まとめ — 次の一歩
有効期限は、鍵を「いつか切れるかもしれない不安の種」から「切れる日付が決まった管理対象」へ変えてくれます。ただし通知を受け取るのは人間なので、無人運用では二重鍵の重ね替えとローカルの失効台帳をあわせて用意しておくと、夜間ジョブを止めずに済みます。
まず一歩として、いま本番で使っているキーに期限を付け、その期限日を台帳へ書き、上のスクリプトを夜間ジョブの先頭に足してみてください。次にキーを更新するときには、消す前に重ねる順序を試せます。小さな仕込みですが、ある朝ログに 401 だけが残る、あの静かな失敗を減らせます。実運用の参考になれば幸いです。