付録 B:コスト見積もりワークシートを作る ── CUR と Application Inference Profile で実測する
なぜワークシートを別建てで用意するのか
ch15「コストを設計する」で、Bedrock のコスト最適化の 戦略 を見た。Intelligent Prompt Routing、Prompt Caching、Batch Inference、Provisioned Throughput(PT)── どれをいつ使うか、損益分岐はどこか、までは押さえた。
だが、実務では 戦略の前にもう一つの壁 が立ちはだかる。「結局、自分のアプリでは月いくらかかるのか」 という壁だ。
この壁は、AWS Pricing Calculator を開いて単価を眺めても越えられない。理由はシンプルで、Bedrock のコストは「叩いてみないと分からない」ものが多すぎるからだ。
- 入出力比率 はプロンプト設計と読者の質問内容に依存する
max_tokensの設定 1 つで TPM 消費量は数十倍変わる- Prompt Cache のヒット率 は実トラフィックを流すまで見えない
- Guardrails の text units はリクエスト長と有効化したフィルター数で線形に増える
- Knowledge Bases は OpenSearch・S3・埋め込みモデルの 3 重課金になる
本付録では、PoC を本番に乗せる前に「いくらかかるか」を 実測ベースで 計算するためのワークシートを提供する。helpdesk-ai を題材に、月 1 万・10 万・100 万リクエストの 3 シナリオで試算もする。
読み方:ch15 が「どう減らすか」、本付録が「いくらかを測るか」。順序としては 本付録が先 だが、ch15 を読んだ後の方がワークシートの行の意味が立体的に見えるはず。
1. 見積もりに必要な要素マップ
最初に、何を数えれば月額がわかるのか、要素を全部洗い出す。helpdesk-ai のような RAG + Agents 構成のアプリでは、以下の 7 系統が登場する。
mindmap
root((Bedrock 月額))
モデル推論
Input トークン × Input 単価
Output トークン × Output 単価
Prompt Cache Write
Prompt Cache Read
Guardrails
Content filters
Denied topics
PII filters
Contextual grounding
Knowledge Bases
Embedding 生成
ベクトルストア
Rerank API
Agents / Action Groups
Lambda 実行
Lambda 課金
Tool 呼び出し回数
ストレージ
S3 KB ドキュメント
DynamoDB セッション
CloudWatch Logs
データ転送
Cross-Region
VPC Endpoint
周辺
Intelligent Prompt Routing
Prompt Optimization
このマップにある全要素を計算しないと「PoC は月 5,000 円でしたが本番は月 580 万円でした」が起きる。逆に言えば、この要素マップを潰せば、見積もりの精度は劇的に上がる。
特に新規開発者が忘れがちなのは次の 3 つだ。
- Knowledge Bases のストレージ系:OpenSearch Serverless の OCU 課金(最低 2 OCU 常時起動)、S3 のリクエスト課金、埋め込みモデルのインデックス時実行コスト
- CloudWatch Logs:Model Invocation Logging を有効化すると、入力プロンプトと出力テキストが丸ごとログに残る。テキスト系ログは GB あたり $0.50 程度で、数千万トークン流れる本番では月数万円〜数十万円の規模になり得る
- Cross-Region Inference のデータ転送費自体は 0。だが、呼び出し元(Lambda などのコンピュート)と Bedrock リージョンが異なる場合の Lambda 側のレイテンシ増 → タイムアウト → リトライ で間接的にコストが膨らむ
2. 「平均」ではなく「P95」で見積もる
ch15 でも触れたが、見積もりで最も多い失敗は 平均トークン数で計算する ことだ。これをやると、ほぼ確実に本番請求は見積もりの 2〜5 倍になる。
理由は スパイク にある。LLM のトークン消費は強い裾の重い分布(long-tail)を描く。helpdesk-ai を例にすると、
- 多数の質問:「経費精算の上限は?」 → 入力 200 トークン / 出力 100 トークン
- 少数の質問:「先月の経費精算ログを全件取ってきて理由別に集計して」 → 入力 8,000 トークン / 出力 3,500 トークン
平均すれば「入力 400 / 出力 200」程度に見える。だが、リクエストの P95(上位 5%)が全体トークン消費の 50% 以上を占める ことが珍しくない。月額を支配しているのはこのスパイクだ。
推奨ルール
- 入力/出力トークンは P95 ベース で見積もる
- リクエスト数は ピーク日(金曜の昼休みなど)× 30 日 で計算する
- 月間トークン量は 平均値の 2〜3 倍 をバッファとして上乗せする
「平均で見積もって、想定の 3 倍払う」より「P95 で見積もって、想定通り払う」の方が、コスト管理としては圧倒的に健全だ。
3. Application Inference Profile + tag による実測
P95 で見積もると言っても、その P95 値そのものをどうやって測るか。答えは Application Inference Profile(AIP) だ。
AIP はモデル ID を独自にラップしたプロファイルで、tag を付与できる。プロファイル自体に追加課金はない。Cost Explorer や Cost and Usage Report(CUR)から、tag 単位で利用量とコストを分離可視化できる。
helpdesk-ai では、たとえば次のような tag を付ける。
tenant=acme(マルチテナント運用なら、テナント別の請求按分)feature=chat/feature=summary/feature=rag(機能別の利用量把握)env=prod/env=staging(環境別)
AIP の作成(TypeScript / AWS SDK v3)
import {
BedrockClient,
CreateInferenceProfileCommand,
} from "@aws-sdk/client-bedrock";
const bedrockMgmt = new BedrockClient({
region: process.env.AWS_REGION ?? "us-east-1",
maxAttempts: 5,
retryMode: "adaptive",
});
export async function createChatProfile(): Promise<string> {
// 既存の Cross-Region Inference Profile (us.anthropic.claude-sonnet-4-5)
// を ARN で参照して、ラップする Application Inference Profile を作る
const sourceArn =
"arn:aws:bedrock:us-east-1::inference-profile/us.anthropic.claude-sonnet-4-5";
const res = await bedrockMgmt.send(
new CreateInferenceProfileCommand({
inferenceProfileName: "helpdesk-ai-chat-prod",
description: "helpdesk-ai のチャット用 AIP(prod)",
modelSource: {
copyFrom: sourceArn,
},
tags: [
{ key: "tenant", value: "acme" },
{ key: "feature", value: "chat" },
{ key: "env", value: "prod" },
],
}),
);
if (!res.inferenceProfileArn) {
throw new Error("AIP の作成に失敗");
}
return res.inferenceProfileArn;
}
作成した AIP の ARN を、Converse API の modelId に渡せばよい。これで CUR の lineItem/ResourceId にプロファイル ARN が、resourceTags/user:feature に chat が乗ってくる。
tag を必ず付けるための仕組み化
開発者の善意に任せると、tag は 必ず 付け忘れる。IAM ポリシーで強制する。
{
"Effect": "Deny",
"Action": "bedrock:CreateInferenceProfile",
"Resource": "*",
"Condition": {
"Null": {
"aws:RequestTag/feature": "true"
}
}
}
feature タグが付いていない AIP 作成リクエストを拒否する。同様に tenant env も必須化できる。
4. 見積もりワークシート(テンプレート)
実測値(または P95 想定値)が揃ったら、次の Markdown 表テンプレートに流し込む。helpdesk-ai を 月 10 万リクエスト の想定で埋めた例を示す。
ベースシナリオ:helpdesk-ai 月 10 万リクエスト
前提:
- モデル:Claude Sonnet 4 系(Cross-Region Inference、
us.anthropic.claude-sonnet-4-5等。具体的なバージョン番号は流動的なので、最新は公式 model-cards を参照) - 単価は本書執筆時点の概算値。最新は AWS Bedrock Pricing を参照のこと
- 平均入力:800 tok / リクエスト(システムプロンプト 500 + ユーザー 100 + RAG 引用 200)
- 平均出力:400 tok / リクエスト
- Prompt Cache ヒット率:40%(システムプロンプト 500 tok を 1 時間 TTL でキャッシュ)
- Guardrails:Content filters + PII filters + Denied topics 有効化
- Knowledge Bases:S3 50 GB、OpenSearch Serverless 2 OCU 常時起動
- Action Group:1 リクエスト平均 1.5 回 Lambda 呼び出し(256 MB / 500 ms)
| 項目 | 単価 | 月間想定値 | 月額 |
|---|---|---|---|
| Sonnet 4 系 Input(非キャッシュ分) | $3 / 1M tok | 48M tok | $144 |
| Sonnet 4 系 Input(Cache Read) | $0.30 / 1M tok | 20M tok | $6 |
| Sonnet 4 系 Cache Write(1h TTL) | $6 / 1M tok | 2M tok | $12 |
| Sonnet 4 系 Output | $15 / 1M tok | 40M tok | $600 |
| Guardrails Content filters | $0.15 / 1K text units | 120K units | $18 |
| Guardrails PII filters | $0.10 / 1K text units | 120K units | $12 |
| Guardrails Denied topics | $0.15 / 1K text units | 120K units | $18 |
| Knowledge Bases Embedding(Titan v2) | $0.02 / 1M tok | 5M tok(初回 + 更新) | $0.10 |
| OpenSearch Serverless(2 OCU 常時) | $0.24 / OCU/h | 2 × 720h | $345 |
| S3(KB ドキュメント、50 GB) | $0.023 / GB/月 | 50 GB | $1.15 |
| Lambda(Action Group、256 MB) | $0.0000000021 / ms | 150K × 500ms × 256MB | $0.04 |
| Lambda リクエスト | $0.20 / 1M req | 150K req | $0.03 |
| CloudWatch Logs(Model Invocation) | $0.50 / GB | 12 GB | $6 |
| DynamoDB(セッション、On-Demand) | $1.25 / 1M write | 100K write | $0.13 |
| 合計 | $1,162.45 / 月 |
このワークシートのキモは、最後の合計だけでなく どの行が支配的か がひと目で分かる点だ。helpdesk-ai のケースでは、
- Output トークン($600) ── 全体の 52%。最初に削るべきはここ
- OpenSearch Serverless($345) ── 全体の 30%。実は KB のストレージが第 2 位
- Input + Cache($162) ── 全体の 14%
「Sonnet の単価が高い」のではなく、Output トークンと OpenSearch の常時起動コストが効いている。これが見えるとチューニングの優先順位がぶれない。
3 シナリオ比較
同じ helpdesk-ai を、リクエスト数だけ変えて 3 シナリオで試算する。
| 項目 | 月 1 万 req | 月 10 万 req | 月 100 万 req |
|---|---|---|---|
| モデル推論(Sonnet 4 系) | $76 | $762 | $7,620 |
| Guardrails 合計 | $4.80 | $48 | $480 |
| OpenSearch Serverless(固定) | $345 | $345 | $345 |
| その他(S3 / Lambda / Logs / DDB 等) | $2 | $7 | $52 |
| 合計 | $428 | $1,162 | $8,497 |
| 1 req あたり | $0.043 | $0.012 | $0.0085 |
xychart-beta
title "月間リクエスト数 vs 月額コスト"
x-axis ["1万req", "10万req", "100万req"]
y-axis "月額 (USD)" 0 --> 10000
bar [428, 1162, 8497]
観察:
- 1 万 req のシナリオは、OpenSearch Serverless の固定費 $345 が全体の 80% を占める。「PoC 段階では S3 Vectors を使う」という選択は、ここの固定費を $50 程度まで圧縮できる強力な手段
- 100 万 req のシナリオでは、Output トークン代が支配的になる。ここに到達した時点で Prompt Routing と Caching の最適化、および Flex Tier への一部移行を真剣に検討する
- スケールに伴って 1 req あたり単価が下がっていく($0.043 → $0.0085)が、これは固定費の希釈効果。本当のスケール効率は「Output / req」を下げないと出ない
注意:DoiT 等のサードパーティブログでは「複合最適化で 40〜60% 削減」「PT 適切利用で 30〜40% 削減」といった目安値が出回っているが、これらは AWS 公式の数値ではない サードパーティブログ概算。自社のワークシートで P95 ベースに計算し直してから判断してほしい。
5. コスト最適化の優先順位(4 段ロケット)
ワークシートが埋まったら、次は「どこから削るか」だ。優先順位は次の通り。
graph TD
A[Step 1: モデル選定] --> B[Step 2: Prompt Caching]
B --> C[Step 3: Batch 化]
C --> D[Step 4: PT or Flex Tier]
A -.40-60% 削減.-> R[コスト]
B -.最大 90% 削減<br/>キャッシュ部分のみ.-> R
C -.50% 割引.-> R
D -.PT は稼働率 80%+<br/>Flex は -50%.-> R
Step 1:モデル選定(Haiku でいけるものを Sonnet で叩かない)
最大のレバー。タスクの 40〜60% は Haiku で十分という AWS 実測がある(Intelligent Prompt Routing の RAG 用途で 87% が Haiku ルーティング)。
helpdesk-ai では、
- 意図分類・クエリ書き換え → Haiku 4 系(Input $1 / Output $5、執筆時点の概算)
- 最終回答生成 → Sonnet 4 系(Input $3 / Output $15、執筆時点の概算)
と分けるだけで、入力の 60〜70% を 3 倍安い Haiku に流せる。これだけで月額は 30〜40% 落ちる。
Step 2:Prompt Caching(ヒット率 30%+ を狙う)
条件付きで強力。ヒット率 30% を下回ると逆にコスト増になる点に注意(書き込みは Input 単価の 1.25〜2 倍)。
system_prompt や RAG の固定コンテキストなど、再利用される 1,024 tok 以上のブロック を 1 時間 TTL でキャッシュするのが基本。
Step 3:Batch 化(ナイトリーで OK なものを Batch へ)
On-Demand の 50% オフ。helpdesk-ai でいえば、
- 過去 24 時間の問い合わせログから FAQ 候補を抽出する夜間バッチ
- ナレッジベースの差分ドキュメントへのタグ付け
- LLM-as-a-Judge による評価データセット生成
など、24 時間以内 SLA で OK なものはすべて Batch にする。
Step 4:PT or Flex Tier
稼働率次第。
- 平均稼働率 60% 未満 → On-Demand 一択
- 60〜80% → Flex Tier(Standard の -50%、レイテンシ許容できる用途)
- 80%+ → 1 か月 PT で検証 → 6 か月 PT(20〜40% 割引)
Step 4 を Step 1 にしないこと。「PT を買えば安くなる」は 80% 稼働率を出せて初めて成り立つ。逆順にやって痛い目を見るのが、ch17 アンチパターン #1「PT 無駄買い」だ。
6. CUR(Cost and Usage Report)からの実測
ワークシートを毎月見直すには、実測値を CUR から取る 仕組みが必要だ。CUR を S3 に出力 → Athena でクエリする、が定番の構成。
Athena クエリ例:tenant 別・モデル別・日次の集計
SELECT
DATE(line_item_usage_start_date) AS usage_date,
resource_tags['user:tenant'] AS tenant,
resource_tags['user:feature'] AS feature,
product['model'] AS model_id,
SUM(line_item_usage_amount) AS usage_tokens,
SUM(line_item_unblended_cost) AS cost_usd
FROM
cur_database.cur_table
WHERE
line_item_product_code = 'AmazonBedrock'
AND line_item_usage_start_date >= DATE_ADD('day', -30, CURRENT_DATE)
GROUP BY
1, 2, 3, 4
ORDER BY
usage_date DESC, cost_usd DESC;
これで「テナント acme の chat 機能で、過去 30 日に Sonnet 4 系を使った日次コスト」が出る。Application Inference Profile に tag が乗っていればこのクエリは強力に効く。逆に言うと、tag を付けていないとここで分解できない。
異常検知:前週比 2 倍以上のスパイクをアラート
WITH daily AS (
SELECT
DATE(line_item_usage_start_date) AS d,
resource_tags['user:feature'] AS feature,
SUM(line_item_unblended_cost) AS cost
FROM cur_database.cur_table
WHERE line_item_product_code = 'AmazonBedrock'
AND line_item_usage_start_date >= DATE_ADD('day', -14, CURRENT_DATE)
GROUP BY 1, 2
),
weekly AS (
SELECT
feature,
SUM(CASE WHEN d >= DATE_ADD('day', -7, CURRENT_DATE) THEN cost END) AS this_week,
SUM(CASE WHEN d < DATE_ADD('day', -7, CURRENT_DATE) THEN cost END) AS last_week
FROM daily
GROUP BY feature
)
SELECT
feature,
last_week,
this_week,
ROUND(this_week / NULLIF(last_week, 0), 2) AS ratio
FROM weekly
WHERE this_week / NULLIF(last_week, 0) >= 2.0
ORDER BY ratio DESC;
このクエリを 1 日 1 回 EventBridge で起動し、結果が 1 行でもあれば Slack に通知する。「気づいたら月末に倍払っていた」を防ぐ最後の砦になる。
7. CloudWatch メトリクスから月額を予測する Lambda
CUR は前日までしか出ない。当月の着地予測 をリアルタイムで欲しい場合は、CloudWatch メトリクスの InputTokenCount / OutputTokenCount を取って線形外挿する。
import {
CloudWatchClient,
GetMetricStatisticsCommand,
} from "@aws-sdk/client-cloudwatch";
const cw = new CloudWatchClient({
region: process.env.AWS_REGION ?? "us-east-1",
maxAttempts: 5,
retryMode: "adaptive",
});
// 単価(Sonnet 4 系の例、USD per 1M tokens。執筆時点の概算)
const PRICE_INPUT_PER_M = 3;
const PRICE_OUTPUT_PER_M = 15;
interface Forecast {
monthToDateTokensInput: number;
monthToDateTokensOutput: number;
monthToDateCostUsd: number;
forecastMonthlyCostUsd: number;
}
export async function forecastMonthlyCost(
modelId: string,
): Promise<Forecast> {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const elapsedHours = (now.getTime() - startOfMonth.getTime()) / 36e5;
const daysInMonth = new Date(
now.getFullYear(),
now.getMonth() + 1,
0,
).getDate();
const totalHours = daysInMonth * 24;
const fetch = async (metric: "InputTokenCount" | "OutputTokenCount") => {
const res = await cw.send(
new GetMetricStatisticsCommand({
Namespace: "AWS/Bedrock",
MetricName: metric,
Dimensions: [{ Name: "ModelId", Value: modelId }],
StartTime: startOfMonth,
EndTime: now,
Period: 3600, // 1h
Statistics: ["Sum"],
}),
);
return (res.Datapoints ?? []).reduce((acc, dp) => acc + (dp.Sum ?? 0), 0);
};
const [inputTokens, outputTokens] = await Promise.all([
fetch("InputTokenCount"),
fetch("OutputTokenCount"),
]);
const mtdCost =
(inputTokens / 1_000_000) * PRICE_INPUT_PER_M +
(outputTokens / 1_000_000) * PRICE_OUTPUT_PER_M;
// 線形外挿(経過時間に対する単純な比例)
const forecastCost = (mtdCost / elapsedHours) * totalHours;
return {
monthToDateTokensInput: inputTokens,
monthToDateTokensOutput: outputTokens,
monthToDateCostUsd: Number(mtdCost.toFixed(2)),
forecastMonthlyCostUsd: Number(forecastCost.toFixed(2)),
};
}
これを 1 日 1 回起動して、forecastMonthlyCostUsd が前月実績の 1.5 倍を超えたらアラートする。CUR を待たずに 当月のうちに気づける ようになる。
8. AWS Pricing Calculator との併用パターン
「PoC 着手前で実測値がない」段階では、Pricing Calculator を併用する。
実務的なフロー:
- Pricing Calculator で叩き台:モデル単価と想定リクエスト数だけ入れた「楽観シナリオ」を作る
- 本付録のワークシートで補完:Calculator が拾わない Knowledge Bases ストレージ・CloudWatch Logs・Guardrails text units を上乗せ
- PoC を 1 週間流す:Application Inference Profile + tag で実測値を取る
- CUR で見直し:Athena で日次コストを見て、ワークシートの想定値を更新
Calculator 単体では「ストレージ系・Guardrails・Logs」が抜け落ちる。これらは合計で月額の 20〜40% を占めることがあるので、ワークシートで足し込む。
9. ストレージ系コストを忘れない(最後にもう一度)
念押しでもう一度書く。Bedrock 周辺のストレージ系は以下を全部数える。
| サービス | 何に課金されるか | 月額の目安(helpdesk-ai 想定) |
|---|---|---|
| S3(KB ドキュメント・Batch I/O) | ストレージ + リクエスト | $1〜10 |
| OpenSearch Serverless | OCU 時間(最低 2 OCU 常時) | $345〜 |
| DynamoDB(セッション・レートリミット) | RCU/WCU or On-Demand | $1〜50 |
| CloudWatch Logs(Model Invocation) | 取り込み + 保管 | $10〜数百 |
| S3 Vectors(代替案) | リクエスト + 容量 | $20〜100 |
特に OpenSearch Serverless の 2 OCU 常時起動 = $345/月 は、小規模アプリで最大の支配的コストになる。コスト最適化の最初の一手として「OpenSearch Serverless を S3 Vectors に切り替える」を検討する価値がある。トレードオフはレイテンシ(S3 Vectors はサブセカンドだが、OpenSearch より遅い場合がある)。
10. ワークシート運用のサイクル
最後に、本付録で示したワークシートをどう運用するかをまとめる。
PoC 前:
Pricing Calculator + ワークシート補完 → 楽観シナリオを作る
↓
PoC 開始:
AIP + tag で実測開始 → 1 週間の P95 値を取る
↓
PoC 終了:
ワークシートを実測 P95 で再計算 → 100 倍スケール時の月額を試算
↓
本番化:
CUR + Athena で日次集計 → Slack に毎朝コスト通知
↓
運用:
月次でワークシート見直し → コスト最適化 4 段ロケットを順に適用
このサイクルが回せると、「PoC は数千円、本番で数百万円」事故 はほぼ起きなくなる。気づく前に CUR がアラートを出してくれるからだ。
ここまで読んで「面倒だな」と思ったかもしれない。だが、ワークシートを 1 度作って CUR クエリをデプロイすれば、あとは自動で回る。最初の 1 時間の投資が、年間数百万円の事故を防ぐと思えば、安い保険だ。
章末まとめ
- Bedrock の月額は モデル推論・Guardrails・Knowledge Bases・Agents・ストレージ・データ転送・周辺機能 の 7 系統を全部数えないと当たらない
- 見積もりは 平均ではなく P95 で。スパイクが月額を支配する
- Application Inference Profile + tag が実測の起点。IAM でタグ必須化して開発者依存にしない
- ワークシート例で見ると、小規模 helpdesk-ai は OpenSearch Serverless の固定費 $345/月 が支配的。1 万 req なら全体の 80%
- コスト最適化は 4 段ロケット:(1) モデル選定 → (2) Prompt Caching → (3) Batch → (4) PT/Flex の順
- CUR + Athena で 日次集計と前週比 2 倍アラート を自動化する。CloudWatch メトリクスからは 月額予測 Lambda で当月着地を見る
- サードパーティブログの削減率(DoiT 等)は 概算として扱い、自社ワークシートで P95 から計算し直す
- ストレージ系(OpenSearch Serverless、CloudWatch Logs、DynamoDB)を見積もりから抜かない ── ここで月額が 1.2〜1.4 倍に化ける