業務システム開発・保守の実務メモを発信中

Bashでバックアップファイルを存在・更新日時・サイズ・世代数で確認する

バックアップジョブが正常終了していても、バックアップファイルが期待どおり残っているとは限りません

たとえば、保存先ディレクトリを間違えていて対象ファイルが1件もない。前回のバックアップから時間が空きすぎている。ファイルはあるけれど、極端に小さい。世代管理に失敗して、必要な数が残っていない。

こういう状態は、ジョブの終了コードだけでは見落とすことがあります。

ここでは GNU/Linux + Bash を前提に、バックアップ後のファイルを次の4条件で確認するスクリプトを作ります。

  • 対象ファイルが存在するか
  • 最新バックアップの更新日時が許容範囲内か
  • 最新バックアップのサイズが最小値以上か
  • 必要な世代数が残っているか

なお、バックアップ処理そのものは作りません。すでにバックアップジョブがある前提で、後からファイルの状態を確認するためのスクリプトです。

tomo

バックアップを作る処理と、できたファイルを確認する処理は分けておくと扱いやすくなります。既存のバックアップジョブを大きく変えず、後段に軽い確認だけを足せるためです。

目次

確認する条件

今回作るスクリプトでは、次の設定値を変えられるようにします。

設定値意味既定値
BACKUP_DIRバックアップファイルを探すディレクトリ./backups
FILE_PATTERN対象ファイル名パターンbackup-*.tar.gz
MAX_AGE_SECONDS最新バックアップの許容経過秒数90000
MIN_SIZE_BYTES最新バックアップの最小サイズ1024
MIN_GENERATIONS必要な最低世代数3

更新日時とサイズは、対象ファイルすべてではなく最新バックアップ1件だけを見ます。

GNU coreutilsのstatは、フォーマット指定でファイル情報を取得できます。ここでは、mtimeをUnix秒で取るためにstat -c '%Y'、サイズをバイト数で取るためにstat -c '%s'を使います。

mtimeは、ファイルの内容が最後に変更された時刻です。atimeやctimeとは別のタイムスタンプなので、バックアップファイルの新しさを見る目的ではmtimeを使います。

スクリプト

check-backup-files.sh は次の内容です。

Bash
#!/usr/bin/env bash
set -u

BACKUP_DIR="${BACKUP_DIR:-./backups}"
FILE_PATTERN="${FILE_PATTERN:-backup-*.tar.gz}"
MAX_AGE_SECONDS="${MAX_AGE_SECONDS:-90000}"
MIN_SIZE_BYTES="${MIN_SIZE_BYTES:-1024}"
MIN_GENERATIONS="${MIN_GENERATIONS:-3}"

die() {
  local code="$1"
  shift
  printf 'NG: %s\n' "$*" >&2
  exit "$code"
}

[[ -d "$BACKUP_DIR" ]] || die 1 "backup directory does not exist: $BACKUP_DIR"

# 一致なしをパターン文字列ではなく空配列として扱う
shopt -s nullglob
candidates=("$BACKUP_DIR"/$FILE_PATTERN)
shopt -u nullglob

# 世代数として数える対象を通常ファイルだけに絞る
backup_files=()
for candidate in "${candidates[@]}"; do
  [[ -f "$candidate" ]] && backup_files+=("$candidate")
done

((${#backup_files[@]} > 0)) || die 1 "no backup files found: dir=$BACKUP_DIR pattern=$FILE_PATTERN"
((${#backup_files[@]} >= MIN_GENERATIONS)) || die 4 "backup generations are not enough: actual=${#backup_files[@]} required=$MIN_GENERATIONS"

latest_file=''
latest_mtime=0

# 通常ファイルの中から、mtimeが一番新しいものを判定対象にする
for file in "${backup_files[@]}"; do
  mtime="$(stat -c '%Y' "$file")" || die 5 "failed to read mtime: $file"
  if ((mtime > latest_mtime)); then
    latest_mtime="$mtime"
    latest_file="$file"
  fi
done

now="$(date +%s)"
age_seconds=$((now - latest_mtime))
size_bytes="$(stat -c '%s' "$latest_file")" || die 5 "failed to read size: $latest_file"

((age_seconds <= MAX_AGE_SECONDS)) || die 2 "latest backup is too old: file=$latest_file age=${age_seconds}s max=${MAX_AGE_SECONDS}s"
((size_bytes >= MIN_SIZE_BYTES)) || die 3 "latest backup is too small: file=$latest_file size=${size_bytes}B min=${MIN_SIZE_BYTES}B"

printf 'OK: latest=%s age=%ss size=%sB generations=%s\n' "$latest_file" "$age_seconds" "$size_bytes" "${#backup_files[@]}"

コメントを、処理の意図を取り違えやすいところにだけ入れています。

nullglobは、一致するファイルがない場合にパターン文字列そのものを候補に残さないための設定です。backup_filesを作る箇所では、世代数として数える対象を通常ファイルに絞っています。最後の探索ループでは、その通常ファイルの中からmtimeが一番新しいものを選び、更新日時とサイズの判定対象にします。

異常時はNG:で始まるメッセージを標準エラーへ出し、非0の終了コードで終わります。

このスクリプトでは、異常の種類ごとに終了コードを分けています。

  • 1: ディレクトリがない、対象ファイルがない、通常ファイルがない
  • 2: 最新バックアップが古い
  • 3: 最新バックアップが小さい
  • 4: 世代数が足りない
  • 5: statで情報を取得できない

cronや監視処理から呼ぶ場合は、終了コードが0かどうかで正常・異常を判定できます。どの条件で落ちたかはメッセージを見れば分かります。

nullglobで0件を0件として扱う

少しだけ注意したいのが、対象ファイルの取得です。

Bashの通常のファイル名展開では、パターンに一致するファイルがない場合、パターン文字列がそのまま残ります。

たとえば、backup-*.tar.gzに一致するファイルがない場合でも、設定によってはbackup-*.tar.gzという文字列を1件のように扱ってしまう余地があります。

そこで、スクリプトでは一時的にnullglobを有効にしています。

Bash
shopt -s nullglob
candidates=("$BACKUP_DIR"/$FILE_PATTERN)
shopt -u nullglob

nullglobが有効な状態では、一致ファイルがないパターンは空になります。その後、通常ファイルだけをbackup_filesへ残します。

Bash
backup_files=()
for candidate in "${candidates[@]}"; do
  [[ -f "$candidate" ]] && backup_files+=("$candidate")
done

これで、対象ファイルなしを${#backup_files[@]}の0件として判定できます。パターンに一致するディレクトリがあっても、世代数には含めません。

tomo

世代数は「バックアップらしい名前のもの」ではなく、実際に通常ファイルとして残っている数で見るのがポイントです。ここを分けておくと、ディレクトリや未展開のパターンを世代として数える事故を避けやすくなります。

正常時の実行例

ダミーのバックアップファイルを3世代用意し、最新ファイルのmtimeとサイズが条件を満たす状態で実行しました。

実行条件は次のとおりです。

Bash
BACKUP_DIR=evidence/backups \
FILE_PATTERN="backup-*.tar.gz" \
MAX_AGE_SECONDS=90000 \
MIN_SIZE_BYTES=1024 \
MIN_GENERATIONS=3 \
./check-backup-files.sh
printf 'exit_code=%s\n' "$?"

出力はこうなりました。

Markdown
OK: latest=evidence/backups/backup-20260811.tar.gz age=7200s size=2048B generations=3
exit_code=0

3世代が残っていて、最新バックアップの経過時間は7200秒、サイズは2048Bです。どちらも設定値の範囲内なので、終了コードは0になりました。

最新バックアップが古い場合

代表的な異常例として、3世代のファイルは残っているものの、最新バックアップのmtimeが許容時間を超えている状態を作りました。

同じ条件で実行すると、次のようになります。

Bash
BACKUP_DIR=evidence/backups \
FILE_PATTERN="backup-*.tar.gz" \
MAX_AGE_SECONDS=90000 \
MIN_SIZE_BYTES=1024 \
MIN_GENERATIONS=3 \
./check-backup-files.sh
printf 'exit_code=%s\n' "$?"
Markdown
NG: latest backup is too old: file=evidence/backups/backup-20260811.tar.gz age=259200s max=90000s
exit_code=2

ファイル自体は存在し、世代数も足りています。ただ、最新バックアップの経過時間が259200秒で、許容値の90000秒を超えています。

この場合、スクリプトは終了コード2を返します。

この場合、ファイルの有無や世代数ではなく「直近のバックアップが更新されていない」異常として扱えます。まず見るべきなのは、バックアップジョブの実行結果やスケジュールずれ、となります。

このスクリプトで見ていないこと

このスクリプトは、バックアップファイルの外側だけを見ています。

確認しているのは、存在、mtime、サイズ、世代数です。バックアップファイルの中身が正しいか、実際にリストアできるか、破損していないかまでは見ていません。

そこまで確認したい場合は、別途リストア確認やチェックサム確認が必要です。

この記事では、バックアップ確認用の小さなBashスクリプトをGNU/Linux上で動かす前提にしています。macOSでも同じ考え方は使えますが、statの呼び出し部分は環境に合わせて書き換える必要があります。この記事では、複数OSに対応する分岐は入れません。

FILE_PATTERNも、管理者がスクリプト内や環境変数で決める固定値として扱う想定です。たとえば backup-*.tar.gz のように、バックアップジョブが出力するファイル名規則に合わせて指定します。

まとめ

バックアップ確認では、ジョブが動いたかどうかだけを見ると足りないことがあります。

最低限、対象ファイルがあるか、最新バックアップが古すぎないか、極端に小さくないか、必要な世代数が残っているかを確認しておくと、よくある取りこぼしに気づきやすくなります。

今回のように終了コードを分けておけば、cronや監視処理からも扱いやすくなります。

バックアップの中身や復元可否までは別の確認が必要ですが、ファイルが残っているかを機械的に見るだけでも、運用上の安心感はだいぶ変わります。

参考資料

よかったらシェアしてね!
  • URLをコピーしました!
目次