SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

cronジョブが実行されない5つの原因と確認方法

cron ジョブが動かない原因を5つに整理します。PATH不足、エスケープしないパーセント記号、誤ったcrontab、メールに消えた出力、シェル依存のスクリプトを確認できます。

cron ジョブが実行されない理由

「まったく実行されない」cron ジョブも、ほとんどの場合は実際には実行されています。シェルとは異なる環境で実行され、最初の 1 秒以内に失敗し、メッセージが確認していない場所に送られています。ほぼすべての報告は、検索パス、パーセント記号、誤った crontab ファイル、メールに送られた出力、ログインセッションを前提とするスクリプトという 5 つの原因で説明できます。

cron は、crontab ファイルを読み取り、スケジュールに従ってコマンドを起動するデーモン(バックグラウンドサービス)です。cron は .bashrc を読み取らず、端末を開かず、ログインシェルを起動せず、コマンドの失敗を通知しません。以下の各原因は、この 4 つの事実から生じます。

以下を順番に確認し、まずすべての確認の前提となる問いから始めてください。cron は実際に起動したでしょうか。「cron がジョブを起動しなかった」と「ジョブが起動した後に終了した」は、共通点のない別の問題です。最初にこの点を確認してください。

cron は実行されたか

ディストリビューションの系統によって、デーモンの unit 名が異なります。両方を確認してから、ログを読みます。

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

Debian と Ubuntu では unit 名を cron とします。Fedora、Rocky、Alma では crond とします。1 台のマシンに存在する名前はどちらか一方だけです。そのため、2 つのコマンドのどちらかが unknown unit を返しても、正常であり障害ではありません。

自分のシステムが書き込んだエントリを読みます。ガイドからコピーした行を探してはいけません。cron の実装やロギング構成によって文言が異なるためです。確認するのは 2 点だけです。スケジュールで指定した分にエントリがあるか、そのエントリに自分のコマンドが記載されているかを確認します。自分のコマンドを記載したエントリがあれば、cron の処理は完了しており、問題はコマンド内部にあります。エントリがまったくなければ、cron にスケジュールが登録されていません。これは後述する原因 3 です。

一部のイメージでは、cron のメッセージを rsyslog 経由で journal ではなくファイルに送ります。/var/log で、cron または syslog にちなんだ名前のファイルを探し、その末尾を読みます。

ls -l /var/log
sudo tail -n 50 /var/log/syslog

unit もログも存在しない場合、cron 自体がインストールされていない可能性があります。最小構成のクラウドイメージやコンテナでは、cron が含まれていないことがよくあります。

dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron

原因 1: cron には PATH がありません

対話型シェルは、/etc/profile~/.profile~/.bashrcおよびそれらのファイルから読み込まれるすべての内容を基に PATH を構成します。cron ジョブでは、これらは実行されません。cron は独自の簡素な環境でコマンドを起動するため、標準のシステムディレクトリ以外にあるプログラムは見つかりません。/usr/local/bin/opt、言語のバージョンマネージャー、Python 仮想環境、Go ワークスペースなどにあるものが該当します。ジョブは最初の行で失敗し、シェルは「not found」形式のエラーを書き込みます。正確な文言は、実行したシェルによって異なります。

ジョブで使用するすべてのコマンドの実際のパスを確認します。

command -v docker
command -v node
readlink -f "$(command -v node)"

その後、ジョブに絶対パスを記述するか、crontab の先頭で PATH を 1 回設定します。

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run

echo "$PATH" を使用して自分のマシンからその一覧を取得し、対話型セッション内にしか存在しないものを削除します。ここで重要な点が 1 つあります。cron は、これらの代入行で変数を展開しません。PATH=$PATH:/usr/local/bin には文字列 $PATH:/usr/local/bin がそのまま保存されるため、ジョブの検索パスには使用可能なディレクトリが 1 つも含まれなくなります。一覧全体を明示的に記述してください。

バージョンマネージャーには、パス以外の設定も必要です。nvm、pyenv、rbenv、asdf は、.bashrc からシェル関数または shims ディレクトリを設定しますが、cron ジョブはそのファイルを読み込みません。バージョン指定済みのバイナリを絶対パスで呼び出すか、自分のスクリプトの最初の行でバージョンマネージャーの init スクリプトを読み込んでください。

原因 2: パーセント記号がコマンドを終了させる

crontab の command フィールドでは、% は通常の文字ではありません。エスケープされていない最初の % でコマンドが終了します。それ以降のすべての文字はコマンドの標準入力として渡され、さらに出現する % はそれぞれ改行になります。これはプログラムに短い入力を渡すための cron の正式な機能です。同時に、日付を付けたファイル名が crontab で典型的な破損原因になる理由でもあります。

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site と記述しても、tar はフォーマット済みの日付を受け取りません。cron は最初の % で行を切るため、shell には未完了のコマンド置換が渡され、行の残りは標準入力として渡されます。すべてのパーセント記号をバックスラッシュでエスケープしてください。

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site

1 行の内容は、順番に 2 つの層で解釈されます。\% は cron のルールで、cron が何かを起動する前に適用します。$(date +\%F)コマンド置換で、cron が起動した shell によって後から適用されます。どの文字をどの層が処理するかを把握することが、この問題の要点です。

より安全な習慣は、crontab からロジックを完全に切り離すことです。ロジックをスクリプトに記述すれば、パーセント記号に特別な意味はありません。

#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site

crontab の行には、パスとリダイレクトだけを記述します。一目で読める crontab は、デバッグもしやすい crontab です。

原因 3: どの crontab を編集しましたか

crontab は 1 つではありません。所有者とフィールド数が異なる複数のファイルがあり、誤ったファイルにジョブを記述すると、そのジョブは認識されません。

  • crontab -e はコマンドを実行したユーザーの crontab を編集します。sudo crontab -e は root の crontab を編集します。同じサーバーを 2 人で調査すると、別々のファイルを読んでしまうことがよくあります。
  • sudo crontab -l -u deploy は別のユーザーの crontab を一覧表示します。ジョブを実行するアカウントに、実際に何が登録されているかを確認できます。
  • /etc/crontab/etc/cron.d 内のすべてのファイルには、スケジュールとコマンドの間に追加のフィールドがあります。ここには実行ユーザーを指定します。5 フィールドのユーザー用 crontab の行を /etc/cron.d に貼り付けると、コマンドの最初の単語がユーザー名として解釈されます。
  • /etc/cron.d 内のファイル名には、英字、数字、アンダースコア、ハイフンだけを使用できます。backup.shsite.conf という名前のファイルは、名前だけを理由にスキップされます。backup に変更して、もう一度ログを確認してください。
  • /etc/cron.d 内のファイルは root が所有している必要があります。また、group や others が書き込めない状態でなければなりません。ls -l /etc/cron.d を使うと、両方を同時に確認できます。
  • /etc/cron.daily とその同種のディレクトリに置いたスクリプトにも、同じ命名規則が適用されます。また、実行ビットも必要です。実行ビットがないスクリプトは、何も表示されずにスキップされます。
  • /etc/cron.allow/etc/cron.deny により、crontab のインストール自体を許可するユーザーが決まります。サーバーにどちらかのファイルがある場合は、ユーザーに crontab が許可されていると判断する前に内容を確認してください。

ユーザー用 crontab は、spool ファイルを手動で編集せず、crontab コマンドでインストールしてください。crontab はインストール前にファイルを解析するためです。保存したら、コマンドが返す内容を確認してください。ファイルが拒否されると、以前のバージョンが有効なままになり、変更は反映されません。これは、cron が指示を無視している場合とまったく同じように見えます。

所有者によって権限も決まります。root の crontab に登録したジョブが作成するファイルは root 所有になります。そのファイルを読み取るアプリケーションが書き込めない場合があります。通常のユーザーの crontab に登録したジョブは、root 専用のディレクトリを読み取れません。作業内容に合わせて所有者を選んでください。アプリケーションの保守は、そのアプリケーション自身のアカウントで行うべきです。これは、WordPress の wp-cron を system cron ジョブに置き換える理由でもあります。ジョブが作成するファイルの mode は、継承した umask によって決まります。この値はシェルの umask とは異なるため、ジョブの出力を読み取れない場合は、umask によるファイル権限の設定方法も確認してください。

原因 4: 出力が誰も読まないメールに送られていた

cron は、ジョブが標準出力と標準エラーに書き込んだ内容をすべて収集します。ジョブが何か1つでも出力すると、cron はそのテキストを、crontab の所有者、または MAILTO が指定する宛先に設定されたローカルメールシステムへ渡します。最小構成の VPS には通常、MTA (mail transfer agent) がインストールされていないため、メールは配信されません。エラーは一瞬だけ存在し、その後どこにも届きません。壊れたジョブが何も出力せずに終了したように見える理由はこれだけです。

出力は、自分で管理できるファイルに送ってください。

0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

>> は標準出力をファイルに追記します。2>&1 は、現時点で標準出力が指している先に標準エラーを送ります。そのため、標準出力のリダイレクトの後に置く必要があります。2>&1 >> file のように逆の順序で記述すると、標準エラーは元の宛先を保持します。調査対象のエラーは、まさにファイルへ届かなくなる部分です。

ジャーナルも適した保存先です。logger は、指定したタグを付けて syslog に書き込みます。

0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site

journalctl -t backup-site で内容を読み返せます。これにより、ジョブ自身の出力を cron エントリの近くに記録できるため、時系列を追いやすくなります。どのユーザーがどのコマンドをサーバー上で実行したかの記録も必要な場合、それは別の仕組みです。サーバー上のユーザーコマンドを監査するで説明しています。

crontab の先頭に MAILTO="" を置くと、それより下にあるジョブのメール送信が無効になります。MAILTO に実在するアドレスを設定しても、動作する MTA がある場合にしか役立ちません。そのため、依存する前にメールがサーバーから送信されることを確認してください。

デバッグ中は、> /dev/null 2>&1 を決して追加しないでください。これはすべての crontab で最もよく使われる行ですが、手元にある唯一の証拠を破棄します。ジョブが動作することを確認した後、必要であれば戻してください。

原因 5: スクリプトが、cron から渡されない環境変数を前提にしている

コマンドが見つかり、その出力を取得できた後に残るのは、セッションが暗黙に提供しているその他すべての要素です。

  • シェルが bash とは限りません。ls -l /bin/sh で確認してください。Debian と Ubuntu では dash を指すため、二重角括弧のテスト、配列、source は構文エラーになります。スクリプトに #!/bin/bash 行を追加して呼び出すか、crontab の先頭で SHELL を設定してください。
  • 作業ディレクトリは、手動実行時にいた場所とは異なります。すべての場所で絶対パスを使用するか、スクリプトの 1 行目で cd を使ってディレクトリへ移動してください。相対パスは、ジョブが「手動で実行すると動く」最大の原因です。
  • ロケールはセッションのものとは異なります。日付や数値の書式設定、テキストの並べ替えを行う処理は、別の LANG の下で異なる出力を生成する可能性があります。後続の処理がその出力を解析する場合は、期待するのではなくスクリプト内でロケールを設定してください。
  • TTY(端末)がありません。確認を求めるコマンド、エディターを開くコマンド、進捗バーを表示するコマンドは、停止したり終了したりする可能性があります。ツールが提供する非対話モードのフラグを指定してください。
  • SSH agent がありません。SSH_AUTH_SOCK は cron の環境に含まれないため、agent が読み込まれていたことで動作していた ssh または rsync コマンドは、認証に失敗します。ジョブ専用の鍵を用意し、その鍵をジョブの実行ユーザーが所有するようにしてください。
  • ユーザーセッションの bus がないため、cron ジョブからの systemctl --userXDG_RUNTIME_DIR が設定されるまで失敗します。system unit を使用する方が適切です。

Fedora、Rocky、Alma では、もう 1 つ確認すべき原因があります。SELinux は cron ジョブを制限するため、ファイル権限が正しく見えても、想定外のラベルが付いたパスにアクセスするジョブは拒否されます。sudo ausearch -m avc -ts recent で拒否を確認し、無効化する前に サーバー向け SELinux の基本 を読んでください。

cron の環境を示す 1 分間の調査

cron の環境に何が含まれているかを推測せず、実際に読み取ります。すべての情報をダンプするスクリプトを作成し、1 分ごとに実行するよう設定してから待ち、ファイルを読み取ります。

cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh

実際のジョブを実行するユーザーの crontab に、両端を絶対パスにした 1 行を追加します。

* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1

1 分待ってから /home/deploy/cron-probe.log を読み取り、自分のシェルで同じコマンドを実行した結果と比較します。PATH の行、作業ディレクトリ、ロケールを確認すれば、通常はそれだけで失敗原因が分かります。設定上の重要な点を 2 つ確認してください。パーセント記号はスクリプト内にあるため、cron の規則は適用されません。また、ログのパスはジョブのユーザーが書き込める場所にします。

原因が分かったら、直ちにその crontab の行を削除します。1 分ごとに実行されてファイルに追記するジョブは、小容量のディスクを満杯にします。しかも、気付かないうちに進行します。

スケジュールは意図した設定になっていますか?

ユーザーの crontab の行は、分、時、月の日、月、曜日の5つのフィールドで始まります。このうち2つは、意外な動作をします。

月の日と曜日の両方を制限している場合、つまりどちらも * ではない場合、cron はどちらか一方のフィールドが一致したときにジョブを実行します。0 0 13 * 5 は「13日の金曜日」を意味しません。毎月13日の0:00と、毎週金曜日の0:00に実行されます。特定の1日だけを指定するには、2つのフィールドの一方を * のままにし、もう一方の条件をスクリプト内で確認します。

cron はシステムのタイムゾーンを使用します。多くの VPS イメージは UTC(協定世界時)に設定されているため、03:00 に設定したジョブは 03:00 UTC に実行されます。これは、現地時間では午後の途中に当たる場合があります。timedatectl で、サーバーが実際に使用しているタイムゾーンを確認できます。ノート PC と同じだと思い込まず、サーバーの設定を確認してください。

スケジュールには、ほかにも知っておくべき落とし穴が2つあります。@reboot は cron 自体の起動時に実行されます。これはネットワークの準備が完了する時点とは限りません。そのため、DNS やリモートホストを必要とするジョブは、ブート時には失敗し、その後の手動実行では毎回成功することがあります。また、時間のかかるジョブは、前の実行がまだ終わっていなくても再度開始されます。ロックで排他制御してください。

*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

ロックがすでに取得されている場合、flock -n は直ちに処理を終了します。そのため、重複した実行が最初の実行に重なることを防げます。

systemd timer がより適している場合

cron は、指定した時刻にコマンドを実行する用途には適しています。それ以外の機能は限定的です。timer なら、リダイレクトを設定しなくても journal を利用でき、後から終了ステータスを確認できます。また、network-online.targetに対する順序を指定でき、100 台のサーバーが同じ秒に一斉に処理を開始しないよう、ランダムな遅延も設定できます。ジョブにこれらの機能が必要な場合は、crontab の 1 行を維持するよりも、VPS 上の systemd service と timer を構成するほうが手間がかかりません。再試行の動作も timer 側で扱います。systemd の再起動ポリシーによって障害後の動作を決められるためです。cron には、この点を制御する仕組みがありません。

小規模なジョブには cron を使い続けてください。依存関係や再試行ポリシーがある処理は timer に移します。両方を同じサーバーで実行できるため、移行を一度に完了させる必要はありません。

FAQ

cron では失敗するのに、手動では実行できるのはなぜですか?

シェルと cron の環境が異なるためです。ログインシェルは /etc/profile~/.bashrc を読み込み、PATH、ロケール、エージェント関連の変数を設定します。cron はそれらを設定せず、異なる作業ディレクトリから、場合によっては異なるシェルでコマンドを起動します。すべてのコマンドに絶対パスを使用し、必要な設定を crontab の先頭またはスクリプト内で指定してください。さらに、1 分後に実行される確認用ジョブを登録し、env | sortpwdid の結果をログファイルに書き出してください。推測ではなく、cron の実際の環境を確認できます。

cron が実際にジョブを実行したかどうかを確認するにはどうすればよいですか?

デーモンのログを確認します。Debian と Ubuntu では journalctl -u cron を使用し、Fedora、Rocky、Alma では journalctl -u crond を使用します。一部のイメージでは、メッセージが rsyslog 経由で /var/log 配下のファイルに記録されます。スケジュールで指定した時刻のエントリを探し、自分のコマンドが記録されていることを確認してください。エントリがなければ、cron はそのスケジュールを認識していません。正しい crontab を編集したか確認してください。エントリがあるのに結果がない場合は、コマンドが起動後すぐに終了しています。リダイレクトで出力を取得してください。

crontab 内で date +%Y が正常に動作しないのはなぜですか?

cron はコマンド欄の % を特別な文字として扱うためです。最初のエスケープされていない % でコマンドが終了し、それ以降の内容は標準入力としてそのコマンドに渡されます。さらに続く % はそれぞれ改行になります。そのため、日付形式を含むファイル名が、想定したプログラムに渡りません。各パーセント記号を \% としてエスケープするか、コマンドをスクリプトに移して cron からスクリプトを呼び出してください。スクリプト内では、パーセント記号に特別な意味はありません。

cron ジョブの出力先はどこですか?

ローカルのメールシステムに送られ、crontab の所有者、または MAILTO で指定した宛先に配信されます。多くの VPS イメージにはメール転送エージェントがインストールされていないため、メッセージは破棄され、ジョブが無出力のように見えます。>> /path/to/log 2>&1 を使用して出力をファイルにリダイレクトしてください。標準エラーが標準出力に続くように、この順序を維持します。または logger -t myjob を経由して出力し、journalctl -t myjob で読み取ってください。デバッグ中は > /dev/null 2>&1 を使用しないでください。

cron と systemd timer のどちらを使用すべきですか?

固定時刻に単純なコマンドを実行する場合は cron を使用してください。特に、systemd を実行しないマシンへ移行する可能性があるジョブに適しています。リダイレクトなしで journal に出力を記録したい場合、終了ステータスを照会したい場合、ネットワーク起動後に実行したい場合、開始時刻にランダムな遅延を設けたい場合、または失敗後の再試行ポリシーが必要な場合は timer を使用します。両方を同じサーバーで実行できるため、必要になったジョブから 1 つずつ移行できます。