動画で読む
まず結論 — launchd の罠は「動かない時にしか気づけない」
結論から書きます。macOS の launchd(ランチディー、起動・常駐プロセスを管理する仕組み)で自動化を 24/7 回すと、一番厄介なのは個々の設定ミスではなく「失敗が静かに起きる」ことです。ジョブは黙って起動に失敗し、黙ってログを吐かず、黙って溜まっていく。
このサイト自体が題材です。AetherEchoes は AI が記事の下書きを書き、私が公開前に確認・編集してから出す運用で、その起稿と動画生成を launchd が曜日・時刻でキックしています。仕組みとしては地味ですが、地味な分だけ壊れても気づきにくい。きっかけは Zenn の launchd 運用罠の記事(bokuwalily)で、読みながら「これ全部踏んだやつだ」と頷いたので、自分の実体験を足して個人運用 (solo-dev) の視点で再点検します。半年回してみて分かったのは、設定そのものより検知の設計でつまずく、ということでした。
cron はもう動かない、PATH と timeout でまた転ぶ
最初の関門は、cron がもう使えないことと、launchd に移しても PATH と timeout で続けて転ぶことです。現代の macOS では cron が事実上動かず、crontab に書いても実行されないことがある。私も最初これに気づかず、半日「なぜ走らない」と悩みました。
launchd に移すと次の罠が待っています。GUI セッションから起動されるジョブは最小限の PATH(/usr/bin:/bin など)しか継承しないため、Homebrew で入れた node や jq が command not found で落ちる。plist(ジョブ定義の XML ファイル)の EnvironmentVariables に /opt/homebrew/bin を明示的に足すまで直りませんでした。さらに macOS には GNU の timeout が無く、暴走したジョブを時間で打ち切る定番手段がそのままでは使えない。私は Homebrew の coreutils で入る gtimeout に書き換えて凌ぎました。何を自動化するかの前に、動く土台を整えるところで時間が溶けるわけです。自動化の対象は勘ではなく測定で決める、という話は作業ログを棚卸しする記事に書きましたが、その手前に「土台が動くか」という関門がもう一つあった、という感じでした。
依存サービスの起動順 — Docker と VOICEVOX で踏んだ
地味に効くのが、自動化ジョブが依存する外部サービスの起動順です。launchd のジョブは PC を起動した直後に走ることがあり、その時点で Docker や音声合成サーバがまだ立ち上がっていないと、ジョブだけ先に走って空振りします。
私のパイプラインは動画のナレーションに VOICEVOX(ローカルの音声合成エンジン)を使っていて、これが Docker コンテナで動いています。ある朝ログを見たら、動画生成ジョブが「ポート 50021 に繋がらない」で連続失敗していた。原因は単純で、Docker Desktop の起動が間に合っていなかっただけです。対処は二段で、Docker Desktop を macOS のログイン項目に登録して自動起動させ、ジョブ側にも「サービスが応答するまで数回リトライして待つ」処理を足しました。依存先が立ち上がっている前提でジョブを書くと、こういう順序のズレで静かに死にます。順番を保証するのは launchd の役目ではなく、ジョブ自身が待つしかない、というのが学びでした。
stuck プロセスは kill されず累積する — 一番怖い罠
個人運用で一番怖いのは、失敗して止まったプロセスが kill されず溜まっていくことです。launchd はジョブを起動はしてくれますが、ジョブが内部でハングしたまま終わらない場合、それを賢く検知して殺してはくれません。
実際に一度やられました。記事の自動選定ジョブが外部ツールの待ち受けで固まり、そのまま 4 日間 stuck していたのに、私は気づかなかった。毎日同じ時刻に新しいジョブが起動するので、ハングしたプロセスが日に日に積み上がっていく。気づいたのは「最近記事が増えてないな」と手で確認した時でした。情けない話です。対処として、gtimeout でジョブ全体に上限時間を被せ、超えたら強制終了するようにしました。失敗を仕組みで吸収する考え方は Git の「しまった」を仕組みで吸収する記事で書いたのと同じで、人間の注意力に頼る運用は必ずどこかで破れます。launchd は何もせず動いてくれる。動かない時にだけ、その存在を思い出す。
個人運用の落としどころ — 監視は「失敗の検知」だけ最低限
最後に、個人運用での折り合いの付け方です。会社のシステムなら監視基盤を組みますが、ソロでそこまでやると本末転倒なので、私は「成功は黙っていい、失敗だけは必ず気づく」という最低限の線で止めています。
具体的には、ジョブの最後に結果を 1 行チャットに飛ばすだけ。成功なら短く、失敗なら理由付きで通知が来る。これだけで「4 日 stuck」のような事故はまず防げます。やりすぎないことも大事で、撤退基準を先に決める発想(始める前に「やめ方」を決める記事)と同じく、監視も「どこまでやらないか」を先に決めておくと沼にはまりません。launchd の罠はどれも、踏んだ瞬間ではなく、しばらく経って静かに表面化します。だからこそ個人運用では、凝った監視より「失敗だけは見逃さない」一本の通知を握っておくのが、現実的な落としどころでした。
よくある質問
- macOS で cron が動かないのはなぜですか?
- 現代の macOS では cron が事実上非推奨・非稼働で、crontab に書いても実行されないことがあります。定期実行は launchd へ移行するのが現在の標準的な方法です。
- launchd ジョブで node や jq が command not found になるのはなぜですか?
- GUI セッションから起動されるジョブは最小限の PATH しか継承しないため、Homebrew で入れたバイナリが見えません。plist の EnvironmentVariables に /opt/homebrew/bin を明示的に追加すると解決します。
- ハングして止まらないジョブが溜まるのを防ぐには?
- macOS には GNU の timeout が無いので、Homebrew の coreutils で入る gtimeout をジョブ全体に被せ、上限時間を超えたら強制終了させます。これで stuck プロセスの累積を防げます。
- 個人運用ではどこまで監視を作り込むべきですか?
- 監視基盤を組むより、成功は黙ってよい・失敗だけ必ず気づく、という最低限の線が現実的です。ジョブ末尾で結果を 1 行チャット通知するだけでも、長期間の stuck 事故はほぼ防げます。