前回の記事の続きです。
- AWSリソース
- CURの作成
- メンバーアカウント共有用S3バケットの作成
- オブジェクトACLを書き換えるLambda関数用の作成
- 過去の CUR データを S3 バケットに出力する
- 定期実行用 Athena クエリの作成
- Athena クエリを実行する Lambda 関数の作成
- Lambda 関数の実行トリガーの設定
- メンバーアカウントの Glue クローラー設定
- 最後に
AWSリソース
本記事に登場する AWS リソースを紹介します。この記事を読んでやってみたい人は各自の環境に沿った値に置き換えてください。
- リージョン:東京(
ap-northeast-1
) - AWSアカウント
- 管理アカウント
- 組織内のAWSアカウントの利用料金をまとめて支払う一括請求アカウント。CUR が設定されている
- アカウント ID:
111111111111
- メンバーアカウント
- 組織内で作成された AWS アカウント
- アカウント ID:
222222222222
- 管理アカウント
- S3バケット
cur-billing
:CURデータの出力先S3バケットlinkedsubacct
:Athenaクエリの出力結果を保存するS3バケット- 出力先のフォルダ:
billing
- 出力先のフォルダ:
- AWS Lambda
S3LinkedPutACL
:S3バケット(linkedsubacct
)にファイルアップロードされたイベントをトリガーにオブジェクト ACL を書き換えるSubAcctSplit
:CUR の Glue クローラーの実行成功をトリガーにメンバーアカウントのコストデータを CTAS クエリでS3バケット(linkedsubacct
)にアップロードする
- IAMロール
LambdaPutLinkedS3ACL
:Lambda関数S3LinkedPutACL
用 IAM ロールLambdaSubAcctSplit
:Lambda関数SubAcctSplit
用 IAM ロール
CURの作成
はじめに管理アカウントで CUR を作成する必要があります。以下のワークショップか、自著「Amazon Web Servicesコスト最適化入門」9章を参考に設定してください。
Level 300: Automated CUR Updates and Ingestion :: AWS Well-Architected Labs
メンバーアカウント共有用S3バケットの作成
Athena クエリの出力結果を保存する S3 バケット(linkedsubacct
)を管理アカウントに作成します。この S3 バケットに対して、メンバーアカウントの Athena と Glue が実行できるようにバケットポリシーを設定します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowListingOfFolders", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111111111111:root" }, "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::linkedsubacct" }, { "Sid": "AllowAllS3ActionsInSubFolder", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111111111111:root" }, "Action": "s3:*", "Resource": "arn:aws:s3:::linkedsubacct/*" }, { "Sid": "AllowSubAccountAthenaGlueAccess", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::222222222222:root" }, "Action": [ "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::linkedsubacct", "arn:aws:s3:::linkedsubacct/*" ] } ] }
オブジェクトACLを書き換えるLambda関数用の作成
先ほど作成した S3 バケット(linkedsubacct
)にアップロードされた S3 オブジェクトは CUR と管理アカウントしか取得することができません。バケットポリシー上はメンバーアカウントからも取得できるのですが、オブジェクトに付与されている ACL でメンバーアカウントが許可されていないのでこのままではアクセスすることができません。そこで、管理アカウントとメンバーアカウントが取得できるようにオブジェクト ACL を書き換える Lambda 関数(S3LinkedPutACL
)を作成します。
IAMポリシーの作成
Lambda 関数(S3LinkedPutACL
)で使用する IAM ロールにアタッチする、IAM ポリシーを作成します。IAM ポリシー名は Lambda_S3Linked_PutACL
とします。権限は以下のとおりです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:PutObjectVersionAcl", "s3:PutObjectAcl" ], "Resource": "arn:aws:s3:::linkedsubacct/*" } ] }
IAM ロールの作成
Lambda 関数(S3LinkedPutACL
)に付与する IAM ロール(LambdaPutLinkedS3ACL
)を作成します。先ほど作成した IAM ポリシー Lambda_S3Linked_PutACL
と CloudWatch Logs にログ出力させるための IAM ポリシー AWSLambdaBasicExecutionRole
をアタッチします。
Lambda 関数の作成
S3LinkedPutACL
という名前で Lambda 関数を作成します。ソースコードは AWS が提供されているものを使います。
項目グループ | 項目 | 値 | 備考 |
---|---|---|---|
コード | ランタイム | Node.js 14.x | |
一般設定 | メモリ | 160MB | AWS Compute Optimizerのレコメンドから |
タイムアウト | 10秒 | ||
ロール名 | LambdaPutLinkedS3ACL | ||
環境変数 | payeraccountid | 111111111111 | 管理アカウントのAWSアカウントID |
linkedaccountid | 222222222222 | メンバーアカウントのAWSアカウントID | |
canonicalidpayer | xxxxxxxxxxxxxx | 管理アカウントのカノニカルID(正規ユーザーID) | |
canonicalidlinked | yyyyyyyyyyyyyy | メンバーアカウントのカノニカルID(正規ユーザーID) |
カノニカル ID(正規ユーザー ID )は以下のコマンドで調べることができます。
aws s3api list-buckets --query Owner.ID --output text
S3 バケット通知の設定
s3://linkedsubacct/billing/
に CUR データファイルがアップロードされる度に、Lambda 関数(S3LinkedPutACL
)が実行されるイベントを設定します。 S3 バケット linkedsubacct
の「プロパティ」タブから「イベント通知」の「イベント通知を作成」を選択します。
項目 | 値 |
---|---|
イベント名 | LambdaPutLinkedS3ACL |
プレフィックス | billing |
イベントタイプ | すべてのオブジェクト作成イベント(s3:ObjectCreated:* ) |
送信先 | Lambda 関数 |
Lambda 関数を特定 | S3LinkedPutACL を選択 |
設定したら s3://linkedsubacct/billing/
に適当なファイルをアップロードして正しく動作していることを確認します。アップロードした S3 オブジェクトの「アクセス許可」タブから「外部アカウント」にメンバーアカウントが追加されていることを確認します。もし付与されていなければ、CloudWatch Logs に出力されているログから原因を調査してください。設定が確認できたらアップロードした S3 オブジェクトを削除してください。
過去の CUR データを S3 バケットに出力する
今まで貯めてきた CUR データがある場合は、s3://linkedsubacct/billing/
にデータをアップロードします。これは一度きりの作業です。管理アカウントの Athena コンソールに移動し、以下のクエリを実行して一時テーブルを作成します。
CREATE TABLE <CURデータベース名>.temp_table WITH ( format = 'Parquet', parquet_compression = 'GZIP', external_location = 's3://linkedsubacct/billing', partitioned_by=ARRAY['year_1','month_1']) AS SELECT *, year as year_1, month as month_1 FROM "<CURデータベース名>"."<CURテーブル名>" WHERE line_item_usage_account_id IN ('222222222222', 'xxxxxxxxxx' ...);
WHERE 句で抽出したいメンバーアカウントの AWS アカウント ID を指定します。前回の記事で述べた部門単位でデータを抽出したい場合は、IN 句に抽出したい AWS アカウント ID をすべて含めてください。
また、CUR すべてのカラムを持ってくるとデータ量が多くなり、S3 のストレージコストが増大します。あらかじめ分析に利用するカラムが明確であれば、そのカラムのみを指定することでストレージコストをおさえることができます。以下のクエリはその例です。各カラムの意味は自著「Amazon Web Servicesコスト最適化入門」の「9.6 よく使うカラム」をご参照ください。
CREATE TABLE <CURデータベース名>.temp_table WITH ( format = 'Parquet', parquet_compression = 'GZIP', external_location = 's3://linkedsubacct/billing', partitioned_by=ARRAY['year_1','month_1']) AS SELECT line_item_usage_account_id, line_item_product_code, line_item_resource_id, line_item_operation, line_item_usage_type, line_item_line_item_description, line_item_line_item_type, bill_bill_type, line_item_unblended_cost, line_item_net_unblended_cost, line_item_usage_start_date, resource_tags_user_xxxx, cost_category_xxx, year as year_1, month as month_1 FROM "<CURデータベース名>"."<CURテーブル名>" WHERE line_item_usage_account_id IN ('222222222222', 'xxxxxxxxxx' ...);
クエリを実行すると s3://linkedsubacct/billing/
にパーティションで区切られた CUR データファイルが作成されます。各ファイルに管理アカウントとメンバーアカウントのカノニカル ID が付与されていることを確認します。
問題なくデータ抽出ができたら、以下のクエリで一時テーブルを削除します。
DROP TABLE "<CURデータベース名>"."temp_table";
定期実行用 Athena クエリの作成
先程の作業は過去の CUR データをコピーする作業でした。今度は新しい CUR データファイルがアップロードされる度に s3://linkedsubacct/billing/
に出力する Athena クエリを作成します。このクエリは後述の Lambda 関数から呼ばれます。SELECT 文で取得するカラムは先ほど実行したクエリに揃えてください。create_linked_billing
というクエリ名で保存します。
CREATE TABLE <CURデータベース名>.temp_table WITH ( format = 'Parquet', parquet_compression = 'GZIP', external_location = 's3://linkedsubacct/billing/__subfolder__') AS SELECT * FROM "<CURデータベース名>"."<CURテーブル名>" WHERE line_item_usage_account_id IN ('222222222222', 'xxxxxxxxxx') AND year=CAST(year(current_date - INTERVAL '__interval__' MONTH) AS VARCHAR) AND month=CAST(month(current_date - INTERVAL '__interval__' MONTH) AS VARCHAR)
__subfolder__
、__interval__
は Lambda 関数で置換されるプレースホルダです。すべての CUR データを毎回洗い替えすると Athena クエリコストが増えてしまうため、先月・当月分の CUR データのみを洗い替えします。
Athena クエリを実行する Lambda 関数の作成
先ほど保存した Athena クエリ(create_linked_billing
)を実行するための Lambda 関数(SubAcctSplit
)を設定します。
IAM ポリシーの作成
Lambda 関数(SubAcctSplit
)で使用する IAM ロールにアタッチする、IAM ポリシーを作成します。IAM ポリシーの名前は LambdaSubAcctSplit
とします。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "athena:GetNamedQuery", "athena:GetQueryExecution", "athena:GetQueryResults", "athena:ListNamedQueries", "athena:ListQueryExecutions", "athena:StartQueryExecution", "s3:GetObject", "s3:DeleteObject", "s3:DeleteObjectVersion", "s3:ListBucket", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::linkedsubacct/*", "arn:aws:athena:*:*:workgroup/*" ] }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::*" }, { "Sid": "VisualEditor2", "Effect": "Allow", "Action": [ "glue:GetDatabase", "glue:CreateTable", "glue:GetPartitions", "glue:GetPartition", "glue:DeleteTable", "glue:GetTable" ], "Resource": "*" }, { "Sid": "VisualEditor3", "Effect": "Allow", "Action": [ "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload", "s3:CreateBucket", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::aws-athena-query-results-*" ] }, { "Sid": "VisualEditor4", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:ListBucket" ], "Resource": "arn:aws:s3:::cur-billing/*" }, { "Sid": "VisualEditor5", "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" } ] }
IAMロールの作成
Lambda 関数(SubAcctSplit
)に付与する IAM ロール(LambdaSubAcctSplit
)を作成します。先ほど作成した IAM ポリシー LambdaSubAcctSplit
をアタッチします。
Lambda 関数の作成
SubAcctSplit
という名前で Lambda 関数を作成します。ソースコードは GitHub Gist に載せました。
項目グループ | 項目 | 値 | 備考 |
---|---|---|---|
コード | ランタイム | Python 3.8 | |
一般設定 | メモリ | 128MB | |
タイムアウト | 180秒 | Athenaクエリの実行時間に応じて設定 | |
ロール名 | LambdaSubAcctSplit | ||
環境変数 | athena_database | athenacurcfn_billing | CUR データベース名 |
athena_output_location | s3://aws-athena-query-results-111111111111-ap-northeast-1/ | Athena クエリ結果の保存先 | |
bucket_name | linkedsubacct | ||
subfolder | billing | ||
extraction_query_name | create_linked_billing | Athena クエリ名 |
作成したらテストボタンをクリックして動作確認します。アップロード先 S3 バケット(s3://linkedsubacct/billing/
)に先月分と当月分の CUR ファイルがアップロードされている(すでにファイルがある場合はタイムスタンプが更新される)ことを確認します。2021年5月に実行したとすると、以下のフォルダが作成されて、抽出した CUR データファイルが保存されているはずです。
s3://linkedsubacct/billing/year_1=2021/month_1=4/ s3://linkedsubacct/billing/year_1=2021/month_1=5/
Lambda 関数の実行トリガーの設定
先ほど作成した Lambda 関数(SubAcctSplit
)を実行するためのトリガーを Amazon EventBridge(以下、EventBridge)で設定します。実行タイミングは、CUR を更新する Glue クローラーが完了した後です。EventBridge のマネジメントコンソールに移動し、イベント→ルールを選択、「ルールを作成」をクリックします。
以下の設定で EventBridge ルールを作成します。
項目 | 値 |
---|---|
名前 | PayerGlueCrawlerRule |
説明 | Rules triggered by Payer Glue Crawler |
説明 | イベントパターン |
イベント一致パターン | カスタムパターン |
イベントパターン | 下記に後述 |
ターゲット | Lambda関数 |
機能 | SubAcctSplit |
イベントパターン。AWSCURCrawler-billing
は CUR 設定時に自動作成された Glue Crawler 名に置き換えてください。
{ "detail-type": [ "Glue Crawler State Change" ], "source": [ "aws.glue" ], "detail": { "crawlerName": [ "AWSCURCrawler-billing" ], "state": [ "Succeeded" ] } }
設定後、Glue のマネジメントコンソールに行き、「クローラーの実行」を選択して Glue クローラーを手動実行します。Lambda 関数 SubAcctSplit
が実行されて正常に動作できていることを確認します。
メンバーアカウントの Glue クローラー設定
メンバーアカウントでマネジメントコンソールにログインし、Glue クローラーを作成します。この Glue クローラーはデータ抽出先(s3://linkedsubacct/billing/
)を定期的にクロールし、データカタログを更新します。Glue のマネジメントコンソールに行き、「Crawler」から「Add crawler」を選択します。
項目 | 値 |
---|---|
クローラーの名前 | SubAcct-Crawler |
Crawler source type | Data stores |
Repeat crawls of S3 data stores | Crawl all folders |
データストアの選択 | S3 |
クロールするデータの場所 | 指定されたパス |
インクルードパス | s3://linkedsubacct/billing/ |
エクスクルードパターン | .json, .yml, .sql, .csv, .gz, .zip |
別のデータストアの追加 | いいえ |
IAM ロールの選択 | IAM ロールを作成する |
IAM ロール | AWSGlueServiceRole-subacct-crawler |
頻度 | 毎日 |
Start Hour (UTC) | 00 |
Start Minute | 10 |
データベース | sub-acct( 「データベースの追加」を選択して入力) |
作成後、「クローラーの実行」を選択してクローラーを手動実行します。Athena のマネジメントコンソールに行き、Database(sub-acct)に billing
というテーブルが作成されていることを確認します。もし、作成されていなければ、CloudWatch Logs のロググループ /aws-glue/crawlers
からクローラのログを見て原因を調査してください。また、クエリを実行できることを確認します。もし、Access Denied になったら S3 バケット(linkedsubacct
)のバケットポリシーと S3 オブジェクトの ACL の設定を確認してください。
最後に
やや複雑な構成になりましたが、CUR のデータの一部を抽出してメンバーアカウントに共有して Athena クエリを実行する方法を書きました。ここまでの構成にすることはあまりないかもしれませんが、きちんとコスト分析と可視化をしたい人たちにとって役に立てれば嬉しいです。