cron からシェルスクリプトを実行すると AWS CLI コマンドが失敗する原因を教えてください

2023.11.30

困っていた内容

Q. cron からシェルスクリプトを実行すると シェルスクリプト内部で使用している AWS CLI コマンドが失敗しますが、シェルスクリプトを手動実行すると成功します。原因を教えてください。

どう対応すればいいの?

A. cron では多くの環境変数が設定されておりません。cron の環境変数 PATH のデフォルト値は /usr/bin:/bin のみですので、cron ジョブ実行時に aws コマンドのパスが通るように実効ユーザーの環境変数が設定されているかどうか確認してください。

原因の特定を行う

検証環境は下記の通りです。

  • OS: macOS Ventura バージョン 13.4.1
  • AWS CLI: バージョン 2.9.22

 

シェルスクリプト

下記は、ローカルファイルを S3 バケットへ同期させるだけのとてもシンプルなシェルスクリプトです。
今回はデバッグが目的のため、AWS CLI コマンドには--debugオプションを付けています。
また、setコマンドのオプション-xで実行コマンドと引数の展開処理を出力させ、オプション-uで変数が未定義の場合エラー出力するようにしています。

sync_files.sh

#!/bin/sh

set -ux

# Replace with your bucket name and file path
BUCKET_NAME="timed-jobs"
LOCAL_PATH="/Users/asano.yuka/Test/Backup_2023/"
PROFILE="asano"

# Sync local files to the AWS S3 bucket
aws s3 sync $LOCAL_PATH s3://$BUCKET_NAME --profile $PROFILE --debug

# Check if sync command executed successfully
if [ $? -eq 0 ]; then
    echo "Sync successful."
else
    echo "Sync failed."
fi

 

実際の crontab の内容

デバッグのため、ログを /tmp/cron.log へ出力するようにしておきます。

*/5 * * * * /Users/asano.yuka/AWS/scripts/sync_files.sh >> /tmp/cron.log 2>&1

 

実行結果

+ BUCKET_NAME=timed-jobs
+ LOCAL_PATH=/Users/asano.yuka/Test/Backup_2023/
+ PROFILE=asano
+ aws s3 sync /Users/asano.yuka/Test/Backup_2023/ s3://timed-jobs --profile asano --debug
/Users/asano.yuka/AWS/scripts/sync_files.sh: line 9: aws: command not found
+ '[' 127 -eq 0 ']'
+ echo 'Sync failed.'
Sync failed.

/tmp/cron.log を確認するとaws s3 syncが実行に失敗していることがわかります。
ここで、aws コマンドの実行時に、「aws: command not found」のエラーが出力されていることに注目します。
上記のエラーが意味する事実はいくつかありますが、スクリプトの手動実行が成功している点から、PATH が設定されていない可能性が疑われます。[1]

私の環境では aws コマンドのパスは /usr/local/bin/aws になっていますが、cron ジョブ実行時は PATH に aws コマンドのパスが通っていない状態になっていました。

# 当該ジョブのログより抜粋
echo $PATH
/usr/bin:/bin

 

解決策

cron ジョブ実行時に aws コマンドのパスが通るように実効ユーザーの環境変数が設定されるようにしてください。

AWS CLI コマンドを正常に実行させるには次のような方法があるかと思います。

  1. シェルスクリプト内で環境変数を設定する
  2. aws コマンドをフルパスの表記で記述する
  3. 環境変数 PATH を crontab ファイル内に記述する
     

方法 1: 環境変数として設定する

シェルスクリプト内で aws コマンドのパスを環境変数 PATH に設定します。

sync_files.sh

# Replace with your bucket name and file path
+ PATH=$PATH:/usr/local/bin

(後略)

 

方法 2: aws コマンドをフルパスの表記に変更する場合

上記見出しの通り、aws コマンドをフルパスの表記で記述します。

sync_files.sh

(前略)

# Sync local files to the AWS S3 bucket
- aws s3 sync $LOCAL_PATH s3://$BUCKET_NAME --profile $PROFILE --debug
+ /usr/local/bin/aws s3 sync $LOCAL_PATH s3://$BUCKET_NAME --profile $PROFILE --debug

(後略)

 

方法 3: 環境変数 PATH を crontab ファイル内に記述する

crontab ファイル内で環境変数を記述する方法です。
シェルスクリプトを変更しなくてもよい点や cron ジョブの定義ごとに使用してよいパスを制限できる点から、個人的にはオススメの方法です。

PATH=/usr/bin:/bin:/usr/local/bin
*/5 * * * * /Users/asano.yuka/AWS/scripts/sync_files.sh >> /tmp/cron.log 2>&1

方法 1 〜 3 のいずれの方法でもaws s3 syncの実行に成功します。

# 当該ジョブのログより抜粋
Sync successful.

おまけ

mac でスケジュールされたジョブを実行させる場合、launchd ジョブを利用する方法もあります。
既にご存知の方も多いと思いますが、Apple のドキュメントによると、launchdcron よりも推奨されています。[2]

というわけで launchd でも同様のスケジュールジョブを実行してみます。  

launchd でスケジュールされたジョブを実行する

launchd を利用してスケジュールされたジョブを実行するためには、ジョブの内容を記述した launchd.plist ファイルをロードさせる必要があります。
この記事では launchd についての詳しい解説は割愛しますので、詳細は Apple のドキュメントや laucnd.plist のマニュアルページ をご参照ください。[3]

 

Note: launchd でスクリプトを実行する場合にも環境変数の設定が必要です

環境変数の設定を行わない状態でスクリプト実行時の PATH を確認すると、/usr/local/bin が含まれていないことがわかります。

echo $PATH
/usr/bin:/bin:/usr/sbin:/sbin

launchd で環境変数を設定する方法はいくつかありますが、環境変数 PATH に /usr/local/bin を追加するためには、実行するスクリプト内に直接環境変数を記述する方法と後述する launchd.plist ファイル内に環境変数の定義を追加する方法があります。
どちらの方法も使えますが、基本的には launchd.plist ファイル内で環境変数を設定する方法がオススメです。なぜなら、環境変数の設定はジョブの構成として管理されるため、スクリプト自体に直接記述する必要はなく、複数のジョブで同じ設定を共有することもできるからです。
 

1. launchd.plist ファイルを記述する

launchd.plist ファイルは、launchd によって管理されるジョブの構成を定義するための設定ファイルです。
lauchd.plist ファイルは次のいずれかのディレクトリに置きます。

  • ~/Library/LaunchAgents
  • /Library/LaunchAgents
  • /Library/LaunchDaemons
  • /System/Library/LaunchAgents
  • /System/Library/LaunchDaemons

今回はユーザーがログインする度にログイン中のユーザー権限で実行するプロセスを起動させるため、~/Library/LaunchAgents ディレクトリ以下に設定ファイルを格納します。
設定ファイルは次のように記述しました。

sync_files.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>sync_files</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin</string>
    </dict>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/asano.yuka/AWS/scripts/sync_files.sh</string>
    </array>
    <key>StandardOutPath</key>
    <string>/tmp/sync_files.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/sync_files.log</string>
    <key>StartInterval</key>
    <integer>300</integer>
</dict>
</plist>

ジョブの実行に関する設定は property list keys と呼ばれるキーとそれに対応する値で定義します。

EnvironmentVariables

  • ジョブ実行の際に使用する環境変数を設定します。文字列以外の値は無視されるので注意です。

StandardOutPath / StandardErrorPath

  • 起動プロセスの標準出力/標準エラー出力データをどのファイルに送信するか定義します。今回はデバッグ目的で /tmp/sync_files.log へ書き込むようにしています。

StartInterval

  • ジョブを繰り返し実行させるようにスケジュールします。インターバルは秒数で指定します。今回は検証目的のため 5 分間隔にしています。

 

2. エージェントを起動させる

挙動のテストが目的のため、今回は「ログイン → ログアウト」ではなく、launchctl コマンドで launchd.plist をロードさせます。

$ launchctl load /Users/asano.yuka/Library/LaunchAgents/sync_files.plist

以下のようにジョブがちゃんと登録されていることが確認できます。

$ launchctl list sync_files
{
	"StandardOutPath" = "/tmp/sync_files.log";
	"LimitLoadToSessionType" = "Aqua";
	"StandardErrorPath" = "/tmp/sync_files.log";
	"Label" = "sync_files";
	"OnDemand" = true;
	"LastExitStatus" = 0;
	"Program" = "/Users/asano.yuka/AWS/scripts/sync_files.sh";
	"ProgramArguments" = (
		"/Users/asano.yuka/AWS/scripts/sync_files.sh";
	);
};

5分後、sync_files.log を確認すると Sync successful. と表示されていました。

# /tmp/sync_files.log より抜粋
Sync successful.

S3 バケットにもオブジェクトがアップロードされていることが確認できます。
launchd でも cron でスクリプトを実行した時と同様の結果を得ることできました。

参考資料

[1] AWS CLI エラーのトラブルシューティング - AWS Command Line Interface
[2] Scheduling Timed Jobs
[3] Creating Launch Daemons and Agents