投稿日:
更新日:

Database Migration Service で Cloud SQL を移設する

Authors

目次

banner.png

はじめに

Google Cloud のデータベースソリューションへの移行を支援するフルマネージドなマイグレーションサービスとして Database Migration Service(DMS)があります。 DMS は、オンプレミスまたは他のクラウドサービスから Google Cloud へのデータベース移行、もしくは Google Cloud プロジェクト間でデータベース移行する際に、複雑な作業プロセスをマネージドに実行・管理してくれます。

データベースの移行作業は、例えば初期データのダンプとインポート、アプリケーションが稼働している間の更新データの整合性の確保、スキーマの互換性チェックと調整、最終的なマスター昇格作業といった多くのステップが必要になります。 DMS では、これらのプロセスを一元的に管理して複雑な移行プロセスを支援するように設計されています。

直近、Google Cloud プロジェクト間で Cloud SQL を移設する際に DMS を使ったマイグレーションを実施したので、今回のブログでは DMS の紹介を交えつつ、データベース移設手順について紹介したいと思います。

Database Migration Service

dms-overview.png

複数の Google Cloud プロジェクトに跨るシステムを構成している場合や、オンプレミスまたは他のクラウドサービスから Google Cloud のマネージドデータベースへ移行するケースでは、ネットワークやデータの整合性、スキーマの互換性等を意識しながら段階的に移設を進める必要があります。 Google Cloud が提供する Database Migration Service(DMS)は、こうした一連のマイグレーションプロセスを集約的に管理してくれるマネージドサービスです。

DMS は、主に Cloud SQL や AlloyDB へのマイグレーションに対応しており、スナップショットの取得と変更データの継続的なレプリケーションによって アプリケーションの切り替えに伴うダウンタイムを最小限に抑える ことができます。

また、Oracle や SQL Server といった異なるデータベースエンジンからの移行にも対応しており、スキーマ変換や互換性の調整を含めた移行パスを構成することが可能です。

移行シナリオ

DMS は次の移行シナリオをサポートしています。

Continuous Migration と One-time Migration 移設の方法に関するもので、Homogeneous Migration と Heterogeneous Migration は、移設元と移設先のデータベースエンジンの種類に関するものです。

Continuous Migration

Continuous Migration による移設では、マイグレーション初期に対象データベース内の全てのレコードを Full Dump してターゲットに Load します。 その後は、CDC(Change Data Capture) でソースからターゲットへの変更データの継続的なレプリケーションを行います。

Full Dump 後は継続的なレプリケーションによって、常にソースとターゲットでデータを同期します。 移設先のインスタンスをスタンドアロン化する際は、ソースとターゲットが同期している状態で切り替えを行うことで、ダウンタイムを最小限に抑えることができます。

One-time Migration

One-time Migration による移設では、データベースの単一の Point-In-Time(PIT)Snapshot を取得して、それをターゲットに適用します。 One-time Migration のマイグレーションプロセスは Full Dump と Load であり、Load 完了後は即座にターゲットのインスタンスが使用可能になります。

ただし、Continuous Migration のように移行中および Full Dump の際はソースデータベースへの書き込みが行えないため、移行プロセス中にダウンタイムが発生する可能性があり、レコード数が多いほどその時間は長くなります。

Homogeneous Migration

Homogeneous Migration は、ソースデータベースから同じデータベースエンジンを使用するターゲットデータベースにマイグレーションする際に利用します。 例えば、MySQL から Cloud SQL for MySQL への移行、PostgreSQL から Cloud SQL for PostgreSQL への移行、SQL Server から Cloud SQL for SQL Server への移行等がこれに該当します。

Homogeneous Migration が正常に終了すると、データベースのスキーマ、データ、メタデータ等、移行されたすべてのデータベースオブジェクトが維持されます。

DMS はデフォルトで全てのデータベースを移行しますが、PostgreSQL から AlloyDB への移行や SQL Server から Cloud SQL Server への移行等、一部ではソースインスタンスからターゲットインスタンスにデータベースのサブセットを選択的に移行します。

同種のデータベースエンジンの移行では、テーブル定義やデータ型といった DDL(Data Definition Language)が同じであるため スキーマの変換は不要 です。 互換性の問題がほとんどなく、比較的スムーズに移行できるシナリオです。

同種移行では次のシナリオがサポートされています。

  • Cloud SQL for MySQL への移行
  • Cloud SQL for SQL Server への移行
  • Cloud SQL for PostgreSQL への移行
  • AlloyDB for PostgreSQL への移行

Heterogeneous Migration

Heterogeneous Migration は、ソースデータベースから異なるデータベースエンジンを使用するターゲットデータベースにマイグレーションする際に利用します。 例えば、Oracle から Cloud SQL for PostgreSQL への移行、SQL Server から AlloyDB for PostgreSQL への移行等がこれに該当します。

Heterogeneous Migration では、ソースデータベースとターゲットデータベース間のスキーマ構造、データ型、メタデータコンポーネントの違いにより、Homogeneous Migration よりも複雑になります。

異種のデータベースエンジンの移行では スキーマ変換が必須 です。 データ型、関数、トリガ、ストアドプロシージャ、シーケンス等がデータベースソリューション毎に異なるため、そのままでは移行することができません。

異種移行では次のシナリオがサポートされています。

Source DatabaseDestination Database
OracleCloud SQL for PostgreSQL
OracleAlloyDB for PostgreSQL
SQL ServerCloud SQL for PostgreSQL
SQL ServerAlloyDB for PostgreSQL

DMS では Gemini を使った変換支援や Conversion Workspace での手動調整もサポートされています。参考

DMS のネットワーク接続形態

DMS は、プライベートネットワークとパブリックネットワークのいずれの方法でも Cloud SQL へ接続することができます。 パブリックネットワークを介する場合でも、データベースマイグレーションサービスによってレプリケーションが行われるため転送データ(レプリケーションされるデータ)は保護されます。参考

ターゲットデーターベースへの接続方法

target-connectivity-decision-tree.png

プライベート接続

プライベート IP アドレスを使用してデータベースをマイグレーションする際はプライベート接続形態を、以下の 2 つから選択できます。

  • Private Service Connect 対応のインスタンス
private-service-connect.png

Private Service Connect(PSC)では Google Cloud 内部のプライベートネットワークを通じてインスタンスへ直接接続します。 PSC は VPC 内に作成したネットワークアタッチメントを使用します。 PSC を利用すると、Google Cloud のバックボーンネットワークを通じてアクセスできるため、セキュリティやレイテンシの観点で優れています。

この構成はシンプルで手軽に利用できますが、PSC 対応のインスタンスには 制限 があるため、事前に構成要件を確認しておく必要があります。

  • Private Service Connect 非対応のインスタンス
non-private-service-connect.png

PSC 対応のインスタンスを移行先データベースとして使用できない場合でも、プライベート接続を利用することができます。

ただし、DMS と移行先のプライベート IP 間のトラフィックを転送するために、ネットワーク内に追加の VM(プロキシ)が必要になるため、構成は複雑になります。

DMS からデータベースインスタンスへのアクセスは VPC を経由して PSA で接続されます。 そのため、事前にインスタンスと対象 VPC の PSA を構成してプライベート IP アドレスを割り当てた上で、カスタムルートの Import / Export を許可して経路情報を学習できるようにしておきます。

プロキシインスタンスには 2 つの NIC を持つ VM を使用して、一つの NIC をソースのデータベースがあるネットワークに接続、もう一方は VPC に接続します。 VPC とターゲットのデータベースは PSA で接続されます。

VM では Dante SOCKS を実行することで、DMS からの接続をターゲットへ転送するようにします。参考

パブリック接続

target-public-connect.png

ターゲットのデータベースインスタンスが PSC / PSA をサポートしておらず、プライベート接続が利用できない場合は、DMS をターゲットのデータベースインスタンスのパブリック IP アドレスに接続します。 接続はパブリックネットワークを介して行われますが、レプリケーションデータは DMS によって暗号化されます。

パブリックネットワークを介してインスタンスに接続する場合は、IP 許可リスト承認済みネットワーク(Authorized Network)を事前に構成し、DMS の IP アドレスからの接続を許可する必要があります。

パブリックネットワークを経由した接続は、プライベートネットワークを使用する場合と比較して、レイテンシやパフォーマンスの観点でボトルネックになる可能性があるため、可能な限りプライベート接続を使用することが推奨されます。

ソースデーターベースへの接続方法

source-connectivity-decision-tree.png

プライベート接続

DMS からソースデータベースへのプライベートアクセスは、DMS と VPC を VPC Peering で接続することで実現します。 VPC Peering ではソースデータベースにアクセスする際に VPC を経由します。

source-private-connect.png
  • VPC Peering:Cloud VPN で接続

移設元および移設先プロジェクトの VPC を Cloud VPN で接続します。

Cloud VPN はプロジェクト間だけでなく、オンプレミスや他クラウドとも共通の仕組みでネットワークを接続できる汎用的な手段です。 IPsec VPN を用いた暗号化トンネルを広域ネットワーク網上に構築し、その経路を通じてインスタンスにアクセスします。

Cloud VPN では VPC を経由してプライベート IP アドレスでインスタンスにアクセスするため、Google Cloud のマネージドデータベースがソースの場合、Private Service Access(PSA) 構成が必要となります。 その上で、VPC 経由でのルーティングを可能とするため、カスタムルートの Import / Export を許可して経路情報を学習できるようにしておきます。参考

PSA 構成の際は、インスタンスが一時的に Pending 状態となります。 サービスの中断(データベースへの Write 処理の停止)が許容できない場合、パブリック接続を選択する必要があります。

  • VPC Peering:Cloud Interconnect で接続

VPC Peering では Cloud VPN の他に Cloud Interconnect を用いて VPC に接続することも可能です。

Cloud Interconnect は、Google Cloud と自社ネットワークを専用回線で直結するための仕組みで、大容量かつ低レイテンシで安定した接続を実現します。 金融や通信といった高トラフィック・高信頼性が求められる環境に適しており、恒常的な接続基盤として非常に強力です。

ただし、専用回線の準備やキャリアとの契約が必要になり、導入コストや移行期間は増大するため、単発的なデータベース移設だけを目的に利用するのは現実的ではありません。

パブリック接続

source-public-connect.png

プライベート接続が利用できない場合は、DMS を移行元のインスタンスのパブリック IP アドレスに接続します。 接続はパブリックネットワークを介して行われますが、レプリケーションデータは DMS によって暗号化されます。

パブリックネットワークを介してインスタンスに接続する場合は、IP 許可リスト承認済みネットワーク(Authorized Network)を事前に構成し、DMS の IP アドレスからの接続を許可する必要があります。

パブリックネットワークを経由した接続は、プライベートネットワークを使用する場合と比較して、レイテンシやパフォーマンスの観点でボトルネックになる可能性があるため、可能な限りプライベート接続を使用することが推奨されます。

また、DMS は、RAC(Oracle Real Application Clusters)における SCAN(Single Client Access Name)機能を使用したデータベースへの直接接続をサポートしていません。 このような環境でパブリック IP 許可リスト接続を使用する場合は、こちらの FAQ を確認します。

ハイブリット接続

source-hybrid-connect.png

Forward-SSH トンネルを使用して DMS からソースデータベースへの接続を確立する方法で、パブリックネットワーク接続とプライベートネットワーク接続の両方を組み合わせたハイブリッドな接続形態です。 接続自体はトンネルホストサーバのパブリック IP アドレスへの SSH ポートを介して確立されますが、接続がアクティブになった後は全てのトラフィックがセキュアトンネルを経由してソースデータベースのプライベート IP アドレスに送信されます。

ソースデータベースをホストしているサーバと同じサーバ上でトンネルを終端することも可能ですが、ソースデータベースをパブリックネットワークに直接公開することになるため、専用のトンネルサーバをプロビジョニングすることが推奨されています。

トンネルサーバはソースデータベースにアクセスできる VM でホスティングして、SSH 経由でアクセス可能なネットワークに配置する必要があります。

Forward-SSH トンネルを使用する方法は構成が複雑で、トンネルサーバのセットアップと管理が必要になるため、可能な限り上記の VPC Peering または IP 許可リストを使用する方法で接続することが推奨されます。

マイグレーションアーキテクチャ

Google Cloud プロジェクト間での Cloud SQL 移設は、以下の構成で進めました。

private-migration-architecture-overview.png
項目内容
DMS の配置場所ターゲットプロジェクト
ターゲット Cloud SQL への接続方法プライベート接続(Cloud VPN 経由)
ソース Cloud SQL への接続方法プライベート接続(PSC)
Source ProfileMySQL(ホスト・ユーザ・パスワードを使用)
Destination ProfileCloud SQL(PSC 使用)

DMS は移設先のターゲットプロジェクトで準備

DMS を実行するための Profile と Job はターゲットプロジェクトに用意 します。

ターゲットデータベースには PSC でプライベート接続

Cloud SQL は PSC に対応しています。 ターゲットプロジェクトで Cloud SQL の外部レプリカ(マイグレーション先インスタンス)を構築する際に PSA を構成してプライベート IP アドレスを割り当てて おきます。 これにより、DMS はプライベート IP アドレスに対して PSC でプライベート接続を確立できます。

ソースデータベースには Cloud VPN 経由でプライベート接続

ソースプロジェクトとターゲットプロジェクトの 両方で PSA が構成されている ことが前提です。

まず、DMS とターゲットプロジェクトの VPC を VPC Peering で接続します。 ターゲットプロジェクトの DMS はターゲットプロジェクトの VPC を経由し、Cloud VPN を通じてソースプロジェクトの VPC に接続します。 その後、PSA で接続されているソースプロジェクトの Cloud SQL にプライベート IP アドレスで接続しにいきます。

単に、プライベート IP アドレスとユーザ名・パスワードを指定しての接続となるため、ソースのデーターベースは『MySQL(Cloud SQL ではない)』を指定 します。

レプリケーション手順

便宜上、次のように定義しておきます。

  • ソースプロジェクト名:project-a
  • ターゲットプロジェクト名:project-b
  • Cloud SQL インスタンス名(ソース・ターゲット):dms-playground-cloud-sql
  • VPC 名(ソース・ターゲット):cloud-vpn-playground-vpc

1. PSA 構成の確認

まず、ソース・ターゲット両方のプロジェクトで PSA を構成します。

VPC に PSA 用の Private IP Address Pool を作成して Cloud SQL インスタンスにプライベート IP アドレスを割り当てます。

resource "google_compute_global_address" "private_ip_address" {
  name          = "cloud-vpn-playground-vpc-private-ip-address-pool"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = module.vpc.network_name

  labels = {
    resource-name     = "cloud-vpn-playground-vpc-private-ip-address-pool"
  }
}

resource "google_service_networking_connection" "private_vpc_connection" {
  network                 = module.vpc.network_name
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]

  depends_on = [
    google_compute_global_address.private_ip_address
  ]
}

resource "google_compute_network_peering_routes_config" "peering_routes" {
  peering = google_service_networking_connection.private_vpc_connection.peering
  network = module.vpc.network_name

  import_custom_routes = true
  export_custom_routes = true

  depends_on = [
    google_service_networking_connection.private_vpc_connection
  ]
}

以下は Project A の出力結果を例に挙げますが、Project B も同様に PSA を構成します。

$ gcloud compute addresses describe cloud-vpn-playground-vpc-private-ip-address-pool --global --project=project-a
address: 10.41.0.0
addressType: INTERNAL
creationTimestamp: '2025-06-12T09:58:44.079-07:00'
description: ''
id: '********'
kind: compute#address
labelFingerprint: 0ff3YkQet3o=
labels:
  goog-terraform-provisioned: 'true'
  resource-name: cloud-vpn-playground-vpc-private-ip-address-pool
name: cloud-vpn-playground-vpc-private-ip-address-pool
network: https://www.googleapis.com/compute/v1/projects/project-a/global/networks/cloud-vpn-playground-vpc
networkTier: PREMIUM
prefixLength: 16
purpose: VPC_PEERING
selfLink: https://www.googleapis.com/compute/v1/projects/project-a/global/addresses/cloud-vpn-playground-vpc-private-ip-address-pool
status: RESERVED
config-1-1.png

VPC と PSA が Peering されており、かつカスタムルートの Import / Export が許可されていることを確認します。

$ gcloud compute networks peerings list --network=cloud-vpn-playground-vpc --project=project-a
NAME                              NETWORK                   PEER_PROJECT           PEER_NETWORK       STACK_TYPE  PEER_MTU  IMPORT_CUSTOM_ROUTES  EXPORT_CUSTOM_ROUTES  STATE   STATE_DETAILS
servicenetworking-googleapis-com  cloud-vpn-playground-vpc  ne44e34e2b0993819p-tp  servicenetworking  IPV4_ONLY             True                  True                  ACTIVE  [2025-06-12T10:06:24.307-07:00]: Connected.
config-1-2.png
$ gcloud services vpc-peerings list --network=cloud-vpn-playground-vpc --project=project-a
---
network: projects/project-a/global/networks/cloud-vpn-playground-vpc
peering: servicenetworking-googleapis-com
reservedPeeringRanges:
- cloud-vpn-playground-vpc-private-ip-address-pool
service: services/servicenetworking.googleapis.com
config-1-3.png

Cloud SQL インスタンスにプライベート IP アドレスが割り当てられていることを確認します。

$ gcloud sql instances describe dms-playground-cloud-sql --project=project-a --format="value(ipAddresses)"
{'ipAddress': '34.80.244.8', 'type': 'PRIMARY'};{'ipAddress': '10.41.0.2', 'type': 'PRIVATE'}
config-1-4.png
$ gcloud sql instances describe dms-playground-cloud-sql --project=project-b --format="value(ipAddresses)"
{'ipAddress': '34.81.254.91', 'type': 'PRIMARY'};{'ipAddress': '10.235.0.5', 'type': 'PRIVATE'}
config-1-5.png

2. ネットワーク経路の確保

ソース・ターゲットプロジェクトの VPC を Cloud VPN で接続します。

プロジェクト間の VPC を Cloud VPN で接続する方法については こちらのブログ で紹介しています。

双方のプロジェクトに PSA で接続した VPC サブネットに VPN 経由のネットワークルートを追加します。

### Project A から Project B へのルート
resource "google_compute_route" "vpn_psa_route_01" {
  for_each = google_compute_vpn_tunnel.vpn_tunnel

  project             = "project-a"
  name                = "project-a-to-project-b-vpn-psa-route-${each.key}"
  dest_range          = "10.235.0.0/16"
  network             = "cloud-vpn-playground-vpc"
  next_hop_vpn_tunnel = each.value.self_link
  priority            = 1000

  depends_on = [
    google_compute_vpn_tunnel.vpn_tunnel
  ]
}
$ gcloud compute routes list --project=project-a --filter="destRange:10.235.0.0/16"
NAME                                    NETWORK                   DEST_RANGE     NEXT_HOP                                                                       PRIORITY
project-a-to-project-b-vpn-psa-route-0  cloud-vpn-playground-vpc  10.235.0.0/16  asia-east1/vpnTunnels/project-a-to-project-b-vpn-tunnel-00  1000
project-a-to-project-b-vpn-psa-route-1  cloud-vpn-playground-vpc  10.235.0.0/16  asia-east1/vpnTunnels/project-a-to-project-b-vpn-tunnel-01  1000
config-2-1.png
### Project B から Project A へのルート
resource "google_compute_route" "vpn_psa_route_01" {
  for_each = google_compute_vpn_tunnel.vpn_tunnel

  project             = "project-b"
  name                = "project-b-to-project-a-vpn-psa-route-${each.key}"
  dest_range          = "10.41.0.0/16"
  network             = "cloud-vpn-playground-vpc"
  next_hop_vpn_tunnel = each.value.self_link
  priority            = 1000

  depends_on = [
    google_compute_vpn_tunnel.vpn_tunnel
  ]
}
$ gcloud compute routes list --project=project-b --filter="destRange:10.41.0.0/16"
NAME                                    NETWORK                   DEST_RANGE    NEXT_HOP                                                                       PRIORITY
project-b-to-project-a-vpn-psa-route-0  cloud-vpn-playground-vpc  10.41.0.0/16  asia-east1/vpnTunnels/project-b-to-project-a-vpn-tunnel-00  1000
project-b-to-project-a-vpn-psa-route-1  cloud-vpn-playground-vpc  10.41.0.0/16  asia-east1/vpnTunnels/project-b-to-project-a-vpn-tunnel-01  1000
config-2-2.png

3. 疎通確認

現在の構成を整理すると以下のようになります。

network-connection.png

各プロジェクトの VPC に GCE を配置して、Cloud SQL インスタンスのプライベート IP アドレスに telnet で接続します。

### Project A の GCE(10.0.0.100)から Project B の Cloud SQL インスタンス(10.235.0.5:3306)への接続確認
$ telnet 10.235.0.5 3306
Trying 10.235.0.5...
Connected to 10.235.0.5.
### Project B の GCE(10.0.1.100)から Project A の Cloud SQL インスタンス(10.41.0.2:3306)への接続確認
$ telnet 10.41.0.2 3306
Trying 10.41.0.2...
Connected to 10.41.0.2.

出力結果から VPN トンネルを経由して Cloud SQL のプライベート IP アドレスに接続できていることが確認できます。

4. レプリケーションユーザの準備

ソースデータベースへの接続に使用するユーザアカウント(レプリケーションユーザ)を準備します。

レプリケーションユーザは全ての接続()を受け入れるようにする必要があります。 また、Cloud SQL(今回はセルフホスト MySQL として扱う)からの移行の場合は以下の MySQL 権限 を付与します。

[REPLICATION SLAVE / EXECUTE / SELECT / SHOW VIEW / REPLICATION CLIENT / RELOAD / TRIGGER]

-- ユーザ作成
CREATE USER 'replicator'@'%' IDENTIFIED BY 'YourStrongPassword';

-- 権限付与
GRANT REPLICATION SLAVE, EXECUTE, SELECT, SHOW VIEW, REPLICATION CLIENT, RELOAD, TRIGGER ON *.* TO 'replicator'@'%';

-- 保存
FLUSH PRIVILEGES;
-- ユーザ確認
SELECT User, Host FROM mysql.user WHERE User = 'replicator';
+------------+------+
| User       | Host |
+------------+------+
| replicator | %    |
+------------+------+
1 row in set (0.00 sec)

-- 権限確認
SHOW GRANTS FOR 'replicator'@'%';
+---------------------------------------------------------------------------------------------------------------------+
| Grants for replicator@%                                                                                             |
+---------------------------------------------------------------------------------------------------------------------+
| GRANT REPLICATION SLAVE, EXECUTE, SELECT, SHOW VIEW, REPLICATION CLIENT, RELOAD, TRIGGER ON *.* TO `replicator`@`%` |
+---------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

5. サンプルデータの準備

ソースデータベースでレプリケーションを確認するためのサンプルデータを準備してみます。

-- サンプルデータベースの追加
CREATE DATABASE IF NOT EXISTS replication_test_db;
USE replication_test_db;

-- サンプルテーブルの追加
CREATE TABLE replication_test_tables (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  message VARCHAR(255),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- サンプルデータの追加
INSERT INTO replication_test_tables (message, created_at)
VALUES
  ('Test message 1', NOW()),
  ('Test message 2', NOW()),
  ('Test message 3', NOW());
-- 確認
SELECT * FROM replication_test_tables;
+----+----------------+---------------------+
| id | message        | created_at          |
+----+----------------+---------------------+
|  1 | Test message 1 | 2025-06-12 17:58:38 |
|  2 | Test message 2 | 2025-06-12 17:58:38 |
|  3 | Test message 3 | 2025-06-12 17:58:38 |
+----+----------------+---------------------+
3 rows in set (0.00 sec)

DMS は Binlog(Binary Log)をターゲットインスタンスにレプリケーションします。 Binlog とは、MySQL や Cloud SQL(MySQL 互換)で使用されるトランザクションログの一種で、データベースに対する INSERT、UPDATE、DELETE 等の変更操作が時系列で記録されています。

DMS で Continuous Migration する場合は、CDC でデータをレプリケーションする際に Binlog 内の位置(POS:Position)を追跡します。

Binlog は以下のコマンドで確認できます。

-- プライマリの過去全ての Binlog を確認
SHOW BINARY LOGS
+-----------------+-----------+-----------+
| Log_name        | File_size | Encrypted |
+-----------------+-----------+-----------+
| mysql-bin.00004 |       244 | No        |
| mysql-bin.00005 |       244 | No        |
| mysql-bin.00006 |      1324 | No        |
+-----------------+-----------+-----------+
3 rows in set (0.01 sec)
-- プライマリの現在の Binglog を取得
SHOW MASTER STATUS
+-----------------+----------+--------------+------------------+-------------------------------------------+
| File            | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+-----------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.00006 |     1324 |              |                  | ad31e7ff-47b0-11f0-bd54-42010a290003:1-24 |
+-----------------+----------+--------------+------------------+-------------------------------------------+
1 row in set (0.00 sec)

6. External Replica の構築

ターゲットプロジェクトに移設先の Cloud SQL インスタンスを External Replica として構築します。 External Replica とは、Cloud SQL インスタンスを外部の MySQL データベースの Read Replica として設定する機能です。

Terraform で管理する場合は、ソースの Cloud SQL インスタンスを tfstate に import します。

resource "google_sql_database_instance" "default" {
  project             = "project-b"
  count               = 1
  name                = "dms-playground-cloud-sql"
  region              = "asia-east1"
  database_version    = "MYSQL_8_0_31" ## ソースの Cloud SQL のバージョンに合わせる
  deletion_protection = true
}
$ terraform import 'google_sql_database_instance.default[0]' project-a/dms-playground-cloud-sql

また、DMS は データベースのみのレプリケーションとなるため、データベースのユーザ自体はターゲットで準備する 必要があります。

resource "google_sql_user" "default" {
  count    = 1
  name     = [MYSQL_USER_NAME]
  instance = google_sql_database_instance.default[count.index].name
  host     = "%"
  password = [MYSQL_USER_PASS]
}

7. Connection Profile / Migration Job の準備

ターゲットプロジェクトに DMS を準備します。 DMS は次の 3 つのリソースで構成されます。

  • Source Connection Profile: ソースデータベースへの接続情報を定義
  • Destination Connection Profile: ターゲットデータベースへの接続情報を定義
  • Migration Job:レプリケーションの設定を定義

Source Connection Profile

ソースのデータベースは MySQL として設定し、ホスト・ユーザ・パスワードを指定してプライベート IP アドレスで接続します。

resource "google_database_migration_service_connection_profile" "src_profile_01" {
  project               = "project-b" ## ターゲットプロジェクトに作成
  location              = "asia-east1"
  connection_profile_id = "dms-playground-cloud-sql-src-profile"
  display_name          = "dms-playground-cloud-sql-src-profile"

  mysql {
    host     = "10.41.0.2"
    port     = 3306
    username = "replicator"
    password = "YourStrongPassword"
  }

  labels = {
    resource-name = "dms-playground-cloud-sql-src-profile"
  }
}
$ gcloud database-migration connection-profiles describe dms-playground-cloud-sql-src-profile --region=asia-east1 --project=project-b
createTime: '2025-06-12T05:59:02.860232149Z'
displayName: dms-playground-cloud-sql-src-profile
labels:
  goog-terraform-provisioned: 'true'
  resource-name: dms-playground-cloud-sql-src-profile
mysql:
  host: 10.41.0.2
  passwordSet: true
  port: 3306
  username: replicator
name: projects/project-b/locations/asia-east1/connectionProfiles/dms-playground-cloud-sql-src-profile
satisfiesPzi: true
satisfiesPzs: true
state: READY
updateTime: '2025-06-13T00:21:10.014319Z'
config-5-1.png

Destination Connection Profile

ターゲットのデータベースは Cloud SQL として設定し、Cloud SQL インスタンス ID(インスタンス名)を指定して PSC でプライベート接続します。

resource "google_database_migration_service_connection_profile" "dst_profile_01" {
  project               = "project-b" ## ターゲットプロジェクトに作成
  location              = "asia-east1"
  connection_profile_id = "dms-playground-cloud-sql-dst-profile"
  display_name          = "dms-playground-cloud-sql-dst-profile"

  mysql {
    cloud_sql_id = "dms-playground-cloud-sql"
  }

  labels = {
    resource-name = "dms-playground-cloud-sql-dst-profile"
  }
}
$ gcloud database-migration connection-profiles describe dms-playground-cloud-sql-dst-profile --region=asia-east1 --project=project-b
createTime: '2025-06-18T15:03:06.264373371Z'
displayName: dms-playground-cloud-sql-dst-profile
labels:
  goog-terraform-provisioned: 'true'
  resource-name: dms-playground-cloud-sql-dst-profile
mysql:
  cloudSqlId: dms-playground-cloud-sql
name: projects/project-b/locations/asia-east1/connectionProfiles/dms-playground-cloud-sql-dst-profile
provider: CLOUDSQL
satisfiesPzi: false
satisfiesPzs: false
state: READY
updateTime: '2025-06-18T15:03:06.347052Z'

⚠️ 本来 Google Cloud Console では、Destination Profile ID から対象の Cloud SQL インスタンス(ターゲットプロジェクトに作成した External Replica)のページへ遷移するはずですが、2025 年 6 月時点では機能不全のようです。

config-5-2.png

こちらに関しては Google Cloud に報告・起票して対応方針と ETA を確認しています。参考

Migration Job

Migration Job は Continuous Migration として設定し、VPC Peering で VPC に接続します。

resource "google_database_migration_service_migration_job" "dms_job_01" {
  project          = "project-b" ## ターゲットプロジェクトに作成
  location         = "asia-east1"
  migration_job_id = "dms-playground-cloud-sql-dms-job"
  display_name     = "dms-playground-cloud-sql-dms-job"

  vpc_peering_connectivity {
    vpc = "cloud-vpn-playground-vpc"
  }

  source      = google_database_migration_service_connection_profile.src_profile_01.name ## Source Connection Profile を指定
  destination = google_database_migration_service_connection_profile.dst_profile_01.name ## Destination Connection Profile を指定

  type        = "CONTINUOUS"

  labels = {
    resource-name = "dms-playground-cloud-sql-dms-job"
  }

  depends_on = [
    google_database_migration_service_connection_profile.src_profile_01,
    google_database_migration_service_connection_profile.dst_profile_01
  ]
}
$ gcloud database-migration migration-jobs describe dms-playground-cloud-sql-dms-job --region=asia-east1 --project=project-b
createTime: '2025-06-18T15:03:19.591264032Z'
destination: projects/project-b/locations/asia-east1/connectionProfiles/dms-playground-cloud-sql-dst-profile
destinationDatabase:
  engine: MYSQL
  provider: CLOUDSQL
displayName: dms-playground-cloud-sql-dms-job
duration: 0.182634s
labels:
  goog-terraform-provisioned: 'true'
  resource-name: dms-playground-cloud-sql-dms-job
name: projects/project-b/locations/asia-east1/migrationJobs/dms-playground-cloud-sql-dms-job
objectsConfig: {}
satisfiesPzi: false
satisfiesPzs: false
source: projects/project-b/locations/asia-east1/connectionProfiles/dms-playground-cloud-sql-src-profile
sourceDatabase:
  engine: MYSQL
state: NOT_STARTED
type: CONTINUOUS
updateTime: '2025-06-18T15:03:19.717494Z'
vpcPeeringConnectivity:
  vpc: cloud-vpn-playground-vpc
config-5-3.png

これで Connection Profile と Migration Job の準備が完了しました。

### Connection Profile 一覧
$ gcloud database-migration connection-profiles list --region=asia-east1 --project=project-b
CONNECTION_PROFILE_ID                 DISPLAY_NAME                          REGION      STATE  PROVIDER  ENGINE  HOSTNAME/IP  CREATED
dms-playground-cloud-sql-src-profile  dms-playground-cloud-sql-src-profile  asia-east1  READY            MYSQL   10.41.0.2    2025-06-12T05:59:02
dms-playground-cloud-sql-dst-profile  dms-playground-cloud-sql-dst-profile  asia-east1  READY  CLOUDSQL  MYSQL                2025-06-18T15:03:06

### Migration Job 一覧
$ gcloud database-migration migration-jobs list --region=asia-east1 --project=project-b
MIGRATION_JOB_ID                  DISPLAY_NAME                      REGION      STATUS   SOURCE                                DESTINATION
dms-playground-cloud-sql-dms-job  dms-playground-cloud-sql-dms-job  asia-east1  STOPPED  dms-playground-cloud-sql-src-profile  dms-playground-cloud-sql-dst-profile

最後に、DMS が Migration Job を実行できるように、ターゲットプロジェクトのサービスエージェントに roles/cloudsql.client を付与します。参考

resource "google_project_iam_member" "default" {
  project = "project-b"
  role    = "roles/cloudsql.client"
  member  = "serviceAccount:service-[PROJECT_NUMBER]@gcp-sa-datamigration.iam.gserviceaccount.com"
}

8. External Replica の Demote

手順 6 で構築した External Replica はこの時点ではスタンドアロン状態なので Read Replica に降格(Demote)してソースデータベースからのレプリケーションを待機します。

$ gcloud database-migration migration-jobs demote-destination dms-playground-cloud-sql-dms-job --region=asia-east1 --project=project-b
done: false
metadata:
  '@type': type.googleapis.com/google.cloud.clouddms.v1.OperationMetadata
  apiVersion: v1
  createTime: '2025-06-18T15:11:18.562654495Z'
  requestedCancellation: false
  target: projects/project-b/locations/asia-east1/migrationJobs/dms-playground-cloud-sql-dms-job
  verb: demoteDestination
name: projects/project-b/locations/asia-east1/operations/operation-1750259477086-637da0a658097-40301964-57d61c54

ターゲットの Cloud SQL インスタンス(External Replica)が Read Replica へ Demote されたことを確認します。

config-6-1.pngconfig-6-2.png

Demote が完了すると Read Replica が作成され、レプリケーションを受ける準備が整います。 なお、Demote 処理は Google Cloud のインフラ状況にもよりますが、平均して体感 15 分は掛かりました。(単純にインスタンスを追加する分の時間)

config-6-3.png

9. Migration Job の実行

Google Cloud Console から Migration Job を選択してマイグレーションを開始します。

config-7-1.pngconfig-7-2.pngconfig-7-3.png

DMS はソースとターゲットの両方の Cloud SQL に接続してレプリケーションを開始します。 このタイミングで Connection Profile が正しく構成されていない場合、接続エラーが発生します。

config-7-4.png

最初のフェーズでは Full Dump が実行され、現時点でソースのインスタンスに保存されている全てのデータベースとレコードをターゲットに転送します。

config-7-5.png

Migration Job が開始されると、Cloud SQL の Read Replica が Running 状態になります。

config-7-6.png

Full Dump 完了後は CDC フェーズに移行して、以後は差分のみが継続的にターゲットにレプリケーションされていきます。

config-7-7.png

10. レプリケーション確認

ターゲット側でレプリケーションが正しく行われているか確認します。 この時点で、手順 5 でソースデータベースに追加した内容が、ターゲットにも反映されていることが分かります。

-- 確認
SELECT * FROM replication_test_tables;
+----+----------------+---------------------+
| id | message        | created_at          |
+----+----------------+---------------------+
|  1 | Test message 1 | 2025-06-12 17:58:38 |
|  2 | Test message 2 | 2025-06-12 17:58:38 |
|  3 | Test message 3 | 2025-06-12 17:58:38 |
+----+----------------+---------------------+
3 rows in set (0.00 sec)
-- プライマリの過去全ての Binlog を確認
SHOW BINARY LOGS
+-----------------+-----------+-----------+
| Log_name        | File_size | Encrypted |
+-----------------+-----------+-----------+
| mysql-bin.00001 |       180 | No        |
| mysql-bin.00002 |      1040 | No        |
+-----------------+-----------+-----------+
3 rows in set (0.01 sec)
-- プライマリの現在の Binglog を取得
SHOW MASTER STATUS
+-----------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| File            | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                                                   |
+-----------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| mysql-bin.00002 |     1040 |              |                  | 70e60be1-4c51-11f0-8483-42010aeb0007:1-4, ad31e7ff-47b0-11f0-bd54-42010a290003:1-25 |
+-----------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

ここでソースデータベースにレコードを追加してターゲットで確認してみます。

-- ソースで実行
INSERT INTO replication_test_tables (message, created_at)
VALUES
  ('Test message 4', NOW()),
  ('Test message 5', NOW()),
  ('Test message 6', NOW());
-- ターゲットで実行
SELECT * FROM replication_test_tables;
+----+----------------+---------------------+
| id | message        | created_at          |
+----+----------------+---------------------+
|  1 | Test message 1 | 2025-06-12 17:58:38 |
|  2 | Test message 2 | 2025-06-12 17:58:38 |
|  3 | Test message 3 | 2025-06-12 17:58:38 |
|  4 | Test message 4 | 2025-06-18 15:49:05 | -- ソースで追加されたレコード
|  5 | Test message 5 | 2025-06-18 15:49:05 | -- ソースで追加されたレコード
|  6 | Test message 6 | 2025-06-18 15:49:05 | -- ソースで追加されたレコード
+----+----------------+---------------------+
6 rows in set (0.00 sec)

ターゲットデータベースにもレコードがレプリケーションされているのが分かります。

実際に以下のようなコマンドを利用すればレプリケーションの状況を把握できます。

-- 実際にはレコード数の比較でレプリケーション状況を把握
SELECT COUNT(*) FROM replication_test_tables;

ただし、レコード数が非常に多いテーブルでは以下のようなクエリタイムアウトが発生する可能性があります。

Query execution was interrupted.

代替えコマンドとして information_schema からテーブルのメタ情報を参照することでレコードの概算値を確認することもできます。

-- レコード数の概算値を取得
SELECT table_rows FROM information_schema.tables WHERE table_schema = '[DATABASE_NAME]' AND table_name = 'replication_test_tables';

マスター昇格手順

レプリケーションが完了し、ソースデータベースとターゲットデータベースの Primary を切り替える場合は、以下の手順でマスター昇格作業を実施します。

1. ソースの Cloud SQL インスタンスを Read Only 化

基本的に RDBMS の場合は、原子性(Atomicity)や 一貫性(Consistency) を遵守するために、ソースデータベースを Read Only 化してからターゲットデータベースをマスター昇格することが推奨されます。

$ gcloud sql instances patch [INSTANCE_NAME] --project=[PROJECT_ID] --database-flags=read_only=on

これにより、以後ソースデータベースへの書き込みを受け付けないようにします。

例えば、以下のようなデータベースを追加するクエリを投げてみます。

CREATE TABLE hoge (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100)
);

Cloud SQL が super-read-only モード で動作していた場合、以下のようなエラーメッセージが返されます。

Error 1290 (HY000): The MySQL server is running with the --super-read-only option so it cannot execute this statement

super-read-only モードでは、READ_ONLY を拡張して SUPER 権限を持つユーザからの書き込みさえもブロックします。

2. Migration Job の停止

手順 1 でソースデータベースに対する書き込み処理を停止した後、Migration Job を停止してレプリケーションを終了します。

config-10-1.pngconfig-10-2.png

3. External Replica の Promote

最後にターゲットの Cloud SQL インスタンスをスタンドアロンインスタンスに昇格(Promote)します。

config-11-1.png

Promote を実行すると DMS はソースデータベースとターゲットデータベースの両方への接続を切断します。 これにより、ターゲットの Cloud SQL インスタンスは Read Replica からスタンドアロンインスタンスに昇格し、以後 Primary として機能します。

config-11-2.png

Promote が完了すると、ターゲットの Cloud SQL インスタンスはスタンドアロン化され、以前のソース Cloud SQL インスタンスと同等の状態になります。

config-11-3.pngconfig-11-4.png

Promote が完了したら、上位のアプリケーションは ターゲットデータベースに接続先エンドポイントを切り替えます。

4. ソースの Cloud SQL インスタンスを停止・削除

ソースデータベースの Read Only モードを外してレプリケーションの停止を確認しておきます。

-- ソースで実行
INSERT INTO replication_test_tables (message, created_at)
VALUES
  ('Test message 7', NOW()),
  ('Test message 8', NOW()),
  ('Test message 9', NOW());

SELECT * FROM replication_test_tables;
+----+----------------+---------------------+
| id | message        | created_at          |
+----+----------------+---------------------+
|  1 | Test message 1 | 2025-06-12 17:58:38 |
|  2 | Test message 2 | 2025-06-12 17:58:38 |
|  3 | Test message 3 | 2025-06-12 17:58:38 |
|  4 | Test message 4 | 2025-06-18 15:49:05 |
|  5 | Test message 5 | 2025-06-18 15:49:05 |
|  6 | Test message 6 | 2025-06-18 15:49:05 |
|  7 | Test message 7 | 2025-06-18 15:58:18 | -- ソースで追加したレコード
|  8 | Test message 8 | 2025-06-18 15:58:18 | -- ソースで追加したレコード
|  9 | Test message 9 | 2025-06-18 15:58:18 | -- ソースで追加したレコード
+----+----------------+---------------------+
6 rows in set (0.00 sec)
-- ターゲットで実行
SELECT * FROM replication_test_tables;
+----+----------------+---------------------+
| id | message        | created_at          |
+----+----------------+---------------------+
|  1 | Test message 1 | 2025-06-12 17:58:38 |
|  2 | Test message 2 | 2025-06-12 17:58:38 |
|  3 | Test message 3 | 2025-06-12 17:58:38 |
|  4 | Test message 4 | 2025-06-18 15:49:05 |
|  5 | Test message 5 | 2025-06-18 15:49:05 |
|  6 | Test message 6 | 2025-06-18 15:49:05 |
+----+----------------+---------------------+
6 rows in set (0.00 sec)

ソースデータベースにレコードを追加しても、ターゲットに反映されていないことが分かります。

レプリケーションの停止を確認したら、ソースの Cloud SQL インスタンスを停止・削除します。

トラブルシューティング

DMS を利用したデータベース移行でよく発生する問題をまとめておきます。

Migration Job 実行時にエラーが発生する

以下のようなエラーが発生する場合、ネットワークの問題で DMS がソースデータベースに接続できていない可能性があります。

Failure connecting to the source database. Make sure the connectivity information on the connection profile is correct and the source database is reachable. Address the issues then Start the migration job. generic::unavailable: Unable to connect to source database server: unable to connect to source database server after 5s: generic::failed_precondition: "dial tcp 10.41.0.2:3306: i/o timeout". Check firewall configuration and network connectivity between source database server and the internet. Not attempting further retries.

  • PSA Peering が正しく設定されているか
  • Cloud SQL インスタンスに Private IP アドレスが割り当てられているか
  • カスタムルートが Import / Export されているか
  • Cloud VPN のヘルスチェックステータスは正常か

レプリケーションが開始されない

以下のようなエラーが発生する場合、ソースデータベースの Binlog に問題がある可能性があります。

MySQL binlog is configured incorrectly on the source database. Update the configuration according to the MySQL documentation, then Restart the migration job. Binlog is not enabled on source database server.

まずは Binlog が有効化されているかを確認します。

$ gcloud sql instances describe [INSTANCE_NAME] --project=[PROJECT_ID] --format="value(settings.backupConfiguration.binaryLogEnabled)"
[何も表示されない場合は有効になっていない]

無効化されている場合はコンソールから PITR(Point-In-Time Recovery)を有効化します。 Binlog 有効化はインスタンスの再起動が必要となります。

binlog-enabled.png

DMS のプライベート接続が利用できない

ソースデータベースの PSA が構成されていない場合は、パブリック接続を利用します。

public-migration-architecture-overview.png

DMS がインターネット経由で接続される場合、ソースの Cloud SQL インスタンスの 承認済みネットワーク(Authorized Network)に、ターゲット Cloud SQL インスタンスの IP レンジを追加 します。参考

ターゲット Cloud SQL インスタンスにおいて DMS がレプリケーション用に使用するパブリック IP アドレスは、Google Cloud Console から、ターゲットの Cloud SQL インスタンスの Outgoing IP アドレス で確認できます。

Outgoing IP アドレスは DMS がターゲットの Cloud SQL インスタンスに接続されると、自動的に割り当てられます。参考

dms-outgoing-ip-address.png
### ソース Cloud SQL インスタンスの承認済みネットワークに追加
$ gcloud sql instances patch [INSTANCE_NAME] --authorized-networks=[REPLICATION_OUTGOING_IP_ADDRESS] --project=project-a

また、以下のコマンドでソースデータベースの接続ログ(general_log)から、レプリケーションに使用されているパブリック IP アドレスを確認し、対象の IP アドレスが許可されているか(Authorized Network に追加されているか)を確認することができます。参考

### データベースに接続する
$ gcloud sql connect [INSTANCE_NAME] --project=[PROJECT_ID] --user=[USER_NAME]
$ mysql -u [USER_NAME] -p -h [INSTANCE_EXTERNAL_IP] -P 3306 ## 承認済みネットワークで許可されている場合は mysql コマンドでも接続できる

### general_log の設定を確認する
> SHOW VARIABLES LIKE 'general_log%';
+------------------+-------------------------+
| Variable_name    | Value                   |
+------------------+-------------------------+
| general_log      | OFF                     |
| general_log_file | /mysql/logs/general.log |
+------------------+-------------------------+
2 rows in set (0.00 sec)

### general_log を有効化する(有効化されていない場合)
$ gcloud sql instances patch [INSTANCE_ID] --database-flags=general_log=on --project=[GCP_PROJECT_ID]

### 全ての接続 / クエリのログを取得する
> SELECT event_time, user_host, command_type FROM mysql.general_log ORDER BY event_time DESC LIMIT 100;

### 特定ユーザに限定して接続 / クエリのログを取得する
> SELECT event_time, user_host, command_type FROM mysql.general_log WHERE user_host = 'replicator@[REPLICATION_OUTGOING_IP_ADDRESS]' ORDER BY event_time DESC LIMIT 100;

### 現在の接続状況を確認する
> SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE FROM information_schema.processlist;

まとめ

今回のブログでは DMS を利用して Google Cloud プロジェクト間で Cloud SQL for MySQL のデータベースを移設する方法について紹介しました。

データベースの移行作業は、初期データのダンプとインポート、アプリケーションが稼働している間の更新データの整合性の確保、スキーマの互換性チェックと調整、最終的なマスター昇格作業といった多くのステップが必要になりますが、DMS ではこれらのプロセスを一元管理して複雑なマイグレーションプロセスを簡素化してくれます。 DMS では、MySQL や Linux のマイグレーションコマンドを用いた方法と比較して、大幅に作業量・考慮事項を減らせる上、移設時のダウンタイムも最小限に抑えることが可能です。

ただし、DMS をインスタンスに接続する際は、ネットワークの構築も必要になるため、データベースの知識に加えて Google Cloud のネットワーク構成に対する基本な理解も求められます。

今回 DMS を利用する中でマイグレーション機能そのものに影響しない範囲で、一部 Console の不具合が見受けられました。 昨今では、頻繁に機能リリースやサポートの範囲が広がっているため、今後のアップデートにも期待したいと思います。

参考・引用