B システム設計/インフラ — Cloud Armor(OWASP CRS + レート制限)× Cloud CDN TTL/cache_key_policy × Workload Identity Federation KSA→GSA × 最小権限 IAM × PodDisruptionBudget × securityContext(Restricted)× KEDA Pub/Sub スケーラー(MOps CDN バックエンド Bad→Good)

2026-07-07 (Day 96) 火曜 B: システム設計/インフラ ★★★★☆ Cloud Armor / Cloud CDN / WIF GKE Autopilot / KEDA / PDB / securityContext

概要

🛡️

Cloud Armor でエッジセキュリティを多層防御

Cloud Armor セキュリティポリシーを google_compute_backend_service にアタッチすることで、OWASP CRS(XSS・SQLi)ルールとレート制限をエッジで適用できる。priority 番号が小さいほど先評価されるため、拒否ルールを 1000 番台、レート制限を 2000 番台、デフォルト許可を最大値(2147483647)に配置する。最初は preview: true で False Positive を確認してから本番適用するのが安全。

Cloud CDN TTL と cache_key_policy でオリジン負荷を削減

default_ttl = 3600(エッジキャッシュ1時間)と client_ttl = 300(ブラウザキャッシュ5分)を分けることで、キャンペーン終了後のパージが5分以内にエンドユーザーへ伝播する。request_coalescing = true でキャッシュミス時の同一オリジンへの重複リクエストを集約し、突発スパイク時のオリジン負荷を下げる。

🔐

WIF でキーレス認証・最小権限 IAM でリスク最小化

SA キーファイルを廃止し、KSA Annotation → GSA の WIF バインドで ADC が自動的に短命トークンを取得する。CDN キャッシュパージに必要な権限は roles/compute.admin ではなく roles/compute.viewer + roles/compute.networkAdmin の2ロールのみ。プロジェクトレベルではなく最小スコープで付与する。

📈

KEDA Pub/Sub スケーラーで事前スケールアウト

CPU ベースの HPA のみではキャンペーン公開イベントによる突発スパイクへの対応が遅い。KEDA の google-cloud-pubsub スケーラーを追加することで、Pub/Sub サブスクリプションの未処理メッセージ数に応じて事前スケールアウトし、CDN パージの処理遅延を最小化できる。KEDA は内部的に HPA を管理するため、minReplicaCount を HPA と揃えて競合を防ぐ。

問題

ECサイト MOps チームでは、キャンペーン画像・動画コンテンツの配信 CDN バックエンドを GKE Autopilot + Cloud Load Balancing + Cloud CDN で構築している。以下の Terraform コードと Kubernetes マニフェストには 7つの設計上の問題 が潜んでいる。問題点を全て洗い出し、Cloud Armor × Cloud CDN キャッシュポリシー × Workload Identity Federation × GKE Autopilot セキュリティ強化(securityContext / PodDisruptionBudget / HPA / KEDA) を考慮した Bad→Good リファクタリングを行え。

制約・前提条件

  • Terraform 1.8+(google プロバイダー 5.x)
  • GKE Autopilot 1.30+ クラスタ
  • Cloud Load Balancing(グローバル外部 HTTPS LB)経由で Cloud CDN を使用
  • Pub/Sub トリガーでキャンペーン公開イベントを受信し Cloud CDN キャッシュをパージ
  • サービスアカウントキーファイルを使った認証(WIF 未導入)
  • Cloud CDN パージ用の roles/compute.admin(過剰権限)
  • PodDisruptionBudget・securityContext・KEDA 未設定
期待する回答形式: 問題点の列挙(番号付き)+ 改善後 Terraform コード + 改善後 Kubernetes マニフェスト(YAML)+ 設計意図の説明

悪いコード (Before)

このコード・マニフェストには 7つの設計上の問題 が隠れています。見つけてみてください。
bad_cdn_infra.tf — Cloud Armor なし・過剰権限・SA キー・CDN TTL 未設定
# 問題①: Cloud Armor セキュリティポリシーなし
resource "google_compute_backend_service" "cdn_backend" {
  name     = "mops-cdn-backend"
  protocol = "HTTP"
  enable_cdn = true

  # 問題②: Cloud CDN キャッシュポリシー未設定(デフォルト TTL)
  cdn_policy {
    cache_mode = "CACHE_ALL_STATIC"
    # default_ttl / max_ttl / client_ttl / cache_key_policy なし
  }
  # security_policy なし → Cloud Armor 無防備
}

resource "google_service_account" "cdn_purge_sa" {
  account_id = "mops-cdn-purge"
}

# 問題④: roles/compute.admin(インスタンス削除まで可能な過剰権限)
resource "google_project_iam_member" "cdn_purge_admin" {
  project = var.project_id
  role    = "roles/compute.admin"
  member  = "serviceAccount:${google_service_account.cdn_purge_sa.email}"
}
# 問題③: WIF なし(SA キーファイル使用)
bad_cdn_worker.yaml — SA キー・securityContext なし・PDB なし・KEDA なし
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cdn-purge-worker
  namespace: mops
spec:
  replicas: 1  # 問題⑤: PDB なし → ドレイン時に全停止
  selector:
    matchLabels:
      app: cdn-purge-worker
  template:
    spec:
      # serviceAccountName なし → default SA 使用
      containers:
        - name: cdn-purge-worker
          image: asia-northeast1-docker.pkg.dev/PROJECT/mops/cdn-purge:latest
          env:
            - name: GOOGLE_APPLICATION_CREDENTIALS
              # 問題③: SA キーファイルをマウント
              value: "/secrets/sa-key.json"
          # 問題⑥: securityContext なし(root 実行・特権昇格可能)
          volumeMounts:
            - name: sa-key
              mountPath: /secrets
      volumes:
        - name: sa-key
          secret:
            secretName: cdn-purge-sa-key
---
# 問題⑦: KEDA なし(HPA の CPU ベースのみ)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: cdn-purge-worker-hpa
  namespace: mops
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: cdn-purge-worker
  minReplicas: 1
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          # 問題⑦: CPU 80% 閾値のみ(Pub/Sub キューが溜まっても反応遅い)
          averageUtilization: 80
問題点サマリー(7点)
1Cloud Armor 未設定 — バックエンドサービスにセキュリティポリシーなし。OWASP CRS + レート制限を google_compute_security_policy で定義しアタッチ
2Cloud CDN TTL/キャッシュポリシー未設定 — デフォルト TTL でキャンペーン終了後の反映が遅い。default_ttl / max_ttl / client_ttl / cache_key_policy を明示設定
3SA キーファイル使用/secrets/sa-key.json 漏洩リスク。WIF KSA → GSA バインドに変更しキーレス認証へ
4roles/compute.admin(過剰権限) — CDN パージに不要な権限。roles/compute.viewer + roles/compute.networkAdmin の2ロールに最小化
5PodDisruptionBudget 未設定 — ノードドレイン時に全 Pod 停止の可能性。minAvailable: 2 で保護
6securityContext 未設定 — root 実行・特権昇格・ファイルシステム書き込み可能。runAsNonRoot / readOnlyRootFilesystem / allowPrivilegeEscalation: false / seccompProfile: RuntimeDefault を設定
7HPA のみ(KEDA なし) — CPU ベーススケーリングでは Pub/Sub キュー増加に反応遅い。KEDA google-cloud-pubsub スケーラーで事前スケールアウト

ヒント(段階的開示)

ヒント1 — 方向性
Cloud Load Balancing + CDN + GKE Autopilot の問題は3層に分類できる。(1) セキュリティ/認証 — SA キーファイル・過剰 IAM 権限・Pod securityContext 未設定、(2) 可用性/耐障害性 — PDB 未設定・HPA minReplicas と PDB minAvailable の整合性、(3) CDN/スケーリング設計 — Cloud CDN キャッシュポリシー未設定・KEDA なし。Cloud Armor セキュリティポリシーは Terraform の google_compute_security_policy リソースで定義し、google_compute_backend_servicesecurity_policy 属性でアタッチする。
ヒント2 — アプローチ
  • 問題①: google_compute_security_policy で OWASP CRS(xss-stable, sqli-stable)+ レート制限ルールを定義。google_compute_backend_service.security_policy にアタッチ
  • 問題②: cdn_policy { default_ttl = 3600; max_ttl = 86400; client_ttl = 300; cache_key_policy { include_host = true; include_protocol = true; include_query_string = false } request_coalescing = true }
  • 問題③: KSA に iam.gke.io/gcp-service-account Annotation → google_service_account_iam_memberroles/iam.workloadIdentityUser をバインド
  • 問題④: roles/compute.admin を削除し roles/compute.viewer + roles/compute.networkAdmin を付与
  • 問題⑤: PodDisruptionBudget: minAvailable: 2 + HPA minReplicas: 2 に合わせる
  • 問題⑥: securityContext: { runAsNonRoot: true; runAsUser: 1000; readOnlyRootFilesystem: true; allowPrivilegeEscalation: false; capabilities: { drop: ["ALL"] }; seccompProfile: { type: RuntimeDefault } }
  • 問題⑦: KEDA ScaledObjecttype: google-cloud-pubsub; subscriptionName: campaign-publish-sub; mode: SubscriptionSize; value: "5" を設定
ヒント3 — コードの骨格
# Terraform: Cloud Armor スケルトン
resource "google_compute_security_policy" "cdn_armor" {
  rule {
    action   = "deny(403)"
    priority = 1000
    match { expr { expression = "evaluatePreconfiguredExpr('xss-stable')" } }
  }
  rule {
    action   = "throttle"
    priority = 2000
    match { versioned_expr = "SRC_IPS_V1"; config { src_ip_ranges = ["*"] } }
    rate_limit_options {
      conform_action = "allow"; exceed_action = "deny(429)"
      enforce_on_key = "IP"
      rate_limit_threshold { count = 100; interval_sec = 60 }
    }
  }
  rule { action = "allow"; priority = 2147483647; match { versioned_expr = "SRC_IPS_V1"; config { src_ip_ranges = ["*"] } } }
}

resource "google_compute_backend_service" "cdn_backend" {
  security_policy = google_compute_security_policy.cdn_armor.id  # 修正①
  cdn_policy {
    default_ttl = 3600; max_ttl = 86400; client_ttl = 300         # 修正②
    request_coalescing = true
    cache_key_policy { include_host = true; include_protocol = true }
  }
}

# KEDA ScaledObject スケルトン
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
spec:
  minReplicaCount: 2
  maxReplicaCount: 20
  triggers:
    - type: google-cloud-pubsub
      metadata:
        subscriptionName: campaign-publish-sub
        mode: SubscriptionSize
        value: "5"

問題点分析(7点)

#問題点分類改善方法
1Cloud Armor 未設定セキュリティgoogle_compute_security_policy(OWASP CRS + レート制限)+ security_policy アタッチ
2Cloud CDN TTL/キャッシュポリシー未設定パフォーマンスdefault_ttl=3600 / max_ttl=86400 / client_ttl=300 / request_coalescing=true
3SA キーファイル使用セキュリティWIF KSA → GSA バインド + ADC キーレス認証
4roles/compute.admin(過剰権限)セキュリティroles/compute.viewer + roles/compute.networkAdmin の2ロール
5PodDisruptionBudget 未設定信頼性minAvailable: 2 + HPA minReplicas: 2 の整合
6securityContext 未設定(root 実行可能)セキュリティrunAsNonRoot / readOnlyRootFilesystem / seccompProfile: RuntimeDefault
7HPA のみ(Pub/Sub キュー増加に反応遅い)スケーラビリティKEDA google-cloud-pubsub スケーラー追加(SubscriptionSize mode)

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

Bad(変更前)— 無防備・過剰権限・SA キー Internet → Cloud LB → Cloud CDN 問題①: Cloud Armor なし → DDoS / XSS / SQLi が素通り Cloud CDN Backend Service 問題②: CDN TTL 未設定 → デフォルト挙動(キャッシュ不整合) cache_key_policy なし / request_coalescing なし キャンペーン終了後のキャッシュ反映が遅い GKE Autopilot Pod (cdn-purge-worker) 問題③: /secrets/sa-key.json マウント → キー漏洩リスク 問題⑥: securityContext なし(root 実行・特権昇格可能) 問題⑤: PDB なし → ドレイン時に全 Pod 停止(サービス断) replicas: 1 → 単一障害点 IAM 設定(Bad) 問題④: roles/compute.admin(インスタンス削除まで可能) プロジェクトレベルで付与 → スコープ過剰 スケーリング(Bad) 問題⑦: HPA CPU 80% のみ → キャンペーン大量公開時に反応遅い Pub/Sub キューが溜まっても CPU が低いままだとスケールアウトしない × Cloud Armor なし → OWASP 脆弱性・DDoS に無防備 × SA キー漏洩 → 不正アクセスリスク × PDB なし → ノードアップグレード時にサービス断 Good(変更後)— 多層防御・WIF・PDB・KEDA Internet → Cloud Armor → Cloud LB → Cloud CDN 修正①: OWASP CRS(xss-stable / sqli-stable)+ レート制限 100req/60s priority 1000(XSS拒否)→ 2000(レート制限)→ 2147483647(許可) preview: true で False Positive 確認 → 本番適用(推奨フロー) Cloud CDN Backend Service(改善後) 修正②: default_ttl=3600 / max_ttl=86400 / client_ttl=300 request_coalescing=true / cache_key_policy(host+protocol、QS除外) キャンペーン終了後 5分以内にブラウザへ反映 / オリジン負荷削減 log_config: sample_rate=0.1 でコスト最適化しながら監視 GKE Autopilot Pod(改善後) 修正③: serviceAccountName: cdn-purge-worker(WIF/ADC キーレス認証) 修正⑥: runAsNonRoot / readOnlyRootFilesystem / seccompProfile: RuntimeDefault 修正⑥: capabilities.drop: ["ALL"] / allowPrivilegeEscalation: false topologySpreadConstraints で Pod を複数ノードに分散 readinessProbe / livenessProbe / tmpfs emptyDir マウント IAM + WIF(改善後) 修正④: roles/compute.viewer + roles/compute.networkAdmin(最小権限) 修正③: roles/iam.workloadIdentityUser で KSA → GSA バインド roles/pubsub.subscriber(キャンペーン公開 Sub 読み取りのみ) SA キーファイル廃止 → 自動ローテーション・漏洩リスクゼロ スケーリング(改善後): HPA + KEDA 修正⑤: PDB minAvailable: 2 / HPA minReplicas: 2(整合) 修正⑦: KEDA Pub/Sub SubscriptionSize / value: "5" / maxReplicas: 20 キャンペーン公開イベントでキュー増加 → 事前スケールアウト pollingInterval: 15s / scaleDown.stabilizationWindowSeconds: 300 ✓ Cloud Armor エッジ防御で OWASP 脆弱性を自動ブロック ✓ WIF でキーレス認証 → SA キー管理コストゼロ ✓ KEDA で Pub/Sub キュー深さ連動スケーリング → CDN パージ遅延ゼロ 修正

模範解答

Before — Cloud Armor なし・TTL 未設定・SA キー・過剰権限
# 問題①: Cloud Armor なし
resource "google_compute_backend_service" "cdn_backend" {
  enable_cdn = true
  cdn_policy {
    # 問題②: TTL / cache_key_policy 未設定
    cache_mode = "CACHE_ALL_STATIC"
  }
  # security_policy なし
}

# 問題④: roles/compute.admin(過剰権限)
resource "google_project_iam_member" "cdn_purge_admin" {
  role   = "roles/compute.admin"
  member = "serviceAccount:${google_service_account.cdn_purge_sa.email}"
}
# 問題③: WIF なし(SA キーファイル使用)
After — Cloud Armor + CDN TTL + WIF + 最小権限
# ── Cloud Armor セキュリティポリシー(修正①)──────────────────
resource "google_compute_security_policy" "cdn_armor" {
  name    = "cdn-armor-policy"
  project = var.project_id

  # OWASP CRS: XSS 防御(priority 小 = 先評価)
  rule {
    action      = "deny(403)"
    priority    = 1000
    description = "OWASP XSS ルール"
    match { expr { expression = "evaluatePreconfiguredExpr('xss-stable')" } }
  }

  # OWASP CRS: SQLi 防御
  rule {
    action      = "deny(403)"
    priority    = 1001
    description = "OWASP SQLi ルール"
    match { expr { expression = "evaluatePreconfiguredExpr('sqli-stable')" } }
  }

  # レート制限: IP あたり 100 req/60s
  rule {
    action      = "throttle"
    priority    = 2000
    description = "IP レート制限(100 req/60s → 429)"
    match {
      versioned_expr = "SRC_IPS_V1"
      config { src_ip_ranges = ["*"] }
    }
    rate_limit_options {
      conform_action = "allow"
      exceed_action  = "deny(429)"
      enforce_on_key = "IP"
      rate_limit_threshold { count = 100; interval_sec = 60 }
    }
  }

  # デフォルト: 許可
  rule {
    action   = "allow"
    priority = 2147483647
    match {
      versioned_expr = "SRC_IPS_V1"
      config { src_ip_ranges = ["*"] }
    }
  }
}

# ── バックエンドサービス(Cloud Armor + Cloud CDN)(修正①②)──
resource "google_compute_backend_service" "cdn_backend" {
  name                  = "mops-cdn-backend"
  project               = var.project_id
  protocol              = "HTTP"
  load_balancing_scheme = "EXTERNAL_MANAGED"
  timeout_sec           = 30

  # 修正①: Cloud Armor セキュリティポリシーをアタッチ
  security_policy = google_compute_security_policy.cdn_armor.id

  # 修正②: Cloud CDN キャッシュポリシーを詳細設定
  enable_cdn = true
  cdn_policy {
    cache_mode  = "CACHE_ALL_STATIC"
    default_ttl = 3600    # エッジキャッシュ: 1時間
    max_ttl     = 86400   # 最大 TTL: 24時間
    client_ttl  = 300     # ブラウザキャッシュ: 5分(パージ後5分で反映)

    cache_key_policy {
      include_host         = true
      include_protocol     = true
      include_query_string = false  # QS をキャッシュキーから除外
    }
    request_coalescing = true  # キャッシュミス時の重複リクエスト集約
  }

  log_config { enable = true; sample_rate = 0.1 }

  backend {
    group          = google_compute_network_endpoint_group.cdn_neg.id
    balancing_mode = "RATE"
    max_rate_per_endpoint = 100
  }
}

# ── Service Account(CDN パージ専用)──────────────────────
resource "google_service_account" "cdn_purge_sa" {
  account_id   = "mops-cdn-purge"
  display_name = "MOps CDN Cache Purge SA"
  project      = var.project_id
}

# 修正④: roles/compute.admin → 最小権限(2ロールのみ)
resource "google_project_iam_member" "cdn_purge_viewer" {
  project = var.project_id
  role    = "roles/compute.viewer"  # バックエンドサービス参照用
  member  = "serviceAccount:${google_service_account.cdn_purge_sa.email}"
}

resource "google_project_iam_member" "cdn_cache_invalidator" {
  project = var.project_id
  role    = "roles/compute.networkAdmin"  # キャッシュ無効化 API
  member  = "serviceAccount:${google_service_account.cdn_purge_sa.email}"
}

# 修正③: WIF — KSA → GSA バインド
resource "google_service_account_iam_member" "cdn_purge_wif" {
  service_account_id = google_service_account.cdn_purge_sa.name
  role               = "roles/iam.workloadIdentityUser"
  member = "serviceAccount:${var.project_id}.svc.id.goog[mops/cdn-purge-worker]"
}

# Pub/Sub 読み取り権限
resource "google_pubsub_subscription_iam_member" "cdn_purge_sub" {
  project      = var.project_id
  subscription = "campaign-publish-sub"
  role         = "roles/pubsub.subscriber"
  member       = "serviceAccount:${google_service_account.cdn_purge_sa.email}"
}
# ksa.yaml — KSA(WIF Annotation)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: cdn-purge-worker
  namespace: mops
  annotations:
    # 修正③: KSA → GSA バインド(SA キーファイル廃止)
    iam.gke.io/gcp-service-account: mops-cdn-purge@PROJECT_ID.iam.gserviceaccount.com
---
# deployment.yaml — CDN パージワーカー(改善後)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cdn-purge-worker
  namespace: mops
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cdn-purge-worker
  template:
    metadata:
      labels:
        app: cdn-purge-worker
    spec:
      # 修正③: 専用 KSA(ADC で自動 GSA 認証)
      serviceAccountName: cdn-purge-worker

      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: cdn-purge-worker

      containers:
        - name: cdn-purge-worker
          # digest ピン留め(latest タグ禁止)
          image: asia-northeast1-docker.pkg.dev/PROJECT_ID/mops/cdn-purge-worker@sha256:def456
          imagePullPolicy: IfNotPresent

          ports:
            - name: http
              containerPort: 8080

          env:
            - name: GOOGLE_CLOUD_PROJECT
              value: "PROJECT_ID"
            - name: PUBSUB_SUBSCRIPTION
              value: "campaign-publish-sub"
            # SA キーファイル環境変数は削除(ADC 使用)

          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              memory: "512Mi"

          # 修正⑥: securityContext(GKE Autopilot Restricted 準拠)
          securityContext:
            runAsNonRoot: true            # root 実行禁止
            runAsUser: 1000
            readOnlyRootFilesystem: true  # ルート FS 読み取り専用
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]               # 全 Linux Capabilities 削除
            seccompProfile:
              type: RuntimeDefault        # デフォルト seccomp プロファイル

          readinessProbe:
            httpGet: { path: /healthz, port: 8080 }
            initialDelaySeconds: 5
            periodSeconds: 10

          livenessProbe:
            httpGet: { path: /healthz, port: 8080 }
            initialDelaySeconds: 15
            periodSeconds: 30

          volumeMounts:
            - name: tmp
              mountPath: /tmp

      volumes:
        - name: tmp
          emptyDir: {}
          # SA キーファイル Secret マウントを削除

---
# pdb.yaml — PodDisruptionBudget(修正⑤)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: cdn-purge-worker-pdb
  namespace: mops
spec:
  # ノードドレイン / アップグレード時も最低 2 Pod を維持
  minAvailable: 2
  selector:
    matchLabels:
      app: cdn-purge-worker

---
# hpa.yaml — HPA(CPU + Memory、KEDA と共存)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: cdn-purge-worker-hpa
  namespace: mops
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: cdn-purge-worker
  # PDB minAvailable と揃える
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60  # 80% → 60% でスパイク余裕確保
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300  # スケールイン慎重に(5分待機)

---
# keda-scaler.yaml — KEDA ScaledObject: Pub/Sub(修正⑦)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: cdn-purge-worker-scaler
  namespace: mops
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: cdn-purge-worker
  # HPA minReplicas と揃える(競合防止)
  minReplicaCount: 2
  maxReplicaCount: 20
  pollingInterval: 15    # 15秒ごとにキュー深さチェック
  cooldownPeriod: 60
  triggers:
    - type: google-cloud-pubsub
      metadata:
        # キャンペーン公開イベントの Pub/Sub サブスクリプション
        subscriptionName: campaign-publish-sub
        mode: SubscriptionSize   # 未処理メッセージ数でスケール
        value: "5"               # メッセージ 5件/Pod の比率
      authenticationRef:
        name: keda-gcp-auth

---
# keda-auth.yaml — KEDA GCP WIF 認証
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: keda-gcp-auth
  namespace: mops
spec:
  podIdentity:
    provider: gcp-workload-identity
    identityId: mops-cdn-purge@PROJECT_ID.iam.gserviceaccount.com
問題修正内容効果
① Cloud Armor なしgoogle_compute_security_policy(OWASP CRS + レート制限)XSS / SQLi / DDoS をエッジで自動ブロック
② CDN TTL 未設定default_ttl=3600 / client_ttl=300 / request_coalescing=trueキャンペーン終了後5分以内に反映 / オリジン負荷削減
③ SA キーファイルWIF KSA → GSA バインド + ADC キーレス認証キー漏洩リスクゼロ・自動ローテーション
④ compute.admincompute.viewer + compute.networkAdmin の2ロールインスタンス削除・設定変更権限を除去
⑤ PDB なしminAvailable: 2 + HPA minReplicas: 2 の整合ノードドレイン時もサービス継続保証
⑥ securityContext なしrunAsNonRoot / readOnlyRootFilesystem / seccompProfile: RuntimeDefaultGKE Autopilot Restricted 準拠・特権昇格防止
⑦ HPA のみKEDA Pub/Sub SubscriptionSize スケーラー追加キャンペーン大量公開時に CPU 反応前に事前スケールアウト

ポイント解説

1Cloud Armor の priority 設計 — 番号が小さいほど先評価。高精度な拒否ルール(OWASP CRS)を 1000 番台、レート制限を 2000 番台、デフォルト許可を最大値(2147483647)に配置。最初は preview: true で False Positive を確認してから deny(403) に切り替えるのが本番安全フロー。
2Cloud CDN の client_ttldefault_ttl の使い分けdefault_ttl = 3600 はエッジ(CDN PoP)のキャッシュ保持時間。client_ttl = 300 はブラウザ側のキャッシュ保持時間(Cache-Control: max-age=300 として配信)。キャンペーン終了後の CDN パージはエッジキャッシュを即時削除するが、ブラウザ側は client_ttl 経過後に再取得するため、5分以内に全エンドユーザーへ反映される。
3WIF の仕組みと ADC 認証フロー — Pod 起動時に GKE が自動的に Projected Service Account Token を /var/run/secrets/kubernetes.io/serviceaccount/token に注入。ADC(google.auth.default())はこのトークンを Google Security Token Service に送り、短命な GSA 有効期限付きアクセストークンを取得する。SA キーよりも短命(1時間)で自動ローテーションされるためセキュリティが高い。
4PDB と HPA minReplicas の整合性PDB minAvailable: 2 に対して HPA minReplicas: 1 にすると、HPA がスケールダウンして Pod 数が 1 の状態でノードドレインが発生すると PDB 違反になりドレインがブロックされる(クラスタアップグレードが詰まる)。minReplicas >= minAvailable + 1 を意識するか、少なくとも minReplicas = minAvailable に揃える。
5GKE Autopilot の securityContext Restricted 要件 — GKE Autopilot はデフォルトで Pod Security Standards の Baseline を適用。Restricted(最も厳格)に準拠するには runAsNonRoot: true, seccompProfile: RuntimeDefault, capabilities.drop: ["ALL"], allowPrivilegeEscalation: false が必要。readOnlyRootFilesystem: true は一時書き込みが必要なディレクトリに emptyDir を使うことで対応。
6KEDA ScaledObject と HPA の共存設定 — KEDA の ScaledObject は内部的に HorizontalPodAutoscaler リソースを作成・管理する。独立した HorizontalPodAutoscaler が同じ Deployment を参照していると競合が発生するため注意。KEDA の minReplicaCount / maxReplicaCount を HPA の設定と揃えて管理する。pollingInterval: 15 で15秒ごとにサブスクリプションサイズをチェックし、CPU より先に反応してスケールアウトできる。

実務への応用

Cloud Armor の OWASP CRS を有効にする際は、最初に preview: true で Cloud Logging にルール評価結果を記録し、jsonPayload.enforcedSecurityPolicy.outcome = "DENY" を Logs Explorer でフィルタして False Positive がないか確認する。ECサイトのキャンペーン LP は JavaScript の難読化コードが XSS ルールに引っかかるケースがあるため、ホワイトリストを evaluatePreconfiguredWafRules() で調整する。

KEDA の google-cloud-pubsub スケーラーを使うと、年末セールなどで数千件のキャンペーンが一斉公開されたとき、Pub/Sub に積み上がったメッセージ数に応じて自動的に Pod が 2 → 20 に増え、CDN パージが並列処理される。CPU が低いままでもキューが増えれば即座にスケールアウトするため、処理遅延による「パージ未完了でキャッシュが古いまま」の問題を防ぐ。

Argo Workflows のキャンペーンパイプラインでは、最終ステップで gcloud pubsub topics publish を実行し、KEDA トリガーが CDN パージワーカーをスケールアウトするアーキテクチャが自然。パイプライン完了 → CDN パージ → ユーザーに最新コンテンツが届くまでのレイテンシを DataDog カスタムメトリクス cdn.purge.latency_ms で計測し SLO を設定する。

Cloud CDN キャッシュパージの権限: roles/compute.networkAdmin は本来ネットワーク全般の管理権限で若干スコープが広い。本番では google_project_iam_custom_rolecompute.urlMaps.invalidateCache のみ持つカスタムロールを作成して最小権限を徹底するとよい。

今日のまとめ

Cloud LB + CDN + GKE Autopilot バックエンドの7点チェックリスト: ① Cloud Armor(OWASP CRS + レート制限、preview で確認後適用)② CDN TTL(default_ttl/max_ttl/client_ttl + request_coalescing)③ WIF KSA → GSA バインド(SA キー廃止)④ 最小権限 IAM(compute.viewer + networkAdmin)⑤ PDB minAvailable ≥ HPA minReplicas ⑥ securityContext(runAsNonRoot + readOnlyRootFilesystem + seccompProfile)⑦ KEDA Pub/Sub スケーラー(CPU より先に反応)。 Cloud Armor は preview モードで False Positive を確認してから本番適用し、KEDA と HPA の minReplicas を揃えてスケーリング競合を防ぐことが実務のポイント。

次のステップ

  • 発展問題: Cloud Armor の適応型保護(Adaptive Protection)を有効にして、自動的に DDoS 攻撃パターンを検出するルールを動的に追加する設定を Terraform で実装する。また、Cloud Armor の WAF ルールと Firebase App Check を組み合わせたモバイルアプリ向け Bot 防御設計を考える
  • 参考: Cloud Armor ドキュメント(OWASP CRS・適応型保護) / KEDA GCP Pub/Sub スケーラー / GKE Pod Security Standards(Restricted)/ Cloud CDN キャッシュポリシー設定ガイド

自己評価(あとで記入)