モチベーション
2022 年 2 月 1 日以降、AWS Inc. に代わり AWS Japan が日本国内の AWS サービスの契約当事者となりました。(請求書払いアカウントは 2021 年 11 月 1 日 以降)
そのためほとんどのサービス利用料については現在 AWS Japan が請求元になっているのですが、AWS Marketplace および、Amazon SES, SNS, Connect などの AMCS LLC が販売する一部のサービスについては引き続き請求元は AWS Inc. となっています。
Cost Explorer API を使用してアカウントおよび、請求元の法人 (サービスプロバイダ) 毎に請求額を取得し、以下のような CSV で出力する機会がありましたので紹介します。
Account Id | Account Name | Legal Entity | Amount (USD) |
---|---|---|---|
000000000000 | account-0000 | Amazon Web Services Japan G.K. | 42.7927165287 |
111111111111 | account-1111 | Amazon Web Services Japan G.K. | 32.2633798094 |
222222222222 | account-2222 | Amazon Web Services Japan G.K. | 751.7103483954 |
222222222222 | account-2222 | Amazon Web Services, Inc. | 386.7895005956 |
333333333333 | account-3333 | Amazon Web Services Japan G.K. | 4.6428 |
444444444444 | account-4444 | Amazon Web Services Japan G.K. | 407.74542118 |
444444444444 | account-4444 | Datadog Inc. | 18.03 |
555555555555 | account-5555 | Amazon Web Services Japan G.K. | 4.7548 |
... |
API リクエストの例
AWS Lambda を使用して前月分の請求額を取得する最小限の例です。
import datetime
import boto3
def lambda_handler(event, context):
today = datetime.date.today()
start = today.replace(month=today.month-1, day=1).strftime('%Y-%m-%d')
end = today.replace(day=1).strftime('%Y-%m-%d')
ce = boto3.client('ce')
response = ce.get_cost_and_usage(
TimePeriod={
'Start': start,
'End' : end,
},
Granularity='MONTHLY',
Metrics=[
'NetUnblendedCost'
],
GroupBy=[
{
'Type': 'DIMENSION',
'Key': 'LINKED_ACCOUNT'
},
{
'Type': 'DIMENSION',
'Key': 'LEGAL_ENTITY_NAME'
}
]
)
return response['ResultsByTime'][0]['Groups']
Cost Explorer コンソールではグループ化条件は 1 つまでしか適用できませんが、Cost Explorer API では最大 2 つのグループ化条件を指定することができます。上記の例では GroupBy で Linked Account ごと、請求元法人ごとに請求額を取得しています。
Pandas で結果を加工する
前述の例で得られるレスポンスはネストされた json になっているため、Pandas を使用して CSV に加工する例を考えます。AWS Lambda で Pandas を使用する際には、Lambda Layers を使用する必要がある点についてご注意ください。
pandas.json_normalize を使用して json をフラット化します。
import pandas
normalized_json = pandas.json_normalize(get_cost_json(start, end))
print(normalized_json)
Keys Metrics.NetUnblendedCost.Amount Metrics.NetUnblendedCost.Unit
0 [000000000000, Amazon Web Services Japan G.K.] 42.7927165287 USD
1 [111111111111, Amazon Web Services Japan G.K.] 32.2633798094 USD
2 [222222222222, Amazon Web Services Japan G.K.] 751.7103483954 USD
3 [222222222222, Amazon Web Services, Inc.] 386.7895005956 USD
4 [333333333333, Amazon Web Services Japan G.K.] 4.6428 USD
.. ... ... ...
20 [444444444444, Amazon Web Services Japan G.K.] 407.74542118 USD
21 [444444444444, Datadog Inc.] 18.03 USD
22 [555555555555, Amazon Web Services Japan G.K.] 4.7548 USD
23 [666666666666, Amazon Web Services Japan G.K.] 225.1362646108 USD
24 [777777777777, Amazon Web Services Japan G.K.] 93.5262513848 USD
[25 rows x 3 columns]
上記結果から Keys をリスト化し、請求額 (今回の例では Metrics.NetUnblendedCost.Amount) と pandas.concat で連結すると、以下のような出力になります。目的の形に近づきました。
split_keys = pandas.DataFrame(
normalized_json['Keys'].tolist(),
columns=['Account Id', 'Legal Entity']
)
cost = pandas.concat([split_keys, normalized_json['Metrics.NetUnblendedCost.Amount']], axis=1)
print(cost)
Account Id Legal Entity Metrics.NetUnblendedCost.Amount
0 000000000000 Amazon Web Services Japan G.K. 42.7927165287
1 111111111111 Amazon Web Services Japan G.K. 32.2633798094
2 222222222222 Amazon Web Services Japan G.K. 751.7103483954
4 222222222222 Amazon Web Services, Inc. 386.7895005956
4 333333333333 Amazon Web Services Japan G.K. 4.6428
.. ... ... ...
20 444444444444 Amazon Web Services Japan G.K. 407.74542118
21 444444444444 Datadog, Inc. 18.03
22 555555555555 Amazon Web Services Japan G.K. 4.7548
23 666666666666 Amazon Web Services Japan G.K. 225.1362646108
24 777777777777 Amazon Web Services Japan G.K. 93.5262513848
[25 rows x 3 columns]
上記の Datadog, Inc. のように AWS Marketplace から購入したものは Amazon Web Services, Inc ではなく提供元の法人名が入ります。
最終的な Lambda 関数の例
Cost Explorer API ではアカウント名は取得できないため、AWS Organizations の API で取得した結果をマージし、最終的にアカウント名でソートしています。
from logging import getLogger, INFO
import os
import datetime
import boto3
import pandas
from botocore.exceptions import ClientError
logger = getLogger()
logger.setLevel(INFO)
def upload_s3(output, key, bucket):
try:
s3_resource = boto3.resource('s3')
s3_bucket = s3_resource.Bucket(bucket)
s3_bucket.upload_file(output, key, ExtraArgs={'ACL': 'bucket-owner-full-control'})
except ClientError as err:
logger.error(err.response['Error']['Message'])
raise
def get_ou_ids(org, parent_id):
ou_ids = []
try:
paginator = org.get_paginator('list_children')
iterator = paginator.paginate(
ParentId=parent_id,
ChildType='ORGANIZATIONAL_UNIT'
)
for page in iterator:
for ou in page['Children']:
ou_ids.append(ou['Id'])
ou_ids.extend(get_ou_ids(org, ou['Id']))
except ClientError as err:
logger.error(err.response['Error']['Message'])
raise
else:
return ou_ids
def list_accounts():
org = boto3.client('organizations')
root_id = 'r-xxxx'
ou_id_list = [root_id]
ou_id_list.extend(get_ou_ids(org, root_id))
accounts = []
try:
for ou_id in ou_id_list:
paginator = org.get_paginator('list_accounts_for_parent')
page_iterator = paginator.paginate(ParentId=ou_id)
for page in page_iterator:
for account in page['Accounts']:
item = [
account['Id'],
account['Name'],
]
accounts.append(item)
except ClientError as err:
logger.error(err.response['Error']['Message'])
raise
else:
return accounts
def get_cost_json(start, end):
"""Get Cost Json"""
ce = boto3.client('ce')
response = ce.get_cost_and_usage(
TimePeriod={
'Start': start,
'End' : end,
},
Granularity='MONTHLY',
Metrics=[
'NetUnblendedCost'
],
GroupBy=[
{
'Type': 'DIMENSION',
'Key': 'LINKED_ACCOUNT'
},
{
'Type': 'DIMENSION',
'Key': 'LEGAL_ENTITY_NAME'
}
]
)
return response['ResultsByTime'][0]['Groups']
def lambda_handler(event, context):
output_file = '/tmp/output.csv'
bucket = os.environ['BUCKET']
today = datetime.date.today()
start = today.replace(month=today.month-1, day=1).strftime('%Y-%m-%d')
end = today.replace(day=1).strftime('%Y-%m-%d')
key = 'all-accounts-cost-' + start + '-' + end + '.csv'
normalized_json = pandas.json_normalize(get_cost_json(start, end))
split_keys = pandas.DataFrame(
normalized_json['Keys'].tolist(),
columns=['Account Id', 'Legal Entity']
)
cost = pandas.concat([split_keys, normalized_json['Metrics.NetUnblendedCost.Amount']], axis=1)
account_list = pandas.DataFrame(list_accounts(), columns=['Account Id', 'Account Name'])
merged_cost = pandas.merge(account_list, cost, on='Account Id', how='right')
renamed_cost = merged_cost.rename(columns={'Metrics.NetUnblendedCost.Amount': 'Amount (USD)'})
sorted_cost = renamed_cost.sort_values(by='Account Name')
sorted_cost.to_csv(output_file, index=False)
upload_s3(output_file, key, bucket)
以上です。
参考になれば幸いです。