48RailsAppのつながっているRDS 8台を Aurora 2台にDMS + CDCでゼロダウンタイムデプロイをを実現した記録
TL;DR
- RDS MySQL 8台 + Aurora 1台 → Aurora 2台に統合、48アプリをゼロダウンタイムで移行した
- AWS DMS の Full Load + CDC(Change Data Capture)でリアルタイム同期しながらローリング切り替え
- コスト $596/月 → $297/月(年間約54万円削減)
- DMS には致命的な落とし穴が4つある。知らないと本番データが壊れる
なぜやったか
9台のRDS/Auroraインスタンスが乱立していた。歴史的経緯でアプリごとにRDSを立てた結果、こうなった。
rds-main db.t3.medium 35GB ← ストレージ残り7.3GB 🚨
rds-shared-1 db.t3.small 16GB
rds-shared-2 db.t3.medium 2.7GB
rds-shared-3 db.t3.small 10.6GB
rds-apps db.t3.medium 5.4GB
rds-sub-1 db.t3.small 3GB
rds-sub-2 db.t3.medium 5GB
aurora-sub db.t3.medium (Aurora)
rds-legacy db.t3.small 8GB
────────────────────────────────
合計: 9台 $595.79/月
特に rds-main はストレージ残り7.3GBで、放置すれば本番障害確定だった。
全体アーキテクチャ
Before
┌─────────────────────────────────────────────────┐
│ VPC (ap-northeast-1) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ rds-main │ │ rds-shared-1 │ │
│ │ t3.medium │ │ t3.small │ │
│ │ 35GB │ │ 16GB │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ │
│ │ rds-shared-2 │ │ rds-shared-3 │ │
│ │ t3.medium │ │ t3.small │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌────────┐ │
│ │ rds-apps │ │ rds-sub-1 │ │rds-sub │ │
│ │ t3.medium │ │ t3.small │ │ -2 │ │
│ └──────────────┘ └──────────────┘ └────────┘ │
│ │
│ 48 Rails apps が接続 │
└─────────────────────────────────────────────────┘
After
┌─────────────────────────────────────────────────┐
│ VPC (ap-northeast-1) │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ aurora-primary (r6g.large) │ │
│ │ rds-main + rds-shared-1/2/3 │ │
│ │ + rds-apps + rds-legacy │ │
│ │ Writer / Reader endpoint │ │
│ └────────────────────┬────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ 39 apps 接続 │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ aurora-secondary (r6g.large) │ │
│ │ rds-sub-1 + rds-sub-2 + 他2DB │ │
│ └────────────────────┬────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ 9 apps 接続 │ │
│ └─────────────────────┘ │
│ │
│ コスト: $596/月 → $297/月 (年間 $3,588 削減) │
└─────────────────────────────────────────────────┘
移行戦略: DMS + CDC でゼロダウンタイム
「全アプリ一斉停止 → 切り替え → 一斉再起動」だと48アプリで15〜25分のダウンタイムが発生する。深夜でもこれは許容できない。
DMS の CDC(Change Data Capture)を使えば、旧RDSに書き込みが続いている間もリアルタイムでAuroraに同期できる。 同期が追いついた状態で、アプリを1台ずつ切り替えればダウンタイムはアプリ単位で数秒だけ。
ゼロダウンタイム移行の全体像
===========================
Phase A (日中・本番影響なし)
──────────────────────────────────────────────────
rds-main ──binlog──→ Aurora Read Replica (aurora-primary)
↑ 自動同期
rds-shared-1 ─┐
rds-shared-2 ─┤ DMS ┌─────────────────┐
rds-shared-3 ─┼──Full Load──→│ aurora-primary │
rds-apps ─┘ + CDC └─────────────────┘
rds-sub-1 ─┐ DMS ┌───────────────────┐
rds-sub-2 ─┼──Full Load────→│ aurora-secondary │
(他DB) ─┘ + CDC └───────────────────┘
※ 全て読み取りのみ。本番アプリは旧RDSのまま稼働中。
Phase B (Aurora昇格 + DMS追加)
──────────────────────────────────────────────────
Aurora Read Replica 昇格 → 独立 Writer に
rds-main ──DMS CDC──→ aurora-primary (新たにDMSタスク追加)
※ 全9タスクの CDC ラグ = 0 を確認
Phase C (ALB + Docker Blue-Green でゼロダウンタイム切り替え)
──────────────────────────────────────────────────
アプリ単位で「旧DB接続コンテナ」と「新DB接続コンテナ」を
同時に起動し、ALB のターゲットグループを切り替える。
【切り替え前】
┌──────────┐
User──→│ ALB │
└────┬─────┘
│ ターゲットグループ: TG-old
▼
┌───────────────┐
│ Docker (旧) │──→ 旧 RDS
│ DB_HOST= │ (rds-main)
│ rds-main │
└───────────────┘
┌──────────┐
(待機中) │ DMS CDC │
┌───────────────┐ │ ラグ = 0 │
│ Docker (新) │──→ └────┬─────┘
│ DB_HOST= │ ▼
│ aurora-primary│ ┌──────────┐
└───────────────┘ │ Aurora │
└──────────┘
【切り替え】
1. 新コンテナ起動 (Aurora 接続) → ヘルスチェック OK 確認
2. ALB ターゲットグループを TG-old → TG-new に切り替え
3. 旧コンテナにリクエストが来なくなったことを確認
4. 旧コンテナ停止
【切り替え後】
┌──────────┐
User──→│ ALB │
└────┬─────┘
│ ターゲットグループ: TG-new ✅
▼
┌────────────────┐
│ Docker (新) │──→ Aurora (aurora-primary)
│ DB_HOST= │
│ aurora-primary│
└────────────────┘
→ ユーザーから見るとリクエストが途切れない
→ 旧コンテナの処理中リクエストも drain で完了を待つ
→ アプリ単位で数秒、全体ダウンタイム = 0
ALB Blue-Green 切り替えの仕組み
ポイントは 旧DBに繋がっている Docker コンテナと、新DB(Aurora)に繋がっている Docker コンテナを同時に立てる こと。
┌────────────────────────────────────────────────────────┐
│ EC2 / ECS │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Container OLD │ │ Container NEW │ │
│ │ │ │ │ │
│ │ DB_HOST= │ │ DB_HOST= │ │
│ │ 旧RDS │ │ Aurora │ │
│ │ │ │ │ │
│ │ ← ALB が接続中 │ │ ← ヘルスチェック │ │
│ │ │ │ OK 待ち │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
└────────────┼─────────────────────────┼─────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 旧 RDS │──DMS CDC→│ Aurora │
└──────────────┘ └──────────────┘
手順はこう:
-
新コンテナを起動(環境変数
DB_HOSTを Aurora エンドポイントに設定) -
新コンテナのヘルスチェックが通るまで待つ(
/upが 200 を返す) - ALB のターゲットグループを新コンテナに切り替え
- 旧コンテナへの接続を drain(ALB の deregistration delay でリクエスト処理完了を待つ)
- 旧コンテナを停止
Kamal を使っている場合、この Blue-Green デプロイは kamal app boot が自動でやってくれる。
# 1. 環境変数を更新(DB_HOST を Aurora に変更)
kamal env push -d production
# 2. 新コンテナ起動 → ヘルスチェック → ALB切替 → 旧コンテナ停止
# kamal app boot が以下を自動で行う:
# - 新コンテナを起動(新しい DB_HOST で)
# - ヘルスチェック OK を確認
# - kamal-proxy のルーティングを新コンテナに切替
# - 旧コンテナを graceful shutdown
kamal app boot -d production
DMS の CDC が効いているので、切り替えの瞬間にデータの不整合は起きない。 旧コンテナが旧 RDS に書き込んだデータは CDC でリアルタイムに Aurora に同期されている。新コンテナが Aurora に書き込んだデータはそのまま Aurora に入る。どちらに書いても最終的に Aurora にデータが集まる設計。
具体的な手順
- Aurora Read Replica 作成(最大の35GB RDSから。約1時間で同期完了)
-
DMS レプリケーションインスタンス作成(
dms.t3.medium、50GB) - DMS タスク7本を並列開始(Full Load + CDC)
- Full Load 完了 + CDC ラグ 0 確認(約1時間)
- Aurora 昇格(Writer に昇格)
- 最大RDS 用 DMS タスク追加(昇格で binlog レプリケーション切れるため)
- 全9タスク CDC ラグ 0 確認
-
48アプリを ALB Blue-Green でローリング切り替え
- Kamal アプリ:
kamal env push+kamal app boot(新コンテナ起動 → ALB 切替 → 旧コンテナ停止) - Capistrano アプリ: credentials/env/database.yml 書き換え +
systemctl restart puma
- Kamal アプリ:
- 旧コンテナが全て停止したことを確認
- DMS 全タスク停止 → 旧 RDS 停止
結果: 全体のダウンタイム = 0秒。 ALB が新旧コンテナ間のトラフィックを瞬時に切り替えるため、ユーザーのリクエストは一切途切れない。
DMS の4つの落とし穴 ── そして「事前に潰す」方法
ここからが本題。DMS は便利だが、デフォルト設定のまま使うと何も言わずにデータを壊す。
我々は最初これを知らず、移行後にデータロストを経験した。そこから学んだ結論はこうだ:
DMS デフォルトが壊すもの4つ
まず、DMS をデフォルト設定で使うと何が起きるかを知っておく必要がある。
1. DEFAULT 値が吹っ飛ぶ
DMS の Full Load はテーブルを CREATE TABLE してからデータを流し込むが、カラムの DEFAULT 値を移行しない。
-- ソース(旧 RDS)
CREATE TABLE users (
status varchar(20) DEFAULT 'active',
role varchar(20) DEFAULT 'member',
created_at datetime DEFAULT CURRENT_TIMESTAMP
);
-- DMS が作ったターゲット(Aurora)
CREATE TABLE users (
status varchar(20), -- DEFAULT が消えた!
role varchar(20), -- DEFAULT が消えた!
created_at datetime -- DEFAULT が消えた!
);
2. AUTO_INCREMENT が吹っ飛ぶ
テーブルの AUTO_INCREMENT 値がリセットされる。次の INSERT で既存 ID と衝突して Duplicate entry エラーになる。
-- ソース: AUTO_INCREMENT = 158432
-- DMS が作ったターゲット: AUTO_INCREMENT = 1
-- → 次の INSERT で Duplicate entry エラー!
3. 外部キー・インデックスが全部吹っ飛ぶ
DMS は 外部キー制約(FOREIGN KEY)、セカンダリインデックス、UNIQUE 制約 を移行しない。Full Load 時にデータ投入を高速化するために意図的に省略しているが、CDC フェーズに入っても復元してくれない。
-- ソースにあったもの
FOREIGN KEY (user_id) REFERENCES users(id) → 消える
INDEX idx_orders_status (status) → 消える
UNIQUE INDEX idx_users_email (email) → 消える
4. 14KB 超の TEXT カラムが切り捨てられる
これが一番ハマった。DMS にはLOB(Large Object)の扱いに関する設定があり、デフォルトの LimitedSizeLobMode では LobMaxSize(デフォルト32KB)を超えるデータが切り捨てられる。
しかし実際には、DMS 内部のチャンク処理の都合で 約14KB 前後で切り捨てが発生する ケースがあった。
TEXT/MEDIUMTEXT/LONGTEXT カラムの挙動:
データサイズ 結果
─────────────────────────
< 14KB → OK(正常に移行)
14KB〜32KB → ⚠️ 切り捨てられる可能性あり
> 32KB → ❌ 確実に切り捨て(LobMaxSize 超過)
DMS のログにもエラーは出ない。 メール本文、HTML テンプレート、JSON データなどが無警告でぶった切られる。
正解: 事前にスキーマを作って DMS にはデータだけ流させる
落とし穴 1〜3 は全て 「DMS にテーブル作成をやらせている」から起きる。であれば、答えはシンプル:
Aurora 側に事前に mysqldump --no-data で完全なスキーマを作成し、DMS の TargetTablePrepMode を TRUNCATE_BEFORE_LOAD に設定する。
# Step 1: ソース RDS から DDL(スキーマのみ)をダンプ
mysqldump -h source-rds -u user -p \
--no-data --routines --triggers --events \
--databases db1 db2 db3 > schema.sql
# Step 2: Aurora に完全なスキーマを事前作成
mysql -h aurora-primary -u user -p < schema.sql
これで DEFAULT 値、AUTO_INCREMENT、外部キー、インデックスは 全て Aurora 側に正しく作成される。
次に、DMS タスク設定で「テーブルは既にあるから、データだけ流し込め」と指示する:
{
"FullLoadSettings": {
"TargetTablePrepMode": "TRUNCATE_BEFORE_LOAD"
}
}
| TargetTablePrepMode | 挙動 |
|---|---|
DROP_AND_CREATE(デフォルト) |
テーブルを DROP → DMS が CREATE → スキーマが壊れる |
TRUNCATE_BEFORE_LOAD |
既存テーブルを TRUNCATE → データだけ INSERT → スキーマ保持 |
DO_NOTHING |
何もしない → データを追記 |
落とし穴 4(TEXT 切り捨て)だけはスキーマの問題ではなくデータ転送の問題なので、別途 FullLobMode: true を設定する必要がある:
{
"TargetMetadata": {
"SupportLobs": true,
"FullLobMode": true,
"LobChunkSize": 64,
"LimitedSizeLobMode": false
}
}
ALB Blue-Green で「片方からしか書き込まない」を保証する
スキーマを事前に作ったとしても、旧 RDS と Aurora の両方に同時に書き込みが走ると整合性が崩れる。例えば両方で INSERT が走れば AUTO_INCREMENT が衝突する。
ここで前述の ALB Blue-Green が効いてくる:
【旧コンテナのみ稼働中】
User → ALB → Docker (旧) → 旧 RDS ──DMS CDC──→ Aurora
↑ 書き込みなし
【ALB 切り替え】
User → ALB → Docker (新) → Aurora
Docker (旧) → 旧 RDS ← drain 完了後に停止
↑ 書き込みなし
【切り替え完了】
User → ALB → Docker (新) → Aurora ← ここだけに書き込み
ALB の切り替えは瞬時で、旧コンテナは drain(処理中リクエストの完了待ち)後に停止される。 つまり、あるアプリから見て「旧 RDS と Aurora の両方に同時に書き込まれる」瞬間は存在しない。
この「片方からしか書き込まない」保証があるから:
- DEFAULT → 事前に作ったスキーマがそのまま使われる
- AUTO_INCREMENT → 旧 RDS でインクリメントされた値が DMS で Aurora に同期 → 切り替え後は Aurora の AUTO_INCREMENT がそのまま引き継がれる
- 外部キー・インデックス → 事前に作ったものがそのまま
-
TEXT データ →
FullLobMode: trueで完全転送
DMS 落とし穴まとめ
| 落とし穴 | 原因 | 事前対処 |
|---|---|---|
| DEFAULT 消失 | DMS がテーブルを作り直す |
mysqldump --no-data で事前にスキーマ作成 |
| AUTO_INCREMENT リセット | 同上 | 同上 + ALB Blue-Green で片方書き込み保証 |
| 外部キー・INDEX 消失 | 同上 | 同上 |
| TEXT 14KB 切り捨て | LimitedSizeLobMode のデフォルト |
FullLobMode: true を設定 |
事前準備スクリプト:
#!/bin/bash
# pre-dms-migration-setup.sh
# DMS 開始前に Aurora 側にスキーマを作成する
SOURCE_HOST="source-rds-endpoint"
TARGET_HOST="aurora-endpoint"
DATABASES="db1 db2 db3"
echo "=== 1. ソースから DDL ダンプ ==="
mysqldump -h $SOURCE_HOST -u user -p \
--no-data --routines --triggers --events \
--databases $DATABASES > /tmp/schema.sql
echo "=== 2. Aurora にスキーマ作成 ==="
mysql -h $TARGET_HOST -u user -p < /tmp/schema.sql
echo "=== 3. スキーマ比較(検証) ==="
for DB in $DATABASES; do
mysqldump -h $SOURCE_HOST --no-data $DB > /tmp/src_${DB}.sql
mysqldump -h $TARGET_HOST --no-data $DB > /tmp/tgt_${DB}.sql
DIFF=$(diff /tmp/src_${DB}.sql /tmp/tgt_${DB}.sql)
if [ -z "$DIFF" ]; then
echo " OK $DB"
else
echo " NG $DB ← diff あり、要確認"
echo "$DIFF" | head -20
fi
done
echo "=== 4. DMS タスク設定の確認 ==="
echo "以下を DMS タスク設定に含めること:"
echo ' TargetTablePrepMode: "TRUNCATE_BEFORE_LOAD"'
echo ' FullLobMode: true'
echo ' LimitedSizeLobMode: false'
タイムライン(実際の作業記録)
土曜 Phase A ── 日中、本番影響なし
─────────────────────────────────────────
14:00 Aurora Read Replica 作成開始
14:15 DMS レプリケーションインスタンス作成
14:30 DMS ソース/ターゲットエンドポイント作成 (8個)
15:00 DMS 接続テスト OK
15:30 Aurora 同期完了 (ReplicaLag = 0)
16:00 Aurora ユーザー/権限作成
16:30 DMS 7タスク並列開始 (Full Load + CDC)
17:30 Full Load 完了、CDC ラグ 0 ✅
土曜 Phase B ── Aurora昇格 + DMS追加
─────────────────────────────────────────
18:00 Aurora Read Replica 昇格
18:05 rds-main → aurora-primary DMS タスク追加
18:30 サブDB full-load 完了
19:00 全9タスク CDC active、ラグ 0 ✅
土曜 Phase C ── ALB Blue-Green ローリング切り替え
─────────────────────────────────────────
19:30 Kamal 28アプリ env push + app boot (並列)
20:00 Capistrano credentials 4台 切り替え
20:15 Capistrano .env 4台 切り替え
20:30 サブ系 切り替え (順序依存あり)
20:45 レガシー系 3台 切り替え
21:00 全48アプリ ヘルスチェック OK ✅
21:05 DMS 全タスク停止
翌日 クリーンアップ
─────────────────────────────────────────
午前 全アプリ最終確認 (Sidekiq, バッチ含む)
午後 DMS リソース削除、旧 RDS 8台停止
コスト削減効果
Before: $595.79/月 (9台: t3.small × 3 + t3.medium × 5 + Aurora t3.medium × 1)
After: $297.00/月 (2台: Aurora r6g.large × 2)
──────────────────────────────────
削減: $299/月 (-50%)
年間: $3,588 (約54万円)
Aurora r6g.large(Graviton2)は t3.medium より高性能で、かつスケーラブル。ストレージは自動拡張なので「残り7.3GB」みたいな恐怖とも無縁になった。
教訓
-
DMS にスキーマを作らせるな。
mysqldump --no-dataで事前に完全なスキーマを用意し、TargetTablePrepMode: TRUNCATE_BEFORE_LOADでデータだけ流させる。これだけで DEFAULT、AUTO_INCREMENT、外部キー、インデックスの問題は全て消える -
DMS のLOB処理はデフォルトで壊れている。本番で使うなら
FullLobMode: trueは必須。ログにエラーも出ないので、TEXT 切り捨てに気づかない - ALB Blue-Green で「片方からしか書き込まない」を保証する。旧DB接続コンテナと新DB接続コンテナを同時に立てて ALB で瞬時に切り替えれば、ダウンタイムなし かつ データ整合性も保たれる
- 「移行後に直す」は危険。「移行前に正しく設定する」が正解。我々は最初データロストしてから学んだ。事前準備に時間をかけるほうが圧倒的に安全
- 検証スクリプトは事前に用意しておく。DDL比較、データ長比較、レコード数比較は移行直後に即座に回せるようにしておくべき
まとめ
AWS DMS は強力なツールだが、「マネージドサービスだから全部やってくれるだろう」と思うと痛い目を見る。
DMS を安全に使うための3原則:
-
スキーマは自分で作る(
mysqldump --no-data→ Aurora に流し込み →TRUNCATE_BEFORE_LOAD) -
LOB は
FullLobMode: true(デフォルトだと 14KB 超のデータが無警告で消える) - 書き込みは常に片方だけ(ALB Blue-Green で旧DB → 新DB の瞬時切り替え)
この3つを守れば、DMS + CDC でゼロダウンタイム移行は安全に実現できる。この記事が RDS → Aurora 移行を検討している人の参考になれば幸いです。
Discussion