「df -h を毎朝確認しているが、週末や連休は誰も見ていない。今月だけで2回、残り数GBになっていることに後で気づいた」
この問題は、ディスク使用量を自動監視して古いファイルを定期的に削除するシェルスクリプトを1本組み込むだけで、ほぼ解消できます。
この記事では、dfで使用率を数値取得して段階的にアラートを出す関数と、findで古いログを安全に削除するクリーンアップ設計を組み合わせた実装パターンを解説します。trapによるエラー処理と、cronに組み込む際の落とし穴も含め、RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。
この記事のポイント
・dfの出力から使用率を数値で取得し、80%/90%/95%の段階アラートを実装できる
・findの-mtimeで削除対象を事前確認してから安全に-deleteできる設計
・trapとset -euoでスクリプトの異常終了を確実に検知してsyslogに残せる
・cronで動かすときのPATH・MAILTO・リダイレクトの3点を押さえれば安定する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜディスク監視スクリプトが必要なのか
本番サーバーでディスクフルが起きると、影響範囲は「ディスクが満杯になった」だけにとどまりません。ログが書き込めなくなる→アプリが /tmp にも書き出せなくなる→syslogが止まる→SSHセッションの確立に失敗する。こうした連鎖で、「ディスクフルを検知しようとしていた監視ツール自身が動かなくなる」という状況に陥ることがあります。
私が現場で担当していたWebサーバーでも、Apacheのアクセスログが想定より速いペースで成長し、気づいたときには /var が95%を超えていたことがありました。その夜、cronの実行ログが全件詰まっていて復旧に1時間かかりました。
「ディスク容量不足になってから手動対処する」のではなく、あらかじめ自動監視して閾値を超えたら古いファイルを自動削除する仕組みを組み込むことが、安定運用の基本です。
Zabbix・Prometheus・Datadogなどの監視ツールがある環境でも、特定のパーティション(/var/log・/backup 等)のクリーンアップロジックはシェルスクリプトで実装したほうが、監視ツールとの依存を切り離せて保守しやすくなります。
dfコマンドから使用率を数値として取得する
監視スクリプトの出発点は、dfの出力から「使用率の数値だけ」を取り出すことです。1. df -P でPOSIX形式の出力を得る
df -P は出力をPOSIX形式(長いパス名でも行が折り返されない)で固定します。-h(人間が読みやすいサイズ)はつけず、数値処理に適した出力にします。# /var パーティションの使用率を確認する df -P /var
Filesystem 1024-blocks Used Available Capacity Mounted on /dev/sda2 52428800 45678934 6749866 88% /var
2. awkで使用率の数値だけを抽出する
df -P の6列目が「Capacity(Use%)」です。awkでパーセント記号を除去して整数値に変換します。USAGE=$(df -P /var | awk 'NR==2 {gsub(/%/,""); print $5}') echo "${USAGE}" # → 88
NR==2 でヘッダー行(1行目)をスキップし、gsub(/%/,"") で % を除去してから5列目を出力します。変数 USAGE には整数だけが入り、[ "${USAGE}" -ge 80 ] のような比較演算がそのまま使えます。閾値チェック関数の設計(段階アラート)
使用率の数値が取れたら、次は「いつ何をするか」の判断ロジックを関数化します。段階を設けることで、「警告だけ残す」「クリーンアップを実行する」「強制クリーンアップ」を区別できます。1. スクリプト冒頭の設定セクションで閾値を変数管理する
閾値をハードコードしないために、設定セクションで変数定義します。後から変更するときも1か所を直すだけで済みます。#!/bin/bash set -euo pipefail # ─── 設定セクション ─────────────────────────────────────── TARGET_DIR="/var" # 監視対象パーティション THRESHOLD_WARN=80 # 警告閾値(80%以上でログだけ残す) THRESHOLD_CRIT=90 # 危険閾値(90%以上でクリーンアップ実行) THRESHOLD_EMER=95 # 緊急閾値(95%以上で強化クリーンアップ) LOG_DIR="/var/log" # クリーンアップ対象ディレクトリ LOG_RETENTION_DAYS=30 # 通常の保持日数 SCRIPT_LOG="/var/log/disk_monitor.log" # ──────────────────────────────────────────────────────────
2. check_disk_usage()関数の実装
関数の戻り値(return code)で段階を表現することで、呼び出し側のcase文がアクションを判断しやすくなります。check_disk_usage() { local target="${1:-${TARGET_DIR}}" local usage usage=$(df -P "${target}" | awk 'NR==2 {gsub(/%/,""); print $5}') log_msg "INFO" "${target} 使用率: ${usage}%" if [ "${usage}" -ge "${THRESHOLD_EMER}" ]; then log_msg "EMERGENCY" "${target} が ${usage}% に達しました(緊急クリーンアップ必要)" return 3 elif [ "${usage}" -ge "${THRESHOLD_CRIT}" ]; then log_msg "CRITICAL" "${target} が ${usage}% に達しました" return 2 elif [ "${usage}" -ge "${THRESHOLD_WARN}" ]; then log_msg "WARNING" "${target} が ${usage}% を超えました" return 1 else return 0 fi }
trapとset -eを使ったエラー処理設計
監視スクリプト自身がクラッシュした場合でも原因を追跡できるよう、ログ関数とtrapを必ず組み込みます。1. タイムスタンプ付きログ関数を定義する
log_msg() { local level="${1}" local message="${2}" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[${timestamp}] [${level}] ${message}" | tee -a "${SCRIPT_LOG}" }
tee -a でターミナルとログファイルの両方に出力します。cronからの実行時はターミナル側の出力が捨てられますが、${SCRIPT_LOG} にはすべての記録が残ります。2. trap でスクリプト異常終了を検知する
cleanup_on_exit() { local exit_code=$? if [ "${exit_code}" -ne 0 ]; then log_msg "ERROR" "スクリプトが異常終了しました(終了コード: ${exit_code})" logger -t disk_monitor "disk_monitor.sh が異常終了しました(exit: ${exit_code})" fi } trap cleanup_on_exit EXIT
trap cleanup_on_exit EXIT は正常終了・異常終了を問わずEXIT時に必ず呼ばれます。$? で終了コードを確認して非0の場合だけエラーとして記録します。set -euo pipefail との組み合わせが効果的です。コマンドが1つでも失敗すれば即座にスクリプトが終了し、trapが起動して記録します。cronから「なぜかスクリプトが止まっていた」という問題が追いかけやすくなります。findを使った古いログファイルの自動削除設計
クリーンアップ処理の核はfind コマンドです。削除対象を必ずリストアップしてから削除するステップを分けることで、意図しないファイルを消す事故を防ぎます。1. find -mtime で削除対象を事前確認する
-mtime +30 は「最終更新から30日を超えたファイル」を意味します。まず -print で対象ファイルを表示し、問題がないことを確認してから削除ステップに進みます。# 削除対象の確認(ドライランとして先に実行する) find "${LOG_DIR}" -type f \( -name "*.log" -o -name "*.log.gz" -o -name "*.log.[0-9]*" \) -mtime +${LOG_RETENTION_DAYS} -print
/var/log/httpd/access_log.20250710 /var/log/httpd/access_log.20250717.gz /var/log/messages-20250710 /var/log/maillog-20250710 /var/log/audit/audit.log.3
2. cleanup_old_logs()関数の実装
cleanup_old_logs() { local target_dir="${1}" local retention_days="${2}" # 削除対象ファイルを配列に格納(件数確認のため) local -a targets mapfile -t targets < <(find "${target_dir}" -type f \( -name "*.log" -o -name "*.log.gz" -o -name "*.log.[0-9]*" \) -mtime +"${retention_days}" -print) if [ "${#targets[@]}" -eq 0 ]; then log_msg "INFO" "削除対象ファイルなし(${retention_days}日以上古いファイルは存在しない)" return 0 fi # 削除前に合計サイズをログに記録 local total_kb total_kb=$(find "${target_dir}" -type f \( -name "*.log" -o -name "*.log.gz" -o -name "*.log.[0-9]*" \) -mtime +"${retention_days}" -exec du -sk {} + | awk '{total += $1} END {printf "%.1f", total/1024}') log_msg "INFO" "${#targets[@]} 件のファイルを削除します(合計 ${total_kb} MB)" # 削除実行 find "${target_dir}" -type f \( -name "*.log" -o -name "*.log.gz" -o -name "*.log.[0-9]*" \) -mtime +"${retention_days}" -delete log_msg "INFO" "ログクリーンアップ完了" }
mapfile -t targets でファイルリストを配列に格納し、${#targets[@]} で件数を確認してから削除します。0件なら削除ステップをスキップするため、対象ファイルがない状態でも安全です。古いアーカイブを削除する前に tarコマンドで圧縮アーカイブを作成してからリモートNASへ退避するフローも、長期保存が必要な環境では合わせて検討してください。
シェルスクリプトを体系的に学びたい方は、シェルスクリプト実践ガイド も合わせて活用してほしい。
スクリプト全体の組み立てとcronへの組み込み
ここまでの関数をmain()でまとめ、cronに登録します。1. main()関数で各処理を結合する
main() { log_msg "INFO" "ディスク監視スクリプト開始" # check_disk_usage は set -e の影響を受けないよう || true で受ける local disk_status=0 check_disk_usage "${TARGET_DIR}" || disk_status=$? case "${disk_status}" in 0) log_msg "INFO" "使用率は正常範囲内です" ;; 1) log_msg "WARNING" "警告閾値を超えました。通常クリーンアップを実行します" cleanup_old_logs "${LOG_DIR}" "${LOG_RETENTION_DAYS}" ;; 2) log_msg "CRITICAL" "危険閾値を超えました。通常クリーンアップを実行します" cleanup_old_logs "${LOG_DIR}" "${LOG_RETENTION_DAYS}" ;; 3) log_msg "EMERGENCY" "緊急閾値を超えました。短期クリーンアップを実行します" cleanup_old_logs "${LOG_DIR}" 7 ;; *) log_msg "ERROR" "check_disk_usage が予期しない終了コードを返しました: ${disk_status}" ;; esac log_msg "INFO" "ディスク監視スクリプト終了" } main "$@"
check_disk_usage || disk_status=$? は、set -euo pipefailの環境で関数が非0を返しても即座にスクリプトが止まらないようにするための慣用表現です。disk_status に戻り値を受け取り、case文でアクションを振り分けます。2. cronへの登録と環境変数の設定
# /usr/local/bin/disk_monitor.sh に配置して実行権限を付ける chmod 755 /usr/local/bin/disk_monitor.sh # crontab -e で追加する内容 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin MAILTO="" # 毎日 02:00 に実行し、cron自体のログも残す 0 2 * * * /usr/local/bin/disk_monitor.sh >> /var/log/disk_monitor_cron.log 2>&1
・PATHを明示する:cronの実行環境はPATHが短く、
/usr/local/bin に置いたツールが見つからないことがある。スクリプトや呼び出すコマンドのフルパスを確認しておく・MAILTO=""を設定する:デフォルトでは標準出力・エラーがrootへメール送信される。不要なメールを防ぐため MAILTO="" にして、ログはスクリプト内で明示的に管理する
・
>> logfile 2>&1でリダイレクトする:cronが実際に起動したかどうかの外部記録として残しておくと、スクリプト内ログと組み合わせて問題の切り分けがしやすくなるトラブルシュート
「手動では動くがcronで動かない」典型パターン
最も多いのはPATHの問題です。スクリプト内でlogger や awk を使っている場合、通常は /usr/bin にあるため問題ありませんが、追加インストールしたコマンドは見つからないことがあります。cronのデバッグはスクリプト先頭に
set -x と env > /tmp/cron_env.txt を追加し、実行後に /tmp/cron_env.txt の内容と手動実行時の環境を比較する方法が確実です。「findが意図しないファイルを削除した」対処
-mtime +30 は「最終更新から30日を超えたファイル」を対象にします。アクセス時刻(-atime)や変更時刻(-ctime)ではなく修正時刻(-mtime)を使っていることを確認します。また、削除対象のディレクトリを
LOG_DIR="/var/log" のように明示し、/var などの広すぎる範囲を指定しないことが鉄則です。スクリプト化する前に必ず -print だけで1度実行して対象ファイルを目視確認してください。「スクリプトが途中で止まっている」場合
trap cleanup_on_exit EXIT を組み込んでいれば、${SCRIPT_LOG} に終了コードが記録されます。tail -20 /var/log/disk_monitor.log で直近の実行記録を確認し、どのコマンドで止まったかを追跡します。set -x をデバッグ用に有効にすると、どのコマンドが実行された直後に止まったかをトレースできます。本記事のまとめ
シェルスクリプトによるディスク監視・自動クリーンアップの設計要点を整理します。| やりたいこと | 実装方法 |
|---|---|
| 使用率を数値で取得する | df -P /var | awk 'NR==2 {gsub(/%/,""); print $5}' |
| 段階アラートを実装する | check_disk_usage()で戻り値0/1/2/3を返す設計にする |
| 削除前に対象を確認する | find ... -mtime +30 -printで事前確認してから-deleteする |
| 削除件数・サイズを記録する | mapfile -t targetsで配列に格納して${#targets[@]}を確認する |
| 異常終了を検知する | trap cleanup_on_exit EXITを冒頭に配置してsyslogに記録する |
| cronで安全に動かす | PATH明示・MAILTO=""・>> logfile 2>&1の3点セットを設定する |
シェルスクリプトを体系的に学びたい方は、シェルスクリプト実践ガイド も合わせて活用してほしい。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトで複数サーバーへのSSH並列実行を設計する方法|結果集約とエラー検知の実践パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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