概要
Unit Economics でコスト構造を可視化する
「月額 $9,600 が高い」ではなく「1配信あたり $0.0004 のコストで $0.06 の GMV を生み出している」と Unit Economics に変換することで、削減余地とビジネスへの影響を定量的に判断できる。Unit Margin が 99.3%なら「コスト削減よりも配信数拡大の方が高インパクト」という議論も生まれる。
Cloud Run の min-instances=0 でアイドルコストを撲滅する
min_instance_count=4 + cpu_idle=false は常時4インスタンスが稼働し、バースト時間8.3%に対してアイドル時間91.7%にコストが発生する。min_instance_count=0 + cpu_idle=true でリクエスト処理時のみ課金され、アイドルコスト95%を削減できる。
Pub/Sub の再配信ループはコストを静かに膨張させる
デフォルトの ackDeadlineSeconds=10 で処理に20秒かかるメッセージは再配信ループに入り、実メッセージ数の 3.2倍が課金される。ackDeadlineSeconds=60 に延長するだけで月額 68.75%削減。0.5人日で $660/月の改善は ROI が最高の施策。
ROI を CFO・CTO・エンジニアの言語に変換する
「8.5日でペイバック、年間 ROI 4,207%」という数字は CFO が拒否できない投資案件になる。施策を ROI 降順に並べ「どれから始めるか」を優先順位付きで提案することが、エンジニアリングリードとしての価値を示す最短ルート。
問題
ECサイト MOps チームの Cloud Run v2(配信 API)+ BigQuery(集計)+ Pub/Sub(トリガー) 基盤で、過去6ヶ月のインフラコストが 1.8倍に膨張(GMV 成長は 1.2倍)した。CFO から「コスト増加率がビジネス成長を上回っている」と指摘された。Unit Economics を軸にコスト構造を分解し、3ヶ月以内に Unit Cost を 40%削減する施策を立案・試算せよ。
現状コスト内訳(月額 $9,600)
| リソース | 月額コスト | 月間配信量 | 備考 |
|---|---|---|---|
| Cloud Run v2(配信 API) | $3,840 | 2,400万通 | min-instances=4・CPU always-on |
| BigQuery(集計・分析クエリ) | $2,880 | — | パーティション未設定・フルスキャン |
| Pub/Sub(メッセージング) | $960 | 2,400万メッセージ | ackDeadline 10秒・再配信ループあり |
| Cloud Storage | $720 | — | 削除ポリシーなし |
| Artifact Registry | $480 | — | 世代管理なし・蓄積 |
| Cloud Monitoring / Logging | $720 | — | デフォルト設定・不要ログ多数 |
| 合計 | $9,600 | 2,400万通/月 |
制約・前提条件
- チーム: エンジニア2名(工数上限: 10人日)
- 単価: エンジニア 6,000円/時間、1 USD = 150円
- 配信品質(到達率・遅延)への影響ゼロが必須
- 追加調査: Cloud Run は1日4バースト × 各30分のみ実行(残り22時間はアイドル)
- BigQuery: 平均スキャン量 800GB/クエリ × 720回/月
- Pub/Sub: 処理に平均20秒かかるため ackDeadline 10秒で再配信ループ発生(3.2倍課金)
コスト構造 (Before) — 3つの設計上の問題
# 問題①: Cloud Run min-instances=4 + CPU always-on
resource "google_cloud_run_v2_service" "delivery_api_bad" {
template {
scaling {
min_instance_count = 4 # ← 常時4インスタンス稼働
max_instance_count = 20
}
containers {
resources {
cpu_idle = false # ← CPU always-on(アイドルでも課金)
}
}
}
}
# 実行時間: 4回 × 30分 × 30日 = 60h/月(8.3%)
# アイドル時間: 720h - 60h = 660h/月(91.7%が無駄)
# コスト: $3,840/月 → 実質 $320/月分の稼働
# 問題②: BigQuery パーティション未設定でフルスキャン
SELECT campaign_id, SUM(revenue) AS total
FROM `project.mops_dataset.email_events` # ← 800GB フルスキャン
WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY 1
# コスト: $5/TB × 800GB × 720回/月 = $2,880/月
# 7日分しか使わないのに 180日分をスキャン
# 問題③: Pub/Sub ackDeadline 10秒(デフォルト)
# メッセージ処理に20秒かかる → 10秒でタイムアウト → 再配信ループ
# 実メッセージ: 2,400万通
# 課金対象: 2,400万 × 3.2倍 = 7,680万メッセージ = $960/月
min_instance_count=0 + cpu_idle=true に変更することでリクエスト処理時のみ課金され $192/月まで削減可能created_at にパーティション設定なし。直近7日分(24GB)を取得するために 180日分(800GB)をスキャン。partition_by(event_date, 'date') + cluster_by を設定することで月額 97%削減($2,880 → $86.4)ackDeadlineSeconds=10 で処理が間に合わず再配信。実メッセージ 2,400万通が 7,680万メッセージ課金になっている。ackDeadlineSeconds=60 に延長(0.5人日)で $660/月削減・工数対効果 ROI 最高ヒント(段階的開示)
ヒント1 — 方向性
ヒント2 — アプローチ
- Cloud Run削減: min-instances=4 → min-instances=0 + cpu_idle=true。アイドルコスト $3,520/月 → $0。実行コストのみ約 $192/月
- BigQuery削減: partition_by(event_date) 設定後、7日分スキャンに絞ると 800GB → 24GB(97%削減)= $2,793.6/月削減
- Pub/Sub削減: ackDeadlineSeconds=60 に延長で再配信ループ解消。3.2倍課金 → 1.0倍 = $660/月削減
- Unit Economics 再計算: 施策後の月額 $2,138 ÷ 2,400万通 = $0.0000891/通(77.7%削減)
ヒント3 — 試算の骨格
【Cloud Run: min-instances=0 試算】
現状: $3,840/月(91.7%アイドル)
改善後リクエスト課金:
2,400万リクエスト × 0.1vCPU-s/リクエスト = 240万 vCPU-s
240万 vCPU-s × $0.0000240 ≈ $192/月
削減: $3,648/月(95%削減)
【BigQuery パーティション試算】
現状: $5/TB × 800GB/回 × 720回/月 = $2,880/月
改善後: $5/TB × 24GB/回 × 720回 = $86.4/月
削減: $2,793.6/月(97%削減)
【Pub/Sub ackDeadline 試算】
現状: $960/月(3.2倍ループ)
改善後: $960 / 3.2 = $300/月
削減: $660/月
ROI 試算:
実施コスト: 6.5人日 × 8h × 6,000円 = 312,000円
月次削減: $7,462 × 150円 = 1,119,300円/月
ペイバック: 312,000 / 1,119,300 = 8.5日
問題点分析(3点)
| # | 問題点 | 分類 | 改善方法 |
|---|---|---|---|
| 1 | Cloud Run min-instances=4 + cpu_idle=false(アイドル91.7%) | コスト | min_instance_count=0 + cpu_idle=true + startup_cpu_boost=true |
| 2 | BigQuery パーティション未設定(800GB フルスキャン) | コスト/性能 | partition_by(event_date) + cluster_by([campaign_id, segment_id]) + 7日ルックバック |
| 3 | Pub/Sub ackDeadlineSeconds=10(処理20秒で再配信ループ 3.2倍課金) | コスト/信頼性 | ackDeadlineSeconds=60 + modifyAckDeadline で長時間処理に対応 |
コスト構造図 — Bad vs Good + Unit Economics 分解
Unit Economics 分解 — 施策前後の比較
模範解答
【施策前の Unit Economics】
月次コスト: $9,600
月間配信数: 2,400万通
Unit Cost: $9,600 ÷ 24,000,000 = $0.0004/通(0.06円/通)
月次 GMV: $1,440,000(1,440万ドル)
GMV/通: $1,440,000 ÷ 24,000,000 = $0.06/通(9円/通)
Unit Margin: $0.06 - $0.0004 = $0.0596/通
Margin Rate: $0.0596 / $0.06 = 99.3%
【コスト構造の問題点】
GMV は 1.2倍成長、コストは 1.8倍成長
→ コスト増加率がビジネス成長を 1.5倍上回っている
→ Unit Cost が 1.8倍/1.2倍 = 1.5倍に悪化
コスト比率の分解:
Cloud Run: $3,840 / $9,600 = 40% ← 最大・アイドルコストが主因
BigQuery: $2,880 / $9,600 = 30% ← 2番目・フルスキャンが主因
Pub/Sub: $960 / $9,600 = 10% ← 再配信ループが主因
Storage: $720 / $9,600 = 7.5%
Monitoring: $720 / $9,600 = 7.5%
Artifact: $480 / $9,600 = 5%
【施策後の Unit Economics(目標: Unit Cost 40%削減)】
月次コスト: $2,138/月(77.7%削減 → 目標 40%削減を大幅超過)
Unit Cost: $2,138 ÷ 24,000,000 = $0.0000891/通(77.7%削減)
Unit Margin: $0.06 - $0.0000891 = $0.0599/通
Margin Rate: 99.85%(施策前 99.3% → 0.55pt 改善)
# terraform/cloud_run_delivery.tf
# Bad: min-instances=4 で CPU always-on(91.7%がアイドル)
resource "google_cloud_run_v2_service" "delivery_api_bad" {
template {
scaling {
min_instance_count = 4 # ← 常時4インスタンス稼働・アイドルでも課金
max_instance_count = 20
}
containers {
resources {
cpu_idle = false # ← CPU always-on(リクエストなしでも課金)
}
}
}
}
# Good: min-instances=0 + cpu_idle=true でアイドルコストを撲滅
resource "google_cloud_run_v2_service" "delivery_api_good" {
name = "mops-delivery-api"
location = "asia-northeast1"
template {
scaling {
min_instance_count = 0 # アイドル時はインスタンスゼロ(コスト $0)
max_instance_count = 50 # バースト時に自動スケール
}
containers {
image = "asia-northeast1-docker.pkg.dev/${var.project_id}/mops-images/delivery-api:latest"
resources {
limits = {
cpu = "2"
memory = "1Gi"
}
cpu_idle = true # リクエスト処理時のみ CPU 課金(← 重要)
startup_cpu_boost = true # コールドスタート高速化(初回レイテンシ短縮)
}
}
}
}
コールドスタート対策: コネクションプール再利用
# application/main.py
# コールドスタート対策: グローバル変数で接続プールを使い回す
# Cloud Run では同一インスタンスが複数リクエストを処理するため、
# リクエストごとに接続を張り直すのは最も多いアンチパターン
from google.cloud import bigquery
from google.cloud import pubsub_v1
# モジュールレベルで初期化(コールドスタート時のみ実行)
_bq_client: bigquery.Client | None = None
_publisher: pubsub_v1.PublisherClient | None = None
def get_bq_client() -> bigquery.Client:
"""シングルトンパターン: コールドスタート後は既存接続を再利用"""
global _bq_client
if _bq_client is None:
_bq_client = bigquery.Client() # 接続初期化は1回のみ
return _bq_client
def get_publisher() -> pubsub_v1.PublisherClient:
global _publisher
if _publisher is None:
_publisher = pubsub_v1.PublisherClient()
return _publisher
削減試算
現状: $3,840/月(min-instances=4 × CPU always-on)
- アイドル時間: 91.7%(660h/月)= $3,520 が無駄
- 実行時間: 8.3%(60h/月)= $320 が実質稼働分
改善後(リクエスト課金のみ):
- アイドルコスト: $0(min-instances=0)
- 実行コスト:
2,400万リクエスト × 0.1vCPU-s/リクエスト = 240万 vCPU-s
240万 vCPU-s × $0.0000240/vCPU-s ≈ $192/月
削減額: $3,840 - $192 = $3,648/月(95%削減)
工数: 2人日
削減額/工数: $1,824/人日
-- Bad: パーティション未設定でフルスキャン(800GB/クエリ)
SELECT
campaign_id,
DATE(created_at) AS event_date,
COUNT(*) AS send_count,
COUNTIF(opened) AS open_count
FROM `project.mops_dataset.email_events` -- フルスキャン確定!
WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY 1, 2
-- Good: パーティション + クラスタリング設定済みモデル(24GB/クエリ)
-- models/mart/mart_email_delivery_kpi.sql
{{
config(
materialized='incremental',
incremental_strategy='insert_overwrite',
partition_by={
'field': 'event_date',
'data_type': 'date',
'granularity': 'day'
},
cluster_by=['campaign_id', 'segment_id'], -- よく使うフィルタカラム
on_schema_change='fail'
)
}}
WITH events AS (
SELECT
DATE(created_at) AS event_date, -- パーティションキー
campaign_id,
COALESCE(segment_id, 'unknown') AS segment_id, -- NULL 保護
email_id,
opened,
clicked,
converted,
revenue_amount
FROM {{ ref('stg_email_events') }}
{% if is_incremental() %}
-- パーティション剪定: 直近7日のみスキャン(800GB → 24GB)
WHERE DATE(created_at) >= DATE_SUB(
(SELECT MAX(event_date) FROM {{ this }}),
INTERVAL 7 DAY
)
{% endif %}
)
SELECT
event_date,
campaign_id,
segment_id,
COUNT(DISTINCT email_id) AS send_count,
COUNTIF(opened) AS open_count,
COUNTIF(clicked) AS click_count,
COUNTIF(converted) AS conversion_count,
COALESCE(SUM(revenue_amount), 0) AS total_revenue,
-- KPI 指標(SAFE_DIVIDE でゼロ除算防止)
SAFE_DIVIDE(COUNTIF(opened), COUNT(DISTINCT email_id)) AS open_rate,
SAFE_DIVIDE(COUNTIF(clicked), COUNTIF(opened)) AS ctr,
SAFE_DIVIDE(COUNTIF(converted), COUNTIF(clicked)) AS cvr,
CURRENT_TIMESTAMP() AS _loaded_at
FROM events
GROUP BY event_date, campaign_id, segment_id
削減試算
現状: $5/TB × 800GB/クエリ × 720回/月 = $2,880/月
改善後(パーティション剪定: 7日分のみスキャン):
7日分スキャン: 800GB × (7/180) ≈ 31GB → 実績値 24GB/クエリ(97%削減)
改善後コスト: $5/TB × 24GB/クエリ × 720回/月 = $86.4/月
削減額: $2,880 - $86.4 = $2,793.6/月(97%削減)
工数: 3人日
削減額/工数: $931/人日
"""Pub/Sub ackDeadline 延長 + modifyAckDeadline リース管理パターン"""
import threading
from google.cloud import pubsub_v1
# Terraform で subscription を管理する場合:
# resource "google_pubsub_subscription" "delivery_sub" {
# ack_deadline_seconds = 60 # 20秒処理 → 60秒(3倍の余裕)
# message_retention_duration = "86400s"
# retry_policy {
# minimum_backoff = "10s"
# maximum_backoff = "600s"
# }
# }
ACK_DEADLINE_SECONDS: int = 60 # 名前付き定数(マジックナンバー禁止)
EXTEND_INTERVAL_SECONDS: int = 30 # 期限の半分で延長
def process_message_with_lease_management(
message: pubsub_v1.types.ReceivedMessage,
subscriber: pubsub_v1.SubscriberClient,
subscription_path: str,
) -> None:
"""
長時間処理でも確実に ack するリース管理パターン。
処理時間が ackDeadline を超える可能性がある場合は
modifyAckDeadline で延長する。
"""
stop_event = threading.Event()
def extend_ack_deadline() -> None:
"""バックグラウンドスレッドで定期的に ackDeadline を延長する"""
while not stop_event.wait(timeout=EXTEND_INTERVAL_SECONDS):
subscriber.modify_ack_deadline(
request={
"subscription": subscription_path,
"ack_ids": [message.ack_id],
"ack_deadline_seconds": ACK_DEADLINE_SECONDS,
}
)
extender = threading.Thread(target=extend_ack_deadline, daemon=True)
extender.start()
try:
# メイン処理(平均20秒かかる配信処理)
_deliver_emails(message.data) # type: ignore[name-defined]
subscriber.acknowledge(
request={
"subscription": subscription_path,
"ack_ids": [message.ack_id],
}
)
finally:
stop_event.set() # 延長スレッドを停止
削減試算
現状: 3.2倍再配信ループ
実メッセージ: 2,400万通
課金対象: 2,400万 × 3.2 = 7,680万メッセージ
月額: $960/月
改善後(ackDeadline=60秒で再配信なし):
課金対象: 2,400万メッセージ(1.0倍)
月額: $960 / 3.2 = $300/月
削減額: $660/月(68.75%削減)
工数: 0.5人日
削減額/工数: $1,320/人日 ← ROI 最高(Phase 1 に先着手)
施策別 ROI サマリー(優先順位順)
| 施策 | 削減額/月 | 工数 | 削減額/人日 | 優先度 | Phase |
|---|---|---|---|---|---|
| Pub/Sub ackDeadline 延長 | $660 | 0.5人日 | $1,320 | ★★★★★ | Phase 1 |
| Cloud Run min-instances=0 | $3,648 | 2人日 | $1,824 | ★★★★★ | Phase 1 |
| BigQuery パーティション設定 | $2,794 | 3人日 | $931 | ★★★★☆ | Phase 2 |
| Monitoring ログフィルタ | $360 | 1人日 | $360 | ★★★☆☆ | Phase 2 |
| 合計 | $7,462 | 6.5人日 | $1,148(平均) | — | — |
ROI 試算(全ステークホルダー向け)
【実施コスト】
6.5人日 × 8時間 × 6,000円/時間 = 312,000円(≈ $2,080)
【月次削減額】
$7,462/月 × 150円/$ = 1,119,300円/月
【ペイバック期間】
312,000円 ÷ 1,119,300円/月 = 0.279ヶ月 ≈ 8.5日
【年間 ROI】
年間削減額: $7,462 × 12 = $89,544(≈ 1,343万円)
年間 ROI: ($89,544 - $2,080) / $2,080 = 4,207%
【ステークホルダー別の伝え方】
CFO 向け: 「実施コスト $2,080 で年間 $89,544 の削減。ROI 4,207%。
リスクフリーレートの 400倍以上の投資効果。」
CTO 向け: 「6.5人日(約1.5スプリント)で月額 77.7%削減。
ペイバック 8.5日。エンジニア2名で完遂可能。」
エンジニア向け: 「ackDeadline を 10秒→60秒に変えるだけ(0.5人日)で
月 $660 削減。Terraform 1行変更で完了。」
【目標達成確認】
目標: Unit Cost 40%削減
実績: Unit Cost 77.7%削減 → 目標を 37.7pt 超過達成
リスクと対策
| リスク | 影響 | 対策 |
|---|---|---|
| Cloud Run コールドスタート増加 | 最初のリクエストの配信遅延 | startup_cpu_boost=true + Cloud Monitoring Uptime Check(30秒間隔)でウォームアップ維持 |
| BigQuery dbt モデル移行時のデータ欠損 | KPI 数値の不整合 | --full-refresh 実行前に dbt test でデータ品質確認 + 移行後24時間は旧テーブルと並行監視 |
| Pub/Sub ackDeadline 延長で重複配信増加 | メール重複送信(スパム判定リスク) | 冪等性キー(email_id + campaign_id)を BigQuery に保存し、配信前に重複チェック |
ポイント解説
「月額 $9,600 が高い」という絶対額でなく「1配信あたり $0.0004 のコストで $0.06 の GMV を生み出している」と Unit Economics に変換することで、削減余地(Unit Cost を下げる)とスケール戦略(配信数を増やして規模の経済を活かす)の2軸で議論できる。CFO が指摘した「GMV 1.2倍成長・コスト 1.8倍成長」は Unit Cost が 1.5倍悪化していることを意味し、放置するとスケールするほど Margin が圧迫される構造問題になる。
「Pub/Sub ackDeadline 0.5人日で $660削減(ROI $1,320/人日)」は Cloud Run の2人日・BigQuery の3人日より ROI が高い。先に着手することでチームの信頼を得られ、大型施策(BigQuery 3人日)への承認を取りやすくなる。コスト削減提案では「どの施策が ROI 最大か」を定量的に示すことが経営判断を加速させる核心スキル。
cpu_idle=true(CPU throttling)は「リクエスト処理中のみ CPU が課金対象」になる。min_instance_count=0 との組み合わせでアイドルコストをゼロにできる。常時稼働サービス(WebSocket、ストリーミング)には向かないが、バースト型の配信 API には最適。startup_cpu_boost=true を設定することでコールドスタート時の CPU を一時的にブーストし、初回レイテンシの影響を最小化できる。
デフォルトの
ackDeadlineSeconds=10 は多くの実務シナリオで短すぎる。処理時間の3倍程度(余裕係数)を設定し、長時間処理は modifyAckDeadline でリース延長する。再配信ループは Cloud Monitoring → Pub/Sub → subscription/num_undelivered_messages メトリクスの異常な増加で検知できる。冪等性キーによる重複配信チェックは ackDeadline 延長と必ずセットで実装する。
「年間 ROI 4,207%(CFO 向け)」「8.5日でペイバック(CTO 向け)」「Terraform 1行変更(エンジニア向け)」と変換することで意思決定のスピードが上がる。証券マンの視点では「ペイバック 8.5日」は投資として圧倒的な指標であり、CFO が承認しない理由がない。コスト削減提案では必ず ROI とペイバック期間をセットで提示する習慣が、エンジニアリングリードとしての信頼を高める。
実務への応用
- Cloud Run v2 の min-instances と CPU throttling: 現職の配信 API に今すぐ適用可能。
terraform planで変更内容を確認してからapplyし、Cloud Monitoring でリクエスト数・レイテンシを1時間監視してからアイドルコストの削減を確認する。コールドスタートが問題になった場合はmin_instance_count=1に戻すことも検討する。 - Pub/Sub の ackDeadline 問題の発見方法:
gcloud pubsub subscriptions describe <name>で現在の設定を確認。Cloud Monitoring → Pub/Sub → subscription/num_undelivered_messagesが急増していたら再配信ループのサイン。DataDog でもgcp.pubsub.subscription.num_undelivered_messagesメトリクスを監視できる。 - 証券マン視点での意思決定: 「8.5日ペイバック」は株式投資で言えば年率 4,207% の IRR。リスクフリーレートの 400倍以上。CFO が承認しない理由がない数字。コスト削減提案は必ず ROI とペイバック期間をセットで出す。「エンジニアがコストを下げた」ではなく「$2,080 の投資で年間 $89,544 のリターンを生み出した」という言語に変換する。
- BigQuery コスト監視の自動化:
INFORMATION_SCHEMA.JOBSから「スキャン量 TOP 10 クエリ」を日次集計して Slack に投稿する Argo ジョブを追加すると、コスト膨張の早期警戒システムになる。total_bytes_processedカラムを集計し、前日比 20%以上増加した場合にアラートを出す設計が実用的。
今日のまとめ
min-instances=0(アイドルコスト95%削減)と Pub/Sub の ackDeadline=60秒(再配信ループ解消)という「工数対削減額 ROI 最大」の施策を Phase 1 に置くことで、6.5人日で月額 77.7%削減・年間 ROI 4,207%・ペイバック 8.5日という CFO が拒否できない経営インパクトを出せる。エンジニアリングリードの核心スキルは「技術的に正しいこと」だけでなく「ROI を CFO・CTO・エンジニアそれぞれの言語に変換して意思決定を加速させること」であり、Unit Economics × ROI 降順での施策優先順位付けがその実践的なアプローチ。