本稿は、プラグイン任せの「とりあえずバックアップ」から一歩進んで、設計→実装→検証→運用までを自分の手で回せるようになるための学習コンテンツです。題材は小〜中規模のMySQL/MariaDB(WordPressなど)を想定し、mysqldump
での論理バックアップを基軸に、圧縮・暗号化・S3転送・世代管理・復元リハーサルまでを通しで作ります。
現場で本当に役立つのは「取った」事実ではなく確実に戻せることです。そこで本講座では、RPO/RTOを言語化し、失ってよいデータ量と復旧目標時間を前提にバックアップ頻度や保持世代を設計します。実装では--single-transaction
を含むダンプ方針、gzip/zstd
での圧縮、SSE-S3やSSE-KMSによる暗号化、IAM最小権限とS3ライフサイクルによる保管を、コピー&ペーストで再現できるスクリプトと設定例で示します。さらに、月次の復元リハーサルで整合性を確認し、失敗時の通知と再実行ポリシーまで運用に乗せます。
学習を通じて得られる成果物は、日次取得に耐えるbackup-db.sh
とrestore-db.sh
、Prefixを厳密に絞ったIAMポリシー、S3ライフサイクルのJSON、cron
/systemd timer
の雛形、そして復元チェックのテンプレートです。Linuxの基本操作・Bashの実行・AWS CLIの導入ができる方なら十分に追いかけられる難易度に整えました。まずは検証環境で動かし、「いつでも戻せる」自信を積み上げていきましょう。
この記事で学ぶこと(学習ゴールと前提)
学習ゴール:RPO/RTOを言語化し、設計→実装→検証まで通しで再現
バックアップは「取る」より「戻せる」が本質です。失ってよい最大のデータ量(RPO)と、復旧にかけられる時間(RTO)を自分の言葉で定義し、その数字に見合う取得頻度と保持期間を設計します。本稿は、その設計をmysqldump
の論理バックアップで具体化し、圧縮・暗号化・S3保管・復元リハーサルまでを一連の流れで再現できるようにまとめています。
前提環境と対象読者(Linux/Bash、MySQL/MariaDB、awscli)
LinuxにログインしてBashが扱え、MySQL/MariaDBのクライアントが使える方を想定します。S3に触れたことがなくても大丈夫です。awscli
の認証さえ通っていれば、本文のコマンドをそのまま写経しながら進められます。
本講座の流れと成果物(スクリプト/IAM/Lifecycle/手順書)
backup-db.sh
で取得、S3へアップロード、restore-db.sh
で復元検証、cron
またはsystemd timer
で自動化、IAM最小権限とS3ライフサイクルで保管、という構成です。記事末にテンプレートを揃えてあります。
バックアップ設計の基本(RPO/RTOと戦略)
取得頻度と世代管理(日次/週次/月次の組み合わせ)
日次は直近7世代、週次は4世代、月次は6世代、といった層別保持が扱いやすく、コストも読みやすくなります。S3のプレフィックスを「日次」「週次」「月次」で分けるか、日付で自動的に判別するかはスクリプト側で決めます。頻度はRPOに直結します。RPOが24時間なら日次で十分、数時間以内なら日次に加えてbinlog運用の検討が必要です。
ダンプ方式の選択:–single-transaction/ロック/除外方針
InnoDB中心のWordPressなどは--single-transaction
でオンライン取得が定番です。変更頻度の高いログ系テーブルはダンプから外すと復元が軽くなります。たとえばwp_actionscheduler_logs
や一時テーブルのように再構築可能なものは--ignore-table
で除外します。
mysqldump \
--single-transaction --routines --triggers --events \
--default-character-set=utf8mb4 \
--set-gtid-purged=OFF \
--ignore-table=mydb.wp_actionscheduler_logs \
mydb > mydb.sql
PITRを見据えたbinlog運用と保持期間
RPOをさらに短くしたいときは、日次ダンプ+バイナリログ(binlog)でのポイントインタイム復旧(PITR)を組み合わせます。サーバ側でlog_bin
を有効化し、保持期間をRPOに合わせて設定しておくと、障害直前まで巻き戻せます。
実装準備(資格情報と環境の安全化)
.my.cnfと環境変数の使い分け・権限設定
ダンプに使う資格情報は、コマンドライン引数ではなくホーム直下の.my.cnf
か環境変数で安全に扱います。ファイル権限は600に落としておきます。
# ~/.my.cnf (権限 600)
[client]
user=backup_user password=強固なパスワード host=127.0.0.1
chmod 600 ~/.my.cnf
環境変数派なら、MYSQL_PWD
を使う方法もありますが、プロセス一覧に出にくい.my.cnf
運用の方が実務では好まれます。
S3バケット設計:リージョン/プレフィックス/命名規則
組織名やプロジェクト名を含めたバケットを用意し、ホスト名→DB名→年月日→時刻という階層で保存していくと、人間にも機械にも優しい構造になります。
s3://org-backup/db/{hostname}/{db}/{yyyy}/{mm}/{dd}/{HHmmss}_{db}_{shortsha}.sql.gz
IAM最小権限ポリシー(Prefix制限・必要最小のActions)
バックアップ専用ユーザーに対し、対象プレフィックスに限定したListBucket
・PutObject
・GetObject
・DeleteObject
のみ許可します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PrefixScopedAccess",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": ["arn:aws:s3:::org-backup"],
"Condition": {
"StringLike": {
"s3:prefix": ["db/${aws:username}/*"]
}
}
},
{
"Sid": "ObjectsRW",
"Effect": "Allow",
"Action": ["s3:PutObject","s3:GetObject","s3:DeleteObject"],
"Resource": ["arn:aws:s3:::org-backup/db/${aws:username}/*"]
}
]
}
${aws:username}
の部分をホスト名等に置き換えて固定化しても構いません。
ダンプスクリプトを作る(再現性と堅牢性)
mysqldumpコマンド設計:主要オプションと注意点
オンライン取得の基本は--single-transaction
、ルーチン類は--routines --triggers --events
、GTID環境でも移行しやすいよう--set-gtid-purged=OFF
を添えます。対象DBを配列で持ち、ループで回すと規模が大きくなっても運用が楽です。
#!/usr/bin/env bash
set -Eeuo pipefail
DBS=("mydb") # 取得対象DBの配列
HOSTNAME="$(hostname -s)"
DATE="$(date +%Y/%m/%d)"
TIME="$(date +%H%M%S)"
TMP="${TMPDIR:-/tmp}/backup.$$"
S3_BASE="s3://org-backup/db/${HOSTNAME}/${DATE}"
STORAGE_CLASS="STANDARD" # or STANDARD_IA
mkdir -p "$TMP"
dump_one() {
local db="$1"
local sql="${TMP}/${TIME}_${db}.sql"
mysqldump \
--single-transaction --routines --triggers --events \
--default-character-set=utf8mb4 \
--set-gtid-purged=OFF \
"$db" > "$sql"
# 圧縮(zstdがあれば高速・高圧縮)
if command -v zstd >/dev/null; then
zstd -19 --format=zstd --quiet "$sql" -o "${sql}.zst"
rm -f "$sql"
OUT="${sql}.zst"
EXT="zst"
else
gzip -9 "$sql"
OUT="${sql}.gz"
EXT="gz"
fi
# チェックサムとメタ情報
SUM="$(sha256sum "$OUT" | awk '{print $1}')"
SIZE="$(stat -c%s "$OUT")"
META="${OUT}.meta.json"
cat > "$META" <<JSON
{
"host": "${HOSTNAME}",
"db": "${db}",
"ts": "$(date -u +%FT%TZ)",
"file": "$(basename "$OUT")",
"bytes": ${SIZE},
"sha256": "${SUM}",
"tool": "mysqldump",
"version": "1.0.0"
}
JSON
# S3へ転送(冪等に上書き)
aws s3 cp "$OUT" "${S3_BASE}/${TIME}_${db}.${EXT}" --storage-class "$STORAGE_CLASS"
aws s3 cp "$META" "${S3_BASE}/${TIME}_${db}.${EXT}.meta.json" --storage-class "$STORAGE_CLASS"
echo "OK: ${db} ${SIZE} bytes sha256=${SUM}"
}
for db in "${DBS[@]}"; do
dump_one "$db"
done
rm -rf "$TMP"
圧縮・暗号化・チェックサム(gzip/zstd・SSE-S3/KMS・sha256)
サーバ側でのファイル暗号化を避けたい場合は、S3のサーバサイド暗号化(SSE-S3)やKMSを使います。aws s3 cp
は環境側の設定でSSEを強制できます。
# 常にSSE-S3で暗号化
aws s3 cp file s3://bucket/path --sse AES256
# KMSキー指定
aws s3 cp file s3://bucket/path --sse aws:kms --sse-kms-key-id arn:aws:kms:ap-northeast-1:123456789012:key/...
ファイル命名とメタ情報(JSON)で整合性を担保
人間が見て日付やDBがわかる命名にし、メタJSONにチェックサムとバイト数を必ず記録します。復元時にsha256sum
を照合すれば転送中の欠損をすぐに検知できます。
S3転送とライフサイクルの実装
aws cliでの転送(再試行・冪等性・整合チェック)
ネットワークが不安定でもawscli
は自動再試行します。より厳密にしたいときは、アップロード後にaws s3 cp --no-sign-request
で匿名ヘッダを確認する方法や、aws s3api head-object
でETagとサイズを照合する方法もあります。
aws s3api head-object --bucket org-backup --key "db/host/2025/09/10/120001_mydb.zst"
ライフサイクルルール:IA/Glacier/削除の閾値設計
一定期間を過ぎた古いバックアップは、アクセス頻度の低いストレージへ自動で移行します。たとえば30日後にSTANDARD_IA
、90日後にGLACIER_IR
、365日後に削除というルールです。
{
"Rules": [
{
"ID": "db-backup-lifecycle",
"Status": "Enabled",
"Filter": { "Prefix": "db/" },
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER_IR" }
],
"Expiration": { "Days": 365 }
}
]
}
適用はaws s3api put-bucket-lifecycle-configuration
で行います。
aws s3api put-bucket-lifecycle-configuration \
--bucket org-backup \
--lifecycle-configuration file://lifecycle.json
コスト最適化:リクエスト数・転送料・保管階層のバランス
日次のファイルは標準、30日超はIA、四半期を越えたらGlacier系へ、という流れにしておくと、取り出し頻度と費用のバランスが取りやすくなります。サイズが大きいほど効果が出ます。
自動化と監視(運用に乗せる)
cron / systemd timerの設定例と実行時間帯の決め方
メンテ時間に重ならない深夜帯に1回、というのが扱いやすいです。cron
はシンプルで、systemd timer
はログと状態管理に強いです。
# crontab -e
# 毎日 03:15 に実行(標準出力はsyslogへ)
15 3 * * * /usr/local/bin/backup-db.sh 2>&1 | logger -t db-backup
systemd
派なら次の2ファイルで運用できます。
# /etc/systemd/system/db-backup.service
[Unit]
Description=MySQL logical backup to S3
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-db.sh
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Daily MySQL backup timer
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now db-backup.timer
ログ整形と終了コード設計(syslog/JSONライン)
成功・失敗が一目で分かるよう、最後にOK:
やERROR:
で始まる1行JSONを出すだけでも運用は楽になります。
echo "{\"level\":\"info\",\"event\":\"uploaded\",\"db\":\"${db}\",\"bytes\":${SIZE},\"sha256\":\"${SUM}\"}"
失敗時の通知(メール/Webhook)と再実行ポリシー
シンプルな通知はメールで十分です。Webhook派ならSlackや自前APIへcurl
で投げてもOKです。
# msmtp等の設定がある前提
echo "Backup failed on ${HOSTNAME}" | mail -s "DB Backup Failure" ops@example.com
復元リハーサル(定期検証の型)
検証用DBでの復元手順と所要時間KPI
復元は「慣れ」が命です。月に一度、検証用DBへ戻して所要時間を記録しておくと、いざというときに落ち着いて対処できます。
#!/usr/bin/env bash
set -Eeuo pipefail
HOSTNAME="$(hostname -s)"
S3_KEY="s3://org-backup/db/${HOSTNAME}/2025/09/10/120001_mydb.zst"
TMP="$(mktemp -d)"
aws s3 cp "$S3_KEY" "${TMP}/dump.zst"
if [[ "${S3_KEY}" == *.zst ]]; then
zstd -d "${TMP}/dump.zst" -o "${TMP}/dump.sql"
else
gunzip -c "${TMP}/dump.gz" > "${TMP}/dump.sql"
fi
mysql -e "DROP DATABASE IF EXISTS mydb_restore; CREATE DATABASE mydb_restore DEFAULT CHARSET utf8mb4;"
mysql mydb_restore < "${TMP}/dump.sql"
# 簡易チェック(テーブル数)
SRC=$(mysql -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='mydb'")
DST=$(mysql -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='mydb_restore'")
[[ "$SRC" -eq "$DST" ]] && echo "OK: table count matched ($SRC)" || { echo "ERROR: mismatch"; exit 1; }
rm -rf "$TMP"
文字コード・トリガ・イベント・権限の再現確認
SHOW VARIABLES LIKE 'character_set_%';
や、SHOW TRIGGERS; SHOW EVENTS;
で差分がないかを見ます。権限はアプリ接続ユーザーのGRANTをダンプしておき、必要に応じて適用します。
-- 本番側で一度エクスポート
SHOW GRANTS FOR 'appuser'@'localhost'\G
合格基準と記録テンプレ(チェックリスト)
所要時間、テーブル数、主要画面の読み込み可否など、簡潔な合格基準を決めて記録します。小さくても毎月の記録が自信になります。
応用:PITRと大規模化の選択肢
binlogリプレイによるPITR最小手順
障害直前まで戻したいときは、日次ダンプから復元し、障害時刻の手前までbinlogを適用します。
# 例:2025-09-10T12:34:56Z 手前まで適用
mysqlbinlog --start-datetime="2025-09-10 00:00:00" \
--stop-datetime="2025-09-10 12:34:55" \
/var/lib/mysql/binlog.000123 | mysql mydb_restore
mydumper/Percona XtraBackupを使う判断軸
数十GBを超えるとmysqldump
が厳しくなります。並列化できるmydumper/myloader
や、物理バックアップのPercona XtraBackupを検討すると、取得時間と復元時間の両方を短縮できます。まずは論理バックアップで型を作り、阜(かさね)て必要になった段階で移行するのが安全です。
マルチDB・マルチリージョン・クロスアカウント戦略
複数DBを扱う場合は配列で列挙、S3プレフィックスにDB名を組み込みます。レジリエンスを高めたいなら、別リージョンや別AWSアカウントへのレプリカ保管も選択肢です。費用と取り出し手間のトレードオフを理解して決めます。
学びを現場運用へ(定着と継続改善)
よくあるつまずきと対処(サイズ異常・タイムアウト等)
ダンプサイズが急に増えたら、一時テーブルやログテーブルの肥大化を疑います。--ignore-table
で除外するか、アプリ側でローテーション設定を行います。mysqldump
が遅いときは、索引の多い巨大テーブルに対するオプションや、--single-transaction
有無の影響を見直します。
週次・月次の運用チェックリスト
週次で成功/失敗の件数と所要時間、月次で復元リハーサルの結果とS3コストを見ます。数字が見えるだけで、改善の優先順位が自然と決まっていきます。
まとめ
バックアップは一度整えたら終わりではなく、運用の中で育てていく仕組みです。
RPOとRTOは環境や事業の変化に合わせて見直し、取得頻度や保持世代、ライフサイクルの設定を少しずつ最適化していきましょう。今日つくったスクリプトは、明日の自分が読み返しても迷わないように、命名やログ、メタ情報を丁寧に整えておくと、復元のハードルがぐっと下がります。
大切なのは「戻せる自信」を定期的に積み重ねることです。月に一度の復元リハーサルで、所要時間とチェックサム、テーブル数の一致を記録し、うまくいかなかった点はその日のうちに直します。通知や終了コードの設計がしっかりしていれば、夜間に何かあっても翌朝の判断が早くなります。最初は日次ダンプから始め、必要になったらbinlogでPITR、容量が増えたらmydumperや物理バックアップへ――段階を踏めば無理なく強くできます。
IAMの最小権限やS3のライフサイクルは、コストとセキュリティの土台です。鍵や権限の棚卸し、アカウントやリージョンをまたいだ保管など、できる範囲で堅牢さを上げていきましょう。小さな改善でも、毎週・毎月の記録が残れば成長が可視化されます。
「取れる」から「必ず戻せる」へ。バックアップは安心の土台です。今日の学びをそのまま検証環境で動かし、明日の定常運用へつなげていきましょう。
参考・参照リンク
- MySQL :: mysqldump — https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html
- mysqlbinlog(PITR)— https://dev.mysql.com/doc/refman/8.0/en/mysqlbinlog.html
- MariaDB dump / restore — https://mariadb.com/kb/en/mariadb-dump-and-restore/
- AWS CLI S3 — https://docs.aws.amazon.com/cli/latest/reference/s3/
- Amazon S3 ライフサイクル — https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html
- IAM ポリシー基礎 — https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
- Percona XtraBackup — https://www.percona.com/software/mysql-database/percona-xtrabackup
- mydumper/myloader — https://github.com/mydumper/mydumper