cronだと動かない!「シェル実行と結果が違う」時の原因と解決策【Python / pyenv環境の例】

みなさん、こんにちは。

Linuxでスクリプトを自動化した際、「手動で実行すると成功するのに、cronだと失敗する(あるいは期待した結果にならない)」というのは、もはやLinuxユーザーにとっての「通過儀礼」と言っても過言ではありません。

以前ご紹介した、Withingsの体重計データをGarmin Connectに同期させるスクリプト「withings-sync」。

これをRaspberry Piなどのpyenv環境にインストールし、cronで定期実行させていたところ、しばらくは順調だったのですが、ある日ふと気づきました。「APIの接続は成功している(トークンは更新されている)のに、データが1件も取得できていない……」という現象に。

今回は、この「一見動いているようで、肝心な仕事をしてくれない」状態をどう切り分け、解決に導くか。実例を交えて解説していきます。

 


 

現象 – API接続は成功するが「データなし」になる

 

cronの実行ログを確認すると、以下のような状態でした。

  • APIの認証(Token Refresh)ログは出ている。
  • しかし、データ取得結果が「0件」と表示される。
  • 本来、同期が成功した際に出るはずの「保存完了(Saved)」のログが出力されない。

手動でターミナルから withings-sync を叩くと、何の問題もなく最新の体重データがGarminに飛んでいきます。この「手動 vs 自動」の差こそが、解決の糸口になります。

 


 

原因の切り分け – 立ちはだかる「環境の壁」

 

結論から言うと、原因は「環境変数の欠落」と「.bashrcの実行制限」のダブルパンチでした。

原因1 – $HOME が設定されていない

cronは、セキュリティと軽量化のために、ログイン時とは比較にならないほど「最小限の環境」で動作します。 多くのスクリプトやライブラリ(特に認証が必要なもの)は、設定ファイルやキャッシュを ~/.config$HOME/.something.json から探そうとします。しかし、cron実行時にはこの $HOME が空であったり、想定と異なるディレクトリを指していたりすることがあります。

withings-sync の場合、設定ファイルの .withings_user.json を見つけられないと「初期状態(データなし)」として振る舞ってしまい、結果として「同期するものがない」と判断されていたのです。

原因2 – タイムゾーン(TZ)の不一致

これが「データが0件」になる隠れた主犯格であることも多いです。 多くのLinuxサーバーにおいて、cronは「UTC(協定世界時)」で動いています。

一方、私たちのシェル環境は通常「JST(日本標準時)」です。

例えば、深夜の1時に「今日(JST)」のデータを取得しようとしても、UTCではまだ前日の夕方16時。スクリプトが「今日」の日付をベースにAPIを叩いた場合、9時間のズレによって「該当するデータがまだ存在しない(0件)」という判定を下されてしまうのです。

原因3 – .bashrc に仕込まれた「隠れた return」

これが意外と盲点です。UbuntuなどのDebian系ディストリビューションのデフォルト .bashrc には、冒頭にこのような記述があります。

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

これは「画面(対話型シェル)がない実行(cronなど)の時は、これ以降の設定を読み飛ばす」という命令です。 pyenvの設定(パスの追加や初期化)を .bashrc の下の方に書いている場合、cron実行時にはこの return によって設定が完全に無視されてしまいます。結果、pyenvでインストールした特定のPythonバージョンやライブラリが正しく認識されないのです。

 


 

解決策 – ログインシェルを完全再現する

 

最も確実で、かつメンテナンスしやすい解決方法は、cronのコマンドライン上で「ユーザーがログインした時と同じ環境」を明示的に作り出すことです。

【修正前】

shims(pyenvの実行パス)を直接指定しても、環境変数が足りないため動作が不安定になります。

# 失敗しやすい例
0 1 * * * /home/user/.pyenv/shims/withings-sync --options...

【修正後(推奨)】

TZ 環境変数を指定しつつ、bash -l -c を使ってログイン環境をエミュレートします。

# 成功する例
0 1 * * * TZ=Asia/Tokyo /bin/bash -l -c "cd $HOME && /home/user/.pyenv/shims/withings-sync --garmin-username='xxx' --garmin-password='yyy'" >> /tmp/withings_cron.log 2>&1

設定のポイント

  • TZ=Asia/Tokyo
    → cronに対して日本時間を教えます。これで日付ベースのデータ取得が正常化します。
  • /bin/bash -l
    → 「ログインシェル」として起動します。これにより、.bash_profile.bashrc を正しく読み込ませます。
  • -c “…”
    → 続く文字列を一連のコマンドとして実行します。
  • cd $HOME
    → 実行前に必ずホームディレクトリへ移動させます。これで相対パスによるファイル探索ミスを防げます。
  • >> /tmp/cron.log 2>&1
    → 標準出力とエラー出力をすべてログに書き出します。トラブルが起きた際、ここを見れば一発で原因がわかります。

 


 

さらなるハマりポイントへの対策

 

もし、これでも pyenv のパスが通らない場合は、.bashrc の書き方を少し工夫します。 前述した case $- in ... return よりも上に、pyenvの設定を移動させてしまうのが最も賢い解決策です。

# .bashrc の冒頭付近に移動
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

# この後に interactive check が来るようにする
case $- in
    *i*) ;;
      *) return;;
esac

これにより、cronから呼ばれた際も pyenv の環境だけは確実にセットアップされるようになります。

 


 

cronでのシェルスクリプト実行時に気を付けるべき点

 

「シェルでは動くのに!」と叫びたくなった時は、以下の4点をチェックしてみてください。

  1. 絶対パスを使うのは基本中の基本。
  2. $HOMETZ(タイムゾーン) が正しく認識されているか疑う。
  3. .bashrcreturn 文 より上に環境設定を書く。
  4. bash -l -c「ログイン状態」をエミュレートする。

IoTデバイスやプログラムの連携は、こうした細かい「環境の差」を埋めていく作業の連続です。一度このコツを掴んでしまえば、cronはあなたの強力で忠実な味方になってくれるはずです。

もし同じような問題でお困りの方がいれば、ぜひ今回の方法を試してみてください。

 

本日も最後までお読みいただきありがとうございました。

それでは、よいIoTライフを!

カテゴリ: IoT, Tips

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール