45395 - シコウサクゴ -

macOS launchdで90ジョブを運用する:個人開発のジョブスケジューリング設計

2026-04-03
AI駆動開発
AI駆動開発
macOS
launchd
DevOps
ジョブスケジューリング
Last updated:2026-04-04
8 Minutes
1560 Words

自動化システムの定時実行にmacOSのlaunchdを使っています。本番77ジョブ + Pre環境13ジョブの計90ジョブが24時間稼働中です。

LinuxならsystemdやCronを使うところですが、開発マシンがmacOSなのでlaunchdを選択しました。本記事では、90ジョブの管理で発生した問題と、Claude Codeと組み合わせた運用ノウハウを記録します。


launchdの基本

plistファイル

launchdはXML形式のplistファイルでジョブを定義します。~/Library/LaunchAgents/にファイルを置き、launchctl loadで登録します。

1
<?xml version="1.0" encoding="UTF-8"?>
2
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
3
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
<plist version="1.0">
5
<dict>
6
<key>Label</key>
7
<string>com.dataprocessing.collect.swing.usd_jpy</string>
8
9
<key>ProgramArguments</key>
10
<array>
11
<string>/opt/anaconda3/envs/st312/bin/python</string>
12
<string>/Users/htada/data-processing/_dataProcessingEngineA/execution/main_forward_test.py</string>
13
<string>--pair</string>
14
<string>USD_JPY</string>
15
</array>
23 collapsed lines
16
17
<key>StartCalendarInterval</key>
18
<array>
19
<dict>
20
<key>Hour</key><integer>9</integer>
21
<key>Minute</key><integer>0</integer>
22
</dict>
23
</array>
24
25
<key>EnvironmentVariables</key>
26
<dict>
27
<key>PROJECT_ROOT</key>
28
<string>/Users/htada/data-processing</string>
29
<key>PYTHONPATH</key>
30
<string>/Users/htada/data-processing/_dataProcessingEngineA:/Users/htada/data-processing</string>
31
</dict>
32
33
<key>StandardOutPath</key>
34
<string>/tmp/collect_swing_usd_jpy/stdout.log</string>
35
<key>StandardErrorPath</key>
36
<string>/tmp/collect_swing_usd_jpy/stderr.log</string>
37
</dict>
38
</plist>

launchctl コマンド

Terminal window
1
# ジョブの登録
2
launchctl load ~/Library/LaunchAgents/com.dataprocessing.collect.swing.usd_jpy.plist
3
4
# ジョブの解除
5
launchctl unload ~/Library/LaunchAgents/com.dataprocessing.collect.swing.usd_jpy.plist
6
7
# 登録済みジョブの一覧
8
launchctl list | grep dataprocessing
9
10
# ジョブの即時実行(テスト用)
11
launchctl start com.dataprocessing.collect.swing.usd_jpy

90ジョブの分類

命名規則

1
com.dataprocessing.{エンジン}.{種別}.{対象}
2
3
例:
4
com.dataprocessing.collect.swing.usd_jpy # データ収集 Swing USD/JPY
5
com.dataprocessing.collect.day.eur_jpy # データ収集 Day EUR/JPY
6
com.dataprocessing.collect.monitor.resources # データ収集リソース監視
7
com.dataprocessing.domestic.daily.preparation # 国内データ 日次準備
8
com.dataprocessing.overseas.daily.processing # 海外データ 日次処理
9
com.dataprocessing.daily_health_check # 日次ヘルスチェック
10
com.dataprocessing.pre.collect.swing.usd_jpy # Pre環境 データ収集 Swing USD/JPY

スケジュール一覧(主要ジョブ)

ジョブスケジュール用途
データ収集 Swing(9ペア)毎時00分H4足ベースの中期処理
データ収集 Day(4ペア)毎時00分H1足ベースのデイ処理
データ収集 Monitor毎時30分リソース監視・トレーリングストップ
国内データ Preparation20:00 JST翌日トリガーファイル生成
国内データ Processing09:00 JST国内データ稼働時間処理
海外データ Preparation22:00 JST海外データシグナル生成
海外データ Processing23:30 JST海外データ処理
Health Check06:30 JST全ジョブのヘルスチェック
Pre環境(13ジョブ)本番+5分Dry-Run検証

発生した問題と対策

問題1: 環境変数の罠

launchdはシェルプロファイルを読み込みません。ターミナルでecho $API_KEYすれば値が表示されますが、launchd経由ではNoneになります。

1
解決策(3つの方法):
2
1. plistのEnvironmentVariablesに設定(推奨: 非シークレット値)
3
2. スクリプト内でload_dotenv()を呼ぶ(推奨: シークレット値)
4
3. launchctl setenv(非推奨: macOS再起動で消える)

問題2: PYTHONPATHの2パス設定

1
<!-- NG 1パスだけ設定 -->
2
<key>PYTHONPATH</key>
3
<string>/Users/htada/data-processing</string>
4
5
<!-- OK 2パス設定(エンジン + プロジェクトルート) -->
6
<key>PYTHONPATH</key>
7
<string>/Users/htada/data-processing/_dataProcessingEngineA:/Users/htada/data-processing</string>

from _dataProcessingEngineA.core.shared.X import Y(絶対インポート)とfrom myModules.X import Y(共有モジュール)の両方を解決するには、2つのパスが必要です。

問題3: __pycache__の汚染

本番環境でPython 3.12、開発環境でも3.12を使っていますが、git pull時に開発環境の__pycache__が混入すると、古いバイトコードが使われることがあります。

1
<!-- plistに追加 -->
2
<key>PYTHONDONTWRITEBYTECODE</key>
3
<string>1</string>

PYTHONDONTWRITEBYTECODE=1で、pycファイルの生成を抑制します。

問題4: ログの肥大化

90ジョブが毎時実行すると、ログファイルが急速に肥大化します。

Terminal window
1
# ログローテーション(週次で実行)
2
for log_dir in /tmp/collect_*/; do
3
find "$log_dir" -name "*.log" -mtime +7 -delete
4
done

問題5: macOS再起動後のジョブ消失

macOSを再起動すると、launchctl loadで登録したジョブは自動的に再登録されるはずですが、plistファイルが壊れていると登録されません。

Terminal window
1
# 再起動後の確認スクリプト
2
expected_jobs=(
3
"com.dataprocessing.collect.swing.usd_jpy"
4
"com.dataprocessing.collect.swing.gbp_jpy"
5
# ... 全90ジョブ
6
)
7
8
for job in "${expected_jobs[@]}"; do
9
if ! launchctl list | grep -q "$job"; then
10
echo "MISSING: $job"
11
# 自動復旧: plistを再ロード
12
plist_file=$(find ~/Library/LaunchAgents -name "*${job}*" 2>/dev/null)
13
if [ -n "$plist_file" ]; then
14
launchctl load "$plist_file"
15
fi
2 collapsed lines
16
fi
17
done

ヘルスチェックの設計

validate_launchd_health.sh

毎朝06:30に自動実行されるヘルスチェックスクリプトです。

1
#!/bin/bash
2
# 3つの検証を実行
3
4
# 1. plist構文検証
5
echo "[CHECK] plist構文検証..."
6
for plist in ~/Library/LaunchAgents/com.dataprocessing.*.plist; do
7
if ! plutil -lint "$plist" > /dev/null 2>&1; then
8
echo "INVALID: $(basename $plist)"
9
errors=$((errors + 1))
10
fi
11
done
12
13
# 2. ログ鮮度チェック
14
echo "[CHECK] ログ鮮度チェック..."
15
check_freshness() {
23 collapsed lines
16
local log=$1 threshold=$2 label=$3
17
if [ ! -f "$log" ]; then
18
echo "NOT_FOUND: $label"
19
return
20
fi
21
local age=$(( ($(date +%s) - $(stat -f "%m" "$log")) / 3600 ))
22
if [ $age -gt $threshold ]; then
23
echo "STALE: $label (${age}h > ${threshold}h)"
24
fi
25
}
26
27
# hourlyジョブは3時間、dailyジョブは28時間が閾値
28
check_freshness "/tmp/collect_swing_usd_jpy/stdout.log" 3 "データ収集 Swing USD_JPY"
29
check_freshness "/tmp/domestic_daily_processing/stdout.log" 28 "国内データ Daily Processing"
30
31
# 3. エラーパターン検出
32
echo "[CHECK] エラーパターン検出..."
33
for log_dir in /tmp/collect_* /tmp/domestic_* /tmp/overseas_*; do
34
if grep -l "ModuleNotFoundError\|ImportError\|OSError\|Traceback" \
35
"$log_dir/stderr.log" 2>/dev/null; then
36
echo "ERROR_DETECTED: $log_dir"
37
fi
38
done

Slack通知

異常があればSlackに通知します。

Terminal window
1
if [ $errors -gt 0 ]; then
2
curl -s -X POST "$SLACK_WEBHOOK_URL" \
3
-H "Content-Type: application/json" \
4
-d "{\"text\": \"launchdヘルスチェック: ${errors}件の異常検出\"}"
5
fi

AIによるplist生成

90ジョブ分のplistを手書きするのは現実的ではありません。Claude Codeでplist生成スクリプトを作成しました。

1
import plistlib
2
from pathlib import Path
3
4
def generate_collect_swing_plist(
5
pair: str,
6
project_root: str,
7
is_pre: bool = False,
8
) -> None:
9
label_prefix = "com.dataprocessing.pre" if is_pre else "com.dataprocessing"
10
minute = 5 if is_pre else 0 # Pre環境は+5分オフセット
11
mode = "--dry-run" if is_pre else "--execute"
12
root = f"{project_root}-pre" if is_pre else project_root
13
14
config = {
15
"Label": f"{label_prefix}.collect.swing.{pair.lower()}",
26 collapsed lines
16
"ProgramArguments": [
17
"/opt/anaconda3/envs/st312/bin/python",
18
f"{root}/_dataProcessingEngineA/execution/main_forward_test.py",
19
"--pair", pair,
20
mode,
21
],
22
"StartCalendarInterval": [{"Minute": minute}],
23
"EnvironmentVariables": {
24
"PROJECT_ROOT": root,
25
"PYTHONPATH": f"{root}/_dataProcessingEngineA:{root}",
26
"PYTHONDONTWRITEBYTECODE": "1",
27
},
28
"StandardOutPath": f"/tmp/collect_swing_{pair.lower()}/stdout.log",
29
"StandardErrorPath": f"/tmp/collect_swing_{pair.lower()}/stderr.log",
30
}
31
32
output_dir = Path.home() / "Library" / "LaunchAgents"
33
output_path = output_dir / f"{config['Label']}.plist"
34
35
with open(output_path, "wb") as f:
36
plistlib.dump(config, f)
37
38
# 必ず構文検証
39
import subprocess
40
result = subprocess.run(["plutil", "-lint", str(output_path)], capture_output=True)
41
assert result.returncode == 0, f"Invalid plist: {result.stderr.decode()}"

重要: plistlib.dump()を使うこと。json.dump()で生成するとplist形式にならない(30ファイル破損事故の教訓)。


学んだこと

1. launchdは「設定したら放置」ではない

ジョブが登録されていてもplistが壊れていれば再起動で消えます。定期的なヘルスチェックが必須です。

2. 命名規則が90ジョブの管理を可能にする

com.dataprocessing.{エンジン}.{種別}.{対象}の命名規則により、launchctl list | grep dataprocessingで全ジョブを一覧できます。命名規則なしの90ジョブは管理不能です。

3. Pre環境のジョブは本番+5分が最適

同じデータを使いつつ、データ生成→読み込みの依存関係を保つため、5分のオフセットが実用的でした。

4. plist生成はスクリプト化必須

手書きの90ファイルは保守不能です。スクリプトで生成し、plutil -lintで自動検証します。


まとめ

macOS launchdで90ジョブを運用する際に重要なのは以下の3点です。

  1. 環境変数の明示設定: EnvironmentVariablesにPROJECT_ROOT・PYTHONPATH・PYTHONDONTWRITEBYTECODE。シェルプロファイルは読まれない
  2. ヘルスチェックの自動化: plist構文・ログ鮮度・エラーパターンの3層チェックを毎朝実行
  3. plist生成のスクリプト化: plistlib.dump()で生成 + plutil -lint で検証。手書き・json.dump()は禁止

launchdは「Linuxで言うsystemd + cron」ですが、macOS固有の癖(キャッシュ問題、環境変数の非継承)を理解しないと運用できません。

Article title:macOS launchdで90ジョブを運用する:個人開発のジョブスケジューリング設計
Article author:45395
Release time:2026-04-03

記事へのご質問・ご感想をお聞かせください

フィードバックを送る