
クラスメソッドメンバーズ提供のCURをサクッとコスト分析!(with DuckDB)
弊社のAWS総合支援サービスである「クラスメソッドメンバーズ」の機能として、 メンバーズCUR を提供しています。 これはAWSで利用されたサービスのコスト/使用量についてのレポート (CUR: Cost and Usage Reports)を取得できるものです。
事前にS3バケットを準備して CURエクスポート設定を作成することで、 継続的なCURのS3出力を実現できます。 詳細や具体的な手順は以下ドキュメントに記載があります。
今回は出力されたメンバーズCURをDuckDBでローカルでサクッと分析してみます。 DuckDBはオンライン分析処理(OLAP)に特化したデータベースシステムです。
なお、大規模な分析や、継続的なダッシュボード運用を実現していく際は、 DuckDBよりもAthenaが向いているケースがあります。 以下ブログが参考になるますのでぜひ御覧ください。
セットアップ
クラスメソッドメンバーズ提供のCUR
メンバーズユーザーガイドに沿ってメンバーズCURを設定します。 保存フォーマットはParquet(分析用途に向いている形式)としました。
以下のようにS3バケットに保管されていきます。
aws s3 ls EXAMPLE-DOC-BUCKET/mcur/MCURExport/MCURExport/year=2025/month=3/
# 2025-03-24 08:37:33 10961234 MCURExport-00001.snappy.parquet
# 2025-03-24 08:37:33 10891234 MCURExport-00002.snappy.parquet
# 2025-03-24 08:37:33 11011234 MCURExport-00003.snappy.parquet
# 2025-03-24 08:37:33 10771234 MCURExport-00004.snappy.parquet
# 2025-03-24 08:37:33 10931234 MCURExport-00005.snappy.parquet
# ↑ プレフィクスが mcur 、 エクスポート名が MCURExport の場合
aws s3 ls EXAMPLE-DOC-BUCKET/mcur/MCURExport/20250301-20250401/
# 2025-03-24 08:37:57 29396 MCURExport-Manifest.json
# ↑ これはメタデータファイル(スキーマ情報など)
これら parquetファイルを事前にローカルに落としておきます。
aws s3 cp s3://EXAMPLE-DOC-BUCKET/mcur/MCURExport/MCURExport/year=2025/month=3/ . --recursive --include "*.snappy.parquet"
# download: s3://EXAMPLE-DOC-BUCKET/mcur/MCURExport/MCURExport/year=2025/month=3/MCURExport-00001.snappy.parquet to ./MCURExport-00001.snappy.parquet
# download: s3://EXAMPLE-DOC-BUCKET/mcur/MCURExport/MCURExport/year=2025/month=3/MCURExport-00002.snappy.parquet to ./MCURExport-00002.snappy.parquet
# download: s3://EXAMPLE-DOC-BUCKET/mcur/MCURExport/MCURExport/year=2025/month=3/MCURExport-00003.snappy.parquet to ./MCURExport-00003.snappy.parquet
# download: s3://EXAMPLE-DOC-BUCKET/mcur/MCURExport/MCURExport/year=2025/month=3/MCURExport-00004.snappy.parquet to ./MCURExport-00004.snappy.parquet
# download: s3://EXAMPLE-DOC-BUCKET/mcur/MCURExport/MCURExport/year=2025/month=3/MCURExport-00005.snappy.parquet to ./MCURExport-00005.snappy.parquet
ls -1
# MCURExport-00001.snappy.parquet
# MCURExport-00002.snappy.parquet
# MCURExport-00003.snappy.parquet
# MCURExport-00004.snappy.parquet
# MCURExport-00005.snappy.parquet
DuckDBによる分析準備
以下Installationのとおりに、 DuckDBをローカルにインストールします。
duckdb --version
# v1.2.1 8e52ec4395
duckdb
# v1.2.1 8e52ec4395
# Enter ".help" for usage hints.
# Connected to a transient in-memory database.
# Use ".open FILENAME" to reopen on a persistent database.
# D
DuckDBの起動後、CUR(parquet)を読込できるかチェックします。
SELECT * FROM '*.snappy.parquet' LIMIT 10;
-- ┌──────────────────────┬──────────────────────┬───┬──────────────────────┬──────────────────────┐
-- │ identity_line_item… │ identity_time_inte… │ … │ savings_plan_offer… │ savings_plan_start… │
-- │ varchar │ varchar │ │ varchar │ varchar │
-- ├──────────────────────┼──────────────────────┼───┼──────────────────────┼──────────────────────┤
-- │ lyrqemptwjeexample… │ 2025-03-04T17:00:0… │ … │ NULL │ NULL │
-- │ t5vgwtwcjnjexample… │ 2025-03-03T04:00:0… │ … │ NULL │ NULL │
-- │ hist3kadojfexample… │ 2025-03-22T23:00:0… │ … │ NULL │ NULL │
-- │ qdcaqsbach3example… │ 2025-03-05T15:00:0… │ … │ NULL │ NULL │
-- │ durwy3d6cz5example… │ 2025-03-13T22:00:0… │ … │ NULL │ NULL │
-- │ tv52j3milluexample… │ 2025-03-12T11:00:0… │ … │ NULL │ NULL │
-- │ yhljecfe4abexample… │ 2025-03-11T09:00:0… │ … │ NULL │ NULL │
-- │ d7hok52ehhlexample… │ 2025-03-19T02:00:0… │ … │ NULL │ NULL │
-- │ 7ep6eo4atd5example… │ 2025-03-07T17:00:0… │ … │ NULL │ NULL │
-- │ kl5cvl4ftyhexample… │ 2025-03-05T02:00:0… │ … │ NULL │ NULL │
-- ├──────────────────────┴──────────────────────┴───┴──────────────────────┴──────────────────────┤
-- │ 10 rows 283 columns (4 shown) │
-- └───────────────────────────────────────────────────────────────────────────────────────────────┘
1レコードのみ、内容も見てみます。
-- 出力形式を line に変更 (1レコードの内容を確認するときに便利)
.mode line
-- 1レコードのみ確認
SELECT * FROM '*.snappy.parquet' LIMIT 1;
-- option: 出力形式を元(=デフォルト)に戻す
.mode duckbox
identity_line_item_id = lvhfqje3iexample
identity_time_interval = 2025-03-12T04:00:00Z/2025-03-12T05:00:00Z
bill_invoice_id = NULL
bill_billing_entity = AWS
bill_bill_type = Anniversary
bill_payer_account_id = 111111111111
bill_billing_period_start_date = 2025-03-01 00:00:00
bill_billing_period_end_date = 2025-04-01 00:00:00
bill_invoicing_entity = Amazon Web Services Japan G.K.
line_item_usage_account_id = 222222222222
line_item_line_item_type = Usage
line_item_usage_start_date = 2025-03-12 04:00:00
line_item_usage_end_date = 2025-03-12 05:00:00
line_item_product_code = AmazonCloudWatch
line_item_usage_type = APN1-DataProcessing-Bytes
line_item_operation = PutLogEvents
line_item_availability_zone = NULL
line_item_resource_id = arn:aws:logs:ap-northeast-1:222222222222:log-group:/aws/rds/cluster/example/slowquery
line_item_usage_amount = 5.1396e-05
line_item_normalization_factor = NULL
line_item_normalized_usage_amount = NULL
line_item_currency_code = USD
line_item_unblended_rate = 0.7600000000
line_item_unblended_cost = 3.9061e-05
line_item_line_item_description = $0.76 per GB custom log data ingested in Standard log class - Asia Pacific (Tokyo)
line_item_tax_type = NULL
line_item_legal_entity = Amazon Web Services Japan G.K.
product_product_name = AmazonCloudWatch
----- 長いので省略 -----
pricing_lease_contract_length = NULL
pricing_offering_class = NULL
pricing_purchase_option = NULL
pricing_public_on_demand_cost = 3.9061e-05
pricing_public_on_demand_rate = 0.7600000000
pricing_term = OnDemand
pricing_unit = GB
----- 長いので省略 -----
resource_tags_user_cm_billing_group = NULL
----- 長いので省略 -----
コスト分析!
よく使いそうな分析をいくつか実行してみます。
日ごとの料金
日付ごとの合計コストを出力してみます。
SELECT
CAST(line_item_usage_start_date AS DATE) AS date,
round(SUM(line_item_unblended_cost),1) AS cost
FROM '*.snappy.parquet'
GROUP BY date
ORDER BY date;
-- ┌────────────┬──────────┐
-- │ date │ cost │
-- │ date │ double │
-- ├────────────┼──────────┤
-- │ 2025-03-01 │ 91.0 │
-- │ 2025-03-02 │ 83.4 │
-- │ 2025-03-03 │ 83.6 │
-- │ 2025-03-04 │ 94.6 │
-- │ 2025-03-05 │ 90.2 │
-- │ 2025-03-06 │ 100.2 │
-- │ 2025-03-07 │ 91.8 │
-- ... 省略 ...
-- │ 2025-03-26 │ 0.0 │
-- │ 2025-03-27 │ 0.0 │
-- │ 2025-03-28 │ 0.0 │
-- │ 2025-03-29 │ 0.0 │
-- │ 2025-03-30 │ 0.0 │
-- │ 2025-03-31 │ 0.0 │
-- ├────────────┴──────────┤
-- │ 31 rows 2 columns │
-- └───────────────────────┘
-- ※ 後半が 0.0 になっているのは3月途中でのCURを分析しているため
以下は日付をBETWEEN演算子で指定した例です。
SELECT
CAST(line_item_usage_start_date AS DATE) AS date,
round(SUM(line_item_unblended_cost),1) AS cost
FROM '*.snappy.parquet'
WHERE
CAST(line_item_usage_start_date AS DATE)
BETWEEN DATE '2025-03-01' AND DATE '2025-03-07'
GROUP BY date
ORDER BY date;
-- ┌────────────┬──────────┐
-- │ date │ cost │
-- │ date │ double │
-- ├────────────┼──────────┤
-- │ 2025-03-01 │ 91.0 │
-- │ 2025-03-02 │ 83.4 │
-- │ 2025-03-03 │ 83.6 │
-- │ 2025-03-04 │ 94.6 │
-- │ 2025-03-05 │ 90.2 │
-- │ 2025-03-06 │ 100.2 │
-- │ 2025-03-07 │ 91.8 │
-- ├────────────┴──────────┤
-- │ 7 rows 2 columns │
-- └───────────────────────┘
サービス毎の料金
AWSサービスごとの合計コストを計算して、TOP10を表示します。
SELECT
product_product_name,
round(SUM(line_item_unblended_cost),1) AS cost
FROM '*.snappy.parquet'
GROUP BY product_product_name
ORDER BY cost DESC
LIMIT 10;
-- ┌────────────────────────────────────┬─────────┐
-- │ product_product_name │ cost │
-- │ varchar │ double │
-- ├────────────────────────────────────┼─────────┤
-- │ Amazon Relational Database Service │ 78.6 │
-- │ Amazon Elastic Compute Cloud │ 65.4 │
-- │ AmazonCloudWatch │ 59.8 │
-- │ Amazon CloudFront │ 52.3 │
-- │ Amazon Simple Storage Service │ 47.9 │
-- │ AWS WAF │ 35.6 │
-- │ Amazon Virtual Private Cloud │ 29.4 │
-- │ Amazon ElastiCache │ 22.7 │
-- │ Amazon DynamoDB │ 18.5 │
-- │ Amazon Elastic Container Service │ 12.8 │
-- ├────────────────────────────────────┴─────────┤
-- │ 10 rows 2 columns │
-- └──────────────────────────────────────────────┘
特定サービス内の使用タイプ料金
特定サービス(例:S3)の使用タイプごとの合計コストを計算して、TOP10を表示します。
SELECT
line_item_usage_type,
round(SUM(line_item_unblended_cost),1) AS cost
FROM '*.snappy.parquet'
WHERE
product_product_name = 'Amazon Simple Storage Service'
GROUP BY line_item_usage_type
ORDER BY cost DESC
LIMIT 10;
-- ┌──────────────────────────────────┬─────────┐
-- │ line_item_usage_type │ cost │
-- │ varchar │ double │
-- ├──────────────────────────────────┼─────────┤
-- │ APN1-TimedStorage-ByteHrs │ 25.3 │
-- │ APN1-Requests-Tier1 │ 8.7 │
-- │ APN1-Requests-Tier2 │ 5.2 │
-- │ APN1-DataTransfer-Out-Bytes │ 4.1 │
-- │ APN1-TimedStorage-GlacierByteHrs │ 2.8 │
-- │ APN1-Requests-Tier3 │ 1.5 │
-- │ TimedStorage-ByteHrs │ 1.0 │
-- │ APN1-EUC1-AWS-Out-Bytes │ 0.6 │
-- │ APN1-USE2-AWS-Out-Bytes │ 0.5 │
-- │ APN1-TimedStorage-GIR-ByteHrs │ 0.3 │
-- ├──────────────────────────────────┴─────────┤
-- │ 10 rows 2 columns │
-- └────────────────────────────────────────────┘
CmBillingGroupタグ毎の料金
クラスメソッドメンバーズでは、デフォルトで 「CmBillingGroup」をコスト配分タグとして利用できます。 CmBillingGroupタグの情報は CUR の resource_tags_user_cm_billing_group
にて確認できます。
以下SQLでCmBillingGroupタグごとの合計コストを計算します。
SELECT
resource_tags_user_cm_billing_group as tag,
round(SUM(line_item_unblended_cost),1) AS cost
FROM '*.snappy.parquet'
GROUP BY tag
ORDER BY cost DESC
LIMIT 10;
-- ┌──────────────┬─────────┐
-- │ tag │ cost │
-- │ varchar │ double │
-- ├──────────────┼─────────┤
-- │ aaa │ 450.3 │
-- │ NULL │ 220.5 │
-- │ backend │ 105.7 │
-- │ bbb │ 75.2 │
-- │ hoge │ 58.6 │
-- │ fuga │ 35.8 │
-- │ piyo │ 22.4 │
-- │ foo │ 15.6 │
-- │ bar │ 10.2 │
-- │ test123 │ 5.7 │
-- ├──────────────┴─────────┤
-- │ 10 rows 2 columns │
-- └────────────────────────┘
-- ※ NULL はタグが無いもの or 非対応のもの
リソースごとの料金
line_item_resource_id
にて料金対象のリソースIDが記載されます。
例えば以下SQLでコストになっている「AWSリソースと使用タイプの組み合わせ」を計算できます。
SELECT
line_item_resource_id,
line_item_usage_type,
round(SUM(line_item_unblended_cost),1) AS cost
FROM '*.snappy.parquet'
GROUP BY line_item_resource_id, line_item_usage_type
ORDER BY cost DESC
LIMIT 10;
-- ┌──────────────────────────────────────────────────────────┬────────────────────────────────┬────────┐
-- │ line_item_resource_id │ line_item_usage_type │ cost │
-- │ varchar │ varchar │ double │
-- ├──────────────────────────────────────────────────────────┼────────────────────────────────┼────────┤
-- │ DOC-EXAMPLE-BUCKET1 │ APN1-TimedStorage-ByteHrs │ 125.8 │
-- │ DOC-EXAMPLE-BUCKET2 │ APN1-TimedStorage-ByteHrs │ 85.3 │
-- │ arn:aws:rds:ap-northeast-1:222222222222:cluster:aaa │ APN1-Aurora:StorageUsage │ 68.4 │
-- │ DOC-EXAMPLE-BUCKET3 │ APN1-TimedStorage-ByteHrs │ 62.5 │
-- │ arn:aws:rds:ap-northeast-1:222222222222:db:bbb │ APN1-InstanceUsage:db.r5.8xl │ 54.2 │
-- │ arn:aws:rds:ap-northeast-1:222222222222:cluster:ccc │ APN1-Aurora:StorageUsage │ 42.6 │
-- │ DOC-EXAMPLE-BUCKET4 │ APN1-TimedStorage-ByteHrs │ 28.7 │
-- │ arn:aws:redshift:ap-northeast-1:222222222222:cluster:ddd │ APN1-RMS:ra3.xlplus │ 15.9 │
-- │ arn:aws:rds:ap-northeast-1:222222222222:cluster:eee │ APN1-Aurora:StorageIOUsage │ 10.3 │
-- │ NULL │ APN1-ConfigurationItemRecorded │ 6.3 │
-- ├──────────────────────────────────────────────────────────┴────────────────────────────────┴────────┤
-- │ 10 rows 3 columns │
-- └────────────────────────────────────────────────────────────────────────────────────────────────────┘
例をもうひとつ。 コストが大きくなりがちな CloudWatch Logs の取り込み料金( DataProcessing-Bytes
) をリソースごとに表示してみます。
SELECT
regexp_replace(line_item_resource_id, '^arn:aws:.+:\d+:log-group:', '') as cw_log_group_name,
round(SUM(line_item_unblended_cost),1) AS cost
FROM '*.snappy.parquet'
WHERE
line_item_usage_type = 'APN1-DataProcessing-Bytes'
GROUP BY cw_log_group_name
ORDER BY cost DESC
LIMIT 10;
-- ┌────────────────────────────────────────┬────────┐
-- │ cw_log_group_name │ cost │
-- │ varchar │ double │
-- ├────────────────────────────────────────┼────────┤
-- │ /aws/lambda/aaa-service-get-status │ 13.0 │
-- │ app-server-logs │ 5.6 │
-- │ /aws/lambda/store-container-function │ 3.1 │
-- │ batch-processing-stock-quantity │ 2.0 │
-- │ /aws/rds/cluster/mysql-bbb/general │ 1.3 │
-- │ ccc-custom │ 1.1 │
-- │ ddd-logs │ 0.7 │
-- │ batch-sync-inventory-service │ 0.6 │
-- │ api-general-logs │ 0.5 │
-- │ api-staging-environment │ 0.4 │
-- ├────────────────────────────────────────┴────────┤
-- │ 10 rows 2 columns │
-- └─────────────────────────────────────────────────┘
おわりに
クラスメソッドメンバーズ提供のCURを分析してみました。 とりあえずS3に出力しておくように設定して、 必要なときにDuckDBでサクッと分析する …といった使い方もアリだと思います。
以上、参考になれば幸いです。