2026年7月24日

2026年7月24日

cronジョブの実行監視と失敗通知設定(失敗・タイムアウト・未実行を見分けて知らせる)

cron ジョブの監視では、「失敗した」「長引いて終わらない・重なった」「そもそも実行されなかった」の3つを分けて考えるのがポイントです。前の2つはジョブを包むラッパースクリプトで終了コードと実行時間を見れば通知できますが、3つ目はジョブ側からは検知できないため、一定時間内に「生きている」合図が来なければ知らせる外部の監視(ハートビート監視)を組み合わせます。この記事では、WordPress のサーバーでよくある WP-CLI での WP-Cron 実行やデータベースのバックアップを例に、ログの確認方法、失敗通知のラッパー、ハートビート監視、systemd タイマーで置き換える方法までを説明します。

cronジョブが「失敗」する3つの形

cron 自体は、指定した時刻にコマンドを起動するところまでしか面倒を見ません。コマンドがエラーで終わっても、何時間も終わらなくても、cron はそれを知らせてくれません。監視したいのは次の3つです。

  1. 失敗: コマンドが 0 以外の終了コードで終わった。データベースへの接続エラー、ディスクの空き不足、権限の不足などが典型です。
  2. 長時間化・重複: 処理が終わらないうちに次の回が起動し、同じ処理が重なって動く。バックアップが重なるとディスクや CPU を奪い合い、サイトが遅くなります。
  3. 未実行: サーバーが止まっていた、cron デーモンが動いていない、crontab の書き方を誤って読み込まれていない、などでジョブが一度も起動していない。

1と2はジョブを包む仕組みで検知できます。3は「起動していない」ので、ジョブの側では何も起きません。外から「合図が来ない」ことを見張る仕組みが必要です。

まずcronが動いているかをログで確認する

通知の仕組みを作る前に、ジョブが起動しているかを確認する方法を押さえておきます。

# Debian / Ubuntu
journalctl -u cron --since "today"
grep CRON /var/log/syslog    # rsyslog を使っている場合

# RHEL / AlmaLinux / Rocky Linux など
journalctl -u crond --since "today"

ここに出るのは「どのユーザーで何のコマンドを起動したか」までで、成功したか失敗したかは記録されないのが普通です。起動の記録自体がない場合は、crontab の書式、cron デーモンの状態(systemctl status cron または crond)、ファイルの置き場所と名前を確認します。

cron には MAILTO を設定すると、コマンドが何か出力したときにその内容をメールで送る機能があります。ただし、サーバーにメールを送る仕組み(MTA)がなければ届きませんし、判定の基準は「出力があったか」で「失敗したか」ではありません。成功時にも出力するコマンドでは毎回メールが来てしまい、出力せずに失敗するコマンドでは何も届きません。終了コードで判定する仕組みを別に用意するほうが、失敗を取りこぼしにくくなります。

失敗とタイムアウトを通知するラッパースクリプト

各ジョブを直接 crontab に書く代わりに、共通のラッパーを通して起動します。ラッパーは次のことを行います。

  • 出力をジョブごとのログファイルに残す
  • flock で同じジョブが重なって動かないようにする
  • timeout で一定時間を超えたら打ち切る
  • 0 以外の終了コードなら、理由とログの末尾を添えて通知する
  • ハートビート監視の URL が設定されていれば、結果を送る

通知先の Webhook URL は秘密情報なので、root だけが読めるファイルに分けます(値はダミーです)。

sudo sh -c 'echo "SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your/webhook/url" > /etc/cron-notify.env'
sudo chmod 600 /etc/cron-notify.env
sudo mkdir -p /etc/cron-ping /var/log/cron-jobs
sudo apt install jq
#!/bin/bash
# /usr/local/bin/cron-wrap.sh
# 使い方: cron-wrap.sh ジョブ名 コマンド [引数...]
set -u

name="$1"; shift
TIMEOUT_SEC="${TIMEOUT_SEC:-3600}"
LOG="/var/log/cron-jobs/$name.log"
LOCK="/run/lock/cron-$name.lock"
PING_FILE="/etc/cron-ping/$name.url"   # ハートビート監視のURL(任意)
ENV_FILE=/etc/cron-notify.env

[ -r "$ENV_FILE" ] && . "$ENV_FILE"

start=$(date +%s)
echo "=== $(date '+%F %T') start: $*" >> "$LOG"
flock -n -E 75 "$LOCK" timeout "$TIMEOUT_SEC" "$@" >> "$LOG" 2>&1
status=$?
echo "=== $(date '+%F %T') end: status=$status ($(( $(date +%s) - start ))s)" >> "$LOG"

# ハートビート監視へ結果を送る(URLの形式はサービスの説明に合わせる)
if [ -r "$PING_FILE" ]; then
  curl -fsS -m 10 --retry 3 "$(cat "$PING_FILE")/$status" >/dev/null || true
fi

if [ "$status" -ne 0 ] && [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
  case "$status" in
    124) reason="タイムアウト(${TIMEOUT_SEC}秒)で打ち切り" ;;
    75)  reason="前回の実行が終わっていないためスキップ" ;;
    *)   reason="終了コード $status" ;;
  esac
  msg=$(printf '[%s] cronジョブ %s が失敗: %s\n%s' "$(hostname)" "$name" "$reason" "$(tail -n 20 "$LOG")")
  jq -n --arg text "$msg" '{text: $text}' \
    | curl -fsS -m 10 -X POST -H 'Content-Type: application/json' --data @- "$SLACK_WEBHOOK_URL" >/dev/null
fi

exit "$status"
sudo chmod 700 /usr/local/bin/cron-wrap.sh

終了コードの意味は次のとおりです。timeout は時間切れで打ち切ったときに 124 を返します。flock -n はロックが取れないとすぐに終了し、-E 75 でそのときの終了コードを 75 にしています。ジョブ自身が 75 を返す場合は区別できなくなるので、その場合は別の値に変えてください。

通知にはログの末尾 20 行を添えています。ジョブの出力にパスワードやトークンが含まれないよう、ジョブ側で秘密情報を表示しない作りにしておくことが前提です。ログファイルも root 以外が読めない権限にしておきます。

crontabに登録する

# /etc/cron.d/wp-jobs
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

*/5 * * * * root /usr/local/bin/cron-wrap.sh wp-cron sudo -u www-data wp cron event run --due-now --path=/var/www/html
30 3 * * *  root TIMEOUT_SEC=7200 /usr/local/bin/cron-wrap.sh db-backup /usr/local/bin/db-backup.sh

WP-CLI は root での実行を既定で拒否するため、sudo -u www-data で Web サーバーのユーザーとして実行しています(ユーザー名は環境に合わせてください)。サーバーの cron で WP-Cron を動かす場合は、wp-config.php に define( 'DISABLE_WP_CRON', true ); を書いて、アクセスのたびに WP-Cron が起動する動きを止めておくのが一般的です。

/etc/cron.d/ のファイルは、Debian 系ではファイル名にドット(.)を含むと読み込まれないことがあります。wp-jobs.cron のような名前は避け、wp-jobs のようにします。cron の中ではログイン時と環境変数が違うため、PATH を明示しておくと「手で実行すると動くのに cron では動かない」というつまずきを防げます。

通知が届くかを試す

登録する前に、わざと失敗するコマンドで通知を確認します。

sudo /usr/local/bin/cron-wrap.sh test-fail false
sudo TIMEOUT_SEC=2 /usr/local/bin/cron-wrap.sh test-timeout sleep 5
cat /var/log/cron-jobs/test-fail.log

1つ目は終了コード 1、2つ目はタイムアウト(124)の通知が届けば成功です。

「実行されなかった」をハートビート監視で検知する

ラッパーは、ジョブが起動しなければ動きません。サーバーごと止まった、cron デーモンが落ちた、crontab が消えた、といった場合は何も通知されません。これを補うのがハートビート監視(デッドマンスイッチとも呼ばれます)です。

仕組みは単純で、監視サービス側に「5分ごとに合図が来るはず」と登録しておき、決めた猶予時間を過ぎても合図が来なければ通知してもらいます。代表的なものは次のとおりです(機能や無料で使える範囲は変わることがあるため、2026年10月現在の公式サイトで確認してください)。

  • Healthchecks.io: cron の監視に特化したサービスで、ソースコードが公開されており自分のサーバーで動かすこともできます。チェックごとに発行される URL にアクセスすると合図になり、URL の末尾に終了コードを付けて送ると成功・失敗も区別して記録されます。
  • Uptime Kuma: 自分でホストする監視ツールで、「Push」型のモニターを作ると、指定した URL への合図が途絶えたときに通知できます。

ラッパーでは /etc/cron-ping/ジョブ名.url にチェックの URL を1行で書いておくと、実行のたびに URL/終了コード の形で送ります。この送り方は Healthchecks.io の形式に合わせた例なので、別のサービスを使う場合はそのサービスの説明に合わせて送り先の組み立て方を変えてください。

# 例: wp-cron ジョブのチェックURLを登録(値はダミー)
echo 'https://hc-ping.com/your-check-uuid' | sudo tee /etc/cron-ping/wp-cron.url
sudo chmod 600 /etc/cron-ping/wp-cron.url

監視側の期待間隔は、ジョブの実行間隔に少し余裕を持たせた猶予時間で設定します。5分ごとのジョブなら、猶予を数分とり、一度の遅れで通知が鳴らないようにします。監視サーバーを同じサーバー上で動かすと、サーバーごと止まったときに通知も止まるので、別のサーバーか外部のサービスに置くのが基本です。

systemdタイマーに置き換えるという選択肢

systemd を使う環境なら、cron の代わりに systemd タイマーでジョブを動かす方法もあります。実行結果がジャーナルに残り、失敗時に別のユニットを起動する OnFailure= で通知できます。

# /etc/systemd/system/db-backup.service
[Unit]
Description=Database backup
OnFailure=notify-failure@%n.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-backup.sh
TimeoutStartSec=2h

# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run database backup daily

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

notify-failure@.service は、引数として受け取ったユニット名を通知するスクリプトを動かすテンプレートユニットとして別に用意します。Persistent=true を付けると、サーバーが止まっていて実行できなかった回を、起動後に実行します。systemctl list-timers で次回と前回の実行時刻を、journalctl -u db-backup.service で結果を確認できます。

systemd タイマーでも「サーバーごと止まった」場合は通知できないため、ハートビート監視との併用は同じく必要です。

まとめ

  • cron ジョブの監視は「失敗」「長時間化・重複」「未実行」の3つに分けて考えます。
  • 失敗とタイムアウト・重複は、flock と timeout を組み込んだラッパーで終了コードを見て通知します。
  • 「実行されなかった」はジョブ側では検知できないため、Healthchecks.io や Uptime Kuma の Push 型のようなハートビート監視を、別のサーバーや外部サービスで組み合わせます。
  • cron では PATH を明示し、WP-CLI は Web サーバーのユーザーで実行し、WP-Cron をサーバーの cron で動かすなら DISABLE_WP_CRON を設定します。
  • 通知の仕組みは、わざと失敗させるコマンドで届くことを確かめてから運用に入ります。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲