本記事では、筆者が用いるバックアップスクリプトを元にして、バックアップの大切さを述べます。以前に作成した記事のリファインという形です。
『賢者の石』としてのバックアップ
LinuxのみならずとってITにおけるバックアップは、言うなれば『賢者の石/Philosopher's Stone』です。
ここまで断言できる根拠があります。筆者が前に述べたとおり、Linuxサーバは設定一つの変更、チューニング、誤操作などで全てが壊れる世界です。
そんな中、適切なバックアップを取っておくことで、失敗全てを帳消しにできる確率が飛躍的に高まります。
また、バックアップを取ることにより、他のサーバへの移し替えや「問題が発生した状況の再現」も容易になります。
筆者が手順で
sudo cp -pi /something/very/very/massive/important/file.conf /extra_sensitive_backup.conf.$(date +%Y%m%d)
等でバックアップを取り、
diff -u sudo cp -pi /extra_sensitive_backup.conf.$(date +%Y%m%d) /something/very/very/massive/important/file.conf
と差分を取るのも、「事前のちょっとした面倒な作業」は「後に発生する大災害」を救ってくれます。
特に重要なWebアプリでのDBバックアップの重要性
翻って、Webアプリケーションの運用となると、この「賢者の石」としてのバックアップの役割は、単なるシステム設定の保護を超え、Webサイトのみならず事業の存続に直結します。
なぜなら、Webアプリにおいて最も価値があり、最も頻繁に変化し続ける情報、すなわち『データ(データベース)』こそが、そのアプリケーションの「魂」だからです。
自分を含め
- 長年蓄積した記事
- 画像情報
- (商用利用の場合は)決済情報、顧客情報
これらデータベースに格納された情報は、失われた瞬間、企業の信用も収益もゼロになります。法的な責任も発生すれば、これらは簡単にマイナスになるという、筆者が前にも述べている「理不尽な非対称性」が発生します。
ファイルシステム全体が健在であっても、データベースだけが破損すれば、サイトは事実上停止します。これは、設定ファイル一つを誤った際の「システム停止」とは次元の異なる、「世界の消滅」を意味します。
だからこそ、WebアプリのDBバックアップには、以下の3つの要素が絶対的に求められます。
- 完全なバックアップ(データの保護):
mysqldumpなどを利用し、停止時やトランザクション中のデータも漏れなくダンプすること。 - 確実な安全性(暗号化): 個人情報を含む機密性の高いデータであるため、保管時には必ず強固なパスワードで暗号化し、不正アクセスからデータを守ること。
- 信頼できる世代管理: 毎日確実にバックアップを取り続け、かつ必要な日数分だけ保持し、古すぎるバックアップは自動で削除する仕組み(世代管理)が不可欠です。これにより、ディスク容量の圧迫を防ぎつつ、「昨日の正常な時点」や「1週間前の状態」へ確実に巻き戻せる保証を得るのです。
次に紹介するスクリプトは、このWebアプリの命綱たるデータベース (MySQL) を、安全性と世代管理を両立させながら、コマンド一つで確実に守り抜くために作成したものです。
事実、筆者はこの手のバックアップにより
- AWS
- WebArena
- XServer
と3つのVPSからのWebサイト移行を行いました。
その前に事前確認
Prevention is better than cure.(転ばぬ先の杖)というやつです。
これはUbuntu系での手順です。
MySQL での操作です。
mysqldump確認
which mysqldump
/usr/bin/mysqldumpなどと返ってくることを確認。見つからない場合は
sudo aptitude install mysql-client
- データベースユーザーのアクセス権確認・付与(重要)
データベースユーザー(例: your_db_user)が、バックアップ対象のデータベース($DATABASE_NAME)全体に対してデータを読み取る権限(SELECT)を持っている必要があります。
MySQL/MariaDBにrootでログインし、以下のコマンドで確認します。
SHOW GRANTS FOR 'your_db_user'@'localhost';
- 権限の付与:
もし権限が不足している場合は、rootでしてログインし、必要な権限を付与します。
データベースのダンプ(バックアップ)には、最低限 SELECT 権限が必要です。
また、--single-transactionオプションを使う場合、ほとんどのストレージエンジンでRELOAD(またはPROCESS)権限は必須ではありません。
ここまで来たら、いよいよスクリプトの作成です。
DBというシステムの生命線のバックアップたる「賢者の石」。パスの間違いなどで簡単に「持ってかれる」等価交換以上の代償を伴う作業。慎重にやっていきましょう。
さっくりとは言い難い手順
- 事前準備。DBアカウントファイルを作成します。
- 事前準備。バックアップなどを格納するディレクトリを作成します。
- 自分の環境に合わせて スクリプトを作成します。
- 動作の確認を行い、cron設定を行います。
必要なファイルとディレクトリ群、注意
| 項目 | 説明 | 設定値 (スクリプト内変数) |
|---|---|---|
| MySQLアカウントファイル | mysqldump実行に使用するデータベース接続情報ファイル。パーミッションは400必須。 | $CREDENTIALS_FILE (/home/hoge/script/config/db_account/db_account.txt) |
| バックアップ保管ルート | 圧縮後のバックアップファイルを保管するディレクトリ。 | $BACKUP_ROOT_DIR (/path/to/backup/directory) |
| パスワード保管ディレクトリ | バックアップZIPファイルの解凍パスワードを保管するディレクトリ。 | $PASSWORD_DIR (/home/hoge/dbrestore_password) |
| 実行ユーザー | スクリプトを実行するOSユーザー。 | /home/hogeの所有者 |
事前準備(ファイル・ディレクトリの作成とパーミッション設定)
スクリプトを実行する前に、以下のファイルとディレクトリを準備してください。
- データベースアカウントファイルの作成
$CREDENTIALS_FILEで指定されたパスに、MySQLの接続情報を記述したファイルを作成します。
- ディレクトリの作成:
mkdir -p /home/hoge/script/config/db_account - アカウントファイルの作成 (
/home/hoge/script/config/db_account/db_account.txt):
[mysqldump]
# [mysqldump]セクションが必要ですuser=your_db_user # ダンプ操作が可能なユーザー名 password="your_db_password" # ユーザーのパスワード
パーミッションの設定(重要):
スクリプトの実行条件であり、セキュリティ上必須です。
chmod 400 /home/hoge/script/config/db_account/db_account.txt
※重ねての注意点このDBアカウント情報はrootアカウントと同等以上のリスク管理が必要です。すなわち
「No shown, No stolen, No matter what. (見せても盗まれてもならぬ、何があっても)」
の強い覚悟が必要です。(とはいえ自分の命と天秤にかけた状態で)
バックアップディレクトリの作成
バックアップファイルとパスワードファイルを保管するディレクトリを作成します。
- バックアップルートディレクトリの作成:
bash mkdir -p /path/to/backup/directory - パスワード保管ディレクトリの作成:
bash mkdir -p /home/hoge/dbrestore_password - パーミッションの設定:
スクリプトを実行するOSユーザーが、これらのディレクトリに対して書き込み・読み取り・実行権限を持っていることを確認してください。通常はchownコマンドで所有者を設定します。
スクリプトの作成
backup_mysql.shを任意の方法で、或いは自身の教義・信仰に則ったエディタを用いて作成します。変数の部分は 自分の環境に合わせて 調整します。
#!/bin/bash
# コマンドの実行中にエラーが発生した場合、スクリプトを終了します
set -e
## 変数ここから ##
# $HOMEの変数を指定します。
HOME_DIR="/home/hoge"
# SQLをバックアップする**保管先**ディレクトリを指定します。運用に合わせて指定ください。
BACKUP_ROOT_DIR="/path/to/backup/directory"
# バックアップ処理用の一時ディレクトリ名を指定します。
TMP_SUBDIR="temp_dump"
# パスワードファイルを保管するディレクトリを指定します。
PASSWORD_DIR="${HOME_DIR}/dbrestore_password"
# 保持するバックアップの世代を日数で指定します。
KEEP_DAYS=7
# ファイルに付与する日付/作業ディレクトリ名/バックアップファイル名を指定します。
CURRENT_DATE=$(date +%Y%m%d%H%M%S) # 日付に時刻を加えて一意性を高める
BACKUP_BASE_NAME="database" # バックアップ対象のデータベース名などから命名
SQL_FILE_NAME="${BACKUP_BASE_NAME}.sql"
ZIP_FILE_NAME="${BACKUP_BASE_NAME}.${CURRENT_DATE}.zip"
PASSWORD_FILE_NAME="db-restore.${CURRENT_DATE}.txt"
# 一時的なバックアップディレクトリのフルパス
TMP_BACKUP_DIR="${BACKUP_ROOT_DIR}/${TMP_SUBDIR}"
# 圧縮後のバックアップファイルのフルパス
ZIP_FILE_PATH="${BACKUP_ROOT_DIR}/${ZIP_FILE_NAME}"
# パスワードファイルのフルパス
PASSWORD_FILE_PATH="${PASSWORD_DIR}/${PASSWORD_FILE_NAME}"
# アカウントファイルを指定します。運用に合わせて指定ください。
CREDENTIALS_FILE="${HOME_DIR}/script/config/db_account/db_account.txt"
# redmineのデータベース名を指定します。
DATABASE_NAME="database"
# バックアップ時に指定するオプションを指定します。
OPTIONS="--defaults-extra-file=$CREDENTIALS_FILE --no-tablespaces --single-transaction"
# 世代管理のためのファイルパターン
BACKUP_FILE_PATTERN="${BACKUP_BASE_NAME}.*.zip"
PASSWORD_FILE_PATTERN="db-restore.*.txt"
## 変数ここまで ##
## 処理ここから ##
# 1.アカウントファイルのパーミッションが400かどうかチェックします。
# 400以外は処理そのものを終了します。
permissions=$(stat -c "%a" "$CREDENTIALS_FILE")
if [ "$permissions" != "400" ]; then
echo "🚨 エラー: アカウントファイル(${CREDENTIALS_FILE})のパーミッションは400である必要があります。(現在: ${permissions})" >&2
exit 1
fi
# 2. 一時的なバックアップディレクトリを作成します。
# -pで存在しない親ディレクトリも作成し、既に存在してもエラーにしない
mkdir -p "${TMP_BACKUP_DIR}" || exit 1
mkdir -p "${PASSWORD_DIR}" || exit 1
# 3. mysqldumpを実行してデータベースのバックアップを取ります。
echo "⏳ ${DATABASE_NAME}のデータベースダンプを開始..."
mysqldump $OPTIONS -h localhost "$DATABASE_NAME" > "${TMP_BACKUP_DIR}/${SQL_FILE_NAME}" || {
echo "🚨 エラー: mysqldumpが失敗しました。" >&2
rm -rf "${TMP_BACKUP_DIR}" # 失敗したら一時ディレクトリをクリーンアップ
exit 1
}
echo "✅ データベースダンプ完了。"
# 4. パスワードによる暗号化を実施します。
password=$(openssl rand -base64 12)
# 【ルートディレクトリ圧縮回避のため、一時ディレクトリに移動して圧縮対象ファイルを直接指定】
echo "⏳ バックアップファイルの圧縮・暗号化を開始..."
cd "${TMP_BACKUP_DIR}" || exit 1
# zipコマンドで、SQLファイルのみを対象とし、圧縮後のzipを一つ上の階層に出力
zip -P "$password" "${ZIP_FILE_PATH}" "${SQL_FILE_NAME}" || {
echo "🚨 エラー: zip圧縮が失敗しました。" >&2
cd - > /dev/null # 元のディレクトリに戻る
rm -rf "${TMP_BACKUP_DIR}" # 失敗したら一時ディレクトリをクリーンアップ
exit 1
}
cd - > /dev/null # 元のディレクトリに戻る
echo "✅ 圧縮・暗号化完了: ${ZIP_FILE_PATH}"
# 5. 一時的なバックアップディレクトリを削除します。
echo "🗑️ 一時ディレクトリを削除: ${TMP_BACKUP_DIR}"
rm -rf "${TMP_BACKUP_DIR}"
# 6. 解凍パスワードを指定ディレクトリに保存し、権限を設定します。
echo "🔐 解凍パスワードを保存: ${PASSWORD_FILE_PATH}"
echo "$password" > "$PASSWORD_FILE_PATH"
# 7.パスワードの読み取り権限を600に変更します。
chmod 600 "$PASSWORD_FILE_PATH"
# 8. 【確実にバックアップファイルを世代管理 (古いファイルの削除)】
echo "🧹 保持期間(${KEEP_DAYS}日)より古いバックアップを削除..."
# バックアップファイル本体の削除
find "$BACKUP_ROOT_DIR" -maxdepth 1 -name "$BACKUP_FILE_PATTERN" -type f -mtime +$KEEP_DAYS -print -delete
# 対応するパスワードファイルの削除
find "$PASSWORD_DIR" -maxdepth 1 -name "$PASSWORD_FILE_PATTERN" -type f -mtime +$KEEP_DAYS -print -delete
echo "✅ 世代管理完了。"
## 処理ここまで ##
- スクリプトファイルの作成と配置:
このスクリプトをファイル(例:backup_mysql.sh)として保存します。bash # 例 cp (修正済みスクリプトの内容) /home/hoge/backup_mysql.sh - 実行権限の付与:
bash chmod +x /home/hoge/backup_mysql.sh - テスト実行:
bash /home/hoge/backup_mysql.sh - 定期実行の設定(Cronなど):
Cronを利用して、スクリプトを定期的に実行するように設定します。bash # crontab -e で開き、以下の行を追加 (例: 毎日午前2時に実行) 0 2 * * * /home/hoge/backup_mysql.sh > /dev/null 2>&1
スクリプトの動き
このスクリプトは、運用者(つまり私のようなサーバ管理者)望む機能を盛り込んだ機能を持たせています。
- 予め配備されたアカウント情報に基づき、
mysqldumpで、対象DBのみのバックアップを行います。 - バックアップと同時に圧縮&暗号化を行います。
- 復号に必要なパスワードは同時に別ディレクトリに保管。有り体に言えば、スクリプト自身も知らないランダムパスワードなので推測される可能性はほぼありません。
- その後、スクリプトはDBバックアップディレクトリとパスワード格納ディレクトリを精査。変数に基づいて、特定日数以上が過ぎたファイルを自動的に削除して、ディスクの容量を安定化します。
では、どうやって運用するの?
パスワードによる複合化
スクリプト実行後、対象の.zipファイルを
unzip database.20251028113148.zip
等として、スクリプト実行日時が入ったファイルを展開します。
このとき、パスワードを尋ねられるので、対になる複合化のパスワードファイルdb-restore.20251028113148.txtで開きます。database.sqlが平文で出てきます。
DBの切り戻し
こうして、DBのバックアップさえできていれば、その後の安心感はまるで異なります。
- サーバが吹っ飛んだ
- 別サーバに移行したい
等の場合も、
mysql -h localhost -u your_db -p your_db < /path/to/backup/directory/database.sql
とすることで、切り戻しや移行のハードルが非常に楽になります。
最後に
「何かの事故」
「故意/未必の故意/ミス」
など、「障害」という奴は予測できないからこそ「障害」です。管理者にできることは
「障害が来ないことを祈る」
ではありません。
「障害が来ても大丈夫な備え」をシステム化することです。
「汝平和を欲さば、戦への備えをせよ」/"Si vis pacem, para bellum"
は、ローマより続く言葉であるからこそ、普遍性と不変性があるのです。
コメントを残す