概要
BigQuery: スキャン量課金を正しく理解する
BigQuery On-Demand は スキャン量 $5/TB。パーティション未設定 + SELECT * の組み合わせは最悪。partition_by(ingestion_time または DATE 列)+cluster_by で WHERE 句による partition pruning が効き、スキャン量を 80〜95% 削減できる。SELECT * の廃止は列プルーニングで効果が倍増する。
Cloud Run: CPU idle vs CPU always-on
cpu_idle = true(旧 cpu = "requests")はリクエスト処理中のみ CPU を課金。min-instances > 0 の場合はコンテナは起動し続けるが、アイドル中は CPU 無課金。always-on(cpu = "cpu")はアイドル中も CPU 課金発生。深夜 13 時間アイドルがある MOps 配信 API では 65% 削減が見込める。
GKE: Spot + Cluster Autoscaler でバッチコストを最小化
Spot インスタンスは On-Demand 比最大 91% 安価。バッチジョブは中断許容度が高く Spot との親和性が高い。Cluster Autoscaler でバッチ時間帯のみノードをスケールアップし、アイドル時間帯はゼロに。Argo Workflows の retryStrategy で中断時の自動リトライを設計する。
CFO 言語: コスト削減を投資案件として提案する
エンジニアは「技術的最適化」で語りがちだが、CFO には ROI・ペイバック・年間キャッシュフロー改善額 で話す。「$7,500 の投資で $58,440 のリターン、ペイバック 46 日」は証券マン時代の投資案件分析と同じ論理構造。コスト削減 = 利益改善(EBITDA 直撃)として位置づける。
問題
ECサイト MOps チームのシニアエンジニアとして、四半期末のクラウドコストレビューで CFO から「Q2 の GCP 請求額が予算比 38% 超過している。原因と削減策をエンジニア視点で提示してほしい」と依頼を受けた。コスト最適化 Bad パターン(6点)を洗い出し、削減後の月次コスト試算、ROI・ペイバック期間の計算、CFO への投資提案まで一気通貫で回答せよ。
Q2 GCP コスト内訳(月次平均、予算比 +38%)
| サービス | 現状コスト/月 | 問題の有無 |
|---|---|---|
| BigQuery(スキャン課金) | $4,800 | ??? |
| Cloud Run(キャンペーン配信 API) | $1,240 | ??? |
| GKE ノード(バッチ処理) | $2,160 | ??? |
| Cloud Storage(ログ・メディア) | $890 | ??? |
| Pub/Sub + Cloud Functions | $340 | ??? |
| Cloud Armor + Load Balancer | $280 | 問題なし |
| 合計 | $9,710/月 | 予算: $7,040/月(+38%) |
制約・前提条件
- 削減施策の実装工数は最大 2 スプリント(4 週間)以内
- 可用性・SLO は維持すること(エラー率 1.5% 以下 / P99 レイテンシ 500ms 以下)
- 削減効果は月次コスト削減額($)と削減率(%)で示す
- ROI = 年間削減額 ÷ 実装コスト(エンジニア 1名 × 月額 $15,000 換算)
- ペイバック = 実装コスト ÷ 月次削減額(月数)
現状インフラ構成 (Before)
# BigQuery: パーティション未設定 / クラスタリングなし
# テーブル全スキャン で日次集計クエリを実行中
# 1クエリあたり平均 800GB スキャン × 20クエリ/日
# 問題①: partition_by なし → WHERE 句が効かずフルスキャン
# 問題②: SELECT * → 不要列まで全スキャン(全クエリの43%)
# Cloud Run: CPU always-on 設定(min-instances: 3)
# 実際のリクエスト処理は営業時間(9:00-20:00)のみ
# 深夜帯(20:00-9:00, 13時間)はほぼ idle
# 問題③: cpu always-on → 深夜アイドル 13時間も CPU 課金
# GKE: n2-standard-8 × 5台(On-Demand)
# バッチ処理は 04:00-08:00 の4時間のみ実行
# 残り 20時間はアイドル(Cluster Autoscaler なし)
# 問題④: On-Demand 常時稼働 → アイドル 20時間分浪費
# Cloud Storage: 全データを Standard で保存
# ログ: 90日以降も Standard のまま
# 画像: 配信後 30日以降はほぼアクセスなし
# 問題⑤: ライフサイクルルール未設定 → 全 Standard 課金
# Cloud Functions: メモリ 2GB × 最大 100インスタンス
# 実際の最大同時実行数: 過去3ヶ月で 12インスタンス
# 問題⑥: リソース上限が実績の8倍以上 → 過剰課金
ヒント(段階的開示)
ヒント1 — 方向性
ヒント2 — 課金モデルと削減方法
- BigQuery: スキャン量 $5/TB。800GB × 20 × 30日 = 480TB/月 → $2,400。partition_by + cluster_by で 80% 削減 → -$1,920
- Cloud Run: cpu_idle で深夜アイドル CPU 課金ゼロ。min-instances: 3 → 1 で常時起動コンテナも削減 → $1,240 の 65% 削減
- GKE: Spot は On-Demand 比最大 91% 安。Cluster Autoscaler でアイドル時ゼロスケール → $2,160 の 70% 削減
- Cloud Storage: Nearline $0.01/GB(Standard の 50%)、Coldline $0.004/GB(80% 安)。ライフサイクルルールで自動移行
- Cloud Functions: メモリ 2GB → 512MB(課金 75% 削減)、max-instances 100 → 20(実績 12 の 1.67倍で安全マージン確保)
ヒント3 — 削減額の骨格とROI計算
BigQuery スキャン最適化: $4,800 → $960 = -$3,840(-80%)
Cloud Run CPU idle: $1,240 → $434 = -$806(-65%)
GKE Spot + Autoscaler: $2,160 → $648 = -$1,512(-70%)
Cloud Storage Lifecycle: $890 → $445 = -$445(-50%)
Cloud Functions 最適化: $340 → $153 = -$187(-55%)
────────────────────────────────────────────────────
合計削減額: -$4,870/月(-50%)
最適化後合計: $4,840/月
実装コスト: 1名 × 0.5ヶ月 = $7,500
ROI = $4,870 × 12 ÷ $7,500 = 779%
ペイバック = $7,500 ÷ $4,870 = 1.54ヶ月(約46日)
問題点分析(6点)
| # | 問題点 | 分類 | 削減施策 | 削減額 |
|---|---|---|---|---|
| 1 | BigQuery パーティション未設定 → フルスキャン 480TB/月 | スキャン最適化 | partition_by + cluster_by + WHERE pruning | -$3,840(-80%) |
| 2 | SELECT * 多用(43%)→ 不要列もフルスキャン | スキャン最適化 | 必要列のみ SELECT(施策1に統合) | (施策1に含む) |
| 3 | Cloud Run CPU always-on → 深夜 13h アイドル課金 | 時間課金最適化 | cpu_idle=true + min-instances: 1 | -$806(-65%) |
| 4 | GKE On-Demand 5台 × 24h → アイドル 20h/日 | コンピュート最適化 | Spot + Cluster Autoscaler | -$1,512(-70%) |
| 5 | Cloud Storage ライフサイクル未設定 → 全 Standard | ストレージ最適化 | Nearline 30d / Coldline 90d ライフサイクル | -$445(-50%) |
| 6 | Cloud Functions 2GB × 100インスタンス(実績 12) | リソース最適化 | 512MB / max-instances: 20 | -$187(-55%) |
GCP コスト削減フロー — Bad vs Good(SVG)
| 指標 | 数値 |
|---|---|
| 月次削減額 | $4,870/月(-50%) |
| 年間削減額 | $58,440/年 |
| 実装コスト | $7,500(1名 × 0.5ヶ月) |
| ROI | 779%(= $58,440 ÷ $7,500) |
| ペイバック期間 | 1.54 ヶ月(約 46 日) |
模範解答
## 問題点の列挙(6点)
### 問題①: BigQuery パーティション未設定 → フルスキャン
Bad: テーブル全体をスキャン / 800GB × 20クエリ/日
理由: partition_by がないと WHERE 句が効かずフルスキャン
480TB/月 × $5/TB = $2,400/月 の不要コスト
Fix: partition_by(ingestion_time or DATE 列) + cluster_by(member_rank, campaign_id)
→ 80GB/クエリ(-90%)→ -$1,920/月
### 問題②: SELECT * 多用(全クエリの 43%)
Bad: 不要列も全スキャン
理由: SELECT * は列プルーニングが効かない
Fix: SELECT 必要列のみ記述(施策①と組み合わせて効果倍増)
### 問題③: Cloud Run CPU always-on(深夜アイドル 13h/日)
Bad: cpu="cpu"(always-on)+ min-instances: 3
理由: アイドル中も CPU 課金。13時間/日 × 3インスタンスが無駄
Fix: cpu_idle=true + min-instances: 1 → -$806/月(-65%)
### 問題④: GKE On-Demand × 5台 × 24時間稼働
Bad: n2-standard-8 × 5台 On-Demand / Cluster Autoscaler なし
理由: バッチ使用は 4時間/日のみ。残り 20時間 × 5台が無駄
Fix: Spot + Cluster Autoscaler(04-08時のみスケールアップ)→ -$1,512/月(-70%)
### 問題⑤: Cloud Storage ライフサイクル未設定
Bad: 全データ Standard($0.02/GB/月)
理由: ログ・画像の古いデータが Standard に残存
Fix: Nearline(30日後 $0.01/GB)→ Coldline(90日後 $0.004/GB)
→ -$445/月(-50%)
### 問題⑥: Cloud Functions 過剰リソース設定(実績の 8倍)
Bad: メモリ 2GB × max-instances 100(実績 max 12)
理由: メモリ × 実行時間 × インスタンス数で課金。全て過剰
Fix: 512MB / max-instances: 20(実績 12の 1.67倍でマージン確保)
→ -$187/月(-55%)
### ボーナス問題⑥+: コスト可視化・アラートなし
Bad: 予算超過が四半期末の CFO レビューまで未発覚
Fix: Cloud Billing Budget アラート(80%・100%・120%)
+ BigQuery INFORMATION_SCHEMA.JOBS 日次スキャン量 Top10 監視
# BigQuery コスト最適化: dbt モデル変更例
# ── Bad: パーティション・クラスタリングなし ──────────────────
# models/mart/campaign_summary.sql(Bad)
{{
config(
materialized='table'
# partition_by なし → 全スキャン確定
)
}}
SELECT
* # 問題②: SELECT * で不要列もスキャン
FROM {{ ref('stg_campaigns') }} c
JOIN {{ ref('stg_orders') }} o ON c.campaign_id = o.campaign_id
WHERE c.status = 'active' # partition pruning が効かない
# ── Good: partition_by + cluster_by + 必要列 SELECT ─────────
# models/mart/campaign_summary.sql(Good)
{{
config(
materialized='incremental',
# 修正①: ingestion_time パーティションで partition pruning を有効化
partition_by={
"field": "report_date",
"data_type": "date",
"granularity": "day"
},
# cluster_by で同一パーティション内をさらに絞る
cluster_by=["member_rank", "campaign_id"],
on_schema_change='fail'
)
}}
SELECT
-- 修正②: 必要列のみ明示的に SELECT(列プルーニング有効)
c.campaign_id,
c.campaign_name,
c.member_rank,
DATE(o.created_at) AS report_date, -- partition 列
COUNT(o.order_id) AS order_count,
SUM(o.amount) AS total_amount,
AVG(o.amount) AS avg_order_value
FROM {{ ref('stg_campaigns') }} c
JOIN {{ ref('stg_orders') }} o
ON c.campaign_id = o.campaign_id
-- partition pruning: WHERE に partition 列(report_date)を指定
AND DATE(o.created_at) >= DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)
WHERE c.status = 'active'
AND c.member_rank IN ('platinum', 'gold', 'silver', 'bronze')
{% if is_incremental() %}
-- incremental: 過去 3日分のみ再計算(フルスキャン回避)
AND DATE(o.created_at) >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
{% endif %}
# スキャン量変化:
# Before: 800GB/クエリ × 20クエリ/日 × 30日 = 480TB/月 → $2,400
# After: 80GB/クエリ × 20クエリ/日 × 30日 = 48TB/月 → $240(-90%)
# SELECT * 廃止でさらに 30〜50% 削減可能
# INFORMATION_SCHEMA 監視クエリ(日次実行)
SELECT
user_email,
query,
total_bytes_processed / POW(1024, 3) AS gb_scanned, -- GB 換算
total_bytes_processed / POW(1024, 4) * 5 AS cost_usd -- $5/TB
FROM `region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE DATE(creation_time) = CURRENT_DATE()
AND statement_type = 'SELECT'
ORDER BY total_bytes_processed DESC
LIMIT 10 -- Top 10 スキャン量クエリを監視
# ── Cloud Run: CPU idle 化 ────────────────────────────────
# terraform/cloud_run.tf(Bad → Good)
# Bad: CPU always-on
resource "google_cloud_run_v2_service" "campaign_api_bad" {
template {
containers {
resources {
cpu_idle = false # always-on: アイドル中も CPU 課金
}
}
scaling {
min_instance_count = 3 # 常時 3台起動
max_instance_count = 20
}
}
}
# Good: CPU idle + min-instances: 1
resource "google_cloud_run_v2_service" "campaign_api" {
template {
containers {
resources {
limits = { cpu = "2", memory = "1Gi" }
cpu_idle = true # 修正③: アイドル中 CPU 無課金
startup_cpu_boost = true # 起動時のみ CPU ブースト(レイテンシ改善)
}
}
scaling {
min_instance_count = 1 # コールドスタート防止に 1台のみ常時起動
max_instance_count = 20
}
}
}
# ── GKE: Spot + Cluster Autoscaler ──────────────────────────
# terraform/gke.tf(Bad → Good)
# Bad: On-Demand 固定ノードプール
resource "google_container_node_pool" "batch_bad" {
node_count = 5 # 固定 5台
node_config {
machine_type = "n2-standard-8"
# Spot なし / On-Demand
}
}
# Good: Spot + Cluster Autoscaler
resource "google_container_node_pool" "batch_spot" {
# 修正④: Cluster Autoscaler でバッチ時間帯のみスケール
autoscaling {
min_node_count = 0 # アイドル時はゼロ台
max_node_count = 8 # バッチ最大 8台(余裕)
}
node_config {
machine_type = "n2-standard-8"
spot = true # Spot インスタンス(最大 91% 安)
# Spot 中断時の taint で Argo Workflows が retryStrategy でリカバリ
taint {
key = "cloud.google.com/gke-spot"
value = "true"
effect = "NO_SCHEDULE"
}
}
}
# Argo Workflows: Spot 中断対応
# workflow.yaml
# retryStrategy:
# limit: 3
# retryPolicy: OnError # Spot 中断(preemption)時も再試行
# backoff:
# duration: "30s"
# factor: 2
# ── Cloud Storage: ライフサイクルルール ──────────────────────
# terraform/storage.tf
resource "google_storage_bucket" "mops_logs" {
name = "mops-logs-prod"
location = "asia-northeast1"
# 修正⑤: ライフサイクルルールで自動ダウングレード
lifecycle_rule {
condition { age = 30 } # 30日後: Standard → Nearline
action { type = "SetStorageClass"; storage_class = "NEARLINE" }
}
lifecycle_rule {
condition { age = 90 } # 90日後: Nearline → Coldline
action { type = "SetStorageClass"; storage_class = "COLDLINE" }
}
lifecycle_rule {
condition { age = 365 } # 1年後: Coldline → 削除(ログのみ)
action { type = "Delete" }
}
}
# Cloud Functions: メモリ・インスタンス最適化
resource "google_cloudfunctions2_function" "event_processor" {
build_config { ... }
service_config {
# 修正⑥: 2GB → 512MB、max-instances 100 → 20
available_memory = "512Mi" # 実績ピーク: 320MB / マージン 60%
max_instance_count = 20 # 実績最大 12 の 1.67倍
min_instance_count = 0 # コスト最小化(コールドスタート許容)
}
}
## ROI・ペイバック計算
### 削減施策サマリー
┌─────────────────────────────────────┬──────────┬──────────┬──────────┬──────────┐
│ 施策 │ 現状/月 │ 最適化後 │ 削減額 │ 削減率 │
├─────────────────────────────────────┼──────────┼──────────┼──────────┼──────────┤
│ ① BigQuery partition_by + SELECT最適化│ $4,800 │ $960 │ -$3,840 │ -80% │
│ ② Cloud Run cpu_idle + min-inst: 1 │ $1,240 │ $434 │ -$806 │ -65% │
│ ③ GKE Spot + Cluster Autoscaler │ $2,160 │ $648 │ -$1,512 │ -70% │
│ ④ Cloud Storage Lifecycle │ $890 │ $445 │ -$445 │ -50% │
│ ⑤ Cloud Functions 512MB / max: 20 │ $340 │ $153 │ -$187 │ -55% │
│ ⑥ Cloud Armor + LB(変更なし) │ $280 │ $280 │ $0 │ 0% │
├─────────────────────────────────────┼──────────┼──────────┼──────────┼──────────┤
│ 合計 │ $9,710 │ $4,840 │ -$4,870 │ -50% │
└─────────────────────────────────────┴──────────┴──────────┴──────────┴──────────┘
### 実装コスト内訳
BigQuery 最適化(dbt config + クエリ書き換え): 3日
Cloud Run Terraform 変更 + カナリア検証: 1日
GKE Spot + Autoscaler + Argo retry 設定: 4日
Cloud Storage ライフサイクル設定: 1日
Cloud Functions リソース変更 + 負荷テスト: 1日
Cloud Billing Budget アラート設定: 0.5日
─────────────────────────────────────────────────
合計: 10.5日 ≒ 0.5ヶ月(エンジニア 1名)
実装コスト: $15,000 × 0.5 = $7,500
### ROI 計算(12ヶ月ベース)
年間削減額 = $4,870/月 × 12 = $58,440
ROI = $58,440 ÷ $7,500 = 7.79 = 779%
ペイバック = $7,500 ÷ $4,870 = 1.54ヶ月(約 46日)
### 優先順位(ICE ライクスコア)
施策 | Impact | Ease | Priority
BigQuery 最適化 | 高 | 中 | 最優先(削減額最大)
GKE Spot | 高 | 中 | 次点(工数中)
Cloud Run idle | 中 | 高 | 即実施(設定変更のみ)
Storage LF | 中 | 高 | 即実施(Terraform 変更)
Functions | 低 | 高 | 最後(削減額最小)
CFO 提案文(3文以内)
Q2 GCP 費用 $9,710/月(予算比 +38%)の主因は、BigQuery 全スキャン・GKE 常時起動・Cloud Run CPU アイドル課金の 3 点で、これらは構成変更で即座に解消できます。2 スプリント(実装コスト $7,500)で月次 $4,870(-50%)の削減が見込まれ、年間 $58,440 のコスト圧縮・ROI 779%・ペイバック 46 日を達成できます。あわせて Cloud Billing Budget アラートを設定し、予算超過を月次レビュー前に検知できる体制を整備します。
提案構造(エンジニア → CFO の変換ロジック)
| エンジニア言語 | CFO 言語 |
|---|---|
| BigQuery partition_by 設定 | スキャンコスト 80% 削減 → 年間 $46,080 節約 |
| GKE Spot + Autoscaler | コンピュートコスト 70% 削減 → 年間 $18,144 節約 |
| Cloud Run cpu_idle | アイドル課金ゼロ → 年間 $9,672 節約 |
| 実装工数 0.5ヶ月 | 投資額 $7,500(1回限り) |
| 月次削減 $4,870 | ROI 779% / ペイバック 46日 / EBITDA 直接改善 |
ポイント解説
スキャン削減はクエリチューニングより
partition_by(ingestion_time / DATE 列)設定が即効性が高い。WHERE 句に partition 列を含めると partition pruning が働き、対象 partition のみスキャンされる。cluster_by は同一 partition 内をさらに絞る(BigQuery の block pruning)。SELECT * の廃止は列プルーニングで効果が倍増。800GB → 80GB/クエリ(-90%)は現実的な削減幅。
cpu_idle = true はリクエスト処理中のみ CPU を課金。false(always-on)はアイドル中も CPU 課金発生。min-instances > 0 のコンテナは常時起動するが、cpu_idle ならアイドル中は CPU 無課金。コールドスタートの影響は startup_cpu_boost = true(起動時一時 CPU ブースト)で軽減できる。カナリアデプロイで P99 レイテンシへの影響を必ず確認する。
Spot インスタンスは最大 91% 安価だが、中断時(preemption)最大 60 秒で停止する。バッチジョブは中断許容度が高く Spot との親和性が最も高いワークロード。Argo Workflows の
retryStrategy.limit: 3 + retryPolicy: OnError で中断時の自動リトライを設計する。Cluster Autoscaler の min_node_count: 0 で非バッチ時間帯のノードをゼロにし、アイドル課金を完全に排除できる。
ライフサイクルルールは設定後 24 時間以内に反映。データが蓄積するほど削減効果が増加する(複利的に効く)。ストレージクラスのコスト: Standard $0.02/GB、Nearline $0.01/GB(-50%)、Coldline $0.004/GB(-80%)、Archive $0.0012/GB(-94%)。Coldline は読み取りコスト($0.01/GB)が Standard より高いため、アクセス頻度が確実にゼロになるデータのみ対象とする。
エンジニアはコスト削減を「技術的な最適化」として語りがちだが、CFO には 投資案件 として提示する。「$7,500 の投資で $58,440 のリターン、ペイバック 46 日」は証券マン時代の投資分析と同じ論理構造。コスト削減は EBITDA を直接改善する。営業利益率 5% の EC 会社では「$58,440 のコスト削減 = $1,168,800 の売上増加と同等のインパクト」という換算も有効。
今回のように四半期末に初めて予算超過が発覚するのは可視化の失敗。対策は 3 層: ① Cloud Billing Budget アラート(80%・100%・120% でアラート通知)、② BigQuery INFORMATION_SCHEMA.JOBS の日次スキャン量 Top 10 モニタリング、③ DataDog へのコストメトリクス転送(GCP コスト指標をエンジニア向けダッシュボードに統合)。
gcloud billing budgets create --billing-account=ACCOUNT_ID --display-name="GCP Monthly Budget" --budget-amount=7040USD --threshold-rule=percent=0.8 --threshold-rule=percent=1.0 --threshold-rule=percent=1.2
実務への応用
- BigQuery コスト削減の dbt 実装:
dbtのconfig(partition_by={"field": "report_date"}, cluster_by=["member_rank", "campaign_id"])を既存モデルに追加するだけで partition pruning が有効化。既存クエリの SELECT * はdbt-utils.star()マクロで除外列指定も可能。dbt Unit Tests(1.8+)で集計精度を検証してからリリース。 - Cloud Run のカナリアデプロイ確認: cpu_idle 変更後は traffic split(10% カナリア → 100% 切り替え)で P99 レイテンシへの影響を確認。DataDog の
p99(trace.web.request.duration)メトリクスを 2 スプリント間監視してから全切り替え。 - GKE Spot バッチの Argo 設定: Argo CronWorkflow に
tolerations: [{key: "cloud.google.com/gke-spot", value: "true", effect: "NoSchedule"}]を追加。Spot 中断時のretryStrategy.limit: 3で自動リトライ。04:00-08:00 の時間帯に Node のスケールアップを先行させるPodDisruptionBudgetも合わせて設定。 - CFO への月次コストレポート: Cloud Billing エクスポート(BigQuery)から Looker Studio で「サービス別コスト推移・予算比・前月差」ダッシュボードを自動生成。月次 CFO レビューでは必ず ROI ベースの投資対効果を報告し、コスト削減施策を「投資案件」として継続的に提案する習慣をつける。