はじめに
これまではCost ExplorerでAWSの使用量やコストを確認していた作業をCURで確認できるようにするために本対応を行った。
CUR(レガシー)とCUR2.0の2つが候補として利用できるが、今回はCUR2.0を採用した。
CUR2.0ではAthena統合がAWSからオプションとして提供されておらず、自前で構築する必要があった。本記事ではその実装について触れる。
CUR2.0はAWS Data Exportsが提供している機能の1つである。現時点(2024-06-06)では英語のドキュメントのみ提供されている。
この記事はCUR2.0を初めて使用する方、またはCUR2.0のAthenaへの統合に興味がある方を対象とする。
CUR2.0とは?
CURはAWS のコストと使用状況に関する最も詳細な情報を取得できる。
2.0では主に下記の改良が加えられている。
- 一貫したスキーマ(Consistent schema): 2.0は固定の列セット。レガシーは使用状況により可変になることも。
- ネストされたデータ(Nested data): key-valueのデータ構造で保持。よい感じの説明ができないのでドキュメント確認を推奨。
- 追加の列(Additional columns): 新しく2つの列が追加。
レガシーはCost and Usage Report APIであり、2.0はData Exports APIであるので厳密には別々のサービスが提供しているものである。
CUR2.0を選んだ理由
明確な理由はない。
強いて挙げるならば下記。
- CUR2.0の方が新しい
- 列のスキーマを自由に選択して定義できる
- レガシーのAthena統合で用意されるCloudFormationテンプレートが微妙だと思った
- Athena統合のハードルはレガシーのほうが低いと思う
レガシーでAthena統合をする場合、このテンプレートをデプロイしてAthena統合を行う。
軽く確認しただけなので理解が正しいか分からないが、統合の仕組みは下記認識である。
S3イベント通知を設定して、CURの更新を検知する。(Athena統合の場合、CURの設定はS3オブジェクトの上書きしか選択できず、圧縮タイプもParquetのみになる)
Lambdaが発火してGlueクローラーを実行する。これによりAthenaでクエリするときのパーティションを適宜、更新することができる。
初回はその他の必要なリソースの定義なども行っているはず。
CUR2.0を作成
ポイントのみ記載。
- データエクスポートから標準データエクスポートを選択
- 列選択では全て選択(カスタマイズできるのが利点だが今回はしてない)
- Parquetを選択
- ファイルのバージョニングは上書き
- レガシーでは上書きにする必要があるが、今回のアプローチだと何でもよいはず
- AWSコンソールで作成する場合、S3バケットを新規作成すれば必要なバケットポリシーをつけてくれるので便利
※ 列選択をする場合、最初に最小限に絞り込むと後で他の項目が必要になった際には、CURの編集で対応できないので再作成をする必要がでてくるので注意。Athena側での影響は把握できていない。旧スキーマのS3オブジェクトと新スキーマのS3オブジェクトが混在したときに、Athenaのテーブルが正常に動作するのかが気になる。
CUR2.0をAthenaに統合する
最終的にはCFnテンプレートでテーブルなどを実装したのだが、
その設定値を決めるためにGlueクローラーを実行して大枠を把握するのがおすすめだと思う。
各列のデータ型をクローラーが解析してよい感じに指定したうえでテーブルを作成してくれるので、その内容を活用して詳細な実装を行う。
次にパーティションの更新をどのように実装するかがポイントになる。
結論としては、Partition projection(パーティション射影)を使う。
こうすることで定期的にGlueクローラーを実行する必要もないし、MSCK REPAIR
やALTER TABLE ADD PARTITION
を実行する仕組みを用意する必要もなくなる。
パフォーマンスやユースケースを吟味すると、他の方法がマッチするかもしれない。
一応、テーブル定義のYAML
DemoTable:
Type: AWS::Glue::Table
Properties:
CatalogId: !Ref AWS::AccountId
DatabaseName: !Ref CURDatabase
TableInput:
StorageDescriptor:
Columns:
- Name: bill_bill_type
Type: string
- Name: bill_billing_entity
Type: string
〜省略〜
Location: !Sub s3://${CurBucket}/プレフィックス/CUR名/data/
InputFormat: org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat
OutputFormat: org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat
SerdeInfo:
SerializationLibrary: org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe
Parameters:
parquet.compression: SNAPPY
PartitionKeys:
- Name: billing_period
Type: string
TableType: EXTERNAL_TABLE
Parameters:
projection.enabled: 'true'
projection.billing_period.type: date
projection.billing_period.range: 2024-03,NOW
projection.billing_period.format: yyyy-MM
projection.billing_period.interval: '1'
projection.billing_period.interval.unit: MONTHS
storage.location.template: !Join
- ''
- - s3://
- !Ref CurBucket
- /プレフィックス/CUR名/data/BILLING_PERIOD=${billing_period}/
補足)
CUR2.0定義時の列スキーマに合わせて Columns
に全て列挙する。
Location
はCUR定義時のプレフィックスと名前に合わせる。
パーティション射影ではカスタム S3 ストレージを使う。
AthenaはHIVE形式だとパーティション射影であっても自動で検知(PartitionKeysで定義しているものと対応)できるはずだが、おそらくキーの一部の BILLING_PERIOD
が大文字であることで非HIVE形式になってしまうので、カスタムS3ストレージで明示的にパーティションを定義する。
CFnで定義していることによる面倒なことが1つあり、 !Join
で文字列を作り込んでいる点である。通常ならば !Sub
を使いスマートに記載したいところだが、 ${}
が !Sub
とカスタムS3ストレージの構文で競合してしまうのでこの実装で回避している。
パーティション射影の設定値の補足説明は下記の通り。
billing_period
を日付型する。範囲は実装時点の開始月から現在(NOW)までとする。
CUR2.0で出力されるS3キーの構造に合わせて yyyy-MM
にする。単位は月として1ヶ月間隔とする。
おわりに
本件、かなり大変なことが多かったが一応なんとか形にはできた。
CUR自体を初めて触ったので、レガシーと2.0は何が違うのかということから理解する必要があった。その際にCUR2.0は英語のドキュメントしかなく情報検索に苦労した。(英語ドキュメントのみであること自体はあまり問題ではないが、ドキュメントページの言語を切替えると一般的な挙動をしないのが厄介)
CUR2.0のS3への出力は、一見するとHIVE形式になってそうだが実際は非対応なところも躓きポイント。
CURのAthena統合について、レガシーを使うべきだという意見があればぜひコメントしてほしい。
下記に本筋とは外れる内容をついでに供養しておく。
Tips
MSCK REPAIR TABLEもS3パスに大文字があるとパーティションが読み込めない
Amazon S3 object key casing – Make sure that the Amazon S3 path is in lower case instead of camel case (for example, userid instead of userId), or use ALTER TABLE ADD PARTITION to specify the object key names. For more information, see Change or redefine the Amazon S3 path later in this document.
CUR2.0の場合、 BILLING_PERIOD
という大文字がキーに入るのでMSCKでパーティションが読み込めない。ALTER TABLE ADD PARTITION
を使うと回避はできる。
リンクの内容ではHIVE形式が関係していることが明確に言及されてはいないが、本質はHIVE形式になっているかどうかがポイントであると考えている。
開発中にMSCKで動作確認をしたかった際に、この現象に遭遇して苦戦した。
また、最初からこの原因にたどり着いたわけではない。初めは LOCATION
の末尾のスラッシュが省略されることが原因だと思いこんでしまい沼にハマった。
CREATE TABLE
ステートメントでLOCATION
を指定する場合は、次のガイドラインを使用します。
- 末尾にスラッシュを使用します。
多分、下記DDLでテーブルを作成するとスラッシュが省略されるはず。
CREATE EXTERNAL TABLE cur_demo (
bill_bill_type STRING,
bill_billing_entity STRING,
〜省略〜
)
PARTITIONED BY (billing_period STRING)
STORED AS PARQUET
LOCATION 's3://cur2/hoge/fuga/data/'
TBLPROPERTIES ("parquet.compression" = "SNAPPY");
当初、CURをクロスアカウントで構築するアーキテクチャを実装していたができなかった
The account that creates the Cost and Usage Report must also own the Amazon S3 bucket that AWS sends the reports to. Avoid configuring a Cost and Usage Report with a bucket owned by another account.
ドキュメントでは非推奨であるが実装は可能だと考えて検証してみたが、結論としてはサポートに確認して実装自体ができないとのことだった。
背景としては、アカウントXのS3バケットに全てのCURを集約して、そのS3バケット1つに対してAthenaのテーブルを定義できる構成を考えていた。
最終的には一度、各アカウントのS3に出力してレプリケーション機能でアカウントXに集約するアプローチにした。この場合だと、S3パスの関係でAthenaのテーブルが1つにできず、CURの数だけAthenaテーブルを作成することになった。
レプリケーション後に、更にS3トリガーでパスを整形して再配置するみたいなことをすればこの問題は解決するかもしれない。
CURの設定する単位はAWS Organizationsのルートアカウント単位で作成すれば、CURの作成数は削減できる。