0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudWatch カスタムメトリクスの請求は「ユニークな時系列の数」 だけで決まる、 という話 (tier 料金を honest に扱う)

0
Posted at

CloudWatch の請求書を最近じっくり眺めましたか? 「カスタムメトリクス」 の行が EC2-Instance-Hours より大きい、 という account はそれなりにあると思います。 そこを削ろうとすると面白いことに気付くので、 そのメモと、 削るためのツールを書いた話を残します。

料金は「ユニークな時系列の数」 でしか決まらない

us-east-1 list price (2026-06、 AWS の最新は要確認):

tier (metrics/月) $/metric·月
最初の 10,000 $0.30
10,001 – 250,000 $0.10
250,001 – 1,000,000 $0.05
1,000,000 超 $0.02

バイト数でも request 数でもない、 unique series (= namespace + metric name + dimension-value の組み合わせ) の個数だけが課金軸です。 加えて PutMetricData リクエスト数 ($0.01 / 1,000 req) が乗りますが、 これは通常副次的。

unique series が爆発するパターン

これも単純で、 高 cardinality な dimension を 1 個入れると掛け算で series が増えます:

# AWS SDK の典型コード
cloudwatch.put_metric_data(
    Namespace='MyApp/Web',
    MetricData=[{
        'MetricName': 'http_requests',
        'Value': 1,
        'Dimensions': [
            {'Name': 'service',     'Value': 'checkout'},     # ~10 unique
            {'Name': 'method',      'Value': 'POST'},          # ~5 unique
            {'Name': 'user_id',     'Value': '7f3a...'},       # ~12,000 unique ← !!
            {'Name': 'request_id',  'Value': 'req-...'},       # per-event unique ← !!!
        ],
    }]
)

この 4 dimension の組み合わせは 理論最大 10 × 5 × 12,000 × N (request 数) の time series を作ります。 N は毎リクエスト違う request_id なのでほぼ実 request 数。 たとえば月 100 万 request あったら 数百万 series が一気に出る。

「request_id をメトリクスに入れない」 は誰でも知っているはずなのに、 実際の production アカウントを aws cloudwatch list-metrics で覗くと やってる。 OpenTelemetry の resource.attribute をデフォルトで dimension に転写するライブラリ、 自前の ad-hoc PutMetricData コード、 過去の Datadog から CloudWatch に移行したときに残った設定 — 出所はいろいろです。

なぜ「flat $0.30 × N」 計算は嘘なのか

これが本稿の中核です。 50,000 unique series あるとき、 月いくら? 50,000 × $0.30 = $15,000 ではありません。 tier が下がるので:

   first 10,000 × $0.30 = $3,000
+  next  40,000 × $0.10 = $4,000
                       = $7,000/月

$15,000$7,000桁が変わるくらい違う。 「flat レート × series 数」 で savings を語ると常に二重に間違います:

  • savings を過大評価: 現状コストを inflate
  • 削除後コストを過大評価: 削った series は 安い高 tier から消えるので、 deletion の dollar 価値は均一じゃない

これがどれくらい load-bearing かというと、 S4 Metrics の cost 計算関数 monthly_cost_usdmonthly_cost_usd(50_000) == $7,000.00 という assertion がテストスイートに入っています。 dry-run で出る数字は AWS の本当の tier 計算と bit 一致するように作ってあって、 セールススライド向けの「平均レート」 計算じゃない。

tier が decreasing なので、 削減の dollar 効果は 10k〜250k series の中規模アカウントで最大化されます。 1M 超アカウントは margin が $0.02 まで落ちているので、 同じ %削減でも絶対額は伸びるけど比率としては小さくなる。

解決ツールを書きました — S4 Metrics

ここまでの議論を踏まえて、 cardinality を govern する collector daemon を Rust で書いて Apache-2.0 で公開しました。

アプリと CloudWatch の間に collector daemon metricsd を置いて、 declarative YAML rule で dimension を変形してから emit します。

[App: StatsD / OTLP / EMF / PutMetricData — endpoint override only]
            │ ingest
            ▼
   ┌──────────────────────────────────────────────┐
   │                  metricsd                      │
   │     ingest → govern → aggregate → emit         │
   │  (cardinality engine + DDSketch, 60s window)   │
   └──────┬──────────────────────────┬──────────────┘
          │ governed + aggregated     │ full-resolution tee
          ▼                          ▼
   [CloudWatch PutMetricData]   [S3 / Prometheus remote-write]

migration は他の S4 ファミリと同じ「endpoint 差し替えるだけ」 :

# 元: aws cloudwatch put-metric-data ...
# 後:
aws cloudwatch put-metric-data \
  --endpoint-url http://metricsd.internal:4318 ...

StatsD / DogStatsD / OTLP metrics / EMF / PutMetricData の 5 種の intake が同居している collector で、 受けて出す形式は CloudWatch ですが、 dropped detail は S3 に full-resolution tee されます (これは S4 Logs と同じ思想。 「消すんじゃなくて、 per-series 課金じゃない場所に移す」)。

Govern の 5 つの動詞

cardinality-engine クレートが提供する変換は 5 つで、 keep → drop → rollup → bucket → sample の順に評価されます:

rules:
  # 1. keep — allowlist で残すものだけ宣言
  - name: keep-alert-dims
    namespace: "MyApp/Critical"
    metric: "*"
    keep_dimensions: [service, region]

  # 2. drop — 高 cardinality 軸を消す (the main gun)
  - name: drop-unbounded-ids
    namespace: "MyApp/*"
    metric: "*"
    drop_dimensions: [user_id, request_id]

  # 3. rollup — 具体値を template に折りたたむ
  - name: rollup-paths
    namespace: "MyApp/Web"
    metric: "http_requests"
    rollup:
      dimension: path
      replace_with: "/users/:id"   # 3,000 paths → 1 series

  # 4. bucket — 連続値を境界で離散化
  - name: bucket-latency
    namespace: "MyApp/Web"
    metric: "request_latency_ms"
    bucket:
      dimension: latency_bucket_raw
      boundaries: [10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]

  # 5. sample — datapoint 数の lever (cardinality は減らない!)
  - name: sample-noisy
    namespace: "MyApp/*"
    metric: "debug_*"
    sample:
      rate: 0.1

sample は cardinality を減らさない点が重要なので明示しておきます。 sampled でも series は存在するので CloudWatch の billable series count は同じ。 sample が下げるのは datapoint 数 + PutMetricData request 数 (これも秒額に乗るが副次的)。 cardinality を削りたいなら drop / rollup / bucket / keep のどれか。

rule の compile は deterministic で「同じ入力 + 同じ rule なら同じ出力 (sampling 含む)」 が保証されているので、 dry-run の予測が本番 emit と bit 一致します。 これがないと「試算」 が信用できません。

dry-run で「値段だけ」 計算する

metricsd dry-runPutMetricData を 1 回も叩かず、 tier-aware の before/after を出します。 custom metric 側の課金は増えません (CloudWatch API request 自体は --from-cloudwatch 経由なら ListMetrics 呼び出し分が乗ります、 通常は free tier の範囲内)。 入力は 2 通り:

# (a) 既存メトリクス population を JSON-lines で渡す
metricsd dry-run --rules rules.yaml --from-file population.jsonl

# (b) account に直接問い合わせ (read-only ListMetrics)
metricsd dry-run --rules rules.yaml --from-cloudwatch --region us-east-1

出力:

S4 Metrics dry-run estimate (CloudWatch tiered pricing)
─────────────────────────────────────────────────────────────────
                          billable series       monthly $
current                            50000        $7000.00
after governance                    8000        $2400.00
─────────────────────────────────────────────────────────────────
cardinality reduction              84.0%
monthly savings                                 $4600.00
annual savings                                 $55200.00

これが何より素直で、 「うちは tier どこ?」 「rule 入れたらいくら削れる?」 を 作業 0、 追加 custom metric 課金なしで見れるのが OSS としての価値だと思っています。

計測値

合成 high-cardinality workload (http_requestsuser_id (12,000 distinct) + path (3,000 distinct) + instance_id (400) + per-event-unique request_id + request_latency_ms に numeric dimension 2,500 distinct) を 22,500 distinct series まで膨らませて、 代表的な govern rule (drop-unbounded-ids + rollup-paths + bucket-latency 等) を applied:

削減前 削減後
billable series 22,500 8,900
月額 (tier 計算) $4,250.00 $2,670.00
月次節約 $1,580.00
年換算 $18,960.00

series count では 60.44% 削減、 $ ベースでは 37.2% 削減 — ギャップは tier が decreasing なせいで、 削った series の多くが marginal $0.10 帯 (10,001〜250,000) から外れたものだから (一番 marginal が高い $0.30 帯の 10k 枠はほぼ残る)。 ここを混同して「60% series 削減 = 60% 節約」 と書くのが S4 系で禁止している anti-pattern です。

数字は crates/bench/ の測定ハーネスが実物のパイプラインを回して出力したもので、 ハンドエディットしていません (docs/savings-calc.md 上部に reproduction コマンドあり)。

実 AWS でも動かしました (mock じゃなくて)

2026-06-15、 ap-northeast-1 で:

  • metricsd run --cloudwatch true (default) + S3 tee + flush 3 秒
  • HTTP intake に untimestamped な PutMetricData (AWS Query 形式)、 daemon は 202 を返す
  • StatsD UDP で smoke.timer:120|mssmoke.timer:240|ms を投げる

CloudWatch 側:

$ aws cloudwatch list-metrics --namespace S4Metrics/SmokeTest
smoke.latency    service=checkout
smoke.timer      service=checkout
smoke.requests   service=checkout

get-metric-statisticssmoke.latency の Sum/SampleCount/Max が期待どおり、 S3 tee 側にも zstd 圧縮 jsonl で着地。 LocalStack だけだと CloudWatch の untimestamped PutMetricData の挙動を再現できない (実 AWS は当該分を当該分単位窓に丸める) ので、 ここは実 region で確認する必要がありました。

ロックインしません — S3 tee の format は OSS 復号可

dropped detail は account=…/namespace=…/dt=…/…jsonl.zst 形式で S3 に書かれます。 本体は 標準 RFC 8878 zstd フレームの JSONL (S4F2 コンテナではなく単一の zstd フレーム)、 隣に s4-codecS4IX sidecar を置く構成です。 S4 Metrics が消えても aws s3 cp ... - | zstd -dc | jq ... で読めます。 S4IX サイドカーで range-read 索引できますが、 無視しても本体は普通の zstd JSONL として処理可能。

これも S4 Logs と同じ規律: データ format が contract、 ツールは交換可能

DDSketch で「分布」 を 1 series に畳む

per-request の request_latency_ms を全部別 series にしたら series 数で死ぬので、 distributions は 60 秒窓で DDSketch に畳んで、 quantiles を CloudWatch の values/counts 形式で送ります。 DDSketch は相対誤差 1% (default) を保証する quantile sketch で、 p50/p99 のような quantile queries は CloudWatch 上でそのまま使えます。

正直注記: CloudWatch の values/counts percentile 自体も近似計算なので、 quantile の絶対誤差は (DDSketch 1% × CW percentile 近似) の合成になります。 「exact percentile」 とは謳いません。 また PutMetricData は 1 リクエスト 150 values 上限があるので、 flush ごとに carrying できる DDSketch 解像度はそれで bound されます。

ビルドして起動

v0.1.0 は release binary をまだ配っていないので、 source build から:

git clone https://github.com/abyo-software/s4-metrics
cd s4-metrics
cargo build --release -p metricsd
./target/release/metricsd run \
  --statsd-listen 0.0.0.0:8125 \
  --http-listen 0.0.0.0:4318 \
  --rules rules.yaml \
  --flush-interval 60 \
  --region us-east-1 \
  --s3-bucket my-metrics-archive --s3-prefix s4metrics \
  --default-namespace MyApp/Web

deploy 補助として deploy/systemd/ に unit ファイル、 deploy/cloudformation/ に AWS 上で動かす雛形があります (この CFN テンプレ自体は OSS 同梱、 商用 AMI を subscribe しなくても自分で AMI を焼けば使えます)。

制限 (デプロイ前に読んでください)

  • SigV4 検証はしません。 S4 Logs gateway と違って --auth-mode sigv4 は無く、 intake は届いたものを受け付けます。 private subnet + SG + service mesh の内側に置く前提です。 transport は --tls-cert/--tls-key で TLS 終端可能
  • OTLP の ExponentialHistogramSummary は silently skipGauge / Sum / Histogram (explicit-bounds) は正常にマップされますが、 expo histogram は base-scale 展開が sketch クレートに必要で v0.1.0 では未対応、 Summary は pre-computed 値で再集約不能なので drop。 batch 自体は失敗しません
  • sample は cardinality を減らさない (再掲)。 series は依然存在するので CloudWatch 課金単位は変わらず、 datapoint 数だけが減ります。 cardinality 削減目的では drop/rollup/bucket/keep を使ってください
  • EMF は「節約機構」 ではない。 EMF から抽出された metrics は同じ tier 料金で billable な custom metric、 さらに EMF log 自体が CloudWatch Logs ingest $0.50/GB二重課金になります。 EMF 経路の節約は S4 Logs (兄弟製品) との bundle で初めて閉じる
  • single account / deployment 想定。 multi-account baseline、 ListMetrics 自動ベースライン、 savings dashboard、 S4 Logs と組んだ combined collector は v0.1.0 OSS core にはありません (roadmap で別出し)

ライセンスと連絡先

「うちは tier どこにいて、 rule 入れたらいくら削れそうか」 を試したいだけなら metricsd dry-run --from-cloudwatch を ListMetrics 権限だけ与えて回すのが一番早いです (PutMetricData は呼ばないので custom metric 課金は増えません、 ListMetrics の API request 分のみ)。

質問・要望は GitHub issue にどうぞ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?