B システム設計/インフラ — coupon-service Cloud SQL for PostgreSQL メジャーバージョンアップグレード(14→16)(Database Migration Service継続的レプリケーション × 既存Readレプリカの再構成 × PgBouncer PAUSE/RESUMEによるコネクションドレイン × Argo Workflows CronWorkflowサスペンド × 切り戻し手順の明文化 × ポストアップグレード検証スイート)(MOps クーポンサービス PG14→PG16 EOL対応 Bad→Good)

2026-08-18 (Day 134) 火曜 B: システム設計/インフラ ★★★★☆ Cloud SQL for PostgreSQL 14→16 Database Migration Service / PgBouncer / Argo Workflows

概要

🔀

DMS継続的レプリケーションによる移行

in-place書き換えは実質「隠されたバックアップ→リストア」。Database Migration Serviceで明示的に新PG16インスタンスへ移行することで、リハーサル・監視・ロールバックを正面から設計できる。

🧬

周辺トポロジの棚卸し

Cloud SQLのネイティブレプリケーションはメジャーバージョンを跨げない。Primaryだけでなく、Readレプリカやバッチジョブなど「依存する周辺コンポーネント」を移行対象として洗い出す。

⏸️

PgBouncer PAUSE/RESUME

接続を止めずに切り替えると進行中トランザクションが強制切断される。コネクションプーラーのPAUSE/RESUMEでカットオーバーの瞬間を安全に扱う。

↩️

切り戻し余地の保持

「コマンド成功」と「アプリが正しく動く」は別軸。旧インスタンスを一定時間読み取り専用で残し、検証スイートに落ちても即座に戻れる状態を維持する。

問題

ECサイト MOps チームが運用するクーポンサービス(coupon-serviceのCloud SQL for PostgreSQLは、現在バージョン14系で稼働している。2026年11月にPostgreSQL 14のCloud SQLサポート終了(EOL)が予告され、16系への移行が必須になった。このサービスは2026-08-15の改善で、PgBouncer(transaction pooling)× 同リージョンReadレプリカ(読み書き分離)× statement_timeout × UPDATE ... WHERE ... RETURNINGによる原子的な残数消化、という構成に既に改善済みである。

新人エンジニアが書いた移行計画には7つの設計上の問題が潜んでいる。

問題点を全て洗い出しDatabase Migration Service (DMS) による継続的レプリケーション移行 × 既存Readレプリカの再構成 × PgBouncer PAUSE/RESUMEによるコネクションドレイン × Argo Workflows CronWorkflowの一時サスペンド × 切り戻し(ロールバック)手順の明文化 × ポストアップグレード検証スイートを考慮したBad→Goodリファクタリングを行ってください。

制約・前提条件

  • Cloud SQL for PostgreSQL、Terraform 1.8+(google プロバイダー 5.x)
  • 現行構成: Primary(PG14, availability_type=REGIONAL)+ 同リージョンReadレプリカ(PG14)+ PgBouncer(transaction pooling、K8s Deployment)
  • coupon-serviceはGKE Autopilot上のAPI(読み書き)と、Argo Workflowsで日次実行されるcoupon-expiry-batch(CronWorkflow、期限切れクーポンの一括失効処理でDBに直接書き込む)の2系統からアクセスされる
  • クーポンは会員がリアルタイムに消費するため業務時間帯の停止は許容されないが、深夜1:00-5:00は実質トラフィックがゼロに近い
  • 拡張機能としてpg_trgm(クーポンコードのあいまい検索用)とuuid-ossp(クーポンID生成用)を使用している
期待する回答形式: 問題点の列挙(番号付き)+ 改善後 移行設計(Terraform: DMS接続プロファイル・移行ジョブ含む)+ カットオーバー運用フロー(設計意図の説明)

悪い移行計画 (Before)

この3行の移行計画には 7つの設計上の問題 が隠れています。見つけてみてください。
新人エンジニアが提出した移行計画
# 問題①: in-place書き換え。ダウンタイムがDBサイズに比例、失敗時のロールバック手段なし
# 問題②: 拡張機能(pg_trgm/uuid-ossp)や非互換構文の事前チェックなし
# 問題③: Readレプリカ(PG14)の存在が計画から欠落
# 問題④: PgBouncerのコネクションドレインなし
# 問題⑤: coupon-expiry-batch(CronWorkflow)が計画から欠落
# 問題⑥: メンテナンスウィンドウがSlack予告のみで運用計画に未組込
# 問題⑦: 「コマンド成功」のみが完了判定基準

1. 深夜メンテナンス予告をSlackに投稿する
2. gcloud sql instances patch coupon-db-primary \
     --database-version=POSTGRES_16
3. コマンドが成功したら移行完了とし、Slackに完了報告を投稿する
問題点サマリー(7点)
1in-place書き換え — 実質バックアップ→リストア、ロールバック手段なし
2拡張機能・非互換構文の事前チェックなし — pg_trgm/uuid-ossp未検証
3Readレプリカが計画から欠落 — メジャーバージョンを跨げない制約を見落とし
4コネクションドレインなし — 進行中トランザクションが強制切断
5バッチジョブが計画から欠落 — カットオーバー中の書き込みで不整合
6メンテナンスウィンドウが宣言のみ — 具体的な段取りがない
7「コマンド成功」=完了という判定基準 — アプリ動作・性能劣化を未検証

ヒント(段階的開示)

ヒント1 — 方向性
問題は4層に分類できる。(1) 移行方式そのもの — Cloud SQLのネイティブレプリケーションはメジャーバージョンを跨げないため、「in-placeでバージョンを書き換える」という発想自体がダウンタイム・ロールバック不能というリスクを内包している、(2) 周辺トポロジの見落とし — Primaryだけを見て、Readレプリカという「もう一つの依存コンポーネント」の存在を移行計画から欠落させている、(3) カットオーバー時の接続制御 — アプリ側・バッチ側の接続を正しく止めてから切り替えないと、切替の瞬間にトランザクションが強制切断される、(4) 検証と後戻り可能性 — 「コマンドが成功した」ことと「アプリケーションが正しく動作する」ことは別であり、かつ失敗した場合に旧バージョンへ戻る手段が用意されていなければ、移行はそもそも実行してはいけない。メジャーバージョンアップグレードは「バージョン番号を書き換える作業」ではなく、新しいインスタンスを安全に立ち上げ、検証し、トラフィックを移し、失敗したら戻れる状態を維持したまま初めて旧インスタンスを退役させる一連のプロセスである。
ヒント2 — アプローチ
  • 問題①: gcloud sql instances patch --database-versionの直接実行 → Database Migration Service (DMS) のhomogeneous migration(継続的レプリケーション)で新規PG16インスタンスを別立てし、同期完了後の短時間カットオーバーに留める
  • 問題②: 拡張機能・非互換構文の事前チェックなし → ステージングの本番同等クローンでDMS移行を事前リハーサルし、動作を検証してから本番に着手する
  • 問題③: Readレプリカ(PG14)が計画から欠落 → メジャーバージョンを跨げない制約上、DMSの移行先(新PG16)から新たにPG16のReadレプリカを構成し直す
  • 問題④: PgBouncerのコネクションドレインなし → カットオーバー時はPgBouncerのPAUSE → DMS最終カットオーバー → 接続先切替 → RESUMEの手順を明文化する
  • 問題⑤: coupon-expiry-batch(CronWorkflow)が計画から欠落 → Argo WorkflowsのCronWorkflowをsuspend: trueで一時停止してからカットオーバーする
  • 問題⑥: メンテナンスウィンドウがSlack予告のみ → 深夜1:00-5:00の低トラフィック帯を正式なウィンドウとして選定し、バッチサスペンド・PgBouncer PAUSEを同一時間枠に組み込む
  • 問題⑦: 「コマンド成功」=完了という判定基準 → ポストアップグレード検証スイート(拡張機能smoke test・EXPLAIN比較・アプリヘルスチェック)を自動実行し、旧PG14を24時間読み取り専用で保持して切り戻し余地を残す
ヒント3 — コードの骨格
# Database Migration Service 接続プロファイル + 移行ジョブ のスケルトン
resource "google_database_migration_connection_profile" "source_pg14" {
  connection_profile_id = "coupon-db-source-pg14"
  postgresql { ... }   # 既存Primary(PG14)への接続情報
}

resource "google_database_migration_connection_profile" "destination_pg16" {
  connection_profile_id = "coupon-db-dest-pg16"
  cloudsql {
    cloudsql_id = google_sql_database_instance.coupon_db_pg16.name
  }
}

resource "google_database_migration_migration_job" "pg14_to_pg16" {
  migration_job_id = "coupon-db-pg14-to-pg16"
  type             = "CONTINUOUS"   # 継続的レプリケーションでダウンタイム最小化
  source           = google_database_migration_connection_profile.source_pg14.name
  destination      = google_database_migration_connection_profile.destination_pg16.name
}

問題点分析(7点)

#問題点分類改善方法
1in-place書き換え・ロールバック手段なし移行方式層DMS継続的レプリケーションで新インスタンスへ移行
2拡張機能・非互換構文の未検証移行方式層ステージングでDMS移行リハーサル
3Readレプリカが計画から欠落周辺トポロジ層PG16側でレプリカ再構成
4コネクションドレインなし接続制御層PgBouncer PAUSE/RESUME
5バッチジョブが計画から欠落周辺トポロジ層CronWorkflow suspend
6メンテナンスウィンドウが宣言のみ接続制御層深夜帯を正式ウィンドウ化
7「コマンド成功」=完了判定検証・後戻り層検証スイート+旧インスタンス保持

カットオーバーフロー図 — Bad vs Good の変換

Bad(変更前)— in-place書き換え・3行の計画 Slackに深夜メンテナンス予告を投稿 問題⑥: 具体的な段取り(バッチ停止/接続ドレイン)が計画に無い gcloud sql instances patch --database-version=POSTGRES_16 問題①: in-place書き換え。ダウンタイムがDBサイズに比例、失敗時に戻せない 問題②: pg_trgm/uuid-ossp・非互換構文の事前チェックなし 問題③⑤: Readレプリカ・coupon-expiry-batchが計画に不在 切替の瞬間(PgBouncerドレインなし) 問題④: 進行中トランザクションが強制切断されエラー多発 クーポン残数UPDATE...RETURNINGが中途半端に切れる恐れ コマンド成功 → 完了報告をSlackに投稿 問題⑦: 拡張機能動作・クエリ性能・アプリ挙動を未検証のまま完了扱い 旧PG14インスタンスは既に上書き済みで切り戻し不能 起こり得る事故シナリオ 拡張機能の非互換で検索機能が静かに壊れる Readレプリカが取り残され読み取り分離が機能停止 バッチとカットオーバーが競合しデータ不整合 × 移行を「一発コマンド」として扱っている × 周辺トポロジ(レプリカ/バッチ)が計画から欠落 × 切替時の接続制御・検証・後戻り手段が皆無 Good(変更後)— DMS移行 × 接続制御 × 検証・切り戻し 事前準備: ステージングでDMS移行リハーサル 修正②: pg_trgm/uuid-ossp・主要クエリの動作を事前検証 修正⑦: ポストアップグレード検証スイートを用意 DMS継続的レプリケーション開始(本番) 修正①: 新PG16インスタンスへフルロード後、WALを継続同期 レプリケーション遅延をCloud Monitoringで監視 深夜1:00-5:00 正式メンテナンスウィンドウ 修正⑤: CronWorkflow suspend:true / 修正⑥: 低トラフィック帯を明示 カットオーバー: PAUSE → PROMOTE → 接続先切替 → RESUME 修正④: PgBouncerが進行中トランザクションの完了を待ってから切替 トランザクションの強制切断を回避 ポストアップグレード検証スイート実行 修正⑦: 拡張機能smoke test・EXPLAIN比較・アプリヘルスチェック 全項目パス後CronWorkflowをsuspend:falseへ復帰 旧PG14を24時間 読み取り専用で保持 修正⑦: 異常検知時はPgBouncer接続先を旧PG14へ即座に戻せる 問題なければPG16側でReadレプリカを再構成(修正③)し旧環境を退役 ✓ 移行を「新環境への段階的な移行プロセス」として設計 ✓ Readレプリカ・バッチジョブを移行対象に含める ✓ 接続制御・検証・後戻り手段を全て備える 修正

模範解答

Before — 3行の移行計画(コマンド直接実行)
# 問題①②③⑤⑥⑦: 移行というプロセス設計がまるごと欠落
1. Slackに深夜メンテナンス予告を投稿する
2. gcloud sql instances patch coupon-db-primary \
     --database-version=POSTGRES_16
3. コマンドが成功したら移行完了とし、
   Slackに完了報告を投稿する
After — DMS継続的レプリケーション + レプリカ再構成
# ── 新規PG16インスタンス(DMS移行先)─────────────────
resource "google_sql_database_instance" "coupon_db_pg16" {
  name             = "coupon-db-primary-pg16"
  database_version = "POSTGRES_16"
  region           = "asia-northeast1"

  settings {
    tier              = "db-custom-4-16384"
    availability_type = "REGIONAL"
    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
    }
  }

  deletion_protection = true  # 修正⑦: 切り戻し余地の確保
}

# ── DMS 接続プロファイル: 移行元(PG14) ──────────────
resource "google_database_migration_connection_profile" "source_pg14" {
  connection_profile_id = "coupon-db-source-pg14"
  location               = "asia-northeast1"
  postgresql {
    host     = google_sql_database_instance.coupon_db_primary.private_ip_address
    port     = 5432
    username = "dms_migration_user"
    password = var.dms_migration_user_password
  }
}

# ── DMS 接続プロファイル: 移行先(PG16) ──────────────
resource "google_database_migration_connection_profile" "destination_pg16" {
  connection_profile_id = "coupon-db-dest-pg16"
  location               = "asia-northeast1"
  cloudsql {
    cloudsql_id = google_sql_database_instance.coupon_db_pg16.name
  }
}

# ── DMS 移行ジョブ(修正①: 継続的レプリケーション)───
resource "google_database_migration_migration_job" "pg14_to_pg16" {
  migration_job_id = "coupon-db-pg14-to-pg16"
  location          = "asia-northeast1"
  type              = "CONTINUOUS"  # フルロード後にWALを継続同期
  source            = google_database_migration_connection_profile.source_pg14.id
  destination       = google_database_migration_connection_profile.destination_pg16.id

  reverse_ssh_connectivity {
    vm      = var.dms_bastion_vm_name
    vm_ip   = var.dms_bastion_vm_ip
    vm_port = 22
    vpc     = var.vpc_self_link
  }
}

# ── PG16側のReadレプリカ(修正③: 取り残し解消)──────
# カットオーバー完了・検証OK後にapplyする
resource "google_sql_database_instance" "coupon_db_pg16_replica" {
  name                 = "coupon-db-replica-pg16"
  database_version     = "POSTGRES_16"
  region               = "asia-northeast1"
  master_instance_name = google_sql_database_instance.coupon_db_pg16.name

  settings {
    tier = "db-custom-4-16384"
  }
}
① 事前準備(本番着手の1週間以上前)
   ├─ ステージングの本番同等クローンでDMS移行をリハーサル(修正②)
   │   → pg_trgm / uuid-ossp の再作成、主要クエリの動作確認、EXPLAIN比較
   └─ ポストアップグレード検証スイート(Cloud Run Job化)を用意(修正⑦)
       → 拡張機能smoke test / 主要クエリのEXPLAIN比較 / アプリヘルスチェック

② DMS移行ジョブ開始(本番、業務時間中でも可)
   → CONTINUOUS移行がPG14からPG16へフルロード→継続的にWAL同期
   → レプリケーション遅延をCloud Monitoringで監視し、収束するまで待機

③ メンテナンスウィンドウ突入(深夜1:00、修正⑥)
   ├─ Argo Workflows: coupon-expiry-batch CronWorkflowをsuspend:trueに変更(修正⑤)
   ├─ PgBouncer: PAUSE を発行し、進行中トランザクションの完了を待つ(修正④)
   └─ DMSのレプリケーション遅延が0に収束していることを確認

④ カットオーバー実行
   ├─ DMS migration job を PROMOTE(移行先PG16インスタンスをスタンドアロン化)
   ├─ PgBouncerの接続先設定をPG16インスタンスのIPへ切替
   └─ PgBouncer: RESUME を発行し、保留していたクエリを新Primaryへ流す

⑤ ポストアップグレード検証(修正⑦)
   → 検証スイート(拡張機能smoke test / EXPLAIN比較 / アプリヘルスチェック)を実行
   → 全項目パス後、Argo WorkflowsのCronWorkflowをsuspend:falseに戻す

⑥ 切り戻し余地の保持(修正⑦)
   → 旧PG14 Primaryは削除せず、読み取り専用で24時間保持
   → 検証スイートで異常を検知した場合、PgBouncerの接続先を旧PG14へ戻す
   → 24時間問題なければ、旧PG14 Primary・旧PG14 Readレプリカを退役

⑦ PG16側Readレプリカの構成(修正③)
   → ⑥の退役判断が確定した後、coupon_db_pg16_replicaをterraform applyで作成
問題修正内容効果
① in-place書き換えDMS継続的レプリケーションで新インスタンスへ移行ダウンタイム最小化・ロールバック手段の確保
② 拡張機能・非互換構文未検証ステージングでDMS移行リハーサル本番着手前に互換性問題を発見
③ Readレプリカが計画から欠落PG16側でレプリカ再構成読み書き分離構成の継続
④ コネクションドレインなしPgBouncer PAUSE/RESUMEトランザクションの強制切断を回避
⑤ バッチジョブが計画から欠落CronWorkflow suspendカットオーバー中のデータ不整合を防止
⑥ ウィンドウが宣言のみ深夜1-5時を正式ウィンドウ化具体的な段取りとしてバッチ停止・PAUSEを内包
⑦ 「コマンド成功」=完了判定検証スイート+旧インスタンス24時間保持アプリ挙動の裏付けと即時切り戻し余地

ポイント解説

1メジャーバージョンアップグレードは「バージョン番号の書き換え」ではなく「新インスタンスへの移行」である — Cloud SQLのネイティブレプリケーションはメジャーバージョンを跨げない制約があるため、in-place upgradeはGoogle内部でバックアップ→リストアという重い処理を行っており、実質的には「移行」を隠蔽しているに過ぎない。DMSで明示的に移行として設計することで、リハーサル・監視・ロールバックといった移行に必要なプロセスを正面から設計できる。
2周辺トポロジ(レプリカ・バッチ)を含めて移行対象を洗い出す — Primaryだけを見て移行計画を立てると、Readレプリカやバッチジョブのような「Primaryに依存する周辺コンポーネント」が計画から漏れやすい。移行対象を洗い出す際は、対象DBへの接続元を全て棚卸しすることが出発点になる。
3カットオーバーは「接続を止めてから切り替える」が鉄則 — PgBouncerのPAUSE/RESUMEのようなコネクションプーラーの機能を使わずに接続先を切り替えると、進行中のトランザクションが中途半端な状態で強制切断される。本サービスのようにUPDATE ... RETURNINGで残数を原子的に消化している設計では、切断のタイミング次第で二重計上や取りこぼしのリスクが生じ得る。
4「コマンドが成功した」と「アプリが正しく動く」は別の検証軸 — インフラのAPIが成功レスポンスを返すことは前提条件に過ぎず、拡張機能の互換性・クエリプランの劣化・アプリケーションの実際の挙動という3つの軸で独立して検証しない限り、移行の成功は保証できない。
5ロールバック可能な状態を維持したまま前に進む — 旧インスタンスを即座に削除せず一定時間読み取り専用で保持することで、「検証で問題が見つかった場合に元に戻せる」という選択肢を残せる。削除は「戻る必要がないと確信できた後」に行う最後のステップである。

実務への応用

同様の「メジャーバージョンアップグレード=実質的な移行」という捉え方は、Cloud SQLに限らずBigQueryのスキーマ移行やKafkaのブローカーバージョンアップ、あるいはライブラリのメジャーバージョンアップ(例: Pydantic v1→v2)にも応用できる。「バージョンを上げる」という言葉の軽さに反して、実際には「新しい環境を作り、検証し、トラフィックを移し、戻れる状態を保ったまま旧環境を退役させる」という一連のプロセスであるという認識を持つことが、EOL対応を場当たり的な作業から計画的なプロジェクトへ格上げする第一歩になる。

DMS移行開始のタイミングにも注意する: CONTINUOUS移行はレプリケーション遅延が0に収束するまで待ってからカットオーバーする必要がある。セール直前など書き込み量が急増するタイミングでDMS移行を開始すると、WAL生成量に同期が追いつかず遅延が収束しないまま長時間待たされることがあるため、移行開始自体も比較的落ち着いたトラフィック帯を選ぶのが実務上の勘所になる。

今日のまとめ

Cloud SQLメジャーバージョンアップグレードの7点チェックリスト: ① in-place書き換えではなくDMS継続的レプリケーションで新インスタンスへ移行 ② 拡張機能・非互換構文の事前リハーサル ③ Readレプリカなど周辺トポロジの再構成を計画に含める ④ PgBouncer PAUSE/RESUMEによるコネクションドレイン ⑤ バッチジョブ(Argo Workflows CronWorkflow)のサスペンド ⑥ 低トラフィック帯を正式なメンテナンスウィンドウとして段取りに組み込む ⑦ ポストアップグレード検証スイート+旧インスタンスの一定期間保持による切り戻し余地の確保。 メジャーバージョンアップグレードは「バージョン番号を書き換える一発コマンド」ではなく、新環境への移行・検証・切替・後戻り可能性の確保までを含む一連のプロセス設計である。

次のステップ

  • 発展問題: DMSのCONTINUOUS移行中、旧PG14 Primaryと新PG16インスタンスの両方に書き込みが発生してしまうケース(例: バッチのsuspendを忘れた、手動SQLを直接旧Primaryに実行してしまった)が起きた場合、DMSの同期はどう振る舞うか(片方向レプリケーションのため新PG16側には反映されない)を調査し、この事故を検知する仕組み(新旧インスタンスの行数・チェックサム比較バッチ等)を設計せよ
  • 参考: Database Migration Service(homogeneous migration) / Cloud SQL major version upgrade(in-place) / PgBouncer PAUSE/RESUME / Argo Workflows CronWorkflow suspend / pg_stat_statements

自己評価(あとで記入)