Claude Codeを無人運用する前に見るべき4つの地雷|死活監視・リトライ・ロック・復旧設計

Claude Codeを無人運用する前に見るべき4つの地雷|死活監視・リトライ・ロック・復旧設計のアイキャッチ画像 AI×自動化
記事の根拠: 公式一次資料にもとづく解説・整理
本文の最終更新: 2026年7月
検証: AIツール図鑑編集部(AI自動化・ツール実運用 / 料金・規約は公式資料で確認)

無人稼働パイプラインの地雷とは、監視や保護機構自体が未検証のまま潜む運用上の盲点。

無人で動かすAIエージェントのパイプラインが止まるとき、その多くは派手なクラッシュではありません。ログには「正常終了」と出ているのに、朝に成果物を開くと中身が空だった。原因をたどると、監視の仕組みは書いてあったのに、肝心の場面で働いていなかった。落とし穴の正体は、AIが賢いかどうかではなく、無人前提の監視と復旧の設計が抜けている点にありました。夜間バッチやスケジュール実行でエージェントを回すなら、最初に押さえるべきは「動かし方」よりも「落ちたときにどう気づき、どう止めるか」でしょう。(Claude Codeで自動化を組む手順そのものはClaude Code 自動化の実例で扱っています。本記事は、そうして組んだ処理を無人で回し続けたときに壊れる箇所に絞ります。)ここで扱う4つの地雷は、Claude Codeに固有のものではありません。AIエージェントでも、ローカルLLMの推論バッチでも、夜間に走る長時間処理でも、人手なしで回す構成なら同じ形で表面化します。以下では、症状から原因、回避策の順にたどります。取り上げる数値は、いずれも運用中の内部ログに記録された実インシデントの値で、外部から検証できる公開データではありません(2026年時点)。

この記事の要点

  • 起動時のヘルスチェックが通っても、稼働中に依存サービスが落ちれば検知できない
  • リトライは一時的な瞬断を吸収する仕組みで、恒久的なダウンには被害を広げるだけになる
  • 監視・掃除の仕組み自体が、実行主体を巻き込みうる範囲を見落とすことがある
  • 「ロック」と名のつくファイルが、実際に排他制御として検証されているとは限らない

Claude Codeで長時間タスクを無人実行すると何が起きやすいか

Claude Codeはインタラクティブなセッションとしてだけでなく、非対話モード(-p--print、旧称ヘッドレスモード)でスクリプトやCI/CDから実行できます。そのため、人が画面を見ていない状態で長時間のタスクを走らせる構成も取りやすくなります。この使い方は、対話中には意識しない前提を静かに壊します。対話セッションなら応答がおかしくなった瞬間に人間が気づいて止められますが、無人実行ではその一次検知役がいません。

無人実行を構成する要素は、たいてい重なっています。タスクを実際に実行するエージェントの実行ラッパー(harness、いわば親プロセス)、判断や生成に使うモデルの推論経路(クラウドAPIの場合もあれば、ローカルの推論サーバーを併用する構成もあります)、そして起動そのものを担うスケジューラ。この3つはそれぞれ別の理由で落ちます。harnessプロセスが掃除ロジックに巻き込まれて死ぬこともあれば、推論サーバーが負荷やドライバの問題で応答不能になることもあり、スケジューラは生きているのにトリガーだけ発火しなくなることもあります。どれか1つだけを見張っていても、残り2つの死角は埋まりません。

人間が画面を見ていない前提に立つなら、監視すべきは「タスクが起動したか」ではなく、「起動したあと、実行主体・推論経路・スケジューラのそれぞれが、稼働中を通じてまだ機能しているか」です。以下で見る4つの地雷は、いずれもこの「稼働中の見落とし」という同じ形をしています。

派手なクラッシュより「監視が効いていなかった」で落ちる

無人稼働の障害には、はっきりした傾向があります。プロセスが落ちてスタックトレースが残る分かりやすい事故は、むしろ少数派。多いのは、監視やリトライといった保護の仕組みが「書いてあるのに効いていない」型で、これが形を変えて何度も表面化します。人が張り付いていれば画面の異常にすぐ反応できるのに、無人ではその一次検知が丸ごと欠けている。静かな失敗が積み上がるまで、誰も気づけません。まず解きほぐしたいのは、「動いていること」と「仕事をしていること」の混同です。

「動いている」と「仕事をしている」は別物

プロセスが起動している状態と、そのプロセスが正しい成果を出している状態は、別物として扱う必要があります。ヘルスチェックが「200 OK」を返しても、それは入口の窓口が開いている証拠にすぎません。窓口の奥で処理が壊れていても、入口だけ見ていれば「健康」と判定されてしまう。無人パイプラインでよくあるのが、各ステップが律儀に「成功」を返しているのに、束ねた成果物は空、というパターンです。ステップ単位の戻り値と、パイプライン全体の成果は、必ずしも一致しません。

無人稼働ゆえに誰も異常に気づけない

有人のバッチ処理なら、進行が止まった時点で担当者が気づき、手で止めて調べられます。無人ではその役目がいません。異常に最初に気づく担い手を、人ではなく別の仕組みへ委ねるしかない。検知役を用意する前に、「何をもって異常とみなすか」を決めていないパイプラインが多い。ここが抜けると、どれだけ立派な通知基盤を積んでも鳴りません。

この記事で取り上げる地雷は4つ。起動時チェックだけで稼働中の死活監視を代用してしまうこと、恒久的なダウンでもリトライを続けて被害を広げること、監視や掃除の仕組み自体が実行主体を巻き込んでしまうこと、「ロック」を実装したつもりが実は排他になっていないこと。共通するのは、監視や保護の仕組みそのものが検証されないまま運用に乗っていた、という点です。症状・原因・回避策の順で掘り下げます。

起動時ヘルスチェックと稼働中の死活監視は別物

無人パイプラインで最も多い取りこぼしが、この一点に集約されます。ジョブの開始時に依存サービスの生存を1回だけ確認し、そこを通過したら「あとは大丈夫」とみなす設計。けれど依存先は、処理の途中でいくらでも落ちるもの。起動時の一点観測と、稼働中ずっと続く死活監視では、役割がまるで違います。そのズレがどう被害へ化けるのか。まずは具体的なインシデントから確かめます。

何が起きたか(インシデントの時系列)

あるバッチジョブでの記録を、内部情報を伏せて時系列で追います。処理の前段では、ローカルで動かしていた推論サーバー(OllamaのようにローカルでLLMを動かす構成)に対し、起動時のヘルスチェックとしてcurlで疎通確認用のエンドポイントを叩いていました(この確認先は監視用に用意したもので、推論サーバー本体が公式に /health を備えているとは限りません)。応答は200。チェックは問題なく通過します。ところが処理が走っている最中に、この推論サーバーが500系のエラーを返し始め、ほどなくダウンしました。

ジョブ側はそれでも止まりませんでした。1件ずつリクエストを投げ、応答が返らなければ最大60秒のタイムアウトで打ち切り、そのまま次の件へ。残っていた約1800件を、失敗させながら約72分かけて最後まで処理し切ります。成功したのはごくわずか数件。にもかかわらず全件を走り切ったので、ログ上の記録は「正常終了」でした。

起動時のヘルスチェックが通ったことを「稼働中も安全」と読み替えると、依存先が途中で落ちても処理は止まりません。最悪の場合、全件を失敗させながら最後まで走り切り、ログには「正常終了」だけが残ります。

なぜ起動時チェックだけでは防げないのか

Kubernetesの世界では、コンテナの状態確認を1つの仕組みで済ませず、目的の異なる複数のprobe(診断)に分けて扱います。公式ドキュメントによれば、startup probeは「アプリケーションが起動を終えたか」を示し、これが成功するまで他のprobeは無効化されます。readiness probeは「コンテナがトラフィックを受け付けられる状態か」を示し、失敗するとサービスの振り分け先から外されます。liveness probeは「コンテナが動作し続けているか」を示し、失敗するとkubeletがコンテナを再起動します。

startupProbe: Indicates whether the application within the container is started.

readinessProbe: Indicates whether the container is ready to accept traffic.

livenessProbe: Indicates whether the container is running.

Kubernetes 公式ドキュメント「Pod Lifecycle」より(kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)

ここで見落とされがちなのは、readiness probeも起動時の1回きりではなく、コンテナが動いている間ずっと繰り返し評価される、という点です。今回のインシデントで使っていた「起動時に1回だけの疎通確認」は、startup・readiness・livenessという3つの区別を1つに押しつぶしてしまった状態に近いものでした。本来は「起動できたか」「いま受け付け可能か」「稼働中もずっと生きているか」を別々に、しかも稼働中は繰り返し確認する必要があります。1回きりのチェックで3つを兼ねたつもりになると、稼働中のダウンだけが検知の抜け穴として残ります。

実行ループに死活監視を織り込む(コード例: 最小構成イメージ)

対策の芯は単純です。処理ループの中で、一定件数ごと(あるいは一定時間ごと)に依存サービスの生存を確かめ、連続で失敗したらジョブ自体を止める。起動時の1回だけでなく、走っている間ずっと問い続ける形へ変えます。次のコードは、その考え方を示す最小構成イメージ。実際のエンドポイントや閾値は、扱う対象にあわせて調整してください。以下はそのまま貼って使う完成コードではなく、実行ループの中に死活監視を差し込む位置を示すための疑似コードです(itemsprocess(item)は既存の処理ループを表すプレースホルダーです)。

import requests

HEALTH_URL = "http://localhost:PORT/health"  # 各推論サーバー側で用意した監視用エンドポイント(Ollama等の公式APIとは別。環境に合わせて設定)
CHECK_EVERY = 20        # 何件処理するごとに死活を確認するか
MAX_CONSEC_FAIL = 3     # 連続失敗がこの回数に達したら停止

def is_alive(timeout=5):
    try:
        return requests.get(HEALTH_URL, timeout=timeout).status_code == 200
    except requests.RequestException:
        return False

consec_fail = 0
for i, item in enumerate(items):
    if i % CHECK_EVERY == 0:
        if is_alive():
            consec_fail = 0
        else:
            consec_fail += 1
            if consec_fail >= MAX_CONSEC_FAIL:
                raise SystemExit("依存サービスの死活監視に連続失敗。処理を中断しました。")
    process(item)

このガードがあれば、依存先が途中でダウンしても、数回の確認で処理を打ち切れます。約1800件を無駄に走り切る前に、被害を数十件の範囲で止められるわけです。監視は「入口に置く関所」ではなく「道中ずっと並走する伴走者」。この置き換えが、無人運用の生死を分けます。

エラーを握りつぶすリトライが被害を拡大する

「失敗したら少し待って、もう一度」。このリトライは、無人運用で標準的に使われる仕組みでありながら、使い方を誤ると被害を増幅させます。問われるのは、リトライが何を前提に組まれているのか。前提から外れた障害にぶつけると、保護のつもりが逆へ回ります。

リトライが暗黙に前提している「一時障害」という想定

リトライという仕組みは、障害が「一時的な瞬断」であることを暗黙に見込んでいます。ネットワークの一瞬の途切れ、瞬間的な高負荷、たまたま重なったタイムアウト。こうした短命な失敗なら、少し待って再実行すれば次は通るはず。ところが依存サービスがプロセスごと落ちる恒久的な障害では、この見込みが崩れます。待っても直らない相手に何度ぶつかっても、結果は失敗のまま。

先ほどのインシデントが、まさにこれでした。1件あたりの待ち時間は、本来なら瞬断をやり過ごすための保護策です。ところが相手が復旧しないため、その待ち時間は「失敗を確定させるまでの空回りの時間」に変わりました。残っていた件数をひたすら失敗させながら1時間以上を費やし、成功はわずか数件。タイムアウトという保護が、全体をゆっくり全滅させる装置へ反転した瞬間です。ここで要るのは「もう一度」ではなく「もう止めろ」という判断でした。

連続失敗で回路を開く(コード例: 基本構造)

恒久障害から早く抜けるには、失敗の回数を数えて、続くようなら自分から降りる。このような考え方は、ソフトウェア信頼性の分野で「サーキットブレーカー」と呼ばれます。依存先が落ちているのに呼び出し続けると、待ち時間や失敗が積み上がり、障害が連鎖しやすくなります。連続失敗が一定回数を超えたら回路を「開いて」、以降の呼び出しを即座に遮断する。復旧の見込みがない相手を叩き続けず、早めに手を引く発想です。

class CircuitBreaker:
    """連続失敗が閾値を超えたら回路を開き、以降の呼び出しを止める最小の仕組み。"""

    def __init__(self, fail_threshold=5):
        self.fail_threshold = fail_threshold
        self.consec_fail = 0
        self.is_open = False

    def call(self, func, *args, **kwargs):
        if self.is_open:
            raise RuntimeError("回路が開いています。恒久障害とみなして呼び出しを止めました。")
        try:
            result = func(*args, **kwargs)
        except Exception:
            self.consec_fail += 1
            if self.consec_fail >= self.fail_threshold:
                self.is_open = True
            raise
        self.consec_fail = 0
        return result

あわせて、リトライの間隔を回ごとに広げる指数バックオフと、その上限、総リトライ回数の上限も決めておきます。上限のないバックオフは、いつまでも待ち続けるだけ。恒久障害では「何回で・何分で見切るか」をあらかじめ数値で決めておくことが、被害を止める最短ルートになります。「一時障害なら粘る、恒久障害なら早く降りる」。この線引きを持たないリトライは、保護策の顔をした被害拡大装置に変わってしまうのです。

監視・掃除の仕組みが自分自身を巻き込む

無人運用を続けていると、孤立して残ったプロセスを片付けたくなる場面が出てきます。ここにも、見落としがちな地雷があります。

何が起きたか

孤立プロセスを定期的に掃除するクリーンアップ処理を書いた例があります。判定条件は、コマンドライン引数のパターン(たとえば実行時に付くフラグの種類)と経過時間(10分以上)の組み合わせでした。ところがこの条件は、掃除したかった対象だけでなく、自動化を実行しているエージェントのharnessプロセス自身にも一致してしまいました。harnessも同種のフラグを持って起動していたためです。結果、このクリーンアップは10分おきに自分自身を強制終了していました。発覚のきっかけは、前日の別セッションが原因不明のまま途中で止まっていた件を調べていて、偶然この自己参照バグに気づいたことでした。

なぜ起きたか

プロセスを名前や実行時間だけで判別する掃除ロジックは、「掃除したい対象」しか想定していません。しかし判定条件は、実行主体自身を含むすべてのプロセスに対して評価されます。除外条件を書き忘れれば、監視者が監視対象に紛れ込む。監視や掃除の仕組みを作るとき、対象範囲の外側に「自分自身」がいることを、つい忘れがちです。

回避策

判定条件を絞り込む方向では直しませんでした。コマンドラインの文字列と経過時間だけでプロセスを間引く方式自体をやめ、子プロセスをその生成元から明示的に追跡し、ツリーごと回収する方式に切り替えています。条件を後から継ぎ足すより、そもそも自分自身を巻き込みようのない設計に寄せるほうが、同じ種類の再発を防げます。同種の監視・掃除ロジックを書く場合は、対象の絞り込みに名前や引数の部分一致だけを使わず、実行主体自身のプロセスIDやその子孫であることを明示的に除外条件へ入れておくと、この種の自己参照事故は起きにくくなります。

「ロック」を実装したことと、排他になっていることは別

二重起動を防ぐ目的でロックファイルを使う設計は珍しくありません。ただし、ロックという名前のファイルがあることと、それが検証された排他制御になっていることは、別の話です。

何が起きたか

長時間ジョブ(1件あたり最大90分規模)の二重起動を防ぐために、ロックファイルを使っていた例があります。実装を確認すると、このロックファイルは単なる状態表示用のファイルで、無条件の上書きモードで開かれるだけでした。2つのプロセスが同時に「ロックを取得した」と誤認できる状態で、実質的に排他になっていませんでした。ジョブの実行時間が数分規模から最大90分規模に伸びたタイミングで、二重起動が現実的なリスクになったことをきっかけに発覚しています。

なぜ気づかれなかったのか

ジョブが短時間で終わっていたころは、二重起動が発生しうる窓が狭く、壊れたロックでも実害が出にくかったのです。実行時間が伸びるほど、この窓は広がります。素朴な実装は、負荷や実行時間が小さいうちは問題を隠したまま動き続けてしまいます。

壊れたロックファイルによくある型

今回のケースに限らず、「ロック」と名のつくファイルが実は排他になっていない典型パターンはいくつかあります。無条件の上書きモードで開いて中身を書くだけのもの(複数プロセスが同時に「成功」してしまう)。ファイルの存在確認と作成を2手順に分けているもの(確認と作成のあいだに別プロセスが割り込む余地が残る)。作成はできても、取得したプロセスが生きているかをその後二度と確認しないもの(プロセスが死んでもロックだけ永遠に残る)。共通するのは、「ファイルがある/ない」という状態だけで排他を表現しようとして、OSレベルの排他保証を経由していない点です。

正しい排他制御に必要な条件

最低限そろえたい条件は3つです。ひとつは、作成そのものが原子的であること(既に存在するなら作成自体が失敗する、OSレベルの排他的生成を使う)。ふたつめは、誰が取得したかを記録すること(プロセスIDなど、後から生死を確認できる情報)。みっつめは、取得者が死んでいる場合だけ回収してよいという条件を明示することです。次のコードは、その最小構成のイメージです。

import os

LOCK_PATH = "job.lock"
TTL_SEC = 7200  # このTTLを超えたロックは「死んだプロセスのもの」とみなす候補にする

def acquire_lock(pid: int) -> bool:
    try:
        fd = os.open(LOCK_PATH, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)  # 排他的に新規作成(所有者のみ)
    except FileExistsError:
        return False  # 既に誰かが取得済み
    try:
        os.write(fd, str(pid).encode())
    finally:
        os.close(fd)  # 書き込みで例外が出ても必ず閉じる
    return True

TTLつきロックでやってはいけないこと

この最小構成を実装した修正自体にも、独立したレビューで2つの見落としが見つかりました。ひとつは、生きているプロセスでもTTLを超えたら容赦なくロックを奪ってしまう設計だったことです。TTLはあくまで「これくらい経っていれば死んでいるはずだ」という目安であって、生存確認そのものの代わりにはなりません。奪ってよいのは、TTL超過に加えて相手のプロセスが実際に死んでいることを確認できた場合だけです。もうひとつは、ロックを解放する際に「自分が取得したロックかどうか」を確認せず削除していたことです。これでは、別プロセスが取得し直したロックを誤って解放してしまう余地が残ります。この2点を直し、再レビューで問題なしを確認しています。「ロックを実装した」と「ロックが検証された排他制御になっている」は別物で、修正の修正でもさらに見落としが出ることがある、というのがここでの教訓です。

まとめ:無人運用は「落ちても気づける設計」が本体

無人稼働で繰り返し踏む地雷は、AIの賢さ不足ではなく監視と復旧の設計漏れです。今日から確認したいのは、次の4点。稼働の途中でも依存サービスの死活を見張れているか。連続失敗で自動的に止まる仕組みの有無。監視や掃除の仕組み自体が、実行主体を巻き込みうる範囲を見ているか。「ロック」と名のつくファイルが、実際にOSレベルで排他になっているか。まずは心拍ログと通知を1本、そこから始めてみてください。

よくある質問

Q. 起動時のヘルスチェックだけでは不十分ですか?

起動時のチェックは「始められるか」の確認にすぎません。処理の途中で依存サービスが落ちても検知できず、失敗したまま全件を走り切ってしまうことがあります。実行ループの中で一定間隔ごとに死活を確かめ、連続で失敗したら止める監視を別に用意してください。

Q. リトライは何回まで設定すべきですか?

一律の正解はありません。ただし無制限だけは避けてください。一時的な瞬断を吸収する目的なら数回で足り、間隔を回ごとに広げる指数バックオフと総回数の上限をセットで決めておきます。恒久的な障害なら、早めに見切って止めるのが被害を抑える近道です。

Q. 無人稼働のパイプラインに最低限入れるべき監視は?

最低限は3つです。稼働中に依存サービスの死活を確かめる監視。成功と失敗の件数を集計し、失敗率が高いときは終了コードへ反映する仕組み。そして、一定時間ハートビートが途切れたら通知する経路。ログが正常終了でも成果物は空、という静かな失敗を、この3つで拾えます。

Q. 監視や掃除の仕組み自体は、テストしなくてよいのですか?

本体のロジックと同じか、それ以上に検証が必要です。監視・掃除・ロックといった保護機構は「動いていて当然」という扱いになりがちですが、実行主体自身を巻き込む条件になっていないか、実際に排他制御として機能しているかは、作った時点では見落としやすいところです。

タイトルとURLをコピーしました