B システム設計/インフラ — Cloud SQL for PostgreSQL 本番HA基盤(Private IP × REGIONAL 自動フェイルオーバー × IAM DB 認証 パスワードレス × 自動バックアップ/PITR × deletion_protection + prevent_destroy × リードレプリカ読み取り分離 × Cloud SQL Auth Proxy + PgBouncer 接続プーリング)(MOps キャンペーンDB Bad→Good)

2026-07-14 (Day 103) 火曜 B: システム設計/インフラ ★★★★☆ Cloud SQL / PostgreSQL 16 / Private IP REGIONAL HA / IAM 認証 / PITR / PgBouncer

概要

🔒

Private IP + REGIONAL でネットワーク遮断と自動フェイルオーバー

ipv4_enabled = false + private_network(Service Networking / VPC ピアリング)でパブリック公開を廃止し、authorized_networks 0.0.0.0/0 を除去する。availability_type = "REGIONAL" は別ゾーンに同期スタンバイを常時複製し、プライマリ障害時に自動フェイルオーバー(RPO≈0)。アプリは同じ接続名のまま無変更で追従できる。

🔑

IAM DB 認証でパスワードレス(tfstate 平文を廃止)

DB パスワードの平文ハードコードは tfstate に生で残り漏洩リスクになる。cloudsql.iam_authentication = on + google_sql_user(type=CLOUD_IAM_SERVICE_ACCOUNT) で GSA を DB ユーザー化し、Cloud SQL Auth Proxy の --auto-iam-authn が短命 IAM トークンをパスワード代わりに使う。静的認証情報が完全に消える。

🛟

PITR + deletion_protection + prevent_destroy でデータを二重に守る

point_in_time_recovery_enabled = true は WAL を保持し任意の秒単位へ巻き戻せる(誤 UPDATE/DELETE への保険)。deletion_protection = true(API 側の DELETE 拒否)と lifecycle { prevent_destroy }(Terraform 側の destroy/replace 拒否)は守備範囲が異なるため、両方設定して初めて本番DB消失を確実に防げる。

🔀

リードレプリカ + PgBouncer で読み取り分離と接続枯渇対策

集計・レポート読み取りをリードレプリカ(別 Auth Proxy エンドポイント)に分離し、プライマリを書き込みに専念させる。Pod スケールアウトで max_connections が枯渇する問題は、Cloud SQL Auth Proxy(TLS+IAM)→ PgBouncer(transaction モード、実接続を 20 本に集約)の 2 段構成で解消する。

問題

ECサイト MOps チームでは、キャンペーン参加者・注文トランザクションを扱う Cloud SQL for PostgreSQL 16 を本番稼働させる。以下の Terraform コードと、DB へ接続する GKE Autopilot 上のアプリ Deployment には 7つの設計上の問題 が潜んでいる。問題点を全て洗い出し、Private IP × REGIONAL HA × IAM データベース認証(パスワードレス)× 自動バックアップ/PITR × deletion_protection + prevent_destroy × リードレプリカ × Cloud SQL Auth Proxy + PgBouncer を考慮した Bad→Good リファクタリングを行え。

制約・前提条件

  • Terraform 1.8+(google プロバイダー 5.x)、Cloud SQL for PostgreSQL 16、asia-northeast1
  • DB は VPC 内部からのみアクセス(パブリックインターネットに晒さない)
  • アプリは GKE Autopilot 上の Deployment(Cloud SQL Auth Proxy サイドカー経由で接続)
  • 認証は IAM データベース認証(静的パスワードを廃止)
  • 集計・レポート系の重い読み取りがプライマリの負荷を圧迫している
  • ピーク時に Pod が急増し FATAL: remaining connection slots are reserved で接続枯渇
  • 現状は deletion_protection = false・バックアップ未設定・パスワードを Terraform に平文ハードコード
期待する回答形式: 問題点の列挙(番号付き)+ 改善後 Terraform コード + 改善後 Kubernetes マニフェスト(YAML)+ 設計意図の説明

悪いコード (Before)

このコード・マニフェストには 7つの設計上の問題 が隠れています。見つけてみてください。
bad_campaign_db.tf — パブリック公開・ZONAL・平文パスワード・バックアップなし
resource "google_sql_database_instance" "campaign_db" {
  name             = "mops-campaign-db"
  database_version = "POSTGRES_16"
  region           = "asia-northeast1"

  # 問題⑤: 誤 destroy でDB消失(保護なし)
  deletion_protection = false

  settings {
    tier = "db-custom-4-16384"
    # 問題②: 単一ゾーン → ゾーン障害でDB停止
    availability_type = "ZONAL"

    ip_configuration {
      # 問題①: パブリックIP + 全開放
      ipv4_enabled = true
      authorized_networks {
        name  = "all"
        value = "0.0.0.0/0"
      }
    }
    # 問題④: backup_configuration なし(PITR 無効)
  }
  # 問題⑤: lifecycle prevent_destroy なし
}

# 問題③: パスワードを平文ハードコード → tfstate に生保存
resource "google_sql_user" "app" {
  name     = "app"
  instance = google_sql_database_instance.campaign_db.name
  password = "P@ssw0rd123"
}

# 問題⑥: リードレプリカなし(読み取りもプライマリ集中)
bad_campaign_app.yaml — 直接接続・パスワード注入・プールなし
apiVersion: apps/v1
kind: Deployment
metadata:
  name: campaign-app
  namespace: mops
spec:
  replicas: 3
  selector:
    matchLabels:
      app: campaign-app
  template:
    spec:
      # serviceAccountName なし → default SA
      containers:
        - name: app
          image: .../mops/campaign-app:latest
          env:
            # 問題①: パブリックIPへ直接接続
            - name: DB_HOST
              value: "34.84.xx.xx"
            - name: DB_PORT
              value: "5432"
            # 問題③: パスワードを Secret から注入(静的)
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: password
          # 問題⑦: 各 Pod が直接大量接続 → max_connections 枯渇
          # 問題⑥: 読み取りもプライマリへ(RO 分離なし)
      # Auth Proxy / PgBouncer サイドカーなし
問題点サマリー(7点)
1パブリックIP + 0.0.0.0/0 全開放 — DBがインターネットから接続可能。ipv4_enabled = false + private_network(VPC 内部限定)へ
2availability_type = ZONAL — 単一ゾーン障害でDB停止。REGIONAL(同期スタンバイ + 自動フェイルオーバー)へ
3パスワード平文ハードコード — tfstate に平文保存で漏洩リスク。IAM DB 認証 + Auth Proxy --auto-iam-authn でパスワードレス化
4バックアップ・PITR 未設定 — 論理破壊からの復旧手段なし。backup_configuration + point_in_time_recovery_enabled
5deletion_protection = false / prevent_destroy なし — 誤 destroy でDB消失。API と Terraform の二重防御
6リードレプリカなし — レポート読み取りがプライマリ圧迫。RO レプリカ + 別 Proxy エンドポイントに分離
7コネクションプールなし — Pod 増で max_connections 枯渇。Auth Proxy + PgBouncer(transaction) で実接続を集約

ヒント(段階的開示)

ヒント1 — 方向性
Cloud SQL 本番設計の問題は3層に分類できる。(1) ネットワーク/セキュリティ — パブリックIP + authorized_networks 全開放・静的パスワードの平文ハードコード、(2) 可用性/データ保護ZONAL(単一ゾーン)・バックアップ/PITR 未設定・deletion_protection = false(3) スケーラビリティ/接続管理 — リードレプリカなし(読み取りがプライマリ集中)・コネクションプールなし(max_connections 枯渇)。Private IP は ip_configuration { ipv4_enabled = false, private_network = <VPC> } で構成し、Service Networking(VPC ピアリング)の事前確立を前提にする。
ヒント2 — アプローチ
  • 問題①: ipv4_enabled = false + private_networkauthorized_networks 0.0.0.0/0 を廃止。ssl_mode = "ENCRYPTED_ONLY"
  • 問題②: availability_type = "REGIONAL"(同期スタンバイ + 自動フェイルオーバー、SLA 99.95%)
  • 問題③: database_flags { name = "cloudsql.iam_authentication", value = "on" } + google_sql_user { type = "CLOUD_IAM_SERVICE_ACCOUNT" }、Proxy --auto-iam-authn
  • 問題④: backup_configuration { enabled = true; point_in_time_recovery_enabled = true; transaction_log_retention_days = 7; backup_retention_settings { retained_backups = 14 } }
  • 問題⑤: deletion_protection = true + lifecycle { prevent_destroy = true }(二重)
  • 問題⑥: master_instance_name 指定のリードレプリカ + アプリ読み取りを RO エンドポイントにルーティング
  • 問題⑦: Cloud SQL Auth Proxy(TLS+IAM)→ PgBouncer(POOL_MODE = transaction, DEFAULT_POOL_SIZE = 20)の 2 段
ヒント3 — コードの骨格
# Cloud SQL プライマリ スケルトン
resource "google_sql_database_instance" "campaign_db" {
  database_version    = "POSTGRES_16"
  deletion_protection = true            # 修正⑤
  settings {
    availability_type = "REGIONAL"      # 修正②
    ip_configuration {
      ipv4_enabled    = false           # 修正①
      private_network = google_compute_network.vpc.id
      ssl_mode        = "ENCRYPTED_ONLY"
    }
    backup_configuration {              # 修正④
      enabled                        = true
      point_in_time_recovery_enabled = true
      transaction_log_retention_days = 7
      backup_retention_settings { retained_backups = 14 }
    }
    database_flags { name = "cloudsql.iam_authentication"; value = "on" }  # 修正③
  }
  lifecycle { prevent_destroy = true }  # 修正⑤
}

resource "google_sql_database_instance" "campaign_db_replica" {  # 修正⑥
  master_instance_name = google_sql_database_instance.campaign_db.name
  settings { availability_type = "ZONAL" }
}

# Auth Proxy サイドカー(修正③⑦)
- name: cloud-sql-proxy
  image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0
  args: ["--auto-iam-authn", "PROJECT:asia-northeast1:mops-campaign-db"]

問題点分析(7点)

#問題点分類改善方法
1パブリックIP + authorized_networks 0.0.0.0/0セキュリティipv4_enabled=false + private_network(Service Networking)+ ssl_mode=ENCRYPTED_ONLY
2availability_type = ZONAL(単一ゾーン)可用性REGIONAL(同期スタンバイ + 自動フェイルオーバー、SLA 99.95%)
3パスワード平文ハードコード(tfstate 保存)セキュリティIAM DB 認証 + CLOUD_IAM_SERVICE_ACCOUNT + Auth Proxy --auto-iam-authn
4バックアップ・PITR 未設定データ保護backup_configuration + point_in_time_recovery_enabled + retention
5deletion_protection=false / prevent_destroy なし破壊的操作deletion_protection=true(API)+ lifecycle prevent_destroy(Terraform)
6リードレプリカなし(読み取り集中)スケーラビリティmaster_instance_name 指定レプリカ + RO エンドポイント分離
7コネクションプールなし(max_connections 枯渇)接続管理Cloud SQL Auth Proxy + PgBouncer(transaction, pool_size=20)

アーキテクチャ図 — Bad vs Good の変換フロー

Bad(変更前)— 公開・単一ゾーン・平文PW・保護なし Internet → Cloud SQL パブリックIP(34.84.x.x) 問題①: ipv4_enabled=true + authorized_networks 0.0.0.0/0(全開放) GKE Pod (campaign-app) × 3〜N 問題③: DB_PASSWORD を Secret 注入(静的パスワード) 問題⑦: 各 Pod が直接接続 → max_connections 枯渇 Auth Proxy / PgBouncer なし・読み書き両方プライマリへ Cloud SQL PostgreSQL(プライマリ) 問題②: availability_type=ZONAL(単一ゾーン) 問題④: backup_configuration なし → PITR 無効 問題⑥: リードレプリカなし(レポート読み取りで CPU 圧迫) FATAL: remaining connection slots are reserved Terraform state / 削除保護(Bad) 問題③: password="P@ssw0rd123" → tfstate に平文保存 問題⑤: deletion_protection=false / prevent_destroy なし × DBがインターネット公開 → 総当たり・漏洩リスク × ゾーン障害でDB全停止・復旧手段なし × 誤 terraform destroy で本番DB消失 × Pod スケールアウトで接続枯渇 → サービス停止 × 静的パスワードが state / Secret に残存 Good(変更後)— Private IP・HA・IAM 認証・PITR・レプリカ VPC(Service Networking / Private IP) 修正①: ipv4_enabled=false + private_network + ssl_mode=ENCRYPTED_ONLY GKE Pod(app + サイドカー 3 本) app → localhost:6432(PgBouncer)へ接続(パスワードなし) 修正⑦: PgBouncer transaction / DEFAULT_POOL_SIZE=20 で実接続集約 修正③: Auth Proxy --auto-iam-authn(IAM トークンで認証) 修正⑥: proxy-ro(6433)→ リードレプリカ(読み取り分離) serviceAccountName: campaign-app(WIF / KSA→GSA キーレス) Proxy → PgBouncer → App の 2 段構成が定石 プライマリ(REGIONAL HA) 修正②: 同期スタンバイ 自動フェイルオーバー 修正④: PITR / WAL 7日 backup 14世代保持 書き込み + 強整合読み取り RPO≈0 / SLA 99.95% リードレプリカ(RO) 修正⑥: master_instance_name 非同期複製で追従 集計・レポート読み取り replica_lag を監視し read-after-write は プライマリにフォールバック 認証 + 削除保護(Good) 修正③: cloudsql.iam_authentication=on + CLOUD_IAM_SERVICE_ACCOUNT 静的パスワード廃止 → tfstate に認証情報なし 修正⑤: deletion_protection=true(API)+ prevent_destroy(Terraform) ✓ Private IP でインターネット遮断・TLS 強制 ✓ REGIONAL 自動フェイルオーバー(RPO≈0) ✓ IAM 認証で短命トークン・ローテーション不要 ✓ PITR で任意時点復元・14世代バックアップ ✓ PgBouncer で実接続 20 本に集約・枯渇解消 ✓ API + Terraform の二重削除防止 修正

模範解答

Before — 公開IP・ZONAL・平文PW・バックアップなし・保護なし
resource "google_sql_database_instance" "campaign_db" {
  database_version    = "POSTGRES_16"
  deletion_protection = false          # 問題⑤
  settings {
    availability_type = "ZONAL"        # 問題②
    ip_configuration {
      ipv4_enabled = true              # 問題①
      authorized_networks {
        value = "0.0.0.0/0"
      }
    }
    # 問題④: backup_configuration なし
  }
  # 問題⑤: prevent_destroy なし
}

resource "google_sql_user" "app" {
  name     = "app"
  instance = google_sql_database_instance.campaign_db.name
  password = "P@ssw0rd123"             # 問題③
}
# 問題⑥: リードレプリカなし
After — Private IP + REGIONAL + IAM 認証 + PITR + 削除保護 + レプリカ
# ── VPC + Private Service Access(Private IP の前提)──────────
resource "google_compute_network" "vpc" {
  name                    = "mops-vpc"
  auto_create_subnetworks = false
}
resource "google_compute_global_address" "private_ip_range" {
  name          = "mops-sql-private-ip"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = google_compute_network.vpc.id
}
resource "google_service_networking_connection" "private_vpc" {
  network                 = google_compute_network.vpc.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_ip_range.name]
}

# ── Cloud SQL プライマリ(HA + Private IP + IAM 認証 + PITR)──
resource "google_sql_database_instance" "campaign_db" {
  name                = "mops-campaign-db"
  database_version    = "POSTGRES_16"
  region              = var.region
  deletion_protection = true                  # 修正⑤(1): API 側
  depends_on          = [google_service_networking_connection.private_vpc]

  settings {
    tier              = "db-custom-4-16384"
    availability_type = "REGIONAL"            # 修正②: HA + 自動フェイルオーバー
    disk_autoresize   = true

    ip_configuration {                        # 修正①: パブリックIP廃止
      ipv4_enabled                                  = false
      private_network                               = google_compute_network.vpc.id
      enable_private_path_for_google_cloud_services = true
      ssl_mode                                      = "ENCRYPTED_ONLY"
    }

    backup_configuration {                    # 修正④: バックアップ + PITR
      enabled                        = true
      start_time                     = "17:00"  # UTC(JST 02:00)
      point_in_time_recovery_enabled = true
      transaction_log_retention_days = 7
      backup_retention_settings {
        retained_backups = 14
        retention_unit   = "COUNT"
      }
    }

    database_flags {                          # 修正③: IAM DB 認証
      name  = "cloudsql.iam_authentication"
      value = "on"
    }
    insights_config { query_insights_enabled = true }
  }

  lifecycle { prevent_destroy = true }        # 修正⑤(2): Terraform 側
}

resource "google_sql_database" "campaign" {
  name     = "campaign"
  instance = google_sql_database_instance.campaign_db.name
}

# 修正③: IAM DB ユーザー(.gserviceaccount.com を除いた名前で登録)
resource "google_sql_user" "app_iam_user" {
  name     = "mops-campaign-app@${var.project_id}.iam"
  instance = google_sql_database_instance.campaign_db.name
  type     = "CLOUD_IAM_SERVICE_ACCOUNT"
}

# 修正⑥: リードレプリカ(読み取り分離)
resource "google_sql_database_instance" "campaign_db_replica" {
  name                 = "mops-campaign-db-ro"
  database_version     = "POSTGRES_16"
  region               = var.region
  master_instance_name = google_sql_database_instance.campaign_db.name
  deletion_protection  = true
  settings {
    tier              = "db-custom-2-8192"
    availability_type = "ZONAL"             # レプリカは ZONAL で可
    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.vpc.id
    }
    database_flags { name = "cloudsql.iam_authentication"; value = "on" }
  }
  lifecycle { prevent_destroy = true }
}

# アプリ SA + 最小権限(Auth Proxy 接続 + IAM DB ログイン)
resource "google_service_account" "app_sa" {
  account_id = "mops-campaign-app"
}
resource "google_project_iam_member" "app_sql_client" {
  project = var.project_id
  role    = "roles/cloudsql.client"
  member  = "serviceAccount:${google_service_account.app_sa.email}"
}
resource "google_project_iam_member" "app_sql_instance_user" {
  project = var.project_id
  role    = "roles/cloudsql.instanceUser"
  member  = "serviceAccount:${google_service_account.app_sa.email}"
}
resource "google_service_account_iam_member" "app_wif" {  # 修正③: WIF
  service_account_id = google_service_account.app_sa.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${var.project_id}.svc.id.goog[mops/campaign-app]"
}
# ksa.yaml — KSA(WIF Annotation)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: campaign-app
  namespace: mops
  annotations:
    # 修正③: KSA → GSA バインド(SA キーファイル不要)
    iam.gke.io/gcp-service-account: mops-campaign-app@PROJECT_ID.iam.gserviceaccount.com
---
# deployment.yaml — アプリ + PgBouncer + Auth Proxy ×2(改善後)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: campaign-app
  namespace: mops
spec:
  replicas: 3
  selector:
    matchLabels:
      app: campaign-app
  template:
    metadata:
      labels:
        app: campaign-app
    spec:
      serviceAccountName: campaign-app       # 修正③: WIF / ADC キーレス
      containers:
        # ── アプリ本体 ──
        - name: app
          image: asia-northeast1-docker.pkg.dev/PROJECT_ID/mops/campaign-app@sha256:abc123
          env:
            - { name: DB_HOST, value: "127.0.0.1" }  # 修正⑦: localhost の PgBouncer
            - { name: DB_PORT, value: "6432" }
            - { name: DB_NAME, value: "campaign" }
            # 修正③: IAM DB ユーザー(パスワードなし)
            - { name: DB_USER, value: "mops-campaign-app@PROJECT_ID.iam" }
            # 修正⑥: 読み取り専用エンドポイント(レプリカ側 Proxy)
            - { name: DB_RO_HOST, value: "127.0.0.1" }
            - { name: DB_RO_PORT, value: "6433" }
          securityContext:
            runAsNonRoot: true
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }
            seccompProfile: { type: RuntimeDefault }

        # ── 修正⑦: PgBouncer(transaction プーリング / プライマリ)──
        - name: pgbouncer
          image: edoburu/pgbouncer:1.22.1
          ports:
            - { name: pgbouncer, containerPort: 6432 }
          env:
            - { name: DB_HOST, value: "127.0.0.1" }   # 同 Pod の Auth Proxy(5432)
            - { name: DB_PORT, value: "5432" }
            - { name: POOL_MODE, value: "transaction" }
            - { name: MAX_CLIENT_CONN, value: "500" }  # アプリ側の受け口
            - { name: DEFAULT_POOL_SIZE, value: "20" } # DB 実接続は最大20に集約
          securityContext:
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }

        # ── 修正③⑦: Cloud SQL Auth Proxy(プライマリ / IAM 認証)──
        - name: cloud-sql-proxy
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0
          args:
            - "--port=5432"
            - "--auto-iam-authn"     # IAM DB 認証(静的パスワード廃止)
            - "--structured-logs"
            - "PROJECT_ID:asia-northeast1:mops-campaign-db"
          securityContext:
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits:   { memory: "256Mi" }

        # ── 修正⑥: リードレプリカ用 Auth Proxy(読み取り分離)──
        - name: cloud-sql-proxy-ro
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0
          args:
            - "--port=6433"
            - "--auto-iam-authn"
            - "--structured-logs"
            - "PROJECT_ID:asia-northeast1:mops-campaign-db-ro"
          securityContext:
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }
---
# pdb.yaml — PodDisruptionBudget(ノードドレイン時もサービス継続)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: campaign-app-pdb
  namespace: mops
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: campaign-app
問題修正内容効果
① パブリックIP + 全開放ipv4_enabled=false + private_network + ssl_mode=ENCRYPTED_ONLYインターネット遮断・TLS 強制・攻撃面の除去
② ZONALavailability_type=REGIONAL(同期スタンバイ)自動フェイルオーバー・RPO≈0・SLA 99.95%
③ 平文パスワードIAM DB 認証 + Auth Proxy --auto-iam-authn静的認証情報の廃止・短命トークン・ローテーション不要
④ バックアップ/PITR なしbackup_configuration + point_in_time_recovery_enabled任意時点復元・論理破壊への保険・14世代保持
⑤ 削除保護なしdeletion_protection=true + lifecycle prevent_destroyAPI と Terraform の二重防御で誤 destroy を阻止
⑥ リードレプリカなしmaster_instance_name 指定レプリカ + RO Proxy読み取り分離でプライマリを書き込みに専念
⑦ コネクションプールなしAuth Proxy + PgBouncer(transaction, pool=20)実接続を集約し max_connections 枯渇を解消

ポイント解説

1Private IP は Service Networking の事前確立が必須ip_configuration { ipv4_enabled = false, private_network = ... } だけでは足りず、google_compute_global_addresspurpose = "VPC_PEERING")+ google_service_networking_connection で内部レンジを予約・接続してからでないとインスタンス作成が失敗する。depends_on で順序を保証する点が実務で詰まりやすい。
2REGIONAL の追加コストと引き換えの可用性REGIONAL はスタンバイ分のコンピュート/ストレージが増えるため課金は約2倍。SLA は 99.95%(ZONAL は SLA 対象外)。本番トランザクションDBは REGIONAL、開発/ステージングは ZONAL と使い分けるのがコスト最適。
3IAM DB ユーザー名の登録ルールgoogle_sql_userCLOUD_IAM_SERVICE_ACCOUNT を登録する際、名前は GSA メールから .gserviceaccount.com を除いた mops-campaign-app@PROJECT.iam 形式にする。間違えると Proxy 経由ログインが PAM authentication failed で失敗する。
4deletion_protectionprevent_destroy は守備範囲が違うdeletion_protection(API 側)は DELETE 操作を拒否するが、Terraform が「設定変更で置換が必要」と判断した recreate はブロックしない。prevent_destroy(Terraform 側)は plan 段階で destroy/replace を全て止める。両方あって初めて「うっかり本番DB消失」を確実に防げる。
5PgBouncer の POOL_MODE 選択transaction(トランザクション単位で接続を貸し出す)は接続効率が最も高いが、セッションレベルの機能(SET、prepared statement、advisory lock、LISTEN/NOTIFY)が使えない制約がある。ORM の prepared statement を使う場合はドライバ設定で対処するか、困る場合は session モードを選ぶ。
6リードレプリカのレプリケーションラグ監視 — 非同期複製のため、書き込み直後にレプリカを読むと古いデータが返る(read-after-write 不整合)。pg_stat_replicationreplay_lag や Cloud Monitoring の database/replication/replica_lag を監視し、ラグが許容値を超えたら重要な読み取りをプライマリにフォールバックする。

実務への応用

MOps のキャンペーン参加者DBのように「書き込み(応募受付)」と「集計レポート読み取り」が混在するワークロードでは、リードレプリカへの読み取り分離がプライマリの安定に直結する。ただし応募直後の「参加済み表示」のような read-after-write が必要な箇所はプライマリを読むよう、アプリのリポジトリ層で明示的にルーティングを分ける(例: get_for_display() はプライマリ、aggregate_report() はレプリカ)。

Cloud SQL Auth Proxy の --auto-iam-authn は IAM トークンの有効期限(約1時間)ごとに自動更新されるため、アプリ側は接続文字列に一切パスワードを書かずに済む。Secret Manager でパスワードを管理するよりさらに安全で、ローテーション運用も不要。既存のパスワード認証アプリを移行する場合は、まず IAM ユーザーを追加してパスワードユーザーと併用し、切り替え完了後にパスワードユーザーを削除する段階移行が安全。

PgBouncer の DEFAULT_POOL_SIZE 見積り: 「(max_connections − 予約分)÷ 想定 Pod 数」で算出する。例: max_connections = 200、システム予約・レプリケーション分で 40 を引いて 160、Pod 最大 8 なら 1 Pod あたり 20 が上限の目安。Argo Workflows のバッチ処理(大量 INSERT)が同じDBを叩く場合は、バッチ専用プールを別 PgBouncer で確保し、オンライン系と枠を分けて相互干渉を防ぐ。

今日のまとめ

Cloud SQL for PostgreSQL 本番設計の7点チェックリスト: ① Private IP(ipv4_enabled=false + Service Networking、パブリック廃止)② REGIONAL HA(同期スタンバイ + 自動フェイルオーバー)③ IAM DB 認証 + Auth Proxy --auto-iam-authn(パスワードレス)④ 自動バックアップ + PITR(transaction_log_retention_days)⑤ deletion_protection + prevent_destroy(API + Terraform の二重防御)⑥ リードレプリカ(読み取り分離、read-after-write に注意)⑦ Cloud SQL Auth Proxy + PgBouncer transaction プーリング(max_connections 枯渇対策)。 Private IP は Service Networking の事前確立が必須、IAM DB ユーザー名は .gserviceaccount.com を除いた形式、削除防止は API と Terraform の両レベルで設定するのが実務のポイント。

次のステップ

  • 発展問題: このプライマリDBをクロスリージョン(asia-northeast1asia-northeast2)のリードレプリカ + google_sql_source_representation_instance を使った DR 構成に拡張し、リージョン障害時の昇格(promote)手順を Terraform + runbook 化する。加えて PgBouncer のオンライン系/バッチ系プール分離を 2 系統エンドポイントで実装する
  • 参考: Cloud SQL for PostgreSQL 高可用性(Regional)/ Cloud SQL Auth Proxy --auto-iam-authn / Private Service Access(VPC ピアリング)/ PITR とバックアップ保持 / PgBouncer transaction pooling ドキュメント

自己評価(あとで記入)