【実践シェルスクリプト講座 第2回】「消えちゃった…」では遅すぎる!自動バックアップと通知システムを構築しよう
こんにちは!「リナックス先生」です。
前回は、サーバーのディスク容量不足を防ぐための「監視スクリプト」と「ログローテーション」を作成しましたね。
コウ君、その後サーバーの管理はどう?順調にいじれてる?
先生!前回のスクリプトのおかげで、ログが溢れる心配はなくなりました!
…でも、実はさっき、設定ファイルを編集していて、うっかり rm -rf コマンドのパス指定を間違えてしまって…。
大事な画像データが入ったディレクトリが一瞬で消えてしまったんです(泣)。
これ、ゴミ箱とかに残ってないんですか…?
あちゃー…。WindowsやMacと違って、Linuxのコマンドラインで消したファイルは、基本的に「即座に完全消去」されるのよ。
復旧業者に頼めばなんとかなるかもしれないけど、何十万円もかかるわ。
エンジニアにとって「バックアップ」は、保険ではなく「命綱」なの。
今回は、二度とその悲劇を繰り返さないための「自動バックアップシステム」を作るわよ!
「実践シェルスクリプト講座」第2回のテーマは、全エンジニア必須のスキル「バックアップ」です。
単にファイルをコピーするだけの初心者スクリプトではなく、プロの現場で使われている「世代管理(古いバックアップの自動削除)」、「rsyncによる高速同期」、そして「処理が失敗した時にスマホへ通知を送る仕組み」まで、実務レベルの堅牢なシステムを構築します。
今回の記事を読み終える頃には、あなたのサーバーは「いつ何が起きても、昨日の状態にすぐ戻せる」という鉄壁の要塞に生まれ変わっているはずです。
長丁場になりますが、一つ一つのコードの意味をしっかり理解しながら進めていきましょう。
本講座のカリキュラム(全4回)
本シリーズでは、コピペで終わらせず「なぜその書き方をするのか」という背景まで深く掘り下げて解説します。
- サーバーの異変を未然に防ぐ!「ディスク容量監視」と「ログローテーション」【完了】
- 【今回】バックアップの自動化:tarとrsyncを駆使し、エラー通知まで実装する
- ユーザー管理の効率化:CSVファイルから大量のユーザーとSSH鍵を一括生成する
- セキュリティ監視:不正アクセス(SSH総当たり攻撃)を検知してIPを自動ブロックする
1. バックアップにおける「2つの戦略」と使い分け
一口に「バックアップ」と言っても、データの種類や目的によって最適な方法は異なります。
すべてのファイルを毎回コピーしていては、ディスク容量も時間もいくらあっても足りません。
まずは、Linux運用における代表的な2つの戦略を理解しましょう。
戦略A:アーカイブバックアップ(tar使用)
複数のファイルやディレクトリを、「1つの圧縮ファイル」にまとめて保存する方法です。
- メリット: 「2024年1月1日時点の状態」というように、時点(スナップショット)ごとの管理がしやすい。万が一改ざんされても、過去のファイルに戻しやすい。
- デメリット: 毎回全データを圧縮するため、データ量が多いと時間がかかり、保存容量も食う。
- 向いているデータ: 設定ファイル(/etc)、Webサイトのソースコード、データベースのダンプファイルなど、テキストベースで圧縮効果が高いもの。
戦略B:同期バックアップ(rsync使用)
バックアップ元とバックアップ先のディレクトリの中身を比較し、「変更があった部分だけ」を転送する方法です(ミラーリングとも呼ばれます)。
- メリット: 差分転送なので、2回目以降は非常に高速。常に「最新の状態」をコピー先に保持できる。
- デメリット: 基本的に「最新の状態」しか持たないため、「3日前のファイルに戻したい」といった要望には(工夫しないと)応えにくい。元データを消すとバックアップ先も消える設定になりがち。
- 向いているデータ: 画像・動画などのメディアファイル、ユーザーがアップロードした大量のデータ群。
今回は、この両方のパターンに対応できるスクリプトを作成します。
2. 「tar」で作る世代管理付きバックアップスクリプト
まずは基本となる tar コマンドを使ったバックアップです。
しかし、ただ圧縮するだけではディスクがいっぱいになってしまいます。
「過去7日分だけ残して、それより古いものは自動で捨てる」という「世代管理」機能を実装しましょう。
Step 1: バックアップ対象と保存先を決める
今回は例として、WordPressなどのWebサイトデータが入っている /var/www/html をバックアップ対象とします。
保存先は /backup/www とします。
Step 2: スクリプトの作成(backup_tar.sh)
vim backup_tar.sh でファイルを作成し、以下のコードを記述してください。
#!/bin/bash
# ==========================================
# 設定エリア(環境に合わせて変更してください)
# ==========================================
# バックアップ対象のディレクトリ
TARGET_DIR="/var/www/html"
# バックアップファイルを保存するディレクトリ
BACKUP_DIR="/backup/www"
# ファイル名の先頭につける文字
FILE_PREFIX="webapp_backup"
# 保存しておく期間(日)
RETENTION_DAYS=7
# ログファイル
LOG_FILE="/var/log/backup_tar.log"
# ==========================================
# 変数定義エリア
# ==========================================
# 今日の日付(ファイル名用):例 20240120_1230
DATE_STR=$(date +%Y%m%d_%H%M)
# 出力するファイル名
OUTPUT_FILE="${BACKUP_DIR}/${FILE_PREFIX}_${DATE_STR}.tar.gz"
# ==========================================
# メイン処理開始
# ==========================================
# ログへの書き出し関数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
log "--- バックアップ処理を開始します ---"
# 1. 保存先ディレクトリの確認と作成
# -d はディレクトリが存在するかチェック
if [ ! -d "$BACKUP_DIR" ]; then
log "保存先ディレクトリが存在しないため作成します: $BACKUP_DIR"
mkdir -p "$BACKUP_DIR"
if [ $? -ne 0 ]; then
log "【ERROR】ディレクトリ作成に失敗しました。処理を中断します。"
exit 1
fi
fi
# 2. tarコマンドによる圧縮バックアップ実行
log "対象ディレクトリ: $TARGET_DIR を圧縮中..."
# tarコマンドのオプション解説
# c: 新規作成 (Create)
# z: gzip形式で圧縮 (Zip)
# v: 処理内容を表示 (Verbose) ※ログに残すため
# f: ファイルに出力 (File)
# P: 絶対パスをそのまま保持 (Absolute Path) ※復元時に注意が必要だが便利
tar -czf "$OUTPUT_FILE" -P "$TARGET_DIR" 2>> "$LOG_FILE"
# 直前のコマンド($?)の成功可否をチェック
if [ $? -eq 0 ]; then
log "バックアップ作成成功: $OUTPUT_FILE"
else
log "【ERROR】tarコマンドが失敗しました!詳細はログを確認してください。"
exit 1
fi
# 3. 古いバックアップの削除(世代管理)
log "保存期間(${RETENTION_DAYS}日)を過ぎた古いファイルを検索します..."
# findコマンド解説
# $BACKUP_DIR : 探す場所
# -name : ファイル名のパターン
# -mtime +N : N日より「前」に更新されたファイル
# -type f : ファイルのみを対象にする(ディレクトリは消さない)
# -delete : 削除を実行
# -print : 削除したファイル名を表示(確認用)
# 削除されるファイル一覧をログに残すため、一度変数に入れるか、-printの結果をログへ
DELETED_FILES=$(find "$BACKUP_DIR" -name "${FILE_PREFIX}_*.tar.gz" -type f -mtime +$RETENTION_DAYS -print)
if [ -n "$DELETED_FILES" ]; then
# 実際に削除を実行
find "$BACKUP_DIR" -name "${FILE_PREFIX}_*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete
log "以下の古いバックアップを削除しました:"
echo "$DELETED_FILES" >> "$LOG_FILE"
else
log "削除対象の古いファイルはありませんでした。"
fi
log "--- 全ての処理が完了しました ---"
💡 プロの解説:なぜ「世代管理」が必要なのか?
初心者がよくやる失敗が、backup.tar.gz という固定の名前で毎日上書きしてしまうことです。
これだと、「昨日ファイルが壊れたことに今日気づいた」場合、昨日のバックアップは今日の(壊れた状態の)バックアップで上書きされてしまっているため、復旧できません。
ファイル名に日付を入れ、過去数日分を保持しておくことで、「ファイルが壊れる前の時点」まで時間を巻き戻すことが可能になるのです。
Step 3: スクリプトの動作解説
このスクリプトには、堅牢なシステムに必要な要素が詰め込まれています。
- ディレクトリ自動作成: 保存先フォルダを作り忘れていても、
mkdir -pで自動生成してエラーを防ぎます。 - 終了コードの確認:
if [ $? -eq 0 ];を使い、tarコマンドが本当に成功したかどうかを厳密にチェックしています。圧縮中に容量不足で失敗した場合などを見逃しません。 - ログ出力機能: 画面に出すだけでなく、
/var/log/backup_tar.logに時刻付きで記録を残します。これにより、「いつ失敗したか」を後から追跡できます。
3. プロ御用達「rsync」による高速同期バックアップ
次に、画像ファイルが数十GBもあるような場合に適した rsync のスクリプトを作成します。cp コマンドと違い、転送中にネットワークが切れても途中から再開できたり、変更されたファイルだけを送るので非常に高速です。
ここでは、「外付けHDD(または別パーティション)」へデータをミラーリングする シナリオを想定します。
スクリプト作成(backup_rsync.sh)
vim backup_rsync.sh を作成します。
#!/bin/bash
# ==========================================
# 設定エリア
# ==========================================
# 同期元データ
SOURCE_DIR="/home/user/images/"
# 同期先(外付けHDDのマウントポイントなど)
DEST_DIR="/mnt/backup_disk/images_mirror/"
# ログファイル
LOG_FILE="/var/log/backup_rsync.log"
# ロックファイル(多重起動防止用)
LOCK_FILE="/var/run/backup_rsync.lock"
# ==========================================
# 関数定義
# ==========================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# ==========================================
# メイン処理
# ==========================================
# 1. 多重起動の防止
# rsyncは時間がかかることがあるため、前の処理が終わっていないのに
# 次のcronが動いてしまうとサーバーが重くなる。それを防ぐ仕組み。
if [ -f "$LOCK_FILE" ]; then
# ロックファイルがあったら、プロセスが生きているか確認
PID=$(cat "$LOCK_FILE")
if ps -p "$PID" > /dev/null; then
log "【SKIP】前回のバックアップ処理(PID:$PID)が実行中のため、今回はスキップします。"
exit 0
else
# ロックファイルはあるがプロセスがない(異常終了した)場合
log "【WARN】ロックファイルが残っていましたがプロセスが存在しません。ロックを解除して実行します。"
fi
fi
# 現在のプロセスIDをロックファイルに書き込む
echo $$ > "$LOCK_FILE"
# スクリプト終了時(正常・異常問わず)にロックファイルを必ず消す設定
trap 'rm -f "$LOCK_FILE"; exit' INT TERM EXIT
log "--- rsync同期処理を開始します ---"
# 保存先ディレクトリ確認
if [ ! -d "$DEST_DIR" ]; then
mkdir -p "$DEST_DIR"
fi
# 2. rsync実行
# -a : アーカイブモード(所有者、権限、日付などをそのまま維持)
# -v : 詳細出力
# -h : 人間が読みやすい単位(KB, MB)で表示
# --delete : 元データで削除されたファイルは、同期先でも削除する(完全同期)
# --exclude : 同期したくないファイル(隠しファイルやキャッシュなど)を除外
rsync -avh --delete --exclude '.cache' "$SOURCE_DIR" "$DEST_DIR" >> "$LOG_FILE" 2>&1
# 結果判定
if [ $? -eq 0 ]; then
log "rsync同期が正常に完了しました。"
else
log "【ERROR】rsync同期中にエラーが発生しました!"
# ここでエラー通知処理(後述)を入れるのがベスト
exit 1
fi
log "--- 処理終了 ---"
--delete オプション、怖すぎませんか?
もし僕が間違って元のファイルを全部消した状態でこのスクリプトが動いたら、バックアップ先も全部消えちゃいますよね?
それじゃバックアップの意味がないような気が…。
鋭い指摘ね!その通りよ。rsync のミラーリングはあくまで「予備のハードディスクを持っておく」イメージ。
人為的なミス(誤削除)対策には向かないの。
だから、重要データについては tarによる「世代管理」 と、rsyncによる 「即時同期」 を組み合わせるのが最強の運用なのよ。
発展:SSHを使ったリモートバックアップ
上記のスクリプトは同一サーバー内でのコピーですが、rsync の真価はネットワーク越しに発揮されます。
サーバー自体が物理的に壊れたり、データセンターが災害に遭った場合に備え、別の場所にあるサーバーへ転送するには、宛先を以下のように書き換えるだけです。
# リモートサーバーの情報を指定 # 形式: ユーザー名@ホスト名:ディレクトリパス DEST_DIR="user@backup-server.com:/var/backup/mirror/" # rsyncコマンドに -e ssh を追加 rsync -avh --delete -e "ssh -i /root/.ssh/id_rsa" "$SOURCE_DIR" "$DEST_DIR"
これを行うには、事前にSSHの「公開鍵認証」を設定し、パスワードなしでログインできるようにしておく必要があります(これは第3回以降で詳しく触れますが、興味がある人は「ssh-keygen」で検索してみてください)。
4. 失敗したら教えて!「通知機能」の実装
Cronでバックアップを自動化しても、失敗したことに気づかなければ意味がありません。
「バックアップ取れてると思ったら、半年前に止まってました」というのが、インフラエンジニアが最も顔面蒼白になる瞬間です。
そこで、スクリプトが失敗した時だけ、チャットツール(今回はSlackやDiscord、Microsoft Teamsなどで使えるWebhook)に通知を送る汎用関数を作成しましょう。
Step 1: Webhook URLの取得
各チャットツールには「Incoming Webhook」という機能があります。
「このURLにデータを投げれば、特定のチャンネルにメッセージとして表示するよ」という専用のポストです。
Slackなら「アプリ管理」から、Discordなら「チャンネル編集」→「連携サービス」から取得できます。
Step 2: 通知機能付きスクリプトの完成形
先ほどの backup_tar.sh に、エラー通知機能を組み込んでみましょう。
ここでは汎用性を高めるため、通知処理を関数化します。
#!/bin/bash
# ==========================================
# 設定エリア
# ==========================================
TARGET_DIR="/var/www/html"
BACKUP_DIR="/backup/www"
FILE_PREFIX="webapp_backup"
DATE_STR=$(date +%Y%m%d_%H%M)
OUTPUT_FILE="${BACKUP_DIR}/${FILE_PREFIX}_${DATE_STR}.tar.gz"
# Webhook URL (あなたのURLに書き換えてください)
WEBHOOK_URL="https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ"
# 通知するサーバー名(どのサーバーで起きたかわかるように)
SERVER_NAME=$(hostname)
# ==========================================
# 通知用関数の定義
# ==========================================
send_alert() {
local message=$1
# JSON形式のデータを作成
# Slack/Discord向け(textフィールドにメッセージを入れる)
# エスケープ処理などは簡易的なもの
payload="{\"text\": \"🚨 [${SERVER_NAME}] バックアップエラー発生!\n${message}\"}"
# curlコマンドでPOST送信
curl -s -X POST -H 'Content-type: application/json' --data "$payload" "$WEBHOOK_URL"
}
# ==========================================
# メイン処理
# ==========================================
# (中略... mkdirなどの処理)
# tar実行時にエラーログを一時ファイルに吐き出す
ERROR_LOG="/tmp/backup_error.txt"
tar -czf "$OUTPUT_FILE" "$TARGET_DIR" 2> "$ERROR_LOG"
if [ $? -eq 0 ]; then
echo "バックアップ成功"
else
echo "バックアップ失敗"
# エラーログの中身を読み取る
ERR_MSG=$(cat "$ERROR_LOG")
# 通知関数を呼び出す
send_alert "tarコマンドが失敗しました。\n詳細:\n${ERR_MSG}"
# 失敗したのでスクリプトを終了
exit 1
fi
# 一時ファイルの掃除
rm -f "$ERROR_LOG"
この仕組みを入れておけば、「便りがないのは良い便り(成功)」という運用が可能になり、毎朝ログファイルを目視確認する必要がなくなります。
エラーが起きた瞬間、あなたのスマホが震えて教えてくれるのです。
5. 復旧(リストア)できなきゃ意味がない
最後に、一番重要なことを伝えます。
「リストア(復元)テストをしていないバックアップは、バックアップではない」。
いざ本番環境でデータが消えた時、焦っている状態で正しく解凍コマンドを打てますか?
「バックアップファイル自体が壊れていた」「解凍したらパスが違って動かなかった」というトラブルは日常茶飯事です。
必ず、以下の手順で復旧テストを行ってください。
リストア訓練の手順
安全な場所(/tmpなど)で解凍実験を行います。
# 1. テスト用ディレクトリ作成 mkdir -p /tmp/restore_test cd /tmp/restore_test # 2. バックアップファイルをコピーしてくる(本物をいじらない!) cp /backup/www/webapp_backup_20240120_1230.tar.gz . # 3. 解凍コマンド実行 # x: eXtract (解凍) # z: gzip形式 # v: 詳細表示 # f: ファイル指定 tar -xzvf webapp_backup_20240120_1230.tar.gz # 4. 中身の確認 ls -R # 意図したディレクトリ構造でファイルが出てくるか確認する
⚠️ 恐怖の「絶対パス」トラップ
今回のスクリプトでは tar に -P オプション(絶対パス保存)を付けていません。
もし付けていた場合、解凍した瞬間に /var/www/html/... という元の場所にファイルを上書き展開してしまう可能性があります。
他人のサーバーや、テスト環境で本番データを開くときは、tar -t オプション(解凍せずに中身のリストだけ見る)を使って、パス構造を事前に確認する癖をつけましょう。
まとめ:守りの要を自動化せよ
今回は、エンジニアにとって最も重要な「バックアップ」を自動化しました。
単なるコマンド紹介ではなく、エラー処理や通知機能を含めた「運用に耐えうるスクリプト」を目指しました。
| ツール | 用途 | 重要オプション・ポイント |
|---|---|---|
tar |
時点ごとの保存(世代管理) | czvf (作成), find -mtime で古い削除 |
rsync |
ディレクトリ同期(ミラーリング) | -av --delete (完全同期), ロックファイル制御 |
curl |
外部通知(Webhook) | -X POST でJSONデータを送信 |
これで、第1回の「ディスク容量監視」、第2回の「データバックアップ」が揃いました。
あなたのサーバー運用は、手動作業から解放され、より安全でスマートなものに進化したはずです。
次回予告:面倒な「ユーザー登録」を1秒で終わらせる
サーバーが安定し、守りも固まりました。
次は「攻撃」…ではなく、「効率化」のターンです。
例えば、新入社員が10人入ってきて、「全員分のユーザーアカウントとパスワードを作って、それぞれの公開鍵を設定しておいて」と頼まれたらどうしますか?
GUIでポチポチやりますか?手打ちでコマンドを叩きますか?
次回は、CSVファイルの名簿を読み込んで、ユーザーアカウント・初期パスワード・SSH設定を一括で全自動生成するスクリプトを作成します。while ループやテキスト処理を駆使した、まさに「プログラミング」らしい内容になりますよ!
コウ君、今日作ったスクリプトをCronに仕込むのを忘れないでね。
おすすめは、負荷の低い深夜(AM 3:00〜5:00くらい)に実行すること。
「転ばぬ先の杖」は、転ぶ前に持たないと意味がないからね!
▼スクリプトの実験場に最適!推奨VPS



コメント