Pythonで Azureの請求アカウントIDから請求データを取得してみました - CostManagementモジュール編

## 概要

Azureの請求アカウントIDとその配下の顧客ID（テナントID）から請求データを取得するPythonプログラムです。
本プログラムは CostManagementモジュール を利用して作成されていますが、以下の問題がございます（私の力量不足かもしれません）。

1. リクエストの返り値が1000件を超える場合、CostManagementモジュールを利用できない
2. CostManagementモジュールで利用するAPIバージョンを指定することができず、古いバージョンのため取得できない項目がある

[REST-API利用する場合](https://qiita.com/turupon/items/fcaf63b88745b47f15fb)、上記2点の問題は発生しません。

### 実行環境

macOS Monterey 12.3.1
python 3.8.12

### 事前準備

- Pythonプログラムの実行に必要なサービスプリンシパルの作成
  - Azure AD にて サービスプリンシパル を作成し、以下のアクセス許可を付与する
    - Microsoft Graph/アプリケーションの許可/Directory.AccessAsRead.All
    - Microsoft Pertner Center/アプリケーションの許可/user_impersonation
    - Azure Service Management/アプリケーションの許可/user_impersonation

- ロールの割当
  - 上記サービスプリンシパルに「課金管理者」のロールを割り当てる
  - 「コストの管理と請求」サービスのIAMにおいて、上記サービスプリンシパルに閲覧者のロールを割り当てる
  
- 各種情報の入手
  - 請求アカウントID
  - 請求書アカウントが属するテナントのID
  - 上記サービスプリンシパルのアプリケーションIDとシークレット
  - 顧客ID（顧客のテナントID）


### 実行プログラム

```python:CM_QueryTimePeriodByCustomerId.py
import os
import argparse
import pandas as pd
from datetime import *
import time
from azure.mgmt.costmanagement import CostManagementClient
from azure.mgmt.costmanagement.models import QueryTimePeriod
from azure.identity import ClientSecretCredential

# 請求管理者権限を付与されたサービスプリンシパル情報
TENANT_ID = os.environ['APC_TENANT_ID']
CLIENT_ID = os.environ['APC_CLIENT_ID']
CLIENT_KEY = os.environ['APC_CLIENT_KEY']

# 請求アカウントID
BILLING_ACCOUNT_ID = os.environ['BILLING_ACCOUNT_ID']

# 顧客ID
CUSTOMER_ID = 'xxxxxxxx-1111-2222-3333-xxxxxxxxxxxx'
# CUSTOMER_ID = 'yyyyyyyy-4444-5555-6666-yyyyyyyyyyyy'
# CUSTOMER_ID = 'zzzzzzzz-7777-8888-9999-zzzzzzzzzzzz'


# CostManagement情報 を操作するオブジェクトを取得
def GetCostManagementObject():
    credentials = ClientSecretCredential(
       tenant_id = TENANT_ID,
       client_id = CLIENT_ID,
       client_secret = CLIENT_KEY
    )
    return CostManagementClient(credentials)


# SCOPEの定義
def define_scope() :
    def_scope = f'/providers/Microsoft.Billing/billingAccounts/{BILLING_ACCOUNT_ID}/customers/{CUSTOMER_ID}'
    return def_scope


# Time_Periodの定義
def define_period(fm_tp, to_tp) :
    fm_datetime = datetime.strptime(fm_tp, '%Y-%m-%d')
    to_datetime = datetime.strptime(to_tp, '%Y-%m-%d')
    def_period = QueryTimePeriod(from_property = fm_datetime, to = to_datetime)
    return def_period


# クエリの定義
def define_query(fm_tp, to_tp) :

    # データ取得期間の定義
    def_period = define_period(fm_tp, to_tp)

    # クエリの作成
    def_query = {
        "type": "Usage",
        "timeframe": "Custom",
        "time_period": def_period,
        "dataset": {
            "granularity": "None",
            "aggregation": {
                "totalCost": {"name": "PreTaxCost", "function": "Sum"},
                "totalQuantity": {"name": "UsageQuantity", "function": "Sum"}
            },
            "grouping": [
                # {"type": "Dimension", "name": "ResellerMPNId"},
                # {"type": "Dimension", "name": "InvoiceId"},
                # {"type": "Dimension", "name": "ResourceGroup"},
                {"type": "Dimension", "name": "ServiceFamily"},
                {"type": "Dimension", "name": "ServiceName"}
                # {"type": "Dimension", "name": "MeterCategory"},
                # {"type": "Dimension", "name": "Meter"},
                # # {"type": "Dimension", "name": "MeterSubcategory"},
                # {"type": "Dimension", "name": "Product"},
                # {"type": "Dimension", "name": "UnitOfMeasure"},
                # {"type": "Dimension", "name": "ProductOrderName"}
             ]
        }
    }
    return def_query


# 指定した Subscription について CostManagement からコストを取得
def GetCostManagement(fm_tp, to_tp):

    # CostManagementを操作するオブジェクトの取得
    client = GetCostManagementObject()

    # SCOPEの定義
    def_scope = define_scope()

    # クエリの定義
    def_query = define_query(fm_tp, to_tp)

    # CostManagementから請求データの取得
    costmanagement = client.query.usage(scope = def_scope, parameters=def_query)
    client.close()
    return costmanagement


# とある顧客の指定した期間でもCostManagement情報を取得
def GetCustomerCsotManagement(fm_tp, to_tp):

    # CostManagement からコストを取得
    costmanagement = GetCostManagement(fm_tp, to_tp)

    # columnsデータを取得
    columns = list(map(lambda col: col.name, costmanagement.columns))

    # rowsデータを取得
    rows = costmanagement.rows
    
    # 請求データをDataframe型で取得
    df_cost = pd.DataFrame(rows, columns=columns)
    return df_cost


# 取得した全データの画面出力
def data_to_display(df_cost):
    # 取得した全データの表示
    df_cost = df_cost.drop(columns=['Currency'])
    print(df_cost.to_markdown(floatfmt=",.0f"))
    print(df_cost.dtypes)


# 取得した全データの保存
def data_to_csv(df_cost):
    # 取得した全データのcsvファイルへ保存
    now = datetime.now()
    filename = './data_customers_cost_by_billingid/' + now.strftime('%Y%m%d%H%M%S') + '.csv'
    print(filename)
    df_cost.to_csv(filename, index=False, encoding="utf-8")


# データ取得のための From-To 日付確認
def check_time_period(fm_tp, to_tp):
    try :
        fm_datetime = datetime.strptime(fm_tp, '%Y-%m-%d')
        to_datetime = datetime.strptime(to_tp, '%Y-%m-%d')

        if to_datetime < fm_datetime :
            print("Error ---> 開始日 ≦ 終了日 ")
            return False
        else :
            return True
    except ValueError as e :
        print("ValueError = ", e)
        return False


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='ある顧客の請求情報の取得')
    parser.add_argument('-f', '--fm', type=str, required=True, help='データを取得する開始日(yyyy-mm-dd)')
    parser.add_argument('-t', '--to', type=str, required=True, help='データを取得する終了日(yyyy-mm-dd)')
    parser.add_argument('--mode', type=str, default='lo', help='lo（取得データを画面出力） / csv（取得データをcsv保存）')
    args = parser.parse_args()

    if check_time_period(args.fm, args.to) == False:
        print("NG", "\n")
    else :
        start = time.time()
        df_cost = GetCustomerCsotManagement(args.fm, args.to)

        if args.mode == 'csv':
            # 取得した全データを保存
            data_to_csv(df_cost)
        else :
            # 取得した全データを出力
            data_to_display(df_cost)

        # 合計請求金額と件数の表示
        total = df_cost['PreTaxCost'].sum()
        count = len(df_cost)
        print("\n", "合計請求金額 : {:,.0f}".format(total), "\tデータ件数 : {:,.0f}".format(count))

        generate_time = time.time() - start
        print("\n 取得時間:{0}".format(generate_time) + " [sec] \n")
```

### プログラムのHELP表示
 
```
$ python CM_QueryTimePeriodByCustomerId.py -h                         
usage: CM_QueryTimePeriodByCustomerId.py [-h] [-f FM] [-t TO] [--mode MODE]

ある顧客の請求情報の取得

optional arguments:
  -h, --help      show this help message and exit
  -f FM, --fm FM  データを取得する開始日(yyyy-mm-dd)
  -t TO, --to TO  データを取得する終了日(yyyy-mm-dd)
  --mode MODE     lo（取得データを画面出力） / csv（取得データをcsv保存）
```

### プログラムの実行（画面表示）
 
```
$ python CM_QueryTimePeriodByCustomerId.py -f 2022-09-01 -t 2022-09-15
Datetime with no tzinfo will be considered UTC.
Datetime with no tzinfo will be considered UTC.
|    |   PreTaxCost |   UsageQuantity | ServiceFamily             | ServiceName                            |
|---:|-------------:|----------------:|:--------------------------|:---------------------------------------|
|  0 |           18 |               0 | AI + Machine Learning     | Cognitive Services                     |
|  1 |            0 |               0 | Analytics                 | Azure Data Factory v2                  |
|  2 |        5,725 |             407 | Compute                   | Azure App Service                      |
|  3 |            0 |             644 | Compute                   | Functions                              |
|  4 |       26,069 |           1,923 | Compute                   | Virtual Machines                       |
|  5 |           82 |              12 | Compute                   | Virtual Machines Licenses              |
|  6 |          971 |           1,340 | Containers                | Container Registry                     |
|  7 |           32 |              32 | Databases                 | Azure Cosmos DB                        |
|  8 |       14,250 |           1,095 | Databases                 | Azure Database for PostgreSQL          |
|  9 |          675 |             259 | Databases                 | Redis Cache                            |
| 10 |          316 |              15 | Databases                 | SQL Database                           |
| 11 |          440 |           1,844 | Integration               | Logic Apps                             |
| 12 |            0 |               0 | Integration               | Service Bus                            |
| 13 |          653 |             236 | Internet of Things        | Event Hubs                             |
| 14 |          245 |              51 | Management and Governance | Azure Monitor                          |
| 15 |            0 |               1 | Management and Governance | Log Analytics                          |
| 16 |       14,566 |             458 | Networking                | Azure Bastion                          |
| 17 |          606 |              10 | Networking                | Azure DNS                              |
| 18 |            0 |               4 | Networking                | Bandwidth                              |
| 19 |        3,015 |           1,480 | Networking                | Load Balancer                          |
| 20 |            0 |              31 | Networking                | Network Watcher                        |
| 21 |       11,065 |          16,494 | Networking                | Virtual Network                        |
| 22 |        7,947 |             360 | Networking                | VPN Gateway                            |
| 23 |        6,272 |             360 | Security                  | Azure Active Directory Domain Services |
| 24 |            0 |               0 | Security                  | Key Vault                              |
| 25 |          379 |              11 | Storage                   | Backup                                 |
| 26 |       56,861 |           2,438 | Storage                   | Storage                                |
PreTaxCost       float64
UsageQuantity    float64
ServiceFamily     object
ServiceName       object
dtype: object

 合計請求金額 : 150,187 	データ件数 : 27

 取得時間:14.547715187072754 [sec] 
```

### プログラムの実行（csvファイル保存）

```
$ python CM_QueryTimePeriodByCustomerId.py -f 2022-09-01 -t 2022-09-15 --mode csv
Datetime with no tzinfo will be considered UTC.
Datetime with no tzinfo will be considered UTC.
./data_customers_cost_by_billingid/20221019004416.csv

 合計請求金額 : 150,187 	データ件数 : 27

 取得時間:10.971143245697021 [sec] 
```

作成された csvファイルの確認

```
## ファイル名の確認
$ ls -l data_customers_cost_by_billingid                                            
total 8
drwxr-xr-x  3 ituru  staff    96 10 19 00:44 ./
drwxr-xr-x  5 ituru  staff   160 10 18 17:03 ../
-rw-r--r--  1 ituru  staff  1657 10 19 00:44 20221019004416.csv

## ファイルの中身の確認
$ cat data_customers_cost_by_billingid/20221019004416.csv 
     1	PreTaxCost,UsageQuantity,ServiceFamily,ServiceName,Currency
     2	17.50078003120125,0.027188172043010755,AI + Machine Learning,Cognitive Services,JPY
     3	0.0,0.00048,Analytics,Azure Data Factory v2,JPY
     4	5725.319198374109,406.556664,Compute,Azure App Service,JPY
     5	0.0,643.543625,Compute,Functions,JPY
     6	26069.087660966645,1922.551374,Compute,Virtual Machines,JPY
     7	81.709678368424,11.766742,Compute,Virtual Machines Licenses,JPY
     8	970.5452294659273,1339.796035,Containers,Container Registry,JPY
     9	32.0,32.0,Databases,Azure Cosmos DB,JPY
    10	14249.570321745208,1094.7081044514973,Databases,Azure Database for PostgreSQL,JPY
    11	675.031348809556,259.21666666666664,Databases,Redis Cache,JPY
    12	316.2430939226519,15.000000000000014,Databases,SQL Database,JPY
    13	440.1111821965594,1844.051618530415,Integration,Logic Apps,JPY
    14	0.0,0.000692,Integration,Service Bus,JPY
    15	653.0466945110688,236.063313,Internet of Things,Event Hubs,JPY
    16	244.68841904459103,51.40737007168459,Management and Governance,Azure Monitor,JPY
    17	0.0,1.0262113914764017,Management and Governance,Log Analytics,JPY
    18	14566.386574041495,457.60956586782714,Networking,Azure Bastion,JPY
    19	606.2915004326267,10.468534810633235,Networking,Azure DNS,JPY
    20	0.0,4.077179137475253,Networking,Bandwidth,JPY
    21	3014.7857758902032,1479.826889735452,Networking,Load Balancer,JPY
    22	0.0,31.0,Networking,Network Watcher,JPY
    23	11064.862557472155,16494.0390586638,Networking,Virtual Network,JPY
    24	7947.0,360.0,Networking,VPN Gateway,JPY
    25	6271.999999999999,359.9,Security,Azure Active Directory Domain Services,JPY
    26	0.4986862848134524,0.2847,Security,Key Vault,JPY
    27	378.9713721445092,11.07577962670219,Storage,Backup,JPY
    28	56861.17015396144,2437.87818,Storage,Storage,JPY
```

### まとめ

これで CostManagementモジュール を利用して Azureの請求データを取得することができました。あとは要望に合わせてクエリ・リクエスト文を編集してください。
なお、CostManagementモジュール を利用する場合の time_period には datetime型で渡す必要があります、、、、