この記事では、cron ジョブとして安定運用するシェルスクリプトを設計するために必要な4点——PATH 設定・多重起動防止・ログ出力・エラー処理——を、実機の動作確認とあわせて体系的に解説します。
RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済みです。
この記事のポイント
・cronの実行環境はPATH・LANG・HOMEがすべて異なる
・flockで多重起動防止を確実に実装できる
・set -euo pipefailでcronでもエラーを取りこぼさない
・4点セットを定型テンプレート化して全cron対象に適用する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
cronの実行環境はインタラクティブシェルと別物という落とし穴
手動でシェルスクリプトを実行するとき、あなたのターミナルには bash のログイン処理が走っています。~/.bashrc や /etc/profile が読み込まれ、/usr/local/bin や ~/bin が PATH に含まれます。しかし cron の実行環境は別物です。1. cron のデフォルト環境変数を確認する
cron が実際にどんな環境変数を持つか、次のテスト crontab で確認できます。# テスト用 crontab エントリ(crontab -e で追加する) * * * * * env > /tmp/cron-env-test.txt 2>&1
/tmp/cron-env-test.txt を確認すると、RHEL 9.4 の実機ではこのような出力になります。[opr01@app-server01 ~]$ cat /tmp/cron-env-test.txt HOME=/root LOGNAME=root PATH=/usr/bin:/bin LANG= SHELL=/bin/sh
・PATH が最小限:
/usr/local/bin や /usr/local/sbin が含まれない。手動では動いていた mysqldump や自作コマンドが「コマンドが見つからない」になる・LANG が未設定または空:日本語ログが文字化けする原因
・SHELL が /bin/sh:bash 拡張構文(
[[ ]] や配列)がエラーになる場合があるこの違いを知らずにスクリプトを書くと、「手元では動くが cron では動かない」という典型的な罠にはまります。
環境変数を冒頭で明示設定する
1. PATH を冒頭で明示する
cron 向けスクリプトの先頭に必ず PATH を宣言し、必要なディレクトリをすべて列挙します。#!/bin/bash # PATH: cron環境でも外部コマンドを確実に見つけるために明示宣言 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export PATH # 日本語ログのためLANGを設定 LANG=ja_JP.UTF-8 export LANG # スクリプト自身のディレクトリを変数化しておくと便利 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PATH を宣言した後に export PATH で子プロセスにも伝搬させることを忘れないでください。2. crontab 側でも設定できる(補足)
crontab ファイルの先頭行に環境変数を書く方法もあります。# crontab -e の冒頭に記載する方法(ファイル全体に適用) PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANG=ja_JP.UTF-8 # ジョブ定義 0 2 * * * /opt/scripts/backup.sh
flockで多重起動を確実に防ぐ
1. 多重起動が問題になるケース
処理に5分かかるスクリプトを毎分 cron で動かすと、前の実行が終わらないうちに次の実行が始まります。データベースの更新やファイル操作では、これが競合を引き起こします。#!/bin/bash # flock によるロックファイル取得(多重起動防止) # -n: 非ブロッキングモード(既にロック取得済みなら即終了) # -e 200: flock 失敗時の終了コード LOCKFILE="/var/lock/myjob.lock" exec 200>"${LOCKFILE}" flock -n 200 || { echo "[$(date '+%Y-%m-%d %H:%M:%S')] 前回実行中のためスキップ" >&2 exit 0 } # ここから本処理(多重起動されない) echo "[$(date '+%Y-%m-%d %H:%M:%S')] 処理開始" # ... 本処理 ...
exec 200> でファイルディスクリプタ200にロックファイルを紐付け、flock -n 200 でロックを取得します。スクリプト終了時にファイルディスクリプタが自動でクローズされ、ロックも解放されます。2. 実機での動作確認
2つのターミナルから同じスクリプトを同時に起動すると、2つ目は即スキップします。[opr01@app-server01 ~]$ bash myjob.sh & [1] 12483 [2026-10-03 14:22:01] 処理開始 [opr01@app-server01 ~]$ bash myjob.sh [2026-10-03 14:22:02] 前回実行中のためスキップ
ログ出力設計——teeでファイルとstderrを同時に記録する
1. cronのデフォルト出力先の問題
cron は標準出力・標準エラー出力を合わせてメールで通知します(MAILTO が設定されていれば)。毎回メールが来ても困るし、ログとして残らないのも困る。両方を解決するのがtee を使ったログ設計です。シェルスクリプトの設計パターンをさらに体系的に学びたい方には、シェルスクリプト実践講座もあわせて参考にしてください。
#!/bin/bash # ... (PATH・flock設定の後) # ログ設定 LOG_DIR="/var/log/myjobs" LOG_FILE="${LOG_DIR}/myjob_$(date '+%Y%m%d').log" mkdir -p "${LOG_DIR}" # 以降の標準出力・標準エラーをタイムスタンプ付きでファイルに記録 # stdbuf -oL: バッファリングを無効化して即時ファイル書き込み log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } exec > >(tee -a "${LOG_FILE}") 2>&1
exec > >(tee -a LOG_FILE) 2>&1 の1行で、その後のすべての出力がファイルにも端末にも書き出されます。cron 実行時は端末がないのでファイルだけに残ります。2. 日付ローテーションとログ管理
ログファイル名に日付を含める($(date '+%Y%m%d'))と、自動的に日次ローテーションされます。古いログは find で一定期間後に削除できます。# 30日以上前のログを削除(crontab の別エントリでも可) find "${LOG_DIR}" -name "myjob_*.log" -mtime +30 -delete
set -euo pipefailとERR trapでエラーを取りこぼさない
cron 環境では失敗が無音で起きやすいため、エラーを確実に検知するための宣言が特に重要です。#!/bin/bash set -euo pipefail IFS=$' ' # ERR trap: エラー発生行をログに記録する on_error() { local exit_code=$? log "ERROR: line ${BASH_LINENO[0]}: '${BASH_COMMAND}' が終了コード ${exit_code} で失敗" # 必要に応じてメール通知やSlack通知をここで呼ぶ } trap on_error ERR
set -e なしにすると、コマンドが途中で失敗しても処理が続行され、データが中途半端な状態になることがあります。set -euo pipefail は cron ジョブでこそ必須の設定です。1. 意図的に失敗を許容する行への対処
grep の「該当なし(終了コード1)」など、失敗が正常な行には || true を付けます。# grepで一致なしは正常動作なので || true で許容 MATCHED=$(grep "ERROR" "${LOG_FILE}" || true) if [[ -n "${MATCHED}" ]]; then log "エラー行を検出: ${MATCHED}" fi
timeoutで暴走防止・MAILTO制御で通知を管理する
1. timeoutで最大実行時間を制限する
API 呼び出しや外部サービス依存の処理が無限待ちになると、後続の cron ジョブが詰まります。timeout コマンドで最大実行時間を設定します。# 本処理を最大5分(300秒)で強制終了 timeout 300 /usr/bin/python3 /opt/scripts/process_api.py || { exit_code=$? if [[ ${exit_code} -eq 124 ]]; then log "ERROR: タイムアウト(300秒超過)で強制終了" else log "ERROR: 処理が終了コード ${exit_code} で失敗" fi exit 1 }
timeout の終了コード124はタイムアウトを意味します。通常のエラーと区別してログに記録しておくと障害解析に役立ちます。2. MAILTO制御で不要なメール通知を抑制する
出力がある度にメールが来るのを防ぐには、crontab の先頭にMAILTO="" を設定します。エラー時だけメールを送る場合は、スクリプト内で mail コマンドを使います。# crontab: cronが自動送信するメールを無効化 MAILTO="" 0 2 * * * /opt/scripts/backup.sh
# スクリプト内: エラー時だけメールを送る on_error() { local exit_code=$? local msg="[ERROR] ${0}: line ${BASH_LINENO[0]} で失敗 (exit: ${exit_code})" log "${msg}" echo "${msg}" | mail -s "[CRON ALERT] $(hostname): ${0##*/}" admin@example.com } trap on_error ERR
4点セットを組み合わせた実装テンプレート
これまでの設計を1本のテンプレートにまとめます。新規の cron スクリプトはすべてこの骨格から始めると、設計漏れを防げます。#!/bin/bash # ============================================================ # cronジョブ向けシェルスクリプト 標準テンプレート # ============================================================ set -euo pipefail IFS=$' ' # 1. 環境変数(cron環境で不足するものを明示宣言) PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export PATH LANG=ja_JP.UTF-8 export LANG SCRIPT_NAME=$(basename "$0") SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # 2. ログ設定 LOG_DIR="/var/log/myjobs" LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}_$(date '+%Y%m%d').log" mkdir -p "${LOG_DIR}" exec > >(tee -a "${LOG_FILE}") 2>&1 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } # 3. エラー処理 on_error() { local exit_code=$? log "ERROR: line ${BASH_LINENO[0]}: '${BASH_COMMAND}' が終了コード ${exit_code} で失敗" } trap on_error ERR # 4. 多重起動防止 LOCKFILE="/var/lock/${SCRIPT_NAME%.sh}.lock" exec 200>"${LOCKFILE}" flock -n 200 || { log "前回実行中のためスキップ" exit 0 } # --- ここから本処理 --- log "処理開始" # 例: 本処理を最大10分でタイムアウト # timeout 600 /opt/scripts/actual_process.sh log "処理完了"
本記事のまとめ
cron ジョブ向けシェルスクリプトを安全に設計するための4点セットをまとめます。| 設計点 | 実装方法 | なぜ必要か |
|---|---|---|
| PATH・環境変数 | スクリプト冒頭で PATH=... を明示宣言 |
cron の PATH は最小限で、手動実行と別環境 |
| 多重起動防止 | exec 200>LOCKFILE; flock -n 200 |
長時間ジョブが重なるとデータ競合が起きる |
| ログ出力 | exec > >(tee -a LOG_FILE) 2>&1 |
cron メールに頼ると出力が残らない |
| エラー処理 | set -euo pipefail + ERR trap |
cron は無音で失敗するためエラーを必ず検知 |
| タイムアウト | timeout 秒数 コマンド |
外部依存の処理が無限待ちになる場合の保険 |
| MAILTO 制御 | crontab に MAILTO="" を宣言 |
不要なメール通知を抑制してエラーのみ通知 |
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、
cron ジョブ設計からシェルスクリプト全般のパターンまで、20年以上の現場経験をもとに学べる「Linux Master Pro Seminar」をご紹介します。
>> シェルスクリプト実践講座の詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトのretry設計|コマンド失敗時の自動再試行とタイムアウト制御の実装パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます