
【メンバーズ新機能】メンバーズ組織CURで複数AWSアカウントのコスト分析を簡単に!Athenaによる実践ガイド
はじめに
AWSでは、AWSサービスの利用明細が記載された Cost and Usage Report(CUR)を取得できます。
弊社のAWS総合支援サービスである「クラスメソッドメンバーズ」でも、AWSのレガシーCUR仕様に準拠した メンバーズCUR を提供しています。
このメンバーズCURは個別のAWSアカウント単位でのみ設定・出力が可能で、複数アカウントを跨いだコスト分析には次の手順が必要でした。
- 分析したい各アカウントで個別にメンバーズCUR出力設定
- 各アカウントに出力されたメンバーズCURを単一アカウントへコピーして集約
- 集約したデータをAthena等で分析
しかし上記手順は環境構築の方法が複雑で、お客様からは「もっとシンプルに複数のアカウントを跨いだコスト分析を行いたい」というご要望をいただいていました。
そしてこの度、2025年2月13日に メンバーズ組織CUR がリリースされました!🎉 🎉 🎉
クラスメソッドメンバーズでは、複数のアカウントを「メンバーズ組織」という単位で管理しています。
メンバーズ組織CURでは、この「メンバーズ組織」内の全アカウントのコスト情報をまとめて出力でき、アカウントを跨いだコスト分析環境の構築が大幅に簡素化されました。
既存のメンバーズCUR | メンバーズ組織CUR | |
---|---|---|
出力先 | 指定したAWSアカウントのS3バケット | 指定したAWSアカウントのS3バケット |
出力される情報 | 出力先AWSアカウントのみの情報 | メンバーズ組織内の全AWSアカウントの情報 |
本記事では、このメンバーズ組織CURをAmazon Athenaで分析する方法を2つ紹介します。
- AWS Glueを使った方法
- AthenaのPartition Projectionを使った方法
メンバーズ組織CURの出力フォーマットはParquetとCSVに対応していますが、今回はAthenaでの分析に適しているParquetを前提に説明します。
前提条件
Athenaで分析するメンバーズ組織CURの出力設定例を下記とします。
フォーマット | S3バケット | S3パスプレフィックス | エクスポート名 |
---|---|---|---|
Parquet | cm-cur-123456789012 | cur/classmethod | org-cur-parquet |
このように設定すると、指定のS3バケットにメンバーズ組織CURが出力されます。
出力は概ね一日2回行われます。即時出力ではないのでご注意ください。
下記は2025年1月から3月まで出力した場合のファイル構成の例です。
cm-cur-123456789012/
└ cur/
└ classmethod/
└ org-cur-parquet/
├ org-cur-parquet/
│ └ year=2025/
│ ├ month=1/
│ │ ├ org-cur-parquet-00001.snappy.parquet
│ │ ├ org-cur-parquet-00002.snappy.parquet
│ │ └ org-cur-parquet-00003.snappy.parquet
│ ├ month=2/
│ │ ├ org-cur-parquet-00001.snappy.parquet
│ │ ├ org-cur-parquet-00002.snappy.parquet
│ │ └ org-cur-parquet-00003.snappy.parquet
│ └ month=3/
│ ├ org-cur-parquet-00001.snappy.parquet
│ ├ org-cur-parquet-00002.snappy.parquet
│ └ org-cur-parquet-00003.snappy.parquet
├ 20250101-20250201/
│ └ org-cur-parquet-Manifest.json
├ 20250201-20250301/
│ └ org-cur-parquet-Manifest.json
└ 20250301-20250401/
└ org-cur-parquet-Manifest.json
AWSの上書きの仕様と同様に、同月内で新しいファイルが出力されると古いファイルは上書きされます。
方法1: AWS Glueを使った方法
アーキテクチャ概要
各リソースの説明は次の通りです。
リソース名 | 説明 |
---|---|
S3 Bucket cm-cur-123456789012 |
メンバーズ組織CURが出力されるS3バケット |
Glue Crawler members-cur-crawler |
cm-cur-123456789012バケットを定期的にクロールするCrawler |
Glue Data Catalog members-cur |
Crawlerによって作成されたメタデータが保存されるデータカタログ |
Amazon Athena | CURを分析するAthena |
この環境を構築していきます。
構築手順
AWS Glue Crawlerを利用して、cm-cur-123456789012バケットに出力されたメンバーズ組織CURを分析するためのリソースを作成します。
環境構築用のCloudFormationテンプレートは次の通りです。
CloudFormationテンプレート(YAML)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CFn template that sets up AWS Glue Crawler to analyze Classmethod Members CUR'
Parameters:
S3BucketName:
Type: String
Description: 'S3 Bucket Name (ex. cm-cur-123456789012)'
S3PathPrefix:
Type: String
Description: 'S3 Path Prefix (ex. cur/classmethod)'
Resources:
MembersCurDatabase:
Type: AWS::Glue::Database
Properties:
CatalogId: !Ref AWS::AccountId
DatabaseInput:
Name: 'members-cur'
Description: 'Database for Classmethod Members CUR'
GlueServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: 'AWSGlueServiceRole-MembersCur'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: glue.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole'
GlueServiceRoleS3Policy:
Type: AWS::IAM::Policy
Properties:
PolicyName: S3Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 's3:GetObject'
- 's3:PutObject'
Resource:
- !Sub 'arn:aws:s3:::${S3BucketName}/${S3PathPrefix}/*'
Roles:
- !Ref GlueServiceRole
MembersCurCrawler:
Type: AWS::Glue::Crawler
DependsOn:
- MembersCurDatabase
- GlueServiceRole
Properties:
Name: 'members-cur-crawler'
Description: 'Classmethod Members CUR crawler'
Role: !GetAtt GlueServiceRole.Arn
DatabaseName: 'members-cur'
Targets:
S3Targets:
- Path: !Sub
- 's3://${S3BucketName}/${FirstPathComponent}'
- FirstPathComponent: !Select [0, !Split ["/", !Ref S3PathPrefix]]
Exclusions:
- '**.json'
Schedule:
ScheduleExpression: 'cron(0 0 * * ? *)'
RecrawlPolicy:
RecrawlBehavior: 'CRAWL_NEW_FOLDERS_ONLY'
SchemaChangePolicy:
UpdateBehavior: 'LOG'
DeleteBehavior: 'LOG'
Outputs:
GlueDatabase:
Description: 'The Glue Database name'
Value: 'members-cur'
GlueCrawler:
Description: 'The Glue Crawler name'
Value: 'members-cur-crawler'
GlueServiceRoleArn:
Description: 'The ARN of the IAM role for Glue service'
Value: !GetAtt GlueServiceRole.Arn
CrawlerTargetPath:
Description: 'The actual S3 path being crawled'
Value: !Sub
- 's3://${S3BucketName}/${FirstPathComponent}'
- FirstPathComponent: !Select [0, !Split ["/", !Ref S3PathPrefix]]
テンプレートからスタックを作成する際に下記パラメータの入力が求められるので、ご自身の設定した値を入力してください。
- S3BucketName: 設定したS3バケット名
- S3PathPrefix: 設定したS3パスプレフィックス
AWS CLIで構築する場合は以下の手順を実行してください。
CLIは下記バージョンを使用しています。
aws-cli/2.24.11 Python/3.12.9 Darwin/24.3.0 source/arm64
ステップ1: Glueデータベースの作成
aws glue create-database \
--database-input '{
"Name": "members-cur",
"Description": "Database for Classmethod Members CUR"
}'
ステップ2: Glueクローラ用IAMロールの作成
aws iam create-role \
--role-name AWSGlueServiceRole-MembersCur \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "glue.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
ステップ3: IAMロールにAWS管理ポリシーをアタッチ
aws iam attach-role-policy \
--role-name AWSGlueServiceRole-MembersCur \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
ステップ4: IAMロールにS3アクセス権限を追加
aws iam put-role-policy \
--role-name AWSGlueServiceRole-MembersCur \
--policy-name S3Access \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::cm-cur-123456789012/cur/classmethod/*"
]
}
]
}'
ステップ5: Glueクローラの作成
schedule
は毎日0時に実行されるようにしています。
一時的な利用であれば schedule
の指定を行ごと削除してオンデマンド実行にしてください。
aws glue create-crawler \
--name members-cur-crawler \
--description "Classmethod Members CUR crawler" \
--role "AWSGlueServiceRole-MembersCur" \
--targets '{
"S3Targets": [
{
"Path": "s3://cm-cur-123456789012/cur/",
"Exclusions": ["**.json"]
}
]
}' \
--database-name members-cur \
--schedule "cron(0 0 * * ? *)" \
--recrawl-policy "RecrawlBehavior=CRAWL_NEW_FOLDERS_ONLY" \
--schema-change-policy '{
"UpdateBehavior": "LOG",
"DeleteBehavior": "LOG"
}'
以上で全ての設定は完了です。
クローラの定期実行により、cm-cur-123456789012バケットに出力されたメンバーズ組織CURのメタデータが毎日登録されます。
方法2: AthenaのPartition Projectionを使った方法
Glueを使わずにAthenaのPartition Projection機能を活用する、よりシンプルな方法です。
Glueを使った方法と、AthenaのPartition Projectionを使った方法との比較は後述します。
Partition Projectionについては、下記のエントリ等を参照してください。
アーキテクチャ概要
構築手順
ステップ1: Athenaデータベースの作成
Athenaで以下のDDLを実行してください。
CREATE DATABASE `members-cur`;
ステップ2: テーブルの作成
作成したデータベースで以下のDDLを実行してください。
DDL(長いため折りたたみます)
CREATE EXTERNAL TABLE `cur` (
`identity_line_item_id` string,
`identity_time_interval` string,
`bill_invoice_id` string,
`bill_billing_entity` string,
`bill_bill_type` string,
`bill_payer_account_id` string,
`bill_billing_period_start_date` timestamp,
`bill_billing_period_end_date` timestamp,
`bill_invoicing_entity` string,
`line_item_usage_account_id` string,
`line_item_line_item_type` string,
`line_item_usage_start_date` timestamp,
`line_item_usage_end_date` timestamp,
`line_item_product_code` string,
`line_item_usage_type` string,
`line_item_operation` string,
`line_item_availability_zone` string,
`line_item_resource_id` string,
`line_item_usage_amount` double,
`line_item_normalization_factor` double,
`line_item_normalized_usage_amount` double,
`line_item_currency_code` string,
`line_item_unblended_rate` string,
`line_item_unblended_cost` double,
`line_item_line_item_description` string,
`line_item_tax_type` string,
`line_item_legal_entity` string,
`product_product_name` string,
`product_accelerator_size` string,
`product_accelerator_type` string,
`product_access_type` string,
`product_activity_type` string,
`product_addon_feature` string,
`product_alarm_type` string,
`product_api_type` string,
`product_attachment_type` string,
`product_availability` string,
`product_availability_zone` string,
`product_bit_rate` string,
`product_broker_engine` string,
`product_bundle` string,
`product_cache_engine` string,
`product_cache_memory_size_gb` string,
`product_calling_type` string,
`product_capacitystatus` string,
`product_category` string,
`product_client_location` string,
`product_clock_speed` string,
`product_cloud_search_version` string,
`product_codec` string,
`product_compute_family` string,
`product_compute_type` string,
`product_concurrencyscalingfreeusage` string,
`product_content_type` string,
`product_country` string,
`product_counts_against_quota` string,
`product_cputype` string,
`product_current_generation` string,
`product_data` string,
`product_data_transfer` string,
`product_data_transfer_quota` string,
`product_database_edition` string,
`product_database_engine` string,
`product_datatransferout` string,
`product_dedicated_ebs_throughput` string,
`product_deployment_location` string,
`product_deployment_option` string,
`product_describes` string,
`product_description` string,
`product_device` string,
`product_device_type` string,
`product_direct_connect_location` string,
`product_directory_size` string,
`product_directory_type` string,
`product_directory_type_description` string,
`product_dominantnondominant` string,
`product_durability` string,
`product_ebs_optimized` string,
`product_ecu` string,
`product_edition` string,
`product_elastic_graphics_type` string,
`product_endpoint` string,
`product_endpoint_type` string,
`product_engine` string,
`product_engine_code` string,
`product_enhanced_networking_support` string,
`product_enhanced_networking_supported` string,
`product_entity_type` string,
`product_event_type` string,
`product_execution_frequency` string,
`product_execution_location` string,
`product_fee_code` string,
`product_fee_description` string,
`product_file_system_type` string,
`product_frame_rate` string,
`product_free_overage` string,
`product_free_query_types` string,
`product_free_tier` string,
`product_free_trial` string,
`product_free_usage_included` string,
`product_frequency_mode` string,
`product_from_location` string,
`product_from_location_type` string,
`product_georegioncode` string,
`product_gets` string,
`product_gpu` string,
`product_gpu_memory` string,
`product_graphqloperation` string,
`product_group` string,
`product_group_description` string,
`product_high_availability` string,
`product_indexing_source` string,
`product_ingest_type` string,
`product_input` string,
`product_input_mode` string,
`product_instance` string,
`product_instance_capacity10xlarge` string,
`product_instance_capacity12xlarge` string,
`product_instance_capacity24xlarge` string,
`product_instance_capacity2xlarge` string,
`product_instance_capacity4xlarge` string,
`product_instance_capacity8xlarge` string,
`product_instance_capacity_large` string,
`product_instance_capacity_xlarge` string,
`product_instance_family` string,
`product_instance_function` string,
`product_instance_type` string,
`product_instance_type_family` string,
`product_instances` string,
`product_instancesku` string,
`product_intel_avx2_available` string,
`product_intel_avx_available` string,
`product_intel_turbo_available` string,
`product_io` string,
`product_license` string,
`product_license_model` string,
`product_license_type` string,
`product_line_type` string,
`product_location` string,
`product_location_type` string,
`product_logs_source` string,
`product_logs_type` string,
`product_machine_learning_process` string,
`product_mailbox_storage` string,
`product_max_iops_burst_performance` string,
`product_max_iopsvolume` string,
`product_max_throughputvolume` string,
`product_max_volume_size` string,
`product_maximum_capacity` string,
`product_maximum_extended_storage` string,
`product_maximum_storage_volume` string,
`product_memory` string,
`product_memory_gib` string,
`product_memorytype` string,
`product_message_delivery_frequency` string,
`product_message_delivery_order` string,
`product_metering_type` string,
`product_min_volume_size` string,
`product_minimum_storage_volume` string,
`product_network_performance` string,
`product_newcode` string,
`product_normalization_size_factor` string,
`product_offer` string,
`product_operating_system` string,
`product_operation` string,
`product_operation_type` string,
`product_ops_items` string,
`product_origin` string,
`product_os_license_model` string,
`product_output` string,
`product_output_mode` string,
`product_overage_type` string,
`product_parameter_type` string,
`product_physical_cores` string,
`product_physical_cpu` string,
`product_physical_gpu` string,
`product_physical_processor` string,
`product_pipeline` string,
`product_port_speed` string,
`product_pre_installed_sw` string,
`product_processor_architecture` string,
`product_processor_features` string,
`product_product_family` string,
`product_protocol` string,
`product_provisioned` string,
`product_queue_type` string,
`product_readtype` string,
`product_realtimeoperation` string,
`product_recipient` string,
`product_region` string,
`product_request_description` string,
`product_request_type` string,
`product_resolution` string,
`product_resource_endpoint` string,
`product_resource_type` string,
`product_rootvolume` string,
`product_routing_target` string,
`product_routing_type` string,
`product_running_mode` string,
`product_servicecode` string,
`product_servicename` string,
`product_single_or_dual_pass` string,
`product_sku` string,
`product_software_included` string,
`product_software_type` string,
`product_standard_storage_retention_included` string,
`product_steps` string,
`product_storage` string,
`product_storage_class` string,
`product_storage_description` string,
`product_storage_media` string,
`product_storage_type` string,
`product_subscription_type` string,
`product_supported_modes` string,
`product_tenancy` string,
`product_tenancy_support` string,
`product_throughput` string,
`product_throughput_class` string,
`product_tier` string,
`product_tiertype` string,
`product_to_location` string,
`product_to_location_type` string,
`product_traffic_direction` string,
`product_transcoding_result` string,
`product_transfer_type` string,
`product_type` string,
`product_updates` string,
`product_usage_family` string,
`product_usagetype` string,
`product_uservolume` string,
`product_vcpu` string,
`product_version` string,
`product_video_codec` string,
`product_video_frame_rate` string,
`product_video_memory_gib` string,
`product_video_quality` string,
`product_video_quality_setting` string,
`product_video_resolution` string,
`product_virtual_interface_type` string,
`product_volume_api_name` string,
`product_volume_type` string,
`product_vq_setting` string,
`pricing_lease_contract_length` string,
`pricing_offering_class` string,
`pricing_purchase_option` string,
`pricing_public_on_demand_cost` double,
`pricing_public_on_demand_rate` string,
`pricing_term` string,
`pricing_unit` string,
`reservation_amortized_upfront_cost_for_usage` double,
`reservation_amortized_upfront_fee_for_billing_period` double,
`reservation_availability_zone` string,
`reservation_effective_cost` double,
`reservation_end_time` string,
`reservation_modification_status` string,
`reservation_normalized_units_per_reservation` string,
`reservation_number_of_reservations` string,
`reservation_recurring_fee_for_usage` double,
`reservation_reservation_a_r_n` string,
`reservation_start_time` string,
`reservation_total_reserved_normalized_units` string,
`reservation_total_reserved_units` string,
`reservation_units_per_reservation` string,
`reservation_unused_amortized_upfront_fee_for_billing_period` double,
`reservation_unused_normalized_unit_quantity` double,
`reservation_unused_quantity` double,
`reservation_unused_recurring_fee` double,
`reservation_upfront_value` double,
`resource_tags_user_cm_billing_group` string,
`savings_plan_total_commitment_to_date` double,
`savings_plan_savings_plan_a_r_n` string,
`savings_plan_savings_plan_rate` double,
`savings_plan_used_commitment` double,
`savings_plan_savings_plan_effective_cost` double,
`savings_plan_amortized_upfront_commitment_for_billing_period` double,
`savings_plan_recurring_commitment_for_billing_period` double,
`savings_plan_region` string,
`savings_plan_payment_option` string,
`savings_plan_end_time` string,
`savings_plan_instance_type_family` string,
`savings_plan_purchase_term` string,
`savings_plan_offering_type` string,
`savings_plan_start_time` string
)
PARTITIONED BY (
`year` string,
`month` string
)
ROW FORMAT SERDE
'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'
STORED AS INPUTFORMAT
'org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat'
LOCATION
's3://cm-cur-123456789012/cur/'
TBLPROPERTIES (
'partition_filtering.enabled'='true',
'projection.enabled'='true',
'projection.year.type'='date',
'projection.year.range'='2025,NOW+9HOURS',
'projection.year.format'='yyyy',
'projection.year.interval'='1',
'projection.year.interval.unit'='YEARS',
'projection.month.type'='integer',
'projection.month.range'='1,12',
'projection.month.format'='%d',
'storage.location.template'='s3://cm-cur-123456789012/cur/classmethod/org-cur-parquet/org-cur-parquet/year=${year}/month=${month}'
);
2つの方法の比較
Glueを使った方法と、AthenaのPartition Projectionを使った方法を比較します。
ご自身の環境により使い分けてください。
Glueを使った方法 | Partition Projectionを使った方法 | |
---|---|---|
環境構築の複雑さ | 複雑(複数リソースの設定が必要) | シンプル(SQLクエリのみ) |
クエリのパフォーマンス [1] | 高い | 条件によって低下の可能性あり |
汎用性 [2] | 高い(Redshift、EMRなどと連係可能) | 低い(Athenaのみ対応) |
Athenaでの分析
これまでの設定でメンバーズ組織CURをAthenaで分析可能になります。
CURの各カラムについてはAWSのドキュメントをご確認ください。
クエリサンプル
Athenaの環境構築が完了したことを確認するためクエリを実行してみます。
例として、メンバーズポータルで表示される料金を確認します。
WITH "categorized_billing" AS (
SELECT
"year",
"month",
"line_item_usage_account_id",
CASE
WHEN "line_item_line_item_type" = 'Fee' THEN '前払料金'
WHEN "line_item_line_item_type" IN ('RIFee', 'SavingsPlanRecurringFee') THEN '毎月料金'
WHEN "bill_billing_entity" = 'AWS Marketplace' THEN 'マーケットプレイス料金'
WHEN "bill_billing_entity" = 'CM' AND "product_product_name" = 'Classmethod Members Discount' THEN 'メンバーズ割引'
ELSE '通常利用料金'
END AS "category",
"line_item_unblended_cost"
FROM "members-cur"."cur"
WHERE "line_item_usage_account_id" IN (
'123456789012' -- 分析したいアカウントのID
)
)
SELECT
"year" AS "年",
"month" AS "月",
"line_item_usage_account_id" AS "AWSアカウントID",
"category" AS "料金カテゴリ",
SUM("line_item_unblended_cost") AS "金額"
FROM "categorized_billing"
WHERE "year" = '2025'
AND "month" = '2'
GROUP BY
"year",
"month",
"line_item_usage_account_id",
"category"
ORDER BY
"year",
"month",
"line_item_usage_account_id",
"category";
このクエリを実行すると、メンバーズポータルに表示されている金額と一致することを確認できます。
まとめ
メンバーズ組織CURを活用することで、複数アカウントのコスト情報を一元管理・分析できるようになりました。
本記事で紹介した2つの方法から、ご自身の環境に合った方法を選択して実装してください。
- Glueを使った方法: 他のAWSサービスとの連携が必要な場合に最適
- Partition Projectionを使った方法: シンプルな環境で素早く始めたい場合に最適
詳細なコスト分析を通じて、AWSリソースの最適化にお役立てください!
Partition Projectionでは、クエリ実行時のWHERE句でパーティションを指定せずにフルスキャンした場合、設定した全てのパーティションを読みにいってパフォーマンスが低下する可能性があります。
projection.date.range
の指定には注意してください。
下記エントリが参考になります。
https://dev.classmethod.jp/articles/partition-projection-shikujiri-s3-cost-increase/ ↩︎Partition ProjectionはAthenaのクエリエンジンのみサポートする機能なので、別のクエリエンジンではデータが参照できません。Redshift Spectrum、Glue、EMRなど、別のサービスを通じて同じテーブルを参照する場合はパーティションが登録されていないため利用できません。 ↩︎