🔥

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    │
     └──────────────┘          └──────────────┘

手順はこう:

  1. 新コンテナを起動(環境変数 DB_HOST を Aurora エンドポイントに設定)
  2. 新コンテナのヘルスチェックが通るまで待つ/up が 200 を返す)
  3. ALB のターゲットグループを新コンテナに切り替え
  4. 旧コンテナへの接続を drain(ALB の deregistration delay でリクエスト処理完了を待つ)
  5. 旧コンテナを停止

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 にデータが集まる設計。

具体的な手順

  1. Aurora Read Replica 作成(最大の35GB RDSから。約1時間で同期完了)
  2. DMS レプリケーションインスタンス作成dms.t3.medium、50GB)
  3. DMS タスク7本を並列開始(Full Load + CDC)
  4. Full Load 完了 + CDC ラグ 0 確認(約1時間)
  5. Aurora 昇格(Writer に昇格)
  6. 最大RDS 用 DMS タスク追加(昇格で binlog レプリケーション切れるため)
  7. 全9タスク CDC ラグ 0 確認
  8. 48アプリを ALB Blue-Green でローリング切り替え
    • Kamal アプリ: kamal env push + kamal app boot(新コンテナ起動 → ALB 切替 → 旧コンテナ停止)
    • Capistrano アプリ: credentials/env/database.yml 書き換え + systemctl restart puma
  9. 旧コンテナが全て停止したことを確認
  10. 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 の TargetTablePrepModeTRUNCATE_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」みたいな恐怖とも無縁になった。

教訓

  1. DMS にスキーマを作らせるなmysqldump --no-data で事前に完全なスキーマを用意し、TargetTablePrepMode: TRUNCATE_BEFORE_LOAD でデータだけ流させる。これだけで DEFAULT、AUTO_INCREMENT、外部キー、インデックスの問題は全て消える
  2. DMS のLOB処理はデフォルトで壊れている。本番で使うなら FullLobMode: true は必須。ログにエラーも出ないので、TEXT 切り捨てに気づかない
  3. ALB Blue-Green で「片方からしか書き込まない」を保証する。旧DB接続コンテナと新DB接続コンテナを同時に立てて ALB で瞬時に切り替えれば、ダウンタイムなし かつ データ整合性も保たれる
  4. 「移行後に直す」は危険。「移行前に正しく設定する」が正解。我々は最初データロストしてから学んだ。事前準備に時間をかけるほうが圧倒的に安全
  5. 検証スクリプトは事前に用意しておく。DDL比較、データ長比較、レコード数比較は移行直後に即座に回せるようにしておくべき

まとめ

AWS DMS は強力なツールだが、「マネージドサービスだから全部やってくれるだろう」と思うと痛い目を見る。

DMS を安全に使うための3原則:

  1. スキーマは自分で作るmysqldump --no-data → Aurora に流し込み → TRUNCATE_BEFORE_LOAD
  2. LOB は FullLobMode: true(デフォルトだと 14KB 超のデータが無警告で消える)
  3. 書き込みは常に片方だけ(ALB Blue-Green で旧DB → 新DB の瞬時切り替え)

この3つを守れば、DMS + CDC でゼロダウンタイム移行は安全に実現できる。この記事が RDS → Aurora 移行を検討している人の参考になれば幸いです。

Discussion