環境変数と PATH の注意点
cron で動かしたスクリプトが「手で打つと動くのに失敗する」最大の原因は環境変数の違いです。cron はログインシェルを通さないため PATH が最小限で、.bashrc も読み込まれません。対策として、コマンドは絶対パスで書く、スクリプト内で必要な環境変数を明示的に設定する、crontab の先頭で PATH= を定義する、のいずれかを取ります。systemd の service でも同様で、Environment= や EnvironmentFile= で明示します。
自動化でいちばん多く、そしていちばん厄介なトラブルが、「ターミナルで手打ちすると問題なく動くのに、cron に登録すると失敗する」という現象です。コマンド自体は正しいのに動かないので原因が分かりにくく、多くの人がここで時間を溶かします。結論から言うと、その大半は環境変数の違い、とりわけ PATH の違いが原因です。なぜそうなるのかという理屈を一度きちんと理解しておけば、この症状に出くわしても落ち着いて切り分けられるようになります。逆に理屈を知らないままだと、コマンドを疑って何度も書き直す、という的外れな試行錯誤に陥りがちです。
なぜ cron だと環境が違うのか
あなたがターミナルでコマンドを打つとき、シェルは起動時に /etc/profile や ~/.bashrc、~/.bash_profile といった設定ファイルを読み込みます。この過程で PATH に各種ディレクトリが追加され、別名(alias)や独自の環境変数も設定されます。つまり日頃の対話シェルは、これらの設定をすべて読み込んだ「お膳立てされた状態」なのです。ところが cron がジョブを起動するときは、ログインシェルを通さず、それらの設定ファイルを一切読み込みません。その結果、cron 実行時の PATH は /usr/bin:/bin といったごく最小限のものだけになり、~/.bashrc で足したパスや、そこで定義した環境変数・alias はすべて存在しない状態でコマンドが走ります。さらに、起動の起点となるカレントディレクトリも対話時とは異なり、ホームディレクトリなど決まった場所になります。「手では動くのに cron で動かない」のは、コマンドが間違っているのではなく、この貧弱で違う環境のせいなのです。同じ理由で、at や systemd の service でも環境は最小限になります。
典型的な失敗の現れ方
PATH が最小限だと、/usr/local/bin などにインストールされたコマンド(自分で入れたツールや言語ランタイムに多い)が「command not found」となって失敗します。たとえば普段 node や python3、docker、自作スクリプトを名前だけで呼んでいると、cron 環境ではそのパスが PATH に無いため見つかりません。また、~/.bashrc で export していた API キーや言語の設定(LANG など)も読まれないので、日本語が文字化けする、設定値が見つからないとエラーになる、相対パスで参照していたファイルが見つからない、といった形で表面化します。ここで重要なのは、こうした auto-idempotent な定期処理を安定して回すには、実行環境を実行のたびに同じ・既知の状態にそろえることが前提になる、という発想です。環境がそのつど変わると、冪等であるはずの処理も「動いたり動かなかったり」とぶれてしまい、自動化の信頼性が根本から崩れてしまいます。
対策その1 — 絶対パスと crontab の PATH 定義
もっとも確実な対策は、コマンドを絶対パスで書くことです。node ではなく /usr/local/bin/node、自作スクリプトなら /usr/local/bin/backup.sh と、フルパスで指定すれば PATH に依存しなくなります。どのコマンドがどこにあるかは、対話シェルで which node や command -v node と打てば絶対パスが分かるので、それをそのまま crontab やスクリプトに書き写します。使うコマンドが多い場合は、crontab ファイルの先頭で PATH をまとめて定義する方法も有効です。crontab の冒頭に PATH=/usr/local/bin:/usr/bin:/bin と1行書いておくと、それ以降のジョブはその PATH のもとで実行されます。あわせて SHELL=/bin/bash(ジョブを動かすシェルの指定)や MAILTO=you@example.com(実行結果メールの宛先)も、同様に crontab 冒頭で指定できる設定です。これらの宣言は5フィールドの予定行より前に置く必要がある点に注意してください。
対策その2 — スクリプト内で環境を整える
もう1つの王道は、呼び出されるスクリプトの側で、自分が必要とする環境を明示的に作ることです。スクリプト冒頭で export PATH=/usr/local/bin:$PATH のように PATH を補い、必要な環境変数も export DATA_DIR=/srv/data のように自分で設定します。秘密の値(APIキーやパスワードなど)は、スクリプトに直書きすると漏えいの危険があるので、権限を絞った別ファイルに置いて source /etc/myapp/env のように読み込むと、安全かつ見通しよく管理できます。こうしておけば、cron から呼ばれても、手で実行しても、同じ環境で動くようになり、再現性が高まります。「実行環境を呼び出し元任せにせず、スクリプトが自分で完結して用意する」のが堅実な設計で、どこから・どんな状況で起動されても結果がぶれなくなります。
systemd timer / service の場合とデバッグのコツ
同じ問題は systemd の service ユニットにもあります。service も最小限の環境で実行されるため、必要な環境変数は明示する必要があります。service ユニット内では Environment="PATH=/usr/local/bin:/usr/bin:/bin" のように個別に書くか、別ファイルにまとめて EnvironmentFile=/etc/myapp/env で読み込みます。考え方は cron とまったく同じで、「実行時の環境はデフォルトでは貧弱なので、必要なものは自分で与える」という一点に尽きます。最後に、原因切り分けの実践的なコツを挙げておきます。動かないときは推測でコマンドを直す前に、ジョブの中で env コマンドを実行し、その出力を env >> /tmp/cronenv.log のようにログへ吐かせてみてください。すると cron や service の実行時に、実際どんな PATH・環境変数が見えているかを直接確認できます。手で打ったときの env の出力と見比べれば、何が足りないのかが一目で分かり、ほとんどの「手では動くのに自動だと動かない」問題はここで決着します。