CUR2.0をAthenaに統合してみた

AWS

はじめに

これまでは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を作成

ポイントのみ記載。

  1. データエクスポートから標準データエクスポートを選択
  2. 列選択では全て選択(カスタマイズできるのが利点だが今回はしてない)
  3. Parquetを選択
  4. ファイルのバージョニングは上書き
    1. レガシーでは上書きにする必要があるが、今回のアプローチだと何でもよいはず
  5. AWSコンソールで作成する場合、S3バケットを新規作成すれば必要なバケットポリシーをつけてくれるので便利

※ 列選択をする場合、最初に最小限に絞り込むと後で他の項目が必要になった際には、CURの編集で対応できないので再作成をする必要がでてくるので注意。Athena側での影響は把握できていない。旧スキーマのS3オブジェクトと新スキーマのS3オブジェクトが混在したときに、Athenaのテーブルが正常に動作するのかが気になる。

CUR2.0をAthenaに統合する

最終的にはCFnテンプレートでテーブルなどを実装したのだが、

その設定値を決めるためにGlueクローラーを実行して大枠を把握するのがおすすめだと思う。

各列のデータ型をクローラーが解析してよい感じに指定したうえでテーブルを作成してくれるので、その内容を活用して詳細な実装を行う。

次にパーティションの更新をどのように実装するかがポイントになる。

結論としては、Partition projection(パーティション射影)を使う。

こうすることで定期的にGlueクローラーを実行する必要もないし、MSCK REPAIRALTER 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の作成数は削減できる。

無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!