こういうサーバーは意外に多い。障害が起きてはじめて「dumpファイルが壊れていた」「手順書がなかった」と気づいても、そのとき業務はすでに止まっている。
この記事では、MySQLバックアップをcronで完全自動化する運用設計を解説します。mysqldumpをラップしたスクリプトの作り方、日次7世代・週次4世代のローテーション実装、そして復元テストを定期的にcronで回す仕組みの組み立てまで、Rocky Linux 9.4 / RHEL 9環境で動作確認した構成例を紹介します。
この記事のポイント
・mysqldumpのラッパースクリプトをcronに登録して完全無人化できる
・日次7世代・週次4世代の3層ローテーションでディスク枯渇を防ぐ
・復元テストをcronに組み込むことで「戻せる」を定期的に証明できる
・スクリプト失敗時のメール通知設定でサイレント障害を防ぐ
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜMySQLバックアップの自動化が必要なのか
手動バックアップには2つの致命的な弱点がある。「忘れる」と「記録が残らない」だ。担当者が休んだ日、年末年始、深夜の緊急リリース後——こういうタイミングに限ってデータが消える。cronで自動化されていれば、誰がいなくても毎日決まった時刻にバックアップが走る。ログファイルに実行記録が残るため、「昨日のバックアップは取れていたか」を後から確認できる。
また、自動化はバックアップの「品質の均一化」でもある。手動では「今日は時間がないからフルじゃなくて差分だけでいいか」という判断が入り込む。cronで回せば毎回同じ手順で取得されるため、復元時に「このバックアップはどう取ったんだっけ」と迷わなくて済む。
バックアップスクリプトの基本構成
実行環境:Rocky Linux 9.4 / RHEL 9、MySQL 8.0.x または 8.4 LTS1. バックアップスクリプトを作成する
まずバックアップ用のディレクトリを用意する。# バックアップ保存先を作成(rootで実行) mkdir -p /var/backup/mysql chmod 700 /var/backup/mysql
# /root/.my.cnf を作成してパスワードを格納 cat <<'EOF' > /root/.my.cnf [client] user=root password=your_password_here EOF chmod 600 /root/.my.cnf
# /usr/local/sbin/mysql_backup.sh #!/bin/bash BACKUP_DIR="/var/backup/mysql" LOG_FILE="/var/log/mysql_backup.log" DATE=$(date '+%Y%m%d_%H%M%S') FILENAME="all_databases_${DATE}.sql.gz" RETAIN_DAILY=7 RETAIN_WEEKLY=4 # ログ出力(起動時刻を記録) echo "[$(date '+%Y-%m-%d %H:%M:%S')] mysql_backup start" | tee -a "${LOG_FILE}" # mysqldumpでフルバックアップを取得しgzipで圧縮 mysqldump \ --defaults-file=/root/.my.cnf \ --all-databases \ --single-transaction \ --flush-logs \ --master-data=2 \ --routines \ --triggers \ --events \ | gzip > "${BACKUP_DIR}/${FILENAME}" EXIT_CODE=${PIPESTATUS[0]} if [ "${EXIT_CODE}" -ne 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: mysqldump failed (exit=${EXIT_CODE})" | tee -a "${LOG_FILE}" exit 1 fi # バックアップファイルのサイズを確認 FILESIZE=$(stat -c%s "${BACKUP_DIR}/${FILENAME}") echo "[$(date '+%Y-%m-%d %H:%M:%S')] OK: ${FILENAME} size=${FILESIZE} bytes" | tee -a "${LOG_FILE}" # 古い日次バックアップを削除(RETAIN_DAILY日以上前) find "${BACKUP_DIR}" -name "all_databases_*.sql.gz" \ -mtime +"${RETAIN_DAILY}" -type f -delete echo "[$(date '+%Y-%m-%d %H:%M:%S')] mysql_backup end" | tee -a "${LOG_FILE}" exit 0
chmod 700 /usr/local/sbin/mysql_backup.sh
2. crontabに登録する
rootのcrontabに登録する。時刻は業務時間外(深夜2時など)に設定する。# root の crontab を編集 crontab -e # 以下を追記(毎日 02:15 に実行) 15 2 * * * /usr/local/sbin/mysql_backup.sh
which mysqldump
/usr/bin/mysqldump
3. バックアップ取得を確認する
初回は手動でスクリプトを実行して正常に動くか確認する。/usr/local/sbin/mysql_backup.sh
[2026-09-13 02:15:02] mysql_backup start [2026-09-13 02:15:18] OK: all_databases_20260913_021502.sql.gz size=48293712 bytes [2026-09-13 02:15:18] mysql_backup end
ls -lh /var/backup/mysql/
total 47M -rw-r--r-- 1 root root 46M Sep 13 02:15 all_databases_20260913_021502.sql.gz
gzip -t /var/backup/mysql/all_databases_20260913_021502.sql.gz && echo "OK"
OK
世代ローテーション設計|何世代残すか決める
1. 日次・週次・月次の3層設計
単純に「7日分残す」だけでは、1週間以上前のデータには戻れない。よく使われるのは以下の3層構成だ。・日次:7世代(過去7日分)
・週次:4世代(過去4週分。毎週日曜深夜に取得)
・月次:3世代(過去3ヶ月分。毎月1日に取得)
この設計だと最大で3ヶ月前の状態に戻せる。ディスク使用量は「日次7個 + 週次4個 + 月次3個 = 14個分」で済む。
cronには以下のように3本の定義を追加する。
# 日次バックアップ(毎日 02:15) 15 2 * * * /usr/local/sbin/mysql_backup.sh daily # 週次バックアップ(毎週日曜 03:00) 0 3 * * 0 /usr/local/sbin/mysql_backup.sh weekly # 月次バックアップ(毎月1日 03:30) 30 3 1 * * /usr/local/sbin/mysql_backup.sh monthly
#!/bin/bash # /usr/local/sbin/mysql_backup.sh BACKUP_BASE="/var/backup/mysql" LOG_FILE="/var/log/mysql_backup.log" TIER="${1:-daily}" # 引数なしの場合は daily 扱い case "${TIER}" in daily) BACKUP_DIR="${BACKUP_BASE}/daily" RETAIN=7 ;; weekly) BACKUP_DIR="${BACKUP_BASE}/weekly" RETAIN=4 ;; monthly) BACKUP_DIR="${BACKUP_BASE}/monthly" RETAIN=3 ;; *) echo "usage: $0 [daily|weekly|monthly]" | tee -a "${LOG_FILE}" exit 1 ;; esac mkdir -p "${BACKUP_DIR}" DATE=$(date '+%Y%m%d_%H%M%S') FILENAME="${TIER}_all_databases_${DATE}.sql.gz" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${TIER}] backup start" | tee -a "${LOG_FILE}" /usr/bin/mysqldump \ --defaults-file=/root/.my.cnf \ --all-databases \ --single-transaction \ --flush-logs \ --routines \ --triggers \ --events \ | gzip > "${BACKUP_DIR}/${FILENAME}" EXIT_CODE=${PIPESTATUS[0]} if [ "${EXIT_CODE}" -ne 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${TIER}] ERROR: exit=${EXIT_CODE}" | tee -a "${LOG_FILE}" # 失敗時はmailコマンドで管理者へ通知 echo "MySQL backup [${TIER}] failed on $(hostname)" \ | mail -s "[ALERT] MySQL backup failed" root exit 1 fi FILESIZE=$(stat -c%s "${BACKUP_DIR}/${FILENAME}") echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${TIER}] OK: ${FILENAME} ${FILESIZE}bytes" | tee -a "${LOG_FILE}" # 古い世代を自動削除 find "${BACKUP_DIR}" -name "${TIER}_all_databases_*.sql.gz" \ -type f | sort | head -n -"${RETAIN}" | xargs -r rm -f echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${TIER}] backup end" | tee -a "${LOG_FILE}" exit 0
2. 世代管理の動作を確認する
数回テスト実行してからファイルの状態を確認する。ls -lh /var/backup/mysql/daily/
total 329M -rw-r--r-- 1 root root 47M Sep 7 02:15 daily_all_databases_20260907_021502.sql.gz -rw-r--r-- 1 root root 47M Sep 8 02:15 daily_all_databases_20260908_021503.sql.gz -rw-r--r-- 1 root root 47M Sep 9 02:15 daily_all_databases_20260909_021501.sql.gz -rw-r--r-- 1 root root 47M Sep 10 02:15 daily_all_databases_20260910_021504.sql.gz -rw-r--r-- 1 root root 47M Sep 11 02:15 daily_all_databases_20260911_021502.sql.gz -rw-r--r-- 1 root root 47M Sep 12 02:15 daily_all_databases_20260912_021503.sql.gz -rw-r--r-- 1 root root 47M Sep 13 02:15 daily_all_databases_20260913_021501.sql.gz
復元テストを組み込む|「戻せる」を定期的に証明する
バックアップを取っていても、復元テストをしていなければ「バックアップがある」というだけで「戻せる」とは言えない。テストなしのバックアップは「保険証のないまま手術室に入る」ようなものだ。1. 手動復元テストの手順
復元テストは本番DBを上書きしないよう、専用のテスト用データベースを使う。# テスト用インスタンスまたはテスト用DBを用意 mysql -u root -p -e "CREATE DATABASE restore_test;" # バックアップから特定のDBだけを展開して確認 zcat /var/backup/mysql/daily/daily_all_databases_20260913_021501.sql.gz \ | mysql -u root -p restore_test
mysql -u root -p -e " SELECT table_schema, COUNT(*) AS tables FROM information_schema.tables WHERE table_type='BASE TABLE' GROUP BY table_schema;" restore_test
+-----------------------+--------+ | table_schema | tables | +-----------------------+--------+ | myapp_production | 42 | | wordpress_db | 12 | +-----------------------+--------+
mysql -u root -p -e "DROP DATABASE restore_test;"
2. 定期復元テストをcronに組み込む
週に1回、最新の日次バックアップを展開してテーブル数が0でないことを確認するスクリプトを作る。#!/bin/bash # /usr/local/sbin/mysql_restore_test.sh BACKUP_DIR="/var/backup/mysql/daily" LOG_FILE="/var/log/mysql_restore_test.log" TEST_DB="__restore_test_$(date '+%Y%m%d')" echo "[$(date '+%Y-%m-%d %H:%M:%S')] restore test start" | tee -a "${LOG_FILE}" # 最新のバックアップファイルを取得 LATEST=$(ls -t "${BACKUP_DIR}"/*.sql.gz 2>/dev/null | head -1) if [ -z "${LATEST}" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: no backup file found" | tee -a "${LOG_FILE}" echo "MySQL restore test: no backup file on $(hostname)" \ | mail -s "[ALERT] MySQL restore test failed" root exit 1 fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] using: ${LATEST}" | tee -a "${LOG_FILE}" # gzipの整合性チェック if ! gzip -t "${LATEST}" 2>/dev/null; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: gzip integrity check failed" | tee -a "${LOG_FILE}" echo "MySQL restore test: gzip broken on $(hostname)" \ | mail -s "[ALERT] MySQL restore test failed" root exit 1 fi # テスト用DBを作成して展開 mysql --defaults-file=/root/.my.cnf \ -e "CREATE DATABASE \`${TEST_DB}\`;" 2>/dev/null zcat "${LATEST}" \ | mysql --defaults-file=/root/.my.cnf "${TEST_DB}" 2>/dev/null # テーブル数を確認 TABLE_COUNT=$(mysql --defaults-file=/root/.my.cnf -Nse \ "SELECT COUNT(*) FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema NOT IN ('information_schema','performance_schema','mysql','sys');") # テスト用DBを削除 mysql --defaults-file=/root/.my.cnf \ -e "DROP DATABASE \`${TEST_DB}\`;" 2>/dev/null if [ "${TABLE_COUNT}" -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: 0 tables restored" | tee -a "${LOG_FILE}" echo "MySQL restore test: 0 tables on $(hostname)" \ | mail -s "[ALERT] MySQL restore test failed" root exit 1 fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] OK: ${TABLE_COUNT} tables restored" | tee -a "${LOG_FILE}" exit 0
# 週次復元テスト(毎週月曜 04:00) 0 4 * * 1 /usr/local/sbin/mysql_restore_test.sh
[2026-09-13 04:00:01] restore test start [2026-09-13 04:00:01] using: /var/backup/mysql/daily/daily_all_databases_20260913_021501.sql.gz [2026-09-13 04:16:43] OK: 54 tables restored
トラブルシュート|よくあるエラーと対処
cronからmysqldumpが失敗する(手動では動く)
cronのPATHは `/usr/bin:/bin` のみのことが多く、mysqldumpのパスが通っていない場合がある。対処:スクリプト冒頭にPATHを明示するか、コマンドをフルパスで書く。
# スクリプト冒頭に追加 export PATH=/usr/bin:/usr/sbin:/bin:/sbin:$PATH
バックアップファイルのサイズが0バイトになる
`gzip > file` のパイプではmysqldumpの終了コードを `$?` で取れない。`${PIPESTATUS[0]}` を使うこと。0バイトファイルが生成された場合はスクリプトが誤って「成功」と判断している可能性がある。対処:スクリプト内で `${PIPESTATUS[0]}` でexitコードを確認し、0バイトのファイルはサイズチェックで即エラーにする。
FILESIZE=$(stat -c%s "${BACKUP_DIR}/${FILENAME}") if [ "${FILESIZE}" -eq 0 ]; then echo "ERROR: backup file is empty" | tee -a "${LOG_FILE}" rm -f "${BACKUP_DIR}/${FILENAME}" exit 1 fi
--single-transactionが使えないテーブルがある
`--single-transaction` はInnoDBに対してのみ有効。MyISAMテーブルが混在している場合、一貫性が保証されない。対処:`--lock-tables=false` を指定するか、MyISAMテーブルをInnoDBに変換することを検討する。現状どちらのエンジンが使われているかは以下で確認できる。
mysql -u root -p -e " SELECT table_schema, table_name, engine FROM information_schema.tables WHERE table_type='BASE TABLE' AND engine != 'InnoDB' AND table_schema NOT IN ('information_schema','performance_schema','mysql','sys');"
本記事のまとめ
MySQLバックアップのcron自動化に必要な設計をまとめる。| やりたいこと | コマンド・設定 |
|---|---|
| パスワードをファイルに格納 | /root/.my.cnf に [client] user/password を記載 |
| 全DBをフルバックアップ | mysqldump --all-databases --single-transaction | gzip |
| 日次cron登録 | 15 2 * * * /usr/local/sbin/mysql_backup.sh daily |
| 週次cron登録 | 0 3 * * 0 /usr/local/sbin/mysql_backup.sh weekly |
| 月次cron登録 | 30 3 1 * * /usr/local/sbin/mysql_backup.sh monthly |
| 古い世代を自動削除 | find $DIR -name "*.sql.gz" -type f | sort | head -n -N | xargs rm |
| gzip整合性チェック | gzip -t バックアップファイル.sql.gz |
| 復元テスト実行 | zcat バックアップ.sql.gz | mysql テスト用DB名 |
| 週次復元テスト登録 | 0 4 * * 1 /usr/local/sbin/mysql_restore_test.sh |
Linux無料マニュアルを受け取る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:PostgreSQLでユーザー(ロール)を作成しデータベース単位で権限を絞る手順|CREATE ROLEとGRANTの実務
- この記事の属するカテゴリ:データーベース管理へ戻る

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