Pythonで Azureの請求アカウントIDから請求データを取得してみました - REST API編

## 概要

Azureの請求アカウントIDとその配下の顧客ID（テナントID）から請求データを取得するPythonプログラムです。
本プログラムは CostManagement REST API を利用して作成されています。
[CostManagementモジュールを利用したプログラム](https://qiita.com/turupon/items/d39b5312600ce1d2a4f3)でいろいろ問題があったので、改めて REST API を利用しています。

1. リクエストの返り値が1000件を超える場合でも問題なく動作します
2. 利用するAPIバージョンを指定することができる


### 実行環境

macOS Monterey 12.3.1
python 3.8.12

### 事前準備

[この記事](https://qiita.com/turupon/items/d39b5312600ce1d2a4f3) の「事前準備」を完了していること

### 実行プログラム

```python:REST_QueryTimePeriodByCustomerId.py
import json
import os
import sys
import requests
import argparse
from datetime import *
import time
import logging
import pandas as pd
import numpy as np


# 請求管理者権限を付与されたサービスプリンシパル情報
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']

# REST API 情報
API_URL = 'https://management.azure.com'
API_VERSION = '/providers/Microsoft.CostManagement/query?api-version=2021-10-01'

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


# Azureアクセスのためのアクセストークンの取得
def get_azure_access_token() :

    # access_token を取得するためのヘッダ情報
    headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    payload = {
        'client_id': CLIENT_ID,
        'scope': API_URL + '/.default',
        'grant_type': 'client_credentials',
        'client_secret': CLIENT_KEY
    }

    # access_token を取得するためのURLを生成
    TokenGet_URL = "https://login.microsoftonline.com/" + \
        TENANT_ID + "/oauth2/v2.0/token"
    # print(TokenGet_URL, "\n")

    # 実行
    response = requests.get(
        TokenGet_URL,
        headers=headers,
        data=payload
    )
    # requrest処理のクローズ
    response.close

    # その結果を取得
    jsonObj = json.loads(response.text)
    try :
        return jsonObj["access_token"]
    except KeyError as err :
        print(err, "Not get\n", jsonObj)
        return ''


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

    # 顧客のサブスクリプション毎の今月の請求料金を取得
    payload = {
        "type": "Usage",
        "timeframe": "Custom",
        "timePeriod": {"from": fm_tp, "to": to_tp},
        "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 payload


# URLの定義
def define_scope_url() :

    # SCOPEの定義
    SCOPE = f'/providers/Microsoft.Billing/billingAccounts/{BILLING_ACCOUNT_ID}/customers/{CUSTOMER_ID}'
    # URLを生成
    Post_URL = f'{API_URL}{SCOPE}{API_VERSION}'
    return Post_URL


# CustomerIDから顧客情報の取得
def get_customers_cost_by_billingid(access_token, fm_tp, to_tp) :

    # 取得するためのヘッダ情報
    headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        'Authorization': 'Bearer %s' % access_token
    }

    # 取得するためのURL情報
    Post_URL = define_scope_url()

    # クエリの定義
    payload = define_query_usage_payload(fm_tp, to_tp)

    # 1000件毎（仕様）のデータ取得    
    df_cost = pd.DataFrame()
    while True :
        # 実行
        print("\n" ,Post_URL)
        response = requests.post(
            Post_URL,
            headers=headers,
            data=json.dumps(payload)
        )
        # requrest処理のクローズ
        response.close

        try :
            print(response)
            response.raise_for_status()
            next_link, df_rows = df_customer_cost(response.json())

            df_cost = pd.concat([df_cost, df_rows])

            if next_link is None :
                break
            elif len(next_link) > 0 :
                Post_URL = next_link
            else :
                break
        except requests.exceptions.RequestException as e :
            logging.exception("get_customers_cost_by_billingid request failed. message=(%s)\n", e.response.text)
            sys.exit()
    
    # データの再インデックス化
    df_cost = df_cost.reset_index(drop=True)
    return df_cost


def df_customer_cost(res_json):

    # カラム名の取得してから、値を取得
    df_colums = pd.DataFrame(res_json['properties']['columns'])
    df_rows = pd.DataFrame(res_json['properties']['rows'], columns=df_colums['name'])

    # 1000件を超える場合のURL(next_link)の取得
    next_link = res_json['properties']['nextLink']
    # print("\n next_link = " ,next_link)
    return next_link, df_rows


# 取得した全データの画面出力
def data_to_display(df_cost):

    # 取得した全データの表示
    df_cost = df_cost.drop(columns=['Currency'])
    print(df_cost.to_markdown(floatfmt=",.0f"))
    # print(df_cost.to_markdown())
    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")
        sys.exit()

    start = time.time()
    access_token = get_azure_access_token()

    if len(access_token) > 0 :
        print("\n取得アクセストークン : ")
        print(access_token, "\n")
        df_cost = get_customers_cost_by_billingid(access_token, 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 REST_QueryTimePeriodByCustomerId.py -h                         
usage: REST_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 REST_QueryTimePeriodByCustomerId.py -f 2022-09-01 -t 2022-09-15

取得アクセストークン : 
eyJ0eo9i ・・・省略・・・ nvymk5JA 

 https://management.azure.com/providers/Microsoft.Billing/billingAccounts/[BillingAccountId]/customers/[CustomerId]/providers/Microsoft.CostManagement/query?api-version=2021-10-01
<Response [200]>
|    |   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                                |
name
PreTaxCost       float64
UsageQuantity    float64
ServiceFamily     object
ServiceName       object
dtype: object

 合計請求金額 : 150,187 	レコード件数 : 27

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

### 付録

- クエリ定義の grouping で指定できる項目一覧

```
'ResourceGroup','ResourceGroupName','ResourceType','ResourceId','ResourceLocation',
'SubscriptionId','SubscriptionName',
'MeterCategory','MeterSubcategory','Meter','ServiceFamily','UnitOfMeasure','PartNumber',
'BillingAccountName','BillingProfileId','BillingProfileName',
'InvoiceSection','InvoiceSectionId','InvoiceSectionName',
'Product','ResourceGuid','ChargeType','ServiceName','ProductOrderId','ProductOrderName',
'PublisherType','ReservationId','ReservationName','Frequency','InvoiceId','PricingModel',
'CostAllocationRuleName','MarkupRuleName','BillingMonth','Provider','BenefitId','BenefitName',
'CustomerTenantId','CustomerTenantDomainName','ResellerMPNId','PartnerEarnedCreditApplied',
'CustomerName','PartnerName',''.
```

- クエリ定義の aggregation で指定できる項目一覧

```
'UsageQuantity','PreTaxCost','Cost','CostUSD','PreTaxCostUSD'
```

### まとめ

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