みなさん、こんにちは。
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点をチェックしてみてください。
- 絶対パスを使うのは基本中の基本。
- $HOME や TZ(タイムゾーン) が正しく認識されているか疑う。
.bashrcの return 文 より上に環境設定を書く。bash -l -cで「ログイン状態」をエミュレートする。
IoTデバイスやプログラムの連携は、こうした細かい「環境の差」を埋めていく作業の連続です。一度このコツを掴んでしまえば、cronはあなたの強力で忠実な味方になってくれるはずです。
もし同じような問題でお困りの方がいれば、ぜひ今回の方法を試してみてください。
本日も最後までお読みいただきありがとうございました。
それでは、よいIoTライフを!



