運用DB(MySQL)のPlanetScale → Cloud SQL移行
こんにちは、株式会社TERASSのいわさきです。
運用中サービスのDBをGCP環境(Cloud Run, Cloud SQL)へ移行しました。技術的挑戦としては、Next.js (Cloud Run) と Cloud SQL の接続、Prismaのマイグレーション、そして外部データ連携のすべてを、いかに「プライベートネットワーク」内で実現するかという点でした。
パブリック構成やCloud SQL Auth Proxyを使った構築は参考記事も多かったのですが、プライベート接続を前提としたCDパイプラインの構築情報は少なく、試行錯誤の連続でした。最終的に行き着いたその具体的な構成と課題の解決方法をまとめています。
移行要件
- 運用中DB/テーブル構成を引き継ぐ
- すべての通信をプライベートネットワーク経由にする
- Next.js(Cloud Run)と Prisma を使うアプリケーション側の実装のまま安全に移行
- BigQueryへのデータ連携頻度を高める
キーポイント
- Cloud Run(Gen2)の VPC NIC を使って Cloud SQL にプライベート接続する。
- Prisma のマイグレーションは Cloud Build Private Worker Pool 上で実行し、マイグレーション時の通信経路もプライベートに保つ。
- データ移行は「初期ダンプ → マイグレーションベースラインを生成 → データ投入 → 差分マイグレーション(追加マイグレーション)」の順。
- データ監査・検証は、Datastreamを利用して差分をBigQueryに流す。
環境周り
- Next.js(Cloud Run)+ prisma
- MySQL8.0
- GitHub Actions / Cloud Deploy
構成イメージ

アプリからCloud SQLへの完全プライベート接続
アプリケーションからDBへの接続は、VPCアクセスコネクタを使わず、よりモダンな方法で実現しました。
Cloud Run(Gen2)に VPC ネットワークインターフェイス を付与し、vpc-access-egress: private-ranges-only を指定することで、アウトバウンドをプライベート範囲内に限定します。Cloud SQL は Private Service Connection を使って VPC 内からプライベートにアクセスします。
[Cloud Run (Gen2) Service]
- execution-environment: gen2
- network-interfaces → attaches VPC NIC to vpc / subnet
- vpc-access-egress: private-ranges-only
- runs with Service Account (has roles/cloudsql.client)
|
| (1) outbound via VPC NIC on subnet, egress limited to private ranges
v
[VPC]
- Subnet: main-subnet (privateIpGoogleAccess: true)
- Routes / Firewall rules (allow egress to PSC ranges / Cloud SQL private IP)
|
| (2) traffic from subnetwork flows inside the VPC
v
[Private Service Connection]
- Reserved peering range (ComputeGlobalAddress: private-service-connection-range)
- ServiceNetworkingConnection (service: servicenetworking.googleapis.com)
- Enables peering between VPC and Google-managed service network
|
| (3) VPC peering → resolves access to Cloud SQL private endpoint
v
[Cloud SQL (MySQL)]
- ipConfiguration:
ipv4Enabled: false
privateNetwork: projects/<PROJECT>/global/networks/<VPC_NAME>
enablePrivatePathForGoogleCloudServices: true
- has private IP in the VPC (primary.privateIpAddress)
- Cloud SQL service account + IAM (GCS import IAM, cloudsql IAM auth, etc.)
マイグレーション
マイグレーションについては、元が PlanetScale & prisma 構成で動いていたため、流用して最低限の変更にとどめました。
PlanetScaleはmigrate管理をブランチとの差分でやっているため、新環境ではベースラインを作成し一般的なmigrationsディレクトリによる管理に変更しました。
しかしプライベートな環境でのCDマイグレーションは面倒なので、先に必要なイメージを作っておき、Cloud Build Private Worker Pool上で実行することで、Cloud SQL Proxyなしでのマイグレーションを実現しています。
イメージを薄く保つことでビルド/保守を簡潔にし、entrypointをsh -cにしておくことで、Cloud Buildのargs側でstatusやseedなど柔軟に投げられるようにしました。
- Dockerイメージの作成(オフライン対応のため、ダミーschemaを用意して焼き付ける)
FROM node:22-slim RUN apt-get update \ && apt-get install -y --no-install-recommends openssl ca-certificates curl \ && rm -rf /var/lib/apt/lists/* ENV PRISMA_HIDE_UPDATE_MESSAGE=true RUN npm install -g prisma@6.18.0 tsx@4.x RUN printf 'datasource db { provider = "mysql" url = "mysql://u:p@localhost:3306/db" }\nmodel X { id Int @id }\n' > /tmp/schema.prisma \ && prisma format --schema=/tmp/schema.prisma >/dev/null 2>&1 || true \ && prisma migrate diff --from-empty --to-schema-datamodel /tmp/schema.prisma >/dev/null 2>&1 || true \ && prisma -v >/dev/null 2>&1 || true ENTRYPOINT ["sh","-c"] - 上記DockerImageをビルドしてArtifactRepositoryに乗せておく
- 現行DBからmigrationsベースラインを作成しgithubにコミット
export DATABASE_URL="<既存DBの接続URL>" npx prisma migrate dev --name init --create-only - github workflowにてCloud Build ジョブを起動し、イメージを private pool 上で実行させる
- name: "<LOCATION>-docker.pkg.dev/<PROJECT_ID>/<ARTIFACT_REGISTRY>/ops/<IMAGE_NAME>" id: "migrate" entrypoint: bash secretEnv: ["DATABASE_URL"] args: - -lc - | set -euo pipefail ... prisma migrate deploy --schema="${PRISMA_SCHEMA_PATH}" ...
データ移行
DBについては運用中サービスということもあり、既存DBからdump出力 -> 新DBに投入 -> 移行の変更差分はmigrationsで適用 という方針にします。一度しか行わない作業なので、コマンドから実行します。
PlanetScaleからの抽出
PlanetScale用ツールで認証 -> フルダンプします。
# login
pscale auth login
pscale org switch <ORG_NAME>
# full-dump
pscale database dump <DB_NAME> <BRUNCH_NAME> \
--org <ORG_NAME> \
--output ./pscale_dump_$(date +%Y%m%d)
新DBへの投入
プライベート環境下のDBに対してデータを流し込む場合、GCSバケット上からdumpデータを読むことでデータ移行することが出来ます。前提として、Cloud SQLインスタンスは、自動的にGoogleが管理するサービスアカウント(p[PROJECT_NUMBER]-xxxxxxxx@gcp-sa-cloud-sql.iam.gserviceaccount.com)が紐付けられインポートを実行するため、そちらにroles/storage.objectViewer権限をロール付与する必要があります。
DUMP_FILE=./pscale_dump_$(date +%Y%m%d)/<TABLE_NAME>.gz
BUCKET_PATH=gs://<BUCKET_NAME>/data/<TABLE_NAME>.gz
# upload (gcloud storage を推奨)
gcloud storage cp "$DUMP_FILE" "$BUCKET_PATH"
# import
gcloud sql import sql <INSTANCE_NAME> "$BUCKET_PATH" \
--database=<DATABASE_NAME> --project=<GC_PROJECT_NAME>
BigQueryへの連携
運用中のデータはBigQuery側に流すことでデータの確認や分析をしやすいようにしています。
旧環境下では、定期バッチ処理でデータを飛ばしていましたが、今回はDataStreamを使ってほぼリアルタイムな同期を実現しようとしました。しかし、プライベート環境下では、DataStreamが利用する Google管理のテナントVPCから、CustomVPC内のCloud SQLプライベートIPにルーティングするために、NAT Proxyをリバースプロキシとして設置し、プライベート接続プロファイルを確立することで目的を達成しています。参考
なおDataStreamでは、初回全量データを、以後は差分のみ送ることでパフォーマンスも保たれています。
[Datastream (Google-managed)]
|
| (1) DatastreamPrivateConnection の vpcPeeringConfig で指定した
| CIDRからVPCにピアリングで入ってくる
v
[VPC / datastream ピアリングサブネット (datastreamPeeringCidr)]
|
| (2) Datastream -> NAT Proxy (Compute Instance running HAProxy) : TCP:3306
| (Firewall: sourceRanges = datastreamPeeringCidr, targetTag = nat-proxy)
v
[NAT Proxy VM]
- HAProxy: frontend 0.0.0.0:3306 -> backend mysql: <Cloud SQL private IP>:3306
|
| (3) NAT Proxy VM -> Cloud SQL(Private IP): TCP:3306
| (Firewall egress permit to Cloud SQL private IP)
v
[Cloud SQL (MySQL, Private IP)]
まとめ
最終的に、今回の移行で以下の3つの重要な技術目標を達成し、セキュアなCDパイプラインを確立できました。
完全プライベート認証の実現: Workload Identity Federation (WIF) と Cloud Run Gen2 の VPC NIC を組み合わせることで、サービスアカウントキーを使用せず、アプリケーション接続、CDパイプラインの全通信をプライベートネットワーク内に閉じ込めることに成功しました。
Cloud SQL Proxy 不要のマイグレーション: Cloud Build Private Worker Pool を VPC ネットワークに接続することで、Cloud SQL Proxy をコンテナに含める必要がなくなり、軽量で高速かつセキュアな Prisma Migrate 実行環境を構築しました。
リアルタイムなデータ監査パイプライン: DataStream を利用し、NAT Proxy (HAProxy) を介して Cloud SQL のプライベート IP に接続する堅牢な CDC (Change Data Capture) パイプラインを確立しました。これにより、旧環境のバッチ処理よりも格段に迅速なデータ監査と分析が可能になりました。
(よく考えれば当然なことばかりでしたが)検証段階では罠も一通り踏みましたが、おかげさまで本番は事故もなくDB移行完了。
本記事が、どこかで悩む方の参考なれば幸いです。
Discussion