cronジョブが失敗しても気づかない問題を解消する通知スクリプト設定手順

cronジョブが失敗しても気づかない問題を解消する通知スクリプト設定手順

※本記事にはプロモーション(広告・アフィリエイトリンク)を含みます。

cronでスクリプトを自動実行するとき、失敗しても通知が来ないのがデフォルトの動作だ。毎朝4:30のClamAVスキャンをcronに設定したとき、「これが失敗しても翌日まで気づかないな」と気になった。この記事では、cronジョブの失敗を検知してWebhookやメールで通知するラッパースクリプトの実装手順と、systemdで同じ目的を達成する方法を書く。

cronが失敗しても何も起きない理由

cronはジョブの終了コードを基本的に無視する。stdout/stderrに出力があったとき、システムのMTA(sendmailやpostfix等)が設定されていればMAILTOに指定したアドレスにメールを送るが、自宅サーバーなどではMTAが設定されていないことも多く、結果としてエラー出力は捨てられる。

現在の設定を確認するには:

# 現在のcrontabを確認
crontab -l

出力の先頭行にMAILTO=がなければ、通知は一切来ない。

cronの実行環境にはもう一つ落とし穴がある。ユーザーの.bashrcや.profileを読まないため、PATHは/usr/bin:/bin程度の最小限になる。手元では動くスクリプトがcronから動かない場合、原因のひとつはここにあることが多い。

MAILTO でメール通知を受け取る(最小構成)

sendmail または postfix が動いているサーバーなら、crontabの先頭にMAILTOを書くだけで通知を受け取れる。

MAILTO="you@example.com"
30 4 * * * /usr/local/bin/clam-scan.sh

ジョブが何か出力した場合(エラーメッセージを含む)、そのテキストがメールとして届く。ただし外部メールアドレスへ届けるにはMTAのSMTPリレー設定が必要で、自宅サーバーではその設定自体が手間になる。Webhookに送る方法の方が設定が少なくて済む。

ラッパースクリプトで Webhook 通知を実装する

失敗時だけ通知したい、Slack や Discord の Webhook に送りたい、という要件はラッパースクリプトで対応する。基本の構造は「コマンド実行 → 終了コードが0以外なら通知」だ。

#!/bin/bash
# /usr/local/bin/cron-notify-wrapper.sh
# 使い方: cron-notify-wrapper.sh <WEBHOOK_URL> <コマンド...>

WEBHOOK_URL=$1
shift

LOGFILE=$(mktemp /tmp/cron-XXXXXX.log)

# コマンド実行(終了コードを捕捉するため set -e は使わない)
"$@" >"$LOGFILE" 2>&1
EXIT_CODE=$?

if [ "$EXIT_CODE" -ne 0 ]; then
    HOST=$(hostname)
    LOG_TAIL=$(tail -20 "$LOGFILE")
    MSG=$(printf 'cronジョブ失敗\nホスト: %s\nコマンド: %s\n終了コード: %d\n---\n%s' \
        "$HOST" "$*" "$EXIT_CODE" "$LOG_TAIL")
    jq -n --arg text "$MSG" '{text: $text}' \
    | curl -s -X POST "$WEBHOOK_URL" \
        -H 'Content-Type: application/json' \
        -d @-
fi

rm -f "$LOGFILE"
exit "$EXIT_CODE"

保存して実行権限を付ける:

chmod +x /usr/local/bin/cron-notify-wrapper.sh

crontab に組み込む:

WEBHOOK_URL="https://hooks.slack.com/services/T.../B.../xxxx"
30 4 * * * /usr/local/bin/cron-notify-wrapper.sh "$WEBHOOK_URL" /usr/bin/clamscan -r /home/user --log=/var/log/clamav/daily.log

設計上のポイント

set -e を使わない: ラッパースクリプト自身にset -eを書くと、実行コマンドが失敗した時点でスクリプトが即終了し、通知ロジックが動かない。このため意図的に省く。

jq でJSONを生成する: ログに含まれる改行や引用符をシェルの文字列置換で逃がそうとすると壊れやすい。jqに任せると特殊文字を安全にエスケープできる。jqが入っていない場合はsudo apt install jqで追加する(環境によってパッケージマネージャーは異なります)。

コマンドはフルパスで書く: cronのPATHは最小限なので、スクリプト内のコマンドは/usr/bin/curl、/usr/bin/jqのようにフルパスで書く。または crontab の先頭でPATH=を上書きする方法もある。

systemd タイマー + OnFailure を使う方法

新規でジョブを設定するならsystemdタイマーがcronより失敗ハンドリングを組みやすい。OnFailure=ディレクティブで「失敗したら別のユニットを起動する」仕組みが公式にサポートされている。

systemdサービスを自動起動させる設定手順の基本を踏まえた上で、失敗通知の設定だけを示す。

スキャン本体 /etc/systemd/system/clam-scan.service:

[Unit]
Description=ClamAV Daily Scan
OnFailure=notify-failure@%n.service

[Service]
Type=oneshot
ExecStart=/usr/bin/clamscan -r /home/user --log=/var/log/clamav/daily.log

タイマー /etc/systemd/system/clam-scan.timer:

[Unit]
Description=ClamAV Daily Scan Timer

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

[Install]
WantedBy=timers.target

Persistent=trueは、タイマーが起動すべき時間にシステムが停止していた場合、次回起動時に実行する設定だ(systemdの公式ドキュメントに記載された動作)。

通知用テンプレートサービス /etc/systemd/system/notify-failure@.service:

[Unit]
Description=Notify failure for %i

[Service]
Type=oneshot
ExecStart=/usr/local/bin/send-failure-notify.sh "%i"

@付きのテンプレートユニットにすることで、複数のサービスが失敗しても同じ通知ユニットを使い回せる。%iには失敗したサービス名が入る。send-failure-notify.shの中身は先述のWebhook送信部分と同じ構造でよい。

systemctl daemon-reload
systemctl enable --now clam-scan.timer
# 動作確認
systemctl list-timers --all | grep clam

よくあるつまずきポイント

症状 原因 対処
Webhookが飛ばない cronのPATHにcurlがない /usr/bin/curlでフルパス指定
Permission denied 実行権限なし chmod +xで付与
失敗しても通知が来ない スクリプトがexit 0で終わっている スクリプトの終了コードを確認
systemdタイマーが動かない enableを忘れた systemctl enable clam-scan.timer
通知本文が空 2>&1のリダイレクト漏れ >"$LOGFILE" 2>&1を確認

テストにはfalseコマンドが便利だ。必ず終了コード1を返す:

# cronの実行環境に近い最小PATHでテスト
env -i PATH=/usr/bin:/bin HOME=/root \
    /usr/local/bin/cron-notify-wrapper.sh "https://hooks.slack.com/..." /usr/bin/false

env -iで環境変数を最小化することで、cronの実行条件に近づけてデバッグできる。スクリプトが手元では動くのにcronから動かない場合、多くの場合ここで原因を絞り込める。

まとめ

cronジョブの失敗通知は、ラッパースクリプトで終了コードを捕捉してWebhookへ送る方式が最も環境を選ばず、既存のジョブへの後付けも簡単だ。systemdタイマーを使える環境ならOnFailure=で通知ユニットを繋ぐ方が長期的に管理しやすい。

次の行動として、まずcrontab -lで現在のcronジョブを一覧にして、失敗しても誰にも知らされていないジョブが何本あるか確認してほしい。優先度の高いもの(バックアップ、セキュリティスキャン等)から順に通知を追加していくのが現実的だ。

よくある質問

cronのジョブが失敗しても通知が来ないのはなぜですか?

cronはデフォルトでジョブの終了コードを無視し、MTAが設定されていない環境ではエラー出力も捨てられます。crontabの先頭にMAILTO=がなければ通知は来ません。ラッパースクリプトかsystemdのOnFailure=を使うことで失敗を検知できます。

cronスクリプトが手元では動くのにcronから動かないのはなぜですか?

cronの実行環境はユーザーの.bashrcや.profileを読み込まないため、PATHが/usr/bin:/bin程度の最小限になります。cronから呼ぶコマンドはフルパスで指定するか、crontabの先頭でPATH=を設定してください。env -iで再現テストできます。

systemdタイマーとcronはどちらがよいですか?

新規設定ならsystemdタイマーがOnFailure=やPersistent=trueなど失敗ハンドリングが充実していておすすめです。既存のcronジョブにはラッパースクリプトを後付けするのが手間なく対応できます。