概要
Cloud Run + Cloud SQL のリージョン複製
東京単一リージョンのCloud Runを大阪にも展開し、Cloud SQLはクロスリージョンリードレプリカを配置する。「複製すれば終わり」ではなく、障害時に昇格・切替できる状態まで作り込む。
Cloud DNS フェイルオーバールーティング
静的Aレコード1本・TTL24時間の手動運用を、ヘルスチェック連動の primary_backup ルーティングポリシー + TTL60秒に置き換え、検知から反映までの時間を短縮する。
RPO/RTOの明文化 × DRドリル
「RPO 5分・RTO 15分」を数値目標として合意し、四半期ごとに実地でレプリカ昇格・DNS切替までを演習する。数値目標が具体的な監視・切替設計を導く。
検知は自動化、DB昇格は人間承認
ヘルスチェック失敗の検知・通知はPub/Sub経由で自動化しつつ、決済データを扱うDBの最終的な昇格実行だけはスプリットブレイン防止のため人間の承認を残す。
問題
ECサイト MOps チームでは、外部決済代行会社とのやり取りを仲介する決済連携サービス(payment-gateway-relay)を Cloud Run で運用している。このサービスは注文確定フローの最終段(与信確定・決済確定)を担うため可用性要件が他サービスより厳しく、経営層からも「決済系だけは月間ダウンタイムを数分以内に抑えたい」と要求されている。しかし現状はすべて asia-northeast1(東京)単一リージョンで構築されており、以下の Terraform コードには 7つの設計上の問題 が潜んでいる。
問題点を全て洗い出し、マルチリージョン Cloud Run(東京プライマリ・大阪セカンダリ)× Cloud SQL クロスリージョンリードレプリカ + 昇格手順 × Cloud DNS フェイルオーバールーティングポリシー × Cloud Monitoring による自動フェイルオーバートリガー × RPO/RTO の明文化とリストアテスト × Terraform のリージョンmodule化 × Secret Manager 自動レプリケーション を考慮した Bad→Good リファクタリングを行ってください。
制約・前提条件
- Terraform 1.8+(
googleプロバイダー 5.x) - プライマリリージョン:
asia-northeast1(東京)、セカンダリリージョン:asia-northeast2(大阪) - 現状 Cloud Run サービス・Cloud SQL・Secret Manager・Cloud DNS はすべて東京リージョンのリソースのみで構成され、Terraform コードもリージョン名がハードコードされた単一ファイル
- Cloud SQL for PostgreSQL は
REGIONAL(東京内ゾーン跨ぎの自動フェイルオーバー)まではあるが、東京リージョン自体の障害には対応できない - DNS は
payment-relay.example.comの A レコード1本を東京の Cloud Run にべた書きしており、フェイルオーバーは「障害発生 → 運用者がSlackアラートに気づく → 手動でDNSレコードを書き換える」という人手運用(TTLは86400秒=24時間) - RPO(目標復旧時点)・RTO(目標復旧時間)はドキュメント化されておらず、リードレプリカの整合性・リストア手順の検証(DRドリル)も実施したことがない
- Secret Manager のシークレット(決済代行会社のAPIキー等)は
user_managedレプリケーションで東京リージョンのみに配置されている
悪いコード (Before)
# 問題①: Cloud Runが東京単一リージョンでSPOF
resource "google_cloud_run_v2_service" "payment_relay" {
name = "payment-gateway-relay"
location = "asia-northeast1"
template {
containers {
image = "asia-northeast1-docker.pkg.dev/PROJECT_ID/mops/payment-gateway-relay:latest"
}
}
}
# 問題②: リージョン内HAのみ。クロスリージョンリードレプリカが無い
resource "google_sql_database_instance" "primary" {
name = "payment-relay-db-primary"
region = "asia-northeast1"
database_version = "POSTGRES_16"
settings {
tier = "db-custom-4-16384"
availability_type = "REGIONAL" # 東京内ゾーン跨ぎのみ
}
}
# 問題③: 静的Aレコード1本・TTL24時間・ヘルスチェック連動なし
resource "google_dns_record_set" "payment_relay" {
name = "payment-relay.example.com."
type = "A"
ttl = 86400
rrdatas = ["203.0.113.10"] # 東京のIPを固定でべた書き
}
# 問題④: フェイルオーバーの自動検知・通知の仕組みが無い
# 問題⑤: RPO/RTOの数値目標がどこにも定義されていない
# 問題⑥: リージョン名がべた書きで、大阪用に複製すると二重管理になる
# 問題⑦: user_managedで東京リージョンのみに配置
resource "google_secret_manager_secret" "payment_gateway_api_key" {
secret_id = "payment-gateway-api-key"
replication {
user_managed {
replicas {
location = "asia-northeast1"
}
}
}
}
ヒント(段階的開示)
ヒント1 — 方向性
ヒント2 — アプローチ
- 問題①: Cloud Runが東京単一リージョン → 大阪にも同一構成のCloud Runサービスを配置し、Terraformの module で東京・大阪を同一コードから展開する
- 問題②: Cloud SQLがリージョン内HAのみでクロスリージョン複製がない →
google_sql_database_instanceにクロスリージョンリードレプリカ(大阪)を追加し、レプリケーション遅延をCloud Monitoringで可視化する - 問題③: DNSが静的Aレコード1本・TTL24時間の手動運用 →
routing_policyのprimary_backupに変更し、ヘルスチェック連動のhealth_checked_targetsを設定、TTLも60秒程度に短縮する - 問題④: フェイルオーバーが「人がSlackを見て手動対応」 → Cloud Monitoringのアラートから Pub/Sub 経由で通知・DR runbook起動までを自動化する。DB昇格の最終実行だけは人間の承認を挟む
- 問題⑤: RPO/RTOが未定義・リストア未検証 → 「RPO 5分・RTO 15分」を明文化し、四半期ごとのDRドリルをルーチン化する
- 問題⑥: Terraformがリージョン名べた書きの単一ファイル →
regionを変数化した module(modules/regional-service)を作り、ルートから東京・大阪をmodule呼び出しで展開する - 問題⑦: Secret Managerが
user_managedで東京のみ →replication { auto {} }(自動レプリケーション)に変更する
ヒント3 — コードの骨格
# modules/regional-service/variables.tf のスケルトン
variable "region" {
type = string
}
# ルート main.tf
module "payment_relay_primary" {
source = "./modules/regional-service"
region = "asia-northeast1"
}
module "payment_relay_secondary" {
source = "./modules/regional-service"
region = "asia-northeast2"
}
# Cloud DNS フェイルオーバールーティングポリシー
resource "google_dns_record_set" "payment_relay" {
ttl = 60 # 修正③: 24時間 → 60秒
routing_policy {
primary_backup {
trickle_ratio = 0.0
primary {
internal_load_balancers { ... } # 修正③: ヘルスチェック連動
}
backup_geo {
location = "asia-northeast2"
rrdatas = [module.payment_relay_secondary.lb_ip]
}
}
}
}
問題点分析(7点)
| # | 問題点 | 分類 | 改善方法 |
|---|---|---|---|
| 1 | Cloud Runが東京単一リージョンでSPOF | コンピュート層 | 大阪へのCloud Run複製(module化) |
| 2 | Cloud SQLがリージョン内HAのみ | データ層 | クロスリージョンリードレプリカ + 昇格手順 |
| 3 | DNS静的Aレコード・TTL24時間・手動 | トラフィック制御層 | primary_backupルーティング + TTL60秒 |
| 4 | フェイルオーバーが完全に人手依存 | 運用層 | 検知/通知は自動化・DB昇格は人間承認 |
| 5 | RPO/RTO未定義・DRドリル未実施 | ガバナンス | RPO5分/RTO15分明文化 + 四半期ドリル |
| 6 | Terraformがリージョンべた書き | 保守性 | region変数化module(ドリフト防止) |
| 7 | Secret Managerが東京単一user_managed | データ層 | replication { auto {} } |
DR構成図 — Bad vs Good の変換フロー
模範解答
resource "google_cloud_run_v2_service" "payment_relay" {
name = "payment-gateway-relay"
location = "asia-northeast1" # 問題①⑥
...
}
resource "google_sql_database_instance" "primary" {
region = "asia-northeast1"
settings { availability_type = "REGIONAL" } # 問題②
}
resource "google_dns_record_set" "payment_relay" {
ttl = 86400 # 問題③
rrdatas = ["203.0.113.10"] # 静的固定
}
# 問題④⑤: 自動検知・RPO/RTOなし
resource "google_secret_manager_secret" "payment_gateway_api_key" {
replication {
user_managed {
replicas { location = "asia-northeast1" } # 問題⑦
}
}
}
# ── modules/regional-service/variables.tf ──────────────────
variable "region" {
description = "デプロイ先リージョン(例: asia-northeast1, asia-northeast2)"
type = string
}
variable "project_id" { type = string }
variable "is_primary" {
description = "true: Cloud SQLのプライマリインスタンスを持つリージョンか"
type = bool
}
# ── modules/regional-service/main.tf ────────────────────────
# 修正①⑥: リージョンをmodule変数化し、東京・大阪を同一コードから展開
resource "google_cloud_run_v2_service" "payment_relay" {
project = var.project_id
name = "payment-gateway-relay"
location = var.region
template {
containers {
image = "asia-northeast1-docker.pkg.dev/${var.project_id}/mops/payment-gateway-relay:latest"
}
}
}
# ── ルート main.tf ───────────────────────────────────────────
module "payment_relay_primary" {
source = "./modules/regional-service"
project_id = var.project_id
region = "asia-northeast1"
is_primary = true
}
module "payment_relay_secondary" {
source = "./modules/regional-service"
project_id = var.project_id
region = "asia-northeast2"
is_primary = false
}
# ── Cloud SQL: プライマリ + クロスリージョンリードレプリカ(修正②)
resource "google_sql_database_instance" "primary" {
project = var.project_id
name = "payment-relay-db-primary"
region = "asia-northeast1"
database_version = "POSTGRES_16"
settings {
tier = "db-custom-4-16384"
availability_type = "REGIONAL"
backup_configuration {
enabled = true
point_in_time_recovery_enabled = true
}
}
deletion_protection = true
}
resource "google_sql_database_instance" "replica_osaka" {
project = var.project_id
name = "payment-relay-db-replica-osaka"
region = "asia-northeast2"
database_version = "POSTGRES_16"
master_instance_name = google_sql_database_instance.primary.name
replica_configuration {
failover_target = false # 手動承認による昇格運用(修正④と対応)
}
settings { tier = "db-custom-4-16384" }
}
# 修正②: レプリケーション遅延の可視化(RPO判断の根拠データ)
resource "google_monitoring_alert_policy" "replica_lag" {
project = var.project_id
display_name = "payment-relay-db 大阪レプリカ遅延超過"
combiner = "OR"
conditions {
display_name = "replication_lag > 300秒(RPO 5分を超過)"
condition_threshold {
filter = "resource.type=\"cloudsql_database\" AND metric.type=\"cloudsql.googleapis.com/database/replication/replica_lag\""
comparison = "COMPARISON_GT"
threshold_value = 300
duration = "60s"
}
}
notification_channels = [var.pagerduty_notification_channel_id]
}
# ── Cloud DNS: ヘルスチェック連動フェイルオーバー(修正③)────────
resource "google_compute_health_check" "payment_relay_tokyo" {
project = var.project_id
name = "payment-relay-tokyo-health"
check_interval_sec = 10
unhealthy_threshold = 3
https_health_check {
port = 443
request_path = "/healthz"
}
}
resource "google_dns_record_set" "payment_relay" {
project = var.project_id
managed_zone = var.dns_zone_name
name = "payment-relay.example.com."
type = "A"
ttl = 60 # 修正③: 86400秒 → 60秒
routing_policy {
primary_backup {
trickle_ratio = 0.0
primary {
internal_load_balancers {
load_balancer_type = "regionalL7ilb"
ip_address = module.payment_relay_primary.lb_ip
port = "443"
ip_protocol = "tcp"
network_url = var.network_self_link
project = var.project_id
region = "asia-northeast1"
}
}
backup_geo {
location = "asia-northeast2"
rrdatas = [module.payment_relay_secondary.lb_ip]
}
}
}
}
# ── 検知〜通知の自動化(修正④: DB昇格は人間承認)──────────────
resource "google_monitoring_alert_policy" "tokyo_region_down" {
project = var.project_id
display_name = "payment-relay 東京リージョン ヘルスチェック失敗"
combiner = "OR"
conditions {
display_name = "healthz 3回連続失敗"
condition_threshold {
filter = "resource.type=\"uptime_url\" AND resource.labels.host=\"payment-relay.example.com\""
comparison = "COMPARISON_GT"
threshold_value = 1
duration = "30s"
}
}
notification_channels = [
var.pagerduty_notification_channel_id,
google_monitoring_notification_channel.dr_runbook_trigger.id,
]
}
resource "google_monitoring_notification_channel" "dr_runbook_trigger" {
project = var.project_id
type = "pubsub"
display_name = "DR runbook trigger"
labels = { topic = google_pubsub_topic.dr_failover_trigger.id }
}
resource "google_pubsub_topic" "dr_failover_trigger" {
project = var.project_id
name = "payment-relay-dr-failover-trigger"
}
# ── Secret Manager: 自動レプリケーション(修正⑦)───────────────
resource "google_secret_manager_secret" "payment_gateway_api_key" {
project = var.project_id
secret_id = "payment-gateway-api-key"
replication {
auto {} # user_managed(東京単一)から自動レプリケーションへ変更
}
}
① RPO 5分 / RTO 15分 と明文化(修正⑤)
├─ RPO 5分: 大阪リードレプリカのレプリケーション遅延がこれを超えたらアラート
│ (google_monitoring_alert_policy.replica_lag)
└─ RTO 15分: ヘルスチェック失敗検知 → Pub/Sub通知 → 運用者承認 → レプリカ昇格
→ DNS切替反映(TTL60秒)までの合計時間の目標値
② 平時: 東京プライマリで通常稼働。大阪リードレプリカは読み取り専用で追従。
③ 障害検知: Cloud Monitoringのヘルスチェックが3回連続失敗
→ google_monitoring_alert_policy.tokyo_region_down が発火
→ Pub/SubトピックからDRランブック(承認待ちワークフロー)が自動起動(修正④)
④ 人間承認: オンコール担当者がPagerDuty通知を確認し、大阪レプリカの昇格を承認
(決済データを扱うため、DB昇格の最終実行だけは自動化せず人間の判断を残す)
⑤ 昇格実行: 承認後、大阪リードレプリカをプライマリに昇格
(gcloud sql instances promote-replica payment-relay-db-replica-osaka)
→ Cloud DNSのprimary_backupルーティングポリシーがヘルスチェック失敗を検知済みのため
自動的に大阪のCloud Runへトラフィックを切替
⑥ DRドリル: Cloud Schedulerで四半期ごとに①〜⑤をステージング環境で実地演習し、
実測RTOが15分の目標内に収まっているかを検証・記録する
| 問題 | 修正内容 | 効果 |
|---|---|---|
| ① Cloud Run単一リージョン | region変数化module + 東京/大阪展開 | リージョン障害でもコンピュート層は継続稼働 |
| ② Cloud SQLリージョン内HAのみ | クロスリージョンリードレプリカ + 昇格 | 東京全断でも大阪でデータ復旧可能 |
| ③ DNS静的Aレコード・TTL24時間 | primary_backupルーティング + TTL60秒 | ヘルスチェック連動で自動・高速に切替 |
| ④ フェイルオーバー完全手動 | 検知/通知自動化・DB昇格は人間承認 | 検知遅延を排除しつつ誤操作リスクも抑制 |
| ⑤ RPO/RTO未定義 | RPO5分/RTO15分明文化 + 四半期ドリル | DR設計の妥当性を数値で判断・検証可能に |
| ⑥ Terraformリージョンべた書き | region変数化module | 東京・大阪の構成ドリフトを構造的に防止 |
| ⑦ Secret Manager東京単一 | replication auto{} | 大阪からも低レイテンシ・高可用にアクセス |
ポイント解説
region を変数化したmoduleに切り出すことで、両リージョンの構成が常に同一であることをコード構造で保証できる。実務への応用
決済連携サービスに限らず、注文確定・在庫引当のような「止まると売上に直結するクリティカルパス」上のサービスから優先的にマルチリージョン化を検討するのが投資対効果として合理的。逆にキャンペーンバナー配信やレコメンド表示のような「一時的に落ちても致命傷にならない」サービスまで同じRTO 15分の基準を適用するのはオーバーエンジニアリングであり、サービスごとにRPO/RTOの目標値を分けて管理する(決済系: RTO 15分、レコメンド系: RTO 数時間でも許容、等)ことが実務上のコスト最適化につながる。
今日のまとめ
次のステップ
- 発展問題: 大阪リードレプリカを「読み取り専用の平常運用」でも活用する設計(例: 決済履歴照会APIの読み取りトラフィックを大阪リードレプリカに振り分け、平時から大阪リージョンの経路を"温めておく"ことで、いざという時の切替リスクを下げる Active-Active に近い構成)を検討せよ
- 参考: Cloud SQL クロスリージョンリードレプリカ / Cloud DNS ルーティングポリシー(primary-backup) / Cloud Monitoring アラートポリシー / Secret Manager 自動レプリケーション / Google Cloud Architecture Framework(信頼性の柱)