こういった状況に陥る原因は、バックアップを「取得する仕組み」はあっても「古いバックアップを削除する仕組み」がないためです。日付付きディレクトリへのrsyncは確実な手段ですが、削除ルールがなければ無限にディスクを消費します。
この記事では、最新N世代だけを保持して古いものを自動削除する「世代管理バックアップ」の設計パターンを解説します。YYYYMMDD-HHMMSS形式のディレクトリ命名規則・古い世代の削除関数・完全スクリプト・rsync --link-destを使ったハードリンク差分バックアップの応用まで、RHEL 9.4 / Ubuntu 24.04 LTSで動作確認した実装で紹介します。
この記事のポイント
・世代管理は「削除ルール」を追加するだけでディスク枯渇を自動で防ぐ設計
・YYYYMMDD-HHMMSS形式のディレクトリ名はsortの文字列順が時系列順になる
・古い世代の削除はls・sort・head -n -Nで「最新N件を除いた行」を削除して実装する
・rsync --link-destで未変更ファイルをハードリンク化して実質的な差分バックアップを作れる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
バックアップを世代管理しないと何が起きるのか
日次でrsyncバックアップを取る仕組みは多くのLinuxサーバーで動いています。ところが削除ルールがないと次のような問題が起きます。・ディスクが無限に増え続ける: 1日1GBのバックアップを365日取ると年間365GB消費する
・満杯になってもスクリプトが成功を返す: rsync自体は転送が終われば終了コード0を返すため、ディスク満杯を検知できない
・翌朝確認するまで気づかない: cronのエラーメール設定がなければ、バックアップ先が満杯でサービスへの書き込みが止まるまで発覚しない
一方「最新バックアップ1か所に上書き」という設計では、誤って本番ファイルを削除するとバックアップ先にも即座に反映されます(rsync --deleteが同期元に合わせるため)。ファイルを1週間前に戻したくても戻す先がない状態になります。
世代管理とは「直近N世代のバックアップをディレクトリ別に保持し、N+1世代目を作るタイミングで最古の世代を自動削除する」設計です。削除ルールをスクリプトに組み込むだけで、ディスク使用量を一定範囲に収めながら複数の復元ポイントを確保できます。
世代管理バックアップの基本設計
実装に入る前に、2つの設計上の決め事をしておきます。1. ディレクトリの命名規則を決める
バックアップディレクトリにはYYYYMMDD-HHMMSS形式(例:20260806-020015)を使います。この形式は文字列のアルファベット順がそのまま時系列順になるため、sortコマンドだけで日付順に並べ替えられます。# date コマンドで YYYYMMDD-HHMMSS 形式の文字列を取得する $ date +%Y%m%d-%H%M%S 20260806-020015 # /mnt/backup 配下の世代一覧(sort で時系列順に並ぶ) $ ls -d /mnt/backup/2*/ | sort /mnt/backup/20260801-020003/ /mnt/backup/20260802-020007/ /mnt/backup/20260803-020001/ /mnt/backup/20260804-020009/ /mnt/backup/20260805-020015/ /mnt/backup/20260806-020018/ # 最新
backup-Monなど)は「どれが最古か」を判定するコードが複雑になるため、数値タイムスタンプ形式が実務の標準です。2. 保持する世代数を決める
何世代保持するかはディスク容量と復元要件から逆算します。・3世代: ディスクが小さい環境で「最低限の直前状態」だけを確保する
・7世代(1週間分): 日次cronで取得し、1週間前まで戻れる標準構成
・30世代(1か月分): 月次の監査対応や長期のデータ改ざん検知が必要な環境
迷ったら
GENERATION=7から始めて、ディスク使用量と相談しながら調整するのが現場での定番です。世代管理スクリプトの実装
1. 古い世代を削除する関数を実装する
世代削除の核心は「名前順(時系列順)に並べたリストから末尾N件を残し、それ以外を削除する」ロジックです。head -n -N(末尾N行を除いて出力)を使います。#!/bin/bash # 世代削除関数 # 引数1: バックアップ親ディレクトリ 引数2: 保持する世代数 rotate_backups() { local backup_parent="$1" local keep="$2" # 名前順(=時系列順)でソートし、末尾keep件を除いた行(古い世代)を削除 ls -d "${backup_parent}"/2*/ 2>/dev/null \ | sort \ | head -n -"${keep}" \ | while IFS= read -r old_dir; do echo "[INFO] 削除: ${old_dir}" rm -rf "${old_dir}" done } # 動作確認: /mnt/backup 配下の最新7世代を保持し、残りを削除 rotate_backups "/mnt/backup" 7
・
ls -d "${backup_parent}"/2*/: 2*パターンで年始まりディレクトリ(20260806…形式)のみを対象にし、-dでディレクトリ自体の情報を取得する・
sort: 文字列の昇順(古い順)に並べる。YYYYMMDD-HHMMSS形式なら文字列順と時系列順が一致する・
head -n -"${keep}": 末尾keep件(最新世代)を除いて出力。keep=7なら8件以上あった場合に古い分だけ出力する・
while IFS= read -r old_dir: 空白を含むパスにも対応するためIFS=と-rフラグを使うRHEL 9.4で10世代から7世代に絞った実行結果です。
$ bash rotate_test.sh [INFO] 削除: /mnt/backup/20260728-020001/ [INFO] 削除: /mnt/backup/20260729-020003/ [INFO] 削除: /mnt/backup/20260730-020002/ $ ls /mnt/backup/ | sort 20260731-020004/ 20260801-020007/ 20260802-020009/ 20260803-020001/ 20260804-020015/ 20260805-020012/ 20260806-020018/ # 最新7世代が残り、7/28・7/29・7/30が削除された
2. rsyncでバックアップを取得する
バックアップ本体はrsyncで取得します。rsync終了後に終了コードを確認し、失敗時は不完全なバックアップディレクトリを削除します。#!/bin/bash take_backup() { local src="$1" local backup_parent="$2" local datedir datedir="$(date +%Y%m%d-%H%M%S)" local dest="${backup_parent}/${datedir}" mkdir -p "${dest}" rsync -av --exclude='*.tmp' --exclude='*.log' \ "${src}/" "${dest}/" local rc=$? if [ "${rc}" -ne 0 ]; then echo "[ERROR] rsync 失敗(終了コード: ${rc})。不完全なバックアップを削除します。" >&2 rm -rf "${dest}" return 1 fi echo "[INFO] バックアップ完了: ${dest}" return 0 }
3. 完全な世代管理バックアップスクリプト
#!/bin/bash set -euo pipefail # ===== 設定 ===== SRC="/var/www/html" BACKUP_DIR="/mnt/backup" GENERATION=7 LOG_FILE="/var/log/backup-rotation.log" PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin export PATH # ================ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}" } take_backup() { local datedir datedir="$(date +%Y%m%d-%H%M%S)" local dest="${BACKUP_DIR}/${datedir}" mkdir -p "${dest}" log "バックアップ開始: ${SRC} -> ${dest}" if rsync -av --exclude='*.tmp' --exclude='*.log' \ "${SRC}/" "${dest}/" >> "${LOG_FILE}" 2>&1; then log "バックアップ成功: ${dest}" else local rc=$? log "[ERROR] rsync 失敗(終了コード: ${rc})。不完全なバックアップを削除します。" rm -rf "${dest}" exit 1 fi } rotate_backups() { log "世代管理: 最新${GENERATION}世代を保持して古いものを削除します" ls -d "${BACKUP_DIR}"/2*/ 2>/dev/null \ | sort \ | head -n -"${GENERATION}" \ | while IFS= read -r old_dir; do log "削除: ${old_dir}" rm -rf "${old_dir}" done } main() { log "===== バックアップ処理を開始 =====" take_backup rotate_backups log "===== バックアップ処理が完了 =====" } main
$ sudo bash backup-rotation.sh [2026-08-06 02:00:14] ===== バックアップ処理を開始 ===== [2026-08-06 02:00:14] バックアップ開始: /var/www/html -> /mnt/backup/20260806-020014 [2026-08-06 02:01:52] バックアップ成功: /mnt/backup/20260806-020014 [2026-08-06 02:01:52] 世代管理: 最新7世代を保持して古いものを削除します [2026-08-06 02:01:52] 削除: /mnt/backup/20260730-020003/ [2026-08-06 02:01:53] ===== バックアップ処理が完了 ===== $ ls /mnt/backup/ | sort 20260731-020004/ 20260802-020007/ 20260804-020009/ 20260806-020014/ 20260801-020003/ 20260803-020001/ 20260805-020015/ # 7世代が保持され、20260730が削除された
世代管理を含むシェルスクリプトの設計パターンを体系的に学びたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で即使える設計の「型」を実機ハンズオンで身につけられます。
応用:rsync --link-destでハードリンク差分バックアップにする
通常のrsyncバックアップは世代ごとにファイルの実体をコピーするため、世代数 × バックアップサイズのディスクを消費します。--link-destオプションを使うと、前の世代と変わらないファイルはハードリンク(同一inodeへの参照)として扱われ、変更されたファイルだけが実コピーされます。take_backup_incremental() { local datedir datedir="$(date +%Y%m%d-%H%M%S)" local dest="${BACKUP_DIR}/${datedir}" # 直前の世代(最新のバックアップディレクトリ)を取得 local latest latest=$(ls -d "${BACKUP_DIR}"/2*/ 2>/dev/null | sort | tail -n 1) mkdir -p "${dest}" if [ -n "${latest}" ]; then # --link-dest: latest内と内容が同じファイルはハードリンクで参照 rsync -av --link-dest="${latest}" "${SRC}/" "${dest}/" else # 初回(直前世代なし)はフルコピー rsync -av "${SRC}/" "${dest}/" fi }
# 各世代を個別確認(ハードリンク込みなので大きく見える) $ du -sh /mnt/backup/*/ 1.2G /mnt/backup/20260804-020009/ 1.2G /mnt/backup/20260805-020015/ 1.2G /mnt/backup/20260806-020014/ # /mnt/backup 全体の実際のディスク消費量 $ du -sh /mnt/backup/ 1.4G /mnt/backup/ # フルコピー3件なら3.6Gのはずが、差分分だけで済んでいる
tarアーカイブ形式でバックアップを取っている場合の世代管理も、基本的な考え方は同じです。
*.tar.gzファイルを日付名で作成し、ls *.tar.gz | sort | head -n -"${GENERATION}"で古いファイルを削除します。tarコマンドの実践的な使い方はこちらで解説しています。cronへの組み込みと動作確認
1. crontabへの登録
スクリプトを/usr/local/bin/backup-rotation.shとして配置し、rootのcrontabに登録します。# スクリプトを配置して実行権限付与 $ sudo cp backup-rotation.sh /usr/local/bin/backup-rotation.sh $ sudo chmod 750 /usr/local/bin/backup-rotation.sh # rootのcrontabを編集 $ sudo crontab -e # 毎日午前2時に実行(標準エラーもsyslogへ転送) 0 2 * * * /usr/local/bin/backup-rotation.sh 2>&1 | logger -t backup-rotation
logger -t backup-rotationでsyslogにタグ付きで記録されるため、grep backup-rotation /var/log/messagesでcronからの実行ログをまとめて確認できます。2. 手動実行で世代数を確認する
cronに登録する前に手動実行し、世代が正しく管理されることを確認します。# 手動実行 $ sudo bash /usr/local/bin/backup-rotation.sh # バックアップ先の世代数を確認(GENERATION変数の値と一致するか) $ ls -d /mnt/backup/2*/ | wc -l 7 # 最古世代と最新世代を確認 $ ls -d /mnt/backup/2*/ | sort | head -n 1 /mnt/backup/20260731-020004/ # 最古 $ ls -d /mnt/backup/2*/ | sort | tail -n 1 /mnt/backup/20260806-020014/ # 最新
トラブルシュート・よくある落とし穴
「ls: cannot access '/mnt/backup/2*': No such file or directory」が出る
バックアップが1件も存在しない初期状態でls -d "${BACKUP_DIR}"/2*/を実行するとこのエラーが出ます。set -euo pipefailが有効だとlsの失敗でスクリプトが即終了します。2>/dev/nullを付けることで「0件の場合は空のリストを返す」動作になり、sort・head・whileがスキップされます。# NG: バックアップが0件の場合にエラー終了する ls -d "${BACKUP_DIR}"/2*/ # OK: 0件の場合はエラーを抑制して空リストを返す ls -d "${BACKUP_DIR}"/2*/ 2>/dev/null
cronから実行するとrsyncが見つからない
cronの環境変数PATHは対話シェルより短く(/usr/bin:/bin程度)、/usr/local/binのrsyncが見つからない場合があります。スクリプト先頭でPATHを明示することで防げます。#!/bin/bash # cronからも確実にrsyncを参照できるようPATHを先頭で明示する PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin export PATH
世代数が設定より多く残る・少なくなる
ls -d "${BACKUP_DIR}"/2*/パターンが意図しないディレクトリにもマッチすると世代カウントがずれます。たとえばバックアップ先に2023-archive/のようなディレクトリが混在しているケースです。対策として、YYYYMMDD-HHMMSS形式(8桁-6桁)だけにマッチするグロブパターンに絞ります。
# 8桁数字-6桁数字(YYYYMMDD-HHMMSS形式)のみを対象にする ls -d "${BACKUP_DIR}"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]/ 2>/dev/null
本記事のまとめ
| やりたいこと | 実装方法 |
|---|---|
| 日付付きバックアップディレクトリを作成する | mkdir -p "${BACKUP_DIR}/$(date +%Y%m%d-%H%M%S)" |
| 古い世代を削除する(最新N件を保持) | ls -d .../2*/ | sort | head -n -N | while read dir; do rm -rf "$dir"; done |
| rsync失敗時に不完全バックアップを削除する | rsyncの終了コードを$?で確認し、非ゼロならrm -rf "${dest}"して終了 |
| 差分のみコピーしてディスクを節約する | rsync --link-dest=直前世代ディレクトリ "${SRC}/" "${dest}/" |
| cronから確実に動かす | スクリプト先頭でPATHを明示してexportする |
| 0件状態でlsエラーが出ないようにする | ls -d "${BACKUP_DIR}"/2*/ 2>/dev/null |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:シェルスクリプトでcurlとjqを使ってREST APIを自動操作する方法|認証ヘッダー・エラー処理・ページング設計の実装パターン
- 前のページへ:シェルスクリプトの冪等設計|何度実行しても安全な初期設定スクリプトをステートファイルと条件分岐で実現する方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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