目次を表示する

Bedrockで Production Ready な AI 機能を作る ── 設計・運用・現場の知恵

付録 B:コスト見積もりワークシートを作る ── CUR と Application Inference Profile で実測する

付録 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 つだ。

  1. Knowledge Bases のストレージ系:OpenSearch Serverless の OCU 課金(最低 2 OCU 常時起動)、S3 のリクエスト課金、埋め込みモデルのインデックス時実行コスト
  2. CloudWatch Logs:Model Invocation Logging を有効化すると、入力プロンプトと出力テキストが丸ごとログに残る。テキスト系ログは GB あたり $0.50 程度で、数千万トークン流れる本番では月数万円〜数十万円の規模になり得る
  3. 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:featurechat が乗ってくる。

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 tok48M tok$144
Sonnet 4 系 Input(Cache Read)$0.30 / 1M tok20M tok$6
Sonnet 4 系 Cache Write(1h TTL)$6 / 1M tok2M tok$12
Sonnet 4 系 Output$15 / 1M tok40M tok$600
Guardrails Content filters$0.15 / 1K text units120K units$18
Guardrails PII filters$0.10 / 1K text units120K units$12
Guardrails Denied topics$0.15 / 1K text units120K units$18
Knowledge Bases Embedding(Titan v2)$0.02 / 1M tok5M tok(初回 + 更新)$0.10
OpenSearch Serverless(2 OCU 常時)$0.24 / OCU/h2 × 720h$345
S3(KB ドキュメント、50 GB)$0.023 / GB/月50 GB$1.15
Lambda(Action Group、256 MB)$0.0000000021 / ms150K × 500ms × 256MB$0.04
Lambda リクエスト$0.20 / 1M req150K req$0.03
CloudWatch Logs(Model Invocation)$0.50 / GB12 GB$6
DynamoDB(セッション、On-Demand)$1.25 / 1M write100K write$0.13
合計$1,162.45 / 月

このワークシートのキモは、最後の合計だけでなく どの行が支配的か がひと目で分かる点だ。helpdesk-ai のケースでは、

  1. Output トークン($600) ── 全体の 52%。最初に削るべきはここ
  2. OpenSearch Serverless($345) ── 全体の 30%。実は KB のストレージが第 2 位
  3. 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 を併用する。

実務的なフロー:

  1. Pricing Calculator で叩き台:モデル単価と想定リクエスト数だけ入れた「楽観シナリオ」を作る
  2. 本付録のワークシートで補完:Calculator が拾わない Knowledge Bases ストレージ・CloudWatch Logs・Guardrails text units を上乗せ
  3. PoC を 1 週間流す:Application Inference Profile + tag で実測値を取る
  4. CUR で見直し:Athena で日次コストを見て、ワークシートの想定値を更新

Calculator 単体では「ストレージ系・Guardrails・Logs」が抜け落ちる。これらは合計で月額の 20〜40% を占めることがあるので、ワークシートで足し込む。

9. ストレージ系コストを忘れない(最後にもう一度)

念押しでもう一度書く。Bedrock 周辺のストレージ系は以下を全部数える。

サービス何に課金されるか月額の目安(helpdesk-ai 想定)
S3(KB ドキュメント・Batch I/O)ストレージ + リクエスト$1〜10
OpenSearch ServerlessOCU 時間(最低 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 倍に化ける