Pythonで CostExplorer を利用してアカウント毎の請求情報を Teams に POST してみました

## 概要

AWS CostExplorer を利用してアカウント毎の当月の累計請求情報と日別請求情報を取得し、その情報を Microsoft Teams に投稿する Pythonプログラムを作成してみました。

[このプログラム](https://qiita.com/turupon/items/4e9af748b5d43bddcf8d) をベースに、環境変数からAWSの認証情報を取得し、マークダウン形式で　Teams にPOSTしています。Teamsの投稿欄の仕様により横スクロールができないので、日別請求情報は２分割してPOSTしています。また、Teamsに投稿された請求情報を極力「詳細表示」を押すことなく確認したいので、日別請求情報は日付の降順としています。

### 実行環境

macOS Monterey 12.3.1 
python 3.8.12

### 実行プログラム

```python:CostAccountAll_to_TeamsEndpoint_from_ENV.py
import os
from datetime import datetime, timedelta, date
import boto3
import pandas as pd
import json
import requests
from pandas_datareader.data import get_quote_yahoo


# teams_endpoint = 'Microsoft Teamsの チャネルエンドポイント(Webhook)'
TEAMS_ENDPOINT = {
    "TECH_ALL": os.environ['ENDPOINT_TECH_TEST']
}

# 親アカウントの認証情報
ACCOUNT_ENV = {
    "BILLING_ID": os.environ['AWS_COST_ALL_ID'],
    "BILLING_KEY": os.environ['AWS_COST_ALL_KEY']
}


# 実行月の初日を取得
def get_begin_of_month() -> str:
    return date.today().replace(day=1).isoformat()

# 実行日を取得
def get_today() -> str:
    return date.today().isoformat()

# 実行日の前日取得
def get_yesterday() -> str:
    return (date.today()- timedelta(1)).isoformat()

# 請求金額取得対象期間の取得
def get_total_cost_date_range() -> (str, str):
    start_date = get_begin_of_month()
    end_date = get_today()

    # get_cost_and_usage()のstartとendに同じ日付は指定不可のため、
    # 「今日が1日」なら、「先月1日から今月1日（今日）」までの範囲にする
    if start_date == end_date:
        end_of_month = datetime.strptime(start_date, '%Y-%m-%d') + timedelta(days=-1)
        begin_of_month = end_of_month.replace(day=1)
        return begin_of_month.date().isoformat(), end_date

    return start_date, end_date


# 請求金額データの取得
def get_boto3_session():
    session = boto3.session.Session(aws_access_key_id=ACCOUNT_ENV['BILLING_ID'], aws_secret_access_key=ACCOUNT_ENV['BILLING_KEY'])
    return session

# 請求金額データの取得
def get_billing_data(session):

    # データ取得期間の取得
    (start, end) = get_total_cost_date_range()

    # 請求データ（アカウント毎の日々の請求データ）の取得
    client = session.client('ce')
    response = client.get_cost_and_usage(
        TimePeriod={
            'Start': start,
            'End' :  end,
        },
        Granularity='DAILY',
        Metrics=[
            'NetUnblendedCost'
        ],
        GroupBy=[
            {
                'Type': 'DIMENSION',
                'Key': 'LINKED_ACCOUNT'
            }
        ]
    )
    # pprint.pprint(response)
    return response


# 組織の全アカウント情報の取得：辞書型
def get_organization_accounts(session):

    # 親組織へ接続する
    organizations = session.client('organizations')

    # 全組織のアカウント情報を取得する
    pagenator = organizations.get_paginator('list_accounts')
    response_iterator = pagenator.paginate()

    # アカウント情報用リストの定義
    row_id = []
    row_name = []

    # 各アカウントの Account ID と Account Name の取得
    for response in response_iterator:
        for acct in response['Accounts']:

            # 取得するアカウント情報のリスト化
            row_id.append(acct['Id'])
            row_name.append(acct['Name'])

    # 取得した全アカウント情報を辞書型へ
    accounts = pd.DataFrame({"Id":row_id, "Name":row_name})
    # print(tabulate(accounts, headers='keys'), "\n")
    return accounts


#　組織の全Accountの請求金額データの集計
def get_account_all_billings(crate):

    # boto3 セッションオープン
    session = get_boto3_session()

    # 組織の全アカウント情報の取得
    accounts = get_organization_accounts(session)
    # print(accounts, "\n")

    # 請求金額データの取得
    response = get_billing_data(session)

    # 整形する請求データの入れ物
    merged_cost = pd.DataFrame(
        columns=['Id']
    )

    # 請求データの整形
    for item in response['ResultsByTime']:

        # ['Groups']のデータ取得（アカウント毎の請求データ）
        normalized_json = pd.json_normalize(item['Groups'])

        # アカウントIDの取得
        split_keys = pd.DataFrame(
            normalized_json['Keys'].tolist(),
            columns=['Id']
        )
        
        # アカウントID毎の請求金額の取得
        cost = pd.concat(
            [split_keys, normalized_json['Metrics.NetUnblendedCost.Amount']],
            axis=1
        )

        # 請求金額データのカラムタイプの変更 object -> float
        cost['Metrics.NetUnblendedCost.Amount'] = cost['Metrics.NetUnblendedCost.Amount'].astype('float')
        # 請求金額を日本円に変換
        cost['Metrics.NetUnblendedCost.Amount'] = round(cost['Metrics.NetUnblendedCost.Amount']*crate, 0)

        # ['TimePeriod']['Start']のデータ取得（請求日データ）
        renamed_cost = cost.rename(
            columns={'Metrics.NetUnblendedCost.Amount': item['TimePeriod']['Start']}
        )

        # アカウント毎に請求データと請求日データのマージ（右側への列追加）
        merged_cost = pd.merge(merged_cost, renamed_cost, on='Id', how='right')

    # print(merged_cost, "\n")

    # アカウントIDをキーにしてアカウント名を列結合
    merged_cost = pd.merge(merged_cost, accounts, on="Id", how="left")
    # アカウント名の列を先頭列に作成
    merged_cost.insert(loc=0, column='AccountName', value=merged_cost['Name'])
    # 不要カラムの削除
    merged_cost.drop(['Id', 'Name'], axis=1, inplace=True)

    # 各行の合計を最後の列に追加
    merged_cost['Total'] = round(merged_cost.sum(numeric_only=True, axis=1), 0) 

    # ['Total']列からか請求金額の総合計を求める
    grand_total = round(merged_cost['Total'].sum(), 0)  

    # 合計で降順ソートし、インデックスをリセエットする
    merged_cost.sort_values(by='Total',ascending=False, inplace=True)
    merged_cost.reset_index(drop=True, inplace=True)

    # print(merged_cost, "\n")
    return merged_cost, grand_total


# 請求金額データの通知表示への変更
def post_billings(merged_cost, grand_total, crate):

    # タイトルメッセージの編集
    # title_msg = "総合計 : " + str(grand_total) + "　　　　USD-JPY : " +  str(crate)
    title_msg = "総合計 : " + "{:,}".format(grand_total) + "　　　　USD-JPY : " +  str(crate)
    print("\n", title_msg, "\n")

    # もらったデータの確認
    # print(merged_cost, "\n")

    # POSTするためのデータ分割
    cost_df_1 = merged_cost.iloc[:8,:]
    cost_df_2 = merged_cost.iloc[8:,:]
    # print(cost_df_1, "\n")
    # print(cost_df_2, "\n")

    # インデックスとしてアカウント名を指定
    merged_cost.set_index("AccountName", inplace=True)
    cost_df_1.set_index("AccountName", inplace=True)
    cost_df_2.set_index("AccountName", inplace=True)

    # 行と列の入替え
    merged_cost = merged_cost.transpose()
    cost_df_1 = cost_df_1.transpose()
    cost_df_2 = cost_df_2.transpose()
    
    # Total行（最後の行）の取得 = アカウント毎の請求合計の一覧データの取得
    df_total = merged_cost.tail(1)
    df_total = df_total.transpose()

    # インデックス（日付）で降順ソートする
    cost_df_1.sort_index(ascending=False, inplace=True)
    cost_df_2.sort_index(ascending=False, inplace=True)
    # print(cost_df_1, "\n")
    # print(cost_df_2, "\n")

    # POSTするためにマークダウン形式に変更
    post_str0 = df_total.to_markdown(floatfmt=",.0f")
    post_str1 = cost_df_1.to_markdown(floatfmt=",.0f")
    post_str2 = cost_df_2.to_markdown(floatfmt=",.0f")
    print(post_str0, "\n")
    print(post_str1, "\n")
    print(post_str2, "\n")

    # TeamsEndpointへのPOST（請求データはマークダウン形式で渡す）
    teams_endpoint_post_summary(post_str0, post_str1, post_str2, title_msg, get_yesterday())


# TeamsEndpointへのデータPOST
def teams_endpoint_post_summary(str0, str1, str2, title_mdg, day0):
    # Microsoft Teams へ送信する下ごしらえ
    request = {
        'title': '【 AWS : ' + day0 + ' 】　' + title_mdg,
        # 'text': str0 + '\n\n' + str1 + '\n\n' + str2
        'text': str0
    }

    # 累計請求情報を Microsoft Teams へ送信する
    response = requests.post(TEAMS_ENDPOINT['TECH_ALL'], json.dumps(request))
    print(response)

    # アカウント毎の日別請求情報（上位８アカウント分）を Microsoft Teams へ送信する
    request = {'text': str1}
    response = requests.post(TEAMS_ENDPOINT['TECH_ALL'], json.dumps(request))
    print(response)

    # アカウント毎の日別請求情報（上位９アカウント目以降）を Microsoft Teams へ送信する
    request = {'text': str2}
    response = requests.post(TEAMS_ENDPOINT['TECH_ALL'], json.dumps(request))
    print(response)


# 現在の USD - JPY の為替レートの取得
def get_currency_rate() :
    res1 = get_quote_yahoo('USDJPY=X')
    res2 = res1["price"].values
    crate = res2[0] 
    return crate


# メイン
if __name__ == '__main__':

    # 今の為替レートを取得する
    crate = get_currency_rate()

    # 組織の全Accountの請求金額データの日別集計
    merged_cost, grand_total = get_account_all_billings(crate)

    # 請求金額データの通知表示への変更
    post_billings(merged_cost, grand_total, crate)
```

### プログラムの実行

```
## Teams にPOSTした請求情報
$ python CostAccountAll_to_TeamsEndpoint_from_ENV.py

 総合計 : 343,352.0　　　　USD-JPY : 138.313 

| AccountName   |   Total |
|:--------------|--------:|
| tech-west01   |  60,832 |
| tech-sss0     |  45,707 |
| tech-nnn      |  45,556 |
| tech-aaa      |  39,300 |
| tech-sss1     |  37,871 |
| tech-sss2     |  37,082 |
| tech-msc      |  33,232 |
| tech-ppp2     |  15,881 |
| tech-ppp1     |   9,981 |
| tech-ccc      |   8,761 |
| tech-iiii     |   3,843 |
| aws-master    |   2,713 |
| tech-west02   |   2,593 |
| tech-west03   |       0 | 

|            |   tech-west01 |  tech-sss0 |   tech-nnn |   tech-aaa |   tech-sss1 |   tech-sss2 |   tech-msc |   tech-ppp2 |
|:-----------|--------------:|-----------:|-----------:|-----------:|------------:|------------:|-----------:|------------:|
| Total      |        60,832 |     45,707 |     45,556 |     39,300 |      37,871 |      37,082 |     33,232 |      15,881 |
| 2022-08-29 |         1,550 |      1,640 |        707 |        983 |         566 |         825 |        543 |         268 |
| 2022-08-28 |         1,898 |      1,138 |      1,459 |      1,251 |       1,209 |       1,240 |        487 |         441 |
| 2022-08-27 |         1,898 |      1,151 |      1,389 |      1,256 |       1,209 |       1,135 |        923 |         357 |
| 2022-08-26 |         1,898 |      2,256 |      1,356 |      1,252 |       1,209 |       1,025 |      1,097 |         357 |
| 2022-08-25 |         1,888 |      1,786 |      1,365 |      1,244 |       1,200 |       1,320 |     15,434 |         364 |
| 2022-08-24 |         1,898 |      2,242 |      1,334 |      1,250 |       1,209 |       1,363 |        571 |         422 |
| 2022-08-23 |         1,898 |      1,713 |      1,486 |      1,247 |       1,209 |       1,277 |        479 |         399 |
| 2022-08-22 |         1,898 |      2,027 |      1,735 |      1,250 |       1,209 |       1,095 |        479 |         378 |
| 2022-08-21 |         1,898 |      1,139 |      1,745 |      1,249 |       1,209 |       1,025 |        479 |         381 |
| 2022-08-20 |         1,898 |      1,137 |      1,781 |      1,241 |       1,209 |       1,373 |        479 |         393 |
| 2022-08-19 |         1,888 |      1,145 |      1,814 |      1,228 |       1,230 |       1,382 |        479 |         357 |
| 2022-08-18 |         1,898 |      1,355 |      1,936 |      1,228 |       1,216 |       1,315 |        667 |         357 |
| 2022-08-17 |         1,898 |      1,463 |      1,779 |      1,236 |       1,209 |       1,102 |        479 |         354 |
| 2022-08-16 |         1,898 |      1,138 |      1,778 |      1,231 |       1,209 |       1,028 |        479 |         339 |
| 2022-08-15 |         1,898 |      1,137 |      1,768 |      1,231 |       1,209 |       1,323 |        479 |         339 |
| 2022-08-14 |         1,898 |      1,137 |      1,748 |      1,230 |       1,209 |       1,385 |        479 |         339 |
| 2022-08-13 |         1,888 |      1,137 |      1,778 |      1,236 |       1,209 |       1,288 |        479 |         339 |
| 2022-08-12 |         1,898 |      1,141 |      1,831 |      1,234 |       1,209 |       1,061 |        479 |         339 |
| 2022-08-11 |         1,898 |      1,156 |      1,761 |      1,235 |       1,209 |       1,030 |        479 |         339 |
| 2022-08-10 |         1,898 |      1,533 |      1,727 |      1,239 |       1,209 |       1,328 |        479 |         339 |
| 2022-08-09 |         1,898 |      1,361 |      1,206 |      1,237 |       1,209 |       1,394 |        480 |         339 |
| 2022-08-08 |         1,898 |      1,507 |      1,643 |      1,234 |       1,209 |       1,237 |        479 |         339 |
| 2022-08-07 |         1,898 |      1,107 |      1,095 |      1,237 |       1,209 |       1,205 |        479 |         339 |
| 2022-08-06 |         1,898 |      1,116 |        606 |      1,236 |       1,209 |       1,022 |        479 |         339 |
| 2022-08-05 |         1,908 |      1,469 |        606 |      1,232 |       1,209 |       1,006 |        534 |         339 |
| 2022-08-04 |         2,055 |      1,496 |        965 |      1,232 |       1,209 |       1,039 |        479 |         340 |
| 2022-08-03 |         2,055 |      1,532 |      1,123 |      1,232 |       1,207 |         969 |        479 |         339 |
| 2022-08-02 |         2,055 |      1,502 |      1,108 |      1,235 |       1,205 |         970 |        874 |         339 |
| 2022-08-01 |         7,585 |      6,046 |      4,927 |      4,874 |       4,649 |       4,320 |      3,000 |       5,967 | 

|            |   tech-ppp1 |   tech-ccc |   tech-iiii |    aws-master |   tech-west02 |   tech-west03 |
|:-----------|------------:|-----------:|------------:|--------------:|--------------:|--------------:|
| Total      |       9,981 |      8,761 |       3,843 |         2,713 |         2,593 |             0 |
| 2022-08-29 |          55 |        182 |         132 |            54 |            62 |             0 |
| 2022-08-28 |         154 |        278 |         171 |           137 |            82 |             0 |
| 2022-08-27 |         154 |        278 |         173 |            81 |            82 |             0 |
| 2022-08-26 |         154 |        278 |         173 |            79 |            82 |             0 |
| 2022-08-25 |         269 |        278 |         173 |           171 |            82 |             0 |
| 2022-08-24 |       2,178 |        278 |         173 |           105 |            82 |             0 |
| 2022-08-23 |         883 |        278 |         176 |           111 |            82 |             0 |
| 2022-08-22 |         155 |        278 |         173 |            92 |            82 |             0 |
| 2022-08-21 |         154 |        278 |         197 |            79 |            82 |             0 |
| 2022-08-20 |         154 |        278 |         189 |            79 |            82 |             0 |
| 2022-08-19 |         154 |        278 |         173 |            78 |            82 |             0 |
| 2022-08-18 |         305 |        278 |         173 |            78 |            82 |             0 |
| 2022-08-17 |         387 |        278 |         108 |            78 |            82 |             0 |
| 2022-08-16 |         198 |        278 |          52 |            78 |            82 |             0 |
| 2022-08-15 |         198 |        278 |          52 |            78 |            82 |             0 |
| 2022-08-14 |         198 |        278 |         221 |            78 |            82 |             0 |
| 2022-08-13 |         198 |        278 |          90 |            78 |            82 |             0 |
| 2022-08-12 |         198 |        278 |          35 |            77 |            82 |             0 |
| 2022-08-11 |         198 |        278 |          35 |            78 |            82 |             0 |
| 2022-08-10 |         198 |        278 |         175 |            78 |            82 |             0 |
| 2022-08-09 |         198 |        278 |         198 |            78 |            82 |             0 |
| 2022-08-08 |         242 |        278 |          67 |            78 |            82 |             0 |
| 2022-08-07 |         198 |        278 |          36 |            77 |            82 |             0 |
| 2022-08-06 |         198 |        278 |          35 |            76 |            82 |             0 |
| 2022-08-05 |         379 |        278 |          35 |            77 |            82 |             0 |
| 2022-08-04 |         198 |        278 |          35 |            77 |            82 |             0 |
| 2022-08-03 |         198 |        278 |          35 |            79 |            82 |             0 |
| 2022-08-02 |         478 |        278 |          35 |            78 |            82 |             0 |
| 2022-08-01 |       1,350 |      1,073 |         523 |           326 |           317 |             0 | 

<Response [200]>        # <--- Teams へのPOST 成功（累計請求情報）
<Response [200]>        # <--- Teams へのPOST 成功（日別請求情報、請求金額上位８アカウント分）
<Response [200]>        # <--- Teams へのPOST 成功（日別請求情報、請求金額上位９アカウント目以降）
```

### まとめ

この Pythonプログラムを利用することにより、検証環境のAWSの請求金額の日々の推移を全社情報共有ツールである Teams に通知し、少しでも利用料抑制に役立つことを期待しています。

### 参考記事

以下の記事を参考にさせていただきました。感謝申し上げます。
[Cost Explorer API でアカウント毎に日別の請求額を取得する](https://qiita.com/hayao_k/items/3034456883c08325e398)
[AWS Cost Explorerに渡す、Metricsの値の意味](https://qiita.com/tamura_CD/items/4a9a412faf379b334986)