MySQLのバックアップをcronで自動化する運用設計|世代ローテーションと復元テストの組み立て

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Linuxtipsデーターベース管理 > MySQLのバックアップをcronで自動化する運用設計|世代ローテーションと復元テストの組み立て
「バックアップは取っているけど、実際に戻したことはない」
こういうサーバーは意外に多い。障害が起きてはじめて「dumpファイルが壊れていた」「手順書がなかった」と気づいても、そのとき業務はすでに止まっている。

この記事では、MySQLバックアップをcronで完全自動化する運用設計を解説します。mysqldumpをラップしたスクリプトの作り方、日次7世代・週次4世代のローテーション実装、そして復元テストを定期的にcronで回す仕組みの組み立てまで、Rocky Linux 9.4 / RHEL 9環境で動作確認した構成例を紹介します。

この記事のポイント

・mysqldumpのラッパースクリプトをcronに登録して完全無人化できる
・日次7世代・週次4世代の3層ローテーションでディスク枯渇を防ぐ
・復元テストをcronに組み込むことで「戻せる」を定期的に証明できる
・スクリプト失敗時のメール通知設定でサイレント障害を防ぐ


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜMySQLバックアップの自動化が必要なのか

手動バックアップには2つの致命的な弱点がある。「忘れる」と「記録が残らない」だ。

担当者が休んだ日、年末年始、深夜の緊急リリース後——こういうタイミングに限ってデータが消える。cronで自動化されていれば、誰がいなくても毎日決まった時刻にバックアップが走る。ログファイルに実行記録が残るため、「昨日のバックアップは取れていたか」を後から確認できる。

また、自動化はバックアップの「品質の均一化」でもある。手動では「今日は時間がないからフルじゃなくて差分だけでいいか」という判断が入り込む。cronで回せば毎回同じ手順で取得されるため、復元時に「このバックアップはどう取ったんだっけ」と迷わなくて済む。

バックアップスクリプトの基本構成

実行環境:Rocky Linux 9.4 / RHEL 9、MySQL 8.0.x または 8.4 LTS

1. バックアップスクリプトを作成する

まずバックアップ用のディレクトリを用意する。

# バックアップ保存先を作成(rootで実行) mkdir -p /var/backup/mysql chmod 700 /var/backup/mysql

次にスクリプトを作成する。パスワードをコマンド引数に直接書くとps auxで見えてしまうため、オプションファイル(.my.cnf)を使う方式を採用する。

# /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

cronはPATHが最小限のため、コマンドはフルパスで書く。mysqldumpがどこにあるか確認しておくこと。

which mysqldump

/usr/bin/mysqldump

スクリプト内のmysqldumpをフルパス(/usr/bin/mysqldump)に書き換えておくと、PATH問題で失敗するリスクを排除できる。

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

ファイルサイズが0バイトでないこと、gzipとして正常かを確認する。

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

スクリプト側では引数(daily/weekly/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

7世代分のみが保持されており、8日前以前のファイルは自動削除されている。

復元テストを組み込む|「戻せる」を定期的に証明する

バックアップを取っていても、復元テストをしていなければ「バックアップがある」というだけで「戻せる」とは言えない。テストなしのバックアップは「保険証のないまま手術室に入る」ようなものだ。

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 | +-----------------------+--------+

本番DBのテーブル数と一致していれば復元成功と判断できる。確認が終わったらテスト用DBを削除する。

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

crontabに登録する(毎週月曜の早朝に実行。バックアップ完了後を想定して04:00にする)。

# 週次復元テスト(毎週月曜 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
バックアップの設計で最も大切なのは「定期的に復元テストをすること」だ。どれだけ完璧なスクリプトを書いても、実際に戻せるかどうかは試してみないと分からない。週1回の自動復元テストをcronに組み込んでおけば、障害発生時に「本当に戻せるか」という不安を持たずに作業を始められる。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
Linux無料マニュアルを受け取る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。