3-6. バックアップ - 「取れる」より「戻せる」を本気で考える

所要時間: 50-70分 ゴール: 3-2-1 ルールの意味を腹落ちさせ、MySQL/PostgreSQL の日次バックアップを S3 に暗号化転送し、リストア手順を実機で検証する コミット内容: ~/learn/infra/backup/ にバックアップスクリプト・リストア手順書・実行ログ


このレッスンのゴール

  • 3-2-1 ルールを自分の言葉で語れる
  • RTO と RPO を業務要件から定義できる
  • MySQL / PostgreSQL の論理バックアップと物理バックアップの違いを使い分けられる
  • S3 への暗号化転送+世代管理のスクリプトを書ける
  • リストア手順を月1で実機検証する文化を持っている

なぜ学ぶか(実務悩みベース)

  • ランサムウェア攻撃で本番もバックアップも全滅する事故を避ける
  • 「取れているつもり」のバックアップが本番障害時に役立たない事故を防ぐ
  • 当事者以外が読めるリストア手順書で属人化を解消
  • 監査・コンプライアンス対応の最低ライン

前章とのつながり

3-5 で「壊れたことに気づける」状態を作った。今回はその先、「壊れても戻せる」 仕組み。Level 3 全体(VPS → DNS → HTTPS → CI/CD → 監視 → バックアップ)の総仕上げ。


大前提: バックアップが「保険」ではなく「設計の一部」である理由

「バックアップを取っています」と答えるエンジニアは多い。でも次の質問に答えられる人は少ない:

  • バックアップから 何分で本番が復旧 できる?
  • 直近 1 か月以内に 実際にリストアテストを実施 したのはいつ?
  • バックアップ自体が 改竄・削除されていないこと を確認しているか?
  • リストア手順書を 当事者以外 が読んで実行できるか?
  • バックアップ先のクラウドストレージは 暗号化 されているか?

これらに即答できないバックアップは、「バックアップしている気になっているだけ」 と同義。実例:

  • ランサムウェアに感染し、本番もバックアップも同時に暗号化された
  • 半年前から DB バックアップは取れていたが、スキーマ変更を考慮していなくて復元できないことが本番障害時に発覚
  • AWS S3 バケットがパブリック公開のまま顧客の DB ダンプを世界中に公開
  • 「日次バックアップ」をやっていたつもりが、cron がエラーで 3 か月前から動いていなかった

バックアップは「攻撃を受けた / ヒューマンエラーがあった / ハードウェアが壊れた」時の 最後の砦。設計の段階から、「いつ・どこに・どう取り、どう戻すか」をセットで考えるのが本物のエンジニアリング。


セッション①: 3-2-1 ルールとバックアップ戦略(15-20分)

1. 3-2-1 ルール

3-2-1 ルールとは

アメリカ国土安全保障省 (CISA) も推奨する古典的かつ強力な原則。

  • 3 コピー: オリジナルを含めて 3 つ持つ
  • 2 メディア: 異なる種類の媒体に保存(例: SSD + クラウドストレージ)
  • 1 オフサイト: 1 つは物理的に離れた場所に置く
[本番サーバー]
  ├─ オリジナルデータ                  ← コピー 1
  ├─ 同じサーバーの別ボリュームに dump  ← コピー 2 (同じメディア・同じ場所)
  └─ 別リージョンの S3 に暗号化アップ   ← コピー 3 (別メディア・オフサイト)
                                       ←     これで 3-2-1

3-2-1 を満たさない典型ミス

  • 本番 DB と同じインスタンスにダンプを置く: ディスク故障で両方喪失
  • 同じクラウド・同じリージョン: リージョン障害で全滅
  • コピー数が 2 つ: 一方が壊れたら次がない

2. RPO と RTO

指標意味単位
RPO (Recovery Point Objective)どれくらい前のデータまで戻ってよいか(許容データ損失)時間
RTO (Recovery Time Objective)どれくらいで復旧する必要があるか(許容停止時間)時間

RPO / RTO の決め方

  • 個人ブログ: RPO 24 時間 / RTO 1 日でも許容(前日の状態に戻れば OK、復旧に1日かけてもよい)
  • EC サイト: RPO 1 時間 / RTO 1 時間(売上機会損失と顧客信用)
  • 銀行: RPO 秒単位 / RTO 数分(送金が消えたら大事故)

RPO が短い → 頻繁なバックアップ + WAL / binlog のような差分ログが必要 RTO が短い → 自動復旧 + ホットスタンバイのレプリカが必要

「とりあえず日次バックアップ」が許される業種と、「数分のずれも許されない」業種を区別する。

3. バックアップの種類

種別仕組み用途
論理バックアップSQL 文として dumpクロスバージョン復元、部分復元
物理バックアップデータファイルそのもの高速、巨大DB
スナップショットストレージレベルで時点コピー即時、容量効率
差分バックアップ前回からの差分のみ容量節約
増分バックアップ直近からの増分のみ容量節約、復元は累積必要
WAL / binlogトランザクションログPoint-in-Time Recovery

論理 vs 物理 vs スナップショット

  • 論理バックアップ (mysqldump, pg_dump): SQL に変換。ポータブル(別バージョン・別環境にも復元しやすい)。ただし 巨大 DB だと数時間かかる
  • 物理バックアップ (xtrabackup, pg_basebackup): バイナリで吸い出す。速い、ただし 環境差異に弱い
  • スナップショット (EBS Snapshot, ZFS snapshot): ストレージレベル。取得は秒単位。ただし 一貫性保証はアプリ次第(quiesce 必須)

基本は論理 + スナップショットの併用: 日次は論理(移行可能)、より頻繁にスナップショット(高速復元)。


セッション②: MySQL のバックアップ(20-25分)

4. mysqldump の使い方

# 単一 DB をダンプ
mysqldump -u root -p mydb > mydb.sql
 
# 全 DB をダンプ
mysqldump -u root -p --all-databases > all.sql
 
# 一貫性保証付き(必須オプション)
mysqldump -u root -p \
    --single-transaction \
    --quick \
    --routines \
    --triggers \
    --events \
    --master-data=2 \
    mydb > mydb.sql
 
# gzip 圧縮しながら保存
mysqldump -u root -p mydb | gzip > mydb_$(date +%Y%m%d).sql.gz

--single-transaction の意味

InnoDB テーブルに対して トランザクションを開始してから dump する オプション。これにより、ダンプ中に他のクエリで更新されても 一貫性のあるスナップショット が取れる。

これなしの mysqldump は本番では使えない。データ更新が走るたびに「ダンプの途中の行は古いデータ、後半は新しいデータ」という不整合状態になる。

ただし MyISAM テーブルには効かない。MyISAM が混在する場合は --lock-tables が必要(テーブルロックでサービス停止)。現代の MySQL は InnoDB 一本がベスト

他オプションの意味

  • --quick: 行ごとに fetch して逐次書き出し。大きい DB のメモリ枯渇を防ぐ
  • --routines: ストアドプロシージャ・ファンクションも含める
  • --triggers: トリガーも含める(デフォルトで含まれるが明示推奨)
  • --events: スケジュールイベントも含める
  • --master-data=2: binary log の位置をコメントとして書き出す。レプリカ構築や PITR の起点に必要

5. binary log と PITR

PITR (Point-in-Time Recovery) とは

「2026-05-14 14:23:00 ちょうどの状態」のように、任意の時刻の状態に戻せる復元手法。

必要なもの:

  1. 直近のフルバックアップ(mysqldump)
  2. それ以降のすべての binary log

リストア手順:

  1. フルバックアップを復元
  2. binary log を該当時刻まで replay

誤って DELETE FROM users を実行した時の救い。dump 単体では「丸ごと前日に戻す」しかできず、その間の他の更新も巻き戻る。PITR なら「DELETE 実行直前」まで戻せる。

# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
log-bin = /var/log/mysql/mysql-bin
binlog_format = ROW
expire_logs_days = 7
server-id = 1
# binary log 一覧
mysql -u root -p -e "SHOW BINARY LOGS;"
 
# 特定の binary log を SQL に変換
mysqlbinlog mysql-bin.000123 > replay.sql
 
# 範囲指定で replay
mysqlbinlog --start-datetime="2026-05-14 10:00:00" \
            --stop-datetime="2026-05-14 14:22:59" \
            mysql-bin.000123 mysql-bin.000124 | mysql -u root -p mydb

6. リストア

# 単一 DB
mysql -u root -p mydb < mydb.sql
 
# gzip 圧縮を解いて流す
gunzip -c mydb_20260514.sql.gz | mysql -u root -p mydb
 
# 全 DB(system schema 含む。本番では慎重に)
mysql -u root -p < all.sql

リストア時に DB が消える事故

多くの mysqldump 出力には先頭に:

DROP DATABASE IF EXISTS `mydb`;
CREATE DATABASE `mydb`;
USE `mydb`;

が含まれることがある。間違って本番に流すと既存 DB が消える

対策:

  1. テスト用に空 DB を作って そこに流す
  2. ダンプの先頭部分を head で必ず確認
  3. 本番リストアは別インスタンスで行い、データ確認後に切替

セッション③: PostgreSQL のバックアップ(15-20分)

7. pg_dump

# 平文 SQL 形式
pg_dump -U postgres mydb > mydb.sql
 
# カスタム形式(圧縮 + 並列リストア可、推奨)
pg_dump -U postgres -Fc mydb > mydb.dump
 
# 並列ダンプ(カスタム形式は非対応、ディレクトリ形式が必要)
pg_dump -U postgres -Fd -j 4 mydb -f mydb_dir
 
# 全 DB クラスタを dump
pg_dumpall -U postgres > all.sql

出力形式の選び方

  • -Fp (plain): SQL テキスト。grep/sed で編集可能。個別テーブル抽出に便利
  • -Fc (custom): バイナリ、内部で gzip 圧縮。最も汎用。並列リストア可能
  • -Fd (directory): 並列ダンプ可能、最速。巨大 DB 向け
  • -Ft (tar): tar 形式、レガシー

本番は基本 -Fc。サイズ次第で -Fd-Fp はデバッグ用。

# カスタム形式のリストア
pg_restore -U postgres -d mydb mydb.dump
 
# 並列リストア
pg_restore -U postgres -d mydb -j 4 mydb.dump
 
# 一覧を表示
pg_restore -l mydb.dump
 
# 特定テーブルだけ
pg_restore -U postgres -d mydb -t users mydb.dump

8. WAL アーカイブと PITR

WAL (Write-Ahead Log)

PostgreSQL のすべての変更は、まず WAL に書かれてから本体に反映される。MySQL の binary log に相当

WAL を継続的にバックアップ先に転送する設定:

# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'

これでフルバックアップ + WAL 継続バックアップ → PITR が可能。

# ベースバックアップ
pg_basebackup -U postgres -D /backup/base -F t -z -P
 
# リストア手順(概略)
# 1. 新しいデータディレクトリにベースを展開
# 2. WAL ファイルを WAL ディレクトリに配置
# 3. recovery.signal を作成
# 4. recovery_target_time を postgresql.conf に書く
# 5. PostgreSQL を起動 → WAL 再生で目的時刻まで進む

物理レプリケーション vs バックアップ

「レプリカが本番のコピーを持っているから、バックアップは不要では?」という誤解。

レプリカはバックアップではない:

  • 本番で DROP TABLE すれば、レプリカでも即座に DROP される
  • 論理的破壊(誤操作、ランサムウェア)には無力

レプリカは ハードウェア障害・地域災害 には強いが、人災・攻撃 には弱い。バックアップは「過去のある時点を取り戻せる」ことが本質。


セッション④: ファイルとオフサイト(15-20分)

9. rsync でファイルバックアップ

# 単純コピー(既存ファイルは更新差分のみ転送)
rsync -av /var/www/ /backup/www/
 
# SSH 経由
rsync -av -e ssh /var/www/ deploy@backup.example.com:/backup/www/
 
# 削除も同期(注意)
rsync -av --delete /var/www/ /backup/www/
 
# 増分バックアップ(hard link で容量節約)
rsync -av --link-dest=/backup/2026-05-13 /var/www/ /backup/2026-05-14

rsync の魔法: 差分転送

rsync は ファイルの差分だけを転送 する。1GB のファイルを更新しても、変わった数 KB だけが流れる。

アルゴリズム: ファイルをブロックに分割し、チェックサム比較で変更箇所を特定。転送量を最小化しつつ rename / move も検知できる。

オプション解説:

  • -a (archive): -rlptgoD の組み合わせ。再帰・シンボリックリンク・パーミッション・タイムスタンプ・所有者・グループ・デバイスファイルを保持
  • -v (verbose): 進捗表示
  • -z (compress): 転送時 gzip 圧縮(CPU vs ネットワークのトレードオフ)

--delete の事故

# 危険: 元が空のディレクトリだった場合、宛先のファイルが全削除される
rsync -av --delete /tmp/empty/ /backup/important/

--delete は必ず --dry-run で先に確認:

rsync -av --delete --dry-run /tmp/empty/ /backup/important/

10. tar でアーカイブ

# 圧縮アーカイブ作成
tar -czf backup_$(date +%Y%m%d).tar.gz /var/www /etc/nginx
 
# 中身一覧
tar -tzf backup_20260514.tar.gz
 
# 展開
tar -xzf backup_20260514.tar.gz -C /tmp/restore/
 
# 暗号化(GPG)
tar -cz /var/www | gpg --symmetric --cipher-algo AES256 > backup.tar.gz.gpg

tar の役割

複数ファイル・ディレクトリを 1 つのアーカイブにまとめる。圧縮自体は別ツール(gzip, bzip2, xz)の仕事。

-z は gzip、-j は bzip2、-J は xz。

  • gzip: 速い、互換性最高、圧縮率中
  • xz: 遅い、圧縮率最高(gzip の 2-3 倍小さくなることも)
  • zstd (--zstd): 速度と圧縮率のバランス、現代の本命

11. S3 への転送

# AWS CLI で S3 にアップロード
aws s3 cp backup_20260514.tar.gz s3://my-backup-bucket/db/2026-05-14/ \
    --storage-class STANDARD_IA \
    --sse aws:kms \
    --sse-kms-key-id arn:aws:kms:ap-northeast-1:123456789012:key/abcd...
 
# 並列アップロード(大きいファイル向け)
aws s3 sync /backup s3://my-backup-bucket/ --storage-class GLACIER_IR

S3 ストレージクラス

クラス取り出し速度料金用途
Standard即時頻繁アクセス
Standard-IA即時月数回アクセス
Intelligent-Tiering即時自動最適化アクセスパターン不明
One Zone-IA即時中(低い)再生成可能なデータ
Glacier Instant即時アーカイブだが即時
Glacier Flexible分〜時間最低災害対応
Glacier Deep Archive12時間〜超最低法令保管

日次バックアップ = Standard-IA か Glacier Instant 月次・年次の長期保管 = Glacier Flexible / Deep Archive

適切に分けると コストが 10 分の 1 になる。

S3 セキュリティの必須設定

  • 暗号化: SSE-S3 (AES256) は最低限。SSE-KMS で鍵を IAM で制御するのが推奨
  • バケットポリシー: パブリック公開禁止。「Block Public Access」を有効化
  • MFA Delete: 削除に MFA を要求。ランサムウェア対策に必須
  • バージョニング: 過去のバージョンを保持。誤削除から復元可能
  • オブジェクトロック (WORM): 一定期間削除不可。法令準拠やランサムウェア対策
  • クロスリージョンレプリケーション: 別リージョンに自動コピー

S3 公開設定ミスで顧客 DB が世界に漏洩

過去に Uber、Verizon、Accenture などが S3 バケットの公開設定ミスで顧客データを漏洩させた事案。

対策:

  • アカウントレベルで Block Public Access を有効
  • バケットポリシーでも明示的に拒否
  • aws-nuke 等の不正設定検知ツールを CI に組み込む
  • Macie で機密データの自動検出

セッション⑤: リストアテストと運用(15-20分)

12. リストアテストが本体

「取れている」≠「戻せる」

バックアップが「読める」だけでは無意味。実際に戻せるかは試さない限り分からない。

過去に起きた典型的な失敗:

  • mysqldump の出力ファイルは存在するが、先頭の CREATE DATABASE が抜けていて復元できない
  • PostgreSQL の dump をリストアしたら、外部テーブル参照が解決できず半分失敗
  • 暗号化バックアップの 復号鍵が紛失 していて永遠に開けない
  • GitHub Actions の cron が 3 か月前から失敗していて、最新バックアップが古い

月 1 回のリストアテスト を必ず仕組み化する。

リストアテストの最低限のフロー

#!/bin/bash
# restore_test.sh
set -euo pipefail
 
DATE=$(date +%Y%m%d)
TEST_DB="restore_test_$DATE"
 
# 1. 最新バックアップを取得
aws s3 cp s3://my-backup-bucket/db/latest.sql.gz /tmp/
 
# 2. テスト DB に展開
mysql -u root -p"$PASS" -e "CREATE DATABASE $TEST_DB;"
gunzip -c /tmp/latest.sql.gz | mysql -u root -p"$PASS" "$TEST_DB"
 
# 3. 行数チェック(本番と比べて極端に違わないか)
EXPECTED=$(mysql -u root -p"$PASS" prod -sN -e "SELECT COUNT(*) FROM users;")
ACTUAL=$(mysql -u root -p"$PASS" "$TEST_DB" -sN -e "SELECT COUNT(*) FROM users;")
 
if [ "$ACTUAL" -lt $((EXPECTED * 95 / 100)) ]; then
    echo "ERROR: restored row count too low ($ACTUAL vs $EXPECTED)"
    exit 1
fi
 
# 4. テスト DB を削除
mysql -u root -p"$PASS" -e "DROP DATABASE $TEST_DB;"
 
echo "Restore test OK ($ACTUAL rows)"

これを cron で月 1 回回し、失敗時に Slack 通知。

13. リストア手順書の必要性

「秘伝のタレ」を文書化する

リストアは緊急時の操作。当事者が休暇中、退職済み、入院中 などの状況でも、別エンジニアが実行できる必要がある。

手順書に必須の項目:

  1. 復元先サーバーの準備(IP, スペック)
  2. バックアップファイルの場所と取得方法
  3. 復号方法(暗号化されている場合)
  4. リストアコマンド(コピペで動く形)
  5. 整合性確認の手順(行数、特定レコード)
  6. DNS 切替・アプリ再起動の順序
  7. ロールバック手順(うまく行かなかった場合)
  8. 連絡先(DB 管理者、インフラ責任者)

14. 暗号化と鍵管理

# GPG 対称暗号化
tar -czf - /var/www | gpg -c --cipher-algo AES256 -o backup.tar.gz.gpg
 
# パスフレーズなしで自動化したい場合は公開鍵暗号
tar -czf - /var/www | gpg -e -r backup-key@example.com -o backup.tar.gz.gpg
 
# 復号
gpg -d backup.tar.gz.gpg | tar -xzf -
 
# openssl で対称暗号化(GPG が入っていない環境)
tar -czf - /var/www | \
    openssl enc -aes-256-cbc -salt -pbkdf2 -pass file:/etc/backup.key > backup.tar.gz.enc
 
# 復号
openssl enc -d -aes-256-cbc -pbkdf2 -pass file:/etc/backup.key \
    -in backup.tar.gz.enc | tar -xzf -

鍵管理の原則

  • 鍵をバックアップと同じ場所に置かない: 同一サーバーに鍵を置いていたら、サーバー侵害時に同時侵害
  • 鍵のローテーション: 定期的に交換し、古い鍵で復号できる期間を限定
  • 複数人での共有: Shamir’s Secret Sharing 等で鍵を分割(5 人中 3 人集まれば復元できる)
  • AWS KMS / GCP KMS / HashiCorp Vault: マネージドな鍵管理サービスを使う

15. ランサムウェア対策

ランサムウェアの実態

攻撃者は侵入後、まず バックアップの破壊 を狙う。バックアップが残っていると身代金を払う動機がないため。

観測されている戦術:

  • SMB / NFS 経由でアクセス可能なバックアップサーバーを直接暗号化
  • クラウドストレージの認証情報を盗み、API でバックアップを削除
  • バックアップ用 root ssh 鍵を奪い、別サーバーのバックアップも全滅

対策:

  • Immutable backup (オブジェクトロック): S3 Object Lock や Azure Immutable Blob で「設定した期間は誰も削除できない」状態にする
  • Pull 型バックアップ: バックアップサーバーが本番に接続して取りに行く(本番から書き込めない)
  • オフライン保管: 月次は外付け HDD などのオフライン媒体に焼く(極端だが確実)
  • MFA Delete: 削除に MFA を要求

アンチパターン総まとめ

バックアップ取りっぱなしでリストア未テスト

結果: いざ復元する時に手順が不明、ファイルが壊れている、暗号鍵が見つからない、復元に1週間かかる。月1回のテストを cron 化

本番 DB と同一インスタンスにバックアップ

# NG
mysqldump mydb > /var/lib/mysql/backup.sql

ディスク故障で本番もバックアップも消える。

暗号化なしでクラウドに保存

S3 バケットの設定ミス、IAM キー漏洩などで顧客 DB が漏洩。SSE-KMS は必須

バックアップ用 IAM ロールに最小権限を超える権限

Action: "*"
Resource: "*"

このキーが漏れると S3 内容を全削除される。s3:PutObjects3:GetObject だけに絞る

cron 失敗を放置

0 3 * * * /opt/backup.sh

エラーが起きてもメールが出るだけ。Slack 通知、または「最新バックアップが N 時間以内」のメトリクスを Prometheus で監視

スキーマ変更とバックアップの非整合

アプリ側でマイグレーションを走らせるが、バックアップは旧スキーマで戻る。schema.sqldata.sql を分けて取得し、リストア手順に「マイグレーション後にデータをインポート」を含める

1 種類のバックアップだけに頼る

「日次 mysqldump がある」だけ
→ 半年前から cron が止まっていて、最新は半年前のもの

複数の方式(dump + スナップショット + binary log)を組み合わせる


実例: mysqldump を毎日 S3 に送る cron スクリプト

#!/usr/bin/env bash
# /opt/backup/mysql_backup.sh
# 毎日 03:00 に実行し、S3 に暗号化バックアップを送る
 
set -euo pipefail
 
# 設定
MYSQL_USER="backup"
MYSQL_PASS_FILE="/etc/backup/mysql.passwd"   # 600, root のみ読める
DB_NAME="mydb"
S3_BUCKET="my-backup-bucket"
S3_PREFIX="mysql/$(hostname)"
ENCRYPTION_KEY_FILE="/etc/backup/encryption.key"
RETENTION_DAYS=30
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"
WORK_DIR=$(mktemp -d)
trap "rm -rf '$WORK_DIR'" EXIT
 
DATE=$(date +%Y%m%d_%H%M%S)
DUMP_FILE="${WORK_DIR}/${DB_NAME}_${DATE}.sql.gz"
ENC_FILE="${DUMP_FILE}.enc"
 
notify_slack() {
    if [ -n "$SLACK_WEBHOOK" ]; then
        local level="$1"
        local message="$2"
        curl -s -X POST -H 'Content-Type: application/json' \
            -d "{\"text\":\"[backup ${level}] ${message}\"}" \
            "$SLACK_WEBHOOK" > /dev/null || true
    fi
}
 
on_error() {
    notify_slack "ERROR" "MySQL バックアップ失敗: $(hostname) line $1"
    exit 1
}
trap 'on_error $LINENO' ERR
 
echo "[$(date)] バックアップ開始: $DB_NAME"
 
# 1. mysqldump(gzip 圧縮しながらストリーミング)
mysqldump \
    --user="$MYSQL_USER" \
    --password="$(cat $MYSQL_PASS_FILE)" \
    --single-transaction \
    --quick \
    --routines \
    --triggers \
    --events \
    --master-data=2 \
    --databases "$DB_NAME" \
    | gzip > "$DUMP_FILE"
 
# 2. 暗号化
openssl enc -aes-256-cbc -salt -pbkdf2 \
    -pass file:"$ENCRYPTION_KEY_FILE" \
    -in "$DUMP_FILE" \
    -out "$ENC_FILE"
 
SIZE=$(du -h "$ENC_FILE" | cut -f1)
 
# 3. S3 にアップロード(SSE-KMS で二重暗号化)
aws s3 cp "$ENC_FILE" \
    "s3://${S3_BUCKET}/${S3_PREFIX}/${DATE}/${DB_NAME}.sql.gz.enc" \
    --storage-class STANDARD_IA \
    --sse aws:kms
 
# 4. 古いバックアップを S3 から削除(lifecycle policy 推奨だが手動でも)
CUTOFF=$(date -d "${RETENTION_DAYS} days ago" +%Y%m%d)
aws s3 ls "s3://${S3_BUCKET}/${S3_PREFIX}/" | \
    awk '{print $2}' | \
    while read dir; do
        dir_clean="${dir%/}"
        if [[ "$dir_clean" < "$CUTOFF" ]]; then
            aws s3 rm "s3://${S3_BUCKET}/${S3_PREFIX}/${dir}" --recursive
            echo "  deleted old backup: $dir"
        fi
    done
 
# 5. 結果通知
notify_slack "OK" "MySQL バックアップ完了: ${DB_NAME} (${SIZE})"
echo "[$(date)] バックアップ完了 (${SIZE})"
# /etc/cron.d/mysql_backup
0 3 * * * root /opt/backup/mysql_backup.sh >> /var/log/backup.log 2>&1

補強アイデア

  • systemd timer に置き換える: 失敗時の journal ログが残り、systemctl status で履歴確認可能
  • Prometheus pushgateway に送る: 最終成功時刻をメトリクス化し、24 時間以内に成功していなければアラート
  • リストアテストもセットで cron 化: 翌朝 04:00 に最新バックアップで自動リストアテスト

練習課題

  1. ローカルに MySQL コンテナを立て、テストデータを入れる
  2. mysqldump --single-transaction でダンプを取る
  3. 同コンテナで DROP DATABASE し、ダンプから復元
  4. 復元後の行数がオリジナルと一致することを確認
  5. binary log を有効化し、mysqlbinlog で過去のクエリが見えることを確認
  6. pg_dump -Fc で PostgreSQL のバックアップを取り、pg_restore -l で目録を見る
  7. rsync -av --link-dest で増分バックアップを実装し、du -sh で容量を確認
  8. AWS CLI で S3 バケットを作成、aws s3 cp でファイル転送
  9. S3 バケットに Object Lock を有効化し、削除が拒否されることを確認
  10. 「リストア手順書」を Markdown で書き、第三者に読んで実行してもらう

締め: git で証跡を残す

cd ~/learn/infra/backup
 
ls
# mysql_backup.sh
# pg_backup.sh
# restore_test.sh
# restore_runbook.md   # リストア手順書
# README.md
 
git add .
git commit -m "feat(backup): MySQL/PostgreSQL の S3 暗号化バックアップとリストア手順整備"

チェックリスト

  • 3-2-1 ルールを自分の言葉で説明できる
  • RPO / RTO の違いと用途別の目安が分かる
  • mysqldump --single-transaction の必要性を説明できる
  • PITR の仕組みと用途を理解
  • pg_dump -Fc を使えるようになった
  • WAL アーカイブの設定方法を知っている
  • rsync で差分バックアップを書ける
  • S3 のストレージクラスを使い分けられる
  • バックアップに暗号化と最小権限 IAM を適用できる
  • リストアテストを cron 化する重要性を理解
  • ランサムウェア対策(Object Lock, MFA Delete)を実装できる
  • 手順書を書いて他者が実行できる状態にした

詰まった時のチートシート

やりたいことコマンド
MySQL 全 DB dumpmysqldump --all-databases --single-transaction --routines
MySQL リストアmysql -u root -p < dump.sql
MySQL binary log 確認SHOW BINARY LOGS;
binlog を SQL にmysqlbinlog mysql-bin.000123
PG dump(推奨)pg_dump -Fc mydb > mydb.dump
PG 並列 dumppg_dump -Fd -j 4 mydb -f dir/
PG リストアpg_restore -d mydb mydb.dump
ベースバックアップpg_basebackup -D /backup -F t -z
rsync 差分rsync -av --link-dest=prev/ src/ new/
tar 圧縮tar -czf x.tar.gz dir/
GPG 暗号化gpg -c -o x.gpg input
S3 アップ(KMS)aws s3 cp x s3://b/ --sse aws:kms
S3 同期aws s3 sync local/ s3://b/

対比表: 論理バックアップ vs 物理バックアップ

観点論理(mysqldump / pg_dump)物理(xtrabackup / pg_basebackup)
中身SQL 文データファイルそのもの
速度遅い(変換コスト)速い
ポータビリティ高(バージョン跨ぎOK)低(同バージョン必須)
サイズ中(圧縮効きやすい)大(生データ)
リストア時間長い短い
増分バックアップ苦手得意(WAL/binlog 併用)
適性小〜中規模、別環境への引越し大規模、PITR要件

対比表: RTO と RPO

用語意味
RTO (Recovery Time Objective)復旧までの目標時間「2時間で本番復旧」
RPO (Recovery Point Objective)失っていいデータの最大時間幅「直近5分まで戻せる」

業務要件によって設計が変わる。決済システムなら RPO=0(無停止レプリケーション必須)、ブログなら RPO=24h(日次でOK)。

「実務OK」基準

  • mysqldump / pg_dump の必須オプションを暗唱できる
  • PITR の概念と、その実現に必要な構成要素を説明できる
  • 3-2-1 ルールに照らして自分のシステムを評価できる
  • リストアテストの仕組みを cron / CI で組める
  • S3 のセキュリティ設定(暗号化、Object Lock、Block Public Access)を実装できる
  • ランサムウェア対策としての immutable backup を語れる
  • 「バックアップは取れている、リストアは知らない」状態を絶対に作らない

自己評価チェックリスト

知識レベル

  • 3-2-1 ルールを自分のシステムに当てはめて評価できる
  • RTO / RPO を業務要件から導出できる
  • 論理 / 物理 / PITR の使い分けを判断できる
  • ランサムウェア対策(Object Lock 等)を語れる

実行レベル

  • mysqldump or pg_dump を cron で日次実行する仕組みを書いた
  • S3 に暗号化転送するスクリプトを動作確認した
  • リストア手順書を「他人が読んで実行できる」レベルに書いた
  • 実機でリストア検証を1回完遂した

メタ認知

  • 「最後にリストアテストをやったのはいつか」を即答できる
  • バックアップ失敗を監視で検知する仕組みがあるか確認した

さらに深掘りするなら


まとめ: ここまでのインフラ Level 3 の総括

VPS を借りる (3-1) → ドメインを向ける (3-2) → HTTPS 化する (3-3) → CI/CD で自動化する (3-4) → 観測する (3-5) → バックアップで守る (3-6)。

これで 「個人で本番運用に耐えるバックエンドサービスを 1 人で立てられる」 状態になった。次の Level 4 では、Kubernetes、IaC(Terraform)、マルチリージョン構成、SRE 文化(SLO / Error Budget)へ進む。