我把 Azure 使用费用保存到 Azure Blob 中试试看
简要概述
我将这个程序(使用Python从Azure获取按资源组计量的每日使用费,并将其保存到本地文件中)注册到Azure Functions并进行了改造,使其可以自动执行并将数据上传到Azure Blob。我希望将保存在Blob中的数据作为使用Azure PaaS进行数据分析的其中一个数据来源。
执行环境
macOS Big Sur 11.1 : 苹果操作系统大麦苹果11.1版
python 3.8.3 : Python 3.8.3 版
Azure CLI 2.28.0 : Azure 命令行接口2.28.0版
Azure Functions Core Tools 3.0.3785 : Azure 函数核心工具3.0.3785版
Azure Function Runtime Version: 3.2.0.0 : Azure 函数运行时版本3.2.0.0版
事前的准备 (shì de
注册存储账户
## 使用するテナントのAzure環境へのログイン
$ az login --tenant <tenant_id>
## 使用サブスクリプションを定義します
$ az account set --subscription '<Subscription名>'
## リソースグループの作成
$ az group create --resource-group <ResourceGroup名> --location japaneast
## ストレージアカウントの作成
$ az storage account create --name <StorageAccount名> --resource-group <ResourceGroup名> --location japaneast --sku Standard_LRS
## コンテナの作成
$ az storage container create --name usage-summary-data
创建用于执行程序的 ServicePrincipal
根据此文章,ServicePrincipal的客户端密钥默认有效期为1年,最大可设置为2年。因此,我们可以通过Azure CLI按照以下步骤创建一个有效期为29年的ServicePrincipal(目前已成功创建)。
## 最初は2年で作成します(ロール割当なしで)
$ az ad sp create-for-rbac --name sp_usagecostmanage --skip-assignment --years 2
## 作成された内容を取得できることを確認します(いきなり29年で作成した場合、取得できません、、、)
$ az ad sp list --display-name sp_usagecostmanage
## その後、29年で再作成します(ロール割当なしで)
$ az ad sp create-for-rbac --name sp_usagecostmanage --skip-assignment --years 29
{
"appId": "xxxxxxxx-xxxx-5757-4774-xxxxxxxxxxxx", --> 関数アプリの構成で AZURE_CLIENT_ID として登録
"displayName": "sp_usagecostmanage",
"name": "xxxxxxxx-xxxx-5757-4774-xxxxxxxxxxxx",
"password": "hogehogehogehogehogehogehogehogege", --> 関数アプリの構成で AZURE_CLIENT_SECRET として登録
"tenant": "zzzzzzzz-cccc-4645-5757-zzzzzzzzzzzz" --> 関数アプリの構成で AZURE_TENANT_ID として登録
}
## 必要なスコープに必要なロールを割り与えます
$ APP_ID=$(az ad sp list --display-name sp_usagecostmanage --query '[].{ID:appId}' --output tsv)
### データを保存のために、スコープ:containers ロール:Contributor とします
$ az role assignment create \
--assignee $APP_ID \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/<Subscription-ID>/resourceGroups/<ResouceGroup名>/providers/Microsoft.Storage/storageAccounts/<StorageAccount名>/blobServices/default/containers/usage-summary-data
### 複数Subscriptionの利用料金取得のため、スコープ:Subscription ロール:Reader とします
$ az role assignment create --assignee $APP_ID --scope /subscriptions/<xxx-SubscriptionID> --role Reader
$ az role assignment create --assignee $APP_ID --scope /subscriptions/<yyy-SubscriptionID> --role Reader
$ az role assignment create --assignee $APP_ID --scope /subscriptions/<zzz-SubscriptionID> --role Reader
创建函数应用程序
## Functionアプリの作成
az functionapp create -g <ResourceGroup名> -n func-CostToBlob -s <StorageAccount名> --runtime python --runtime-version 3.7 --consumption-plan-location japaneast --os-type Linux --functions-version 2
## 作成した関数アプリの構成設定
### 関数アプリ実行のための ServicePrincipal 情報の定義
az functionapp config appsettings set -n func-CostToBlob -g <ResourceGroup名> \
--settings "AZURE_TENANT_ID=zzzzzzzz-cccc-4645-5757-zzzzzzzzzzzz"
az functionapp config appsettings set -n func-CostToBlob -g <ResourceGroup名> \
--settings "AZURE_CLIENT_ID=xxxxxxxx-xxxx-5757-4774-xxxxxxxxxxxx"
az functionapp config appsettings set -n func-CostToBlob -g <ResourceGroup名> \
--settings "AZURE_CLIENT_SECRET=hogehogehogehogehogehogehogehogege"
### 定義情報の確認
az functionapp config appsettings list -n func-CostToBlob -g <ResourceGroup名> -o table
创建本地环境以用于Functions。
## Functionのプロジェクトディレクトリの作成
(base)$ mkdir Functions
(base)$ cd Functions
## プロジェクト用のPython仮想環境の構築
(base)$ python -m venv .venv
(base)$ source .venv/bin/activate
(.venv) (base)$ python --version
Python 3.8.3
## Functionのプロジェクトの作成
(.venv) (base)$ func init CostToBlob --python
Found Python version 3.8.3 (python3).
Writing requirements.txt
Writing .funcignore
Writing getting_started.md
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostToBlob/.vscode/extensions.json
## Functionの作成
(.venv) (base)$ cd CostToBlob
(.venv) (base)$ func new
Select a number for template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Durable Functions activity
4. Durable Functions entity
5. Durable Functions HTTP starter
6. Durable Functions orchestrator
7. Azure Event Grid trigger
8. Azure Event Hub trigger
9. HTTP trigger
10. Kafka output
11. Kafka trigger
12. Azure Queue Storage trigger
13. RabbitMQ trigger
14. Azure Service Bus Queue trigger
15. Azure Service Bus Topic trigger
16. Timer trigger
Choose option: 16
Timer trigger
Function name: [TimerTrigger] CostToBlobMontly
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostToBlob/CostToBlobMontly/readme.md
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostToBlob/CostToBlobMontly/__init__.py
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostToBlob/CostToBlobMontly/function.json
The function "CostToBlobMontly" was created successfully from the "Timer trigger" template.
## ディレクトリ構成の確認
(.venv) (base)$ tree -a
.
├── .funcignore
├── .gitignore
├── .vscode
│ └── extensions.json
├── CostToBlobMontly
│ ├── __init__.py
│ ├── function.json
│ └── readme.md
├── getting_started.md
├── host.json
├── local.settings.json
└── requirements.txt
2 directories, 10 files
内蔵されたPythonプログラムの执行
执行程序
import logging
import time
import json
import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.mgmt.resource import SubscriptionClient
from azure.mgmt.costmanagement import CostManagementClient
from azure.storage.blob import BlobServiceClient
from datetime import datetime, timezone, timedelta
import pandas as pd
CONTAINER_NAME = 'usage-summary-data'
# 接続しているテナントのサブスクリプションを操作するオブジェクトを取得
def GetSubscriptionObject():
subscription_client = SubscriptionClient(
credential=DefaultAzureCredential()
)
return subscription_client
# CostManagement情報 を操作するオブジェクトを取得
def GetCostManagementObject():
costmgmt_client = CostManagementClient(
credential=DefaultAzureCredential()
)
return costmgmt_client
# Blobを操作するオブジェクトを取得
def GetBlobServiceObject():
blob_service_client = BlobServiceClient(
account_url="https://usagecostmanage.blob.core.windows.net",
credential=DefaultAzureCredential()
)
return blob_service_client
# Containerを操作するオブジェクトを取得
def GetContainerObject(blob_service_client):
container_client = blob_service_client.get_container_client(CONTAINER_NAME)
return container_client
# 指定した Subscription について CostManagement からコストを取得
def GetCostManagement(costmgmt_client, subs_id):
# Query costmanagement(先月の情報を取得する)
SCOPE = '/subscriptions/{}'.format(subs_id)
costmanagement = costmgmt_client.query.usage(
SCOPE,
{
"type": "Usage",
"timeframe": "TheLastMonth",
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "ResourceGroup"
}
]
}
}
)
return costmanagement
# サブスクリプションIDを指定しリソースグループ毎に CostManagement情報を取得
def GetSubscriptionCsotManagement():
# サブスクリプションを操作するオブジェクトの取得
subscription_list = GetSubscriptionObject()
# CostManagementを操作するオブジェクトの取得
costmgmt_client = GetCostManagementObject()
# Blobを操作するオブジェクトを取得
blob_service_client = GetBlobServiceObject()
# Containerを操作するオブジェクトを取得
container_client = GetContainerObject(blob_service_client)
# 取得コストの キーと値
row_key = ['UsageCost', 'Date', 'ResourceGroup', 'Currency']
row_value = []
# サブスクリプション毎に CostManagement からコストを取得
for n, subs in enumerate(subscription_list.subscriptions.list()):
# 指定のサブスクリプションの CostManagement からコストを取得
logging.info("\n\n ##### サブスクリプション : {} #####".format(subs.display_name))
costmanagement = GetCostManagement(costmgmt_client, subs.subscription_id)
# rowsカラムデータを取得し、サブスクリプションのコスト合計値の取得
SubTotalCost = sum(cost[0] for cost in costmanagement.rows)
# その合計値が「0」の場合、次のサブスクリプションへ
if SubTotalCost == 0 :
continue
# 取得したコスト情報を日付で昇順ソートし、辞書型に変換する
row_value = sorted(costmanagement.rows, key=lambda x:x[1], reverse=False)
row_dict = [dict(zip(row_key,item)) for item in row_value]
# リソースグループでソートして、とりあえずファイルに保存しておく
rows = sorted(row_dict, key=lambda x:x['ResourceGroup'], reverse=False)
UsageDataToBlobContainer(rows, subs.display_name, blob_service_client)
# break
# コンテナ一覧の取得
BlobList(container_client)
# クローズ処理
costmgmt_client.close()
subscription_list.close()
container_client.close()
blob_service_client.close()
# サブスクリプション毎に CostManagement情報を取得
def UsageDataToBlobContainer(rows, subname, blob_service_client):
# データを DataFrame化し、不必要項目(Currency)の削除と必要項目(Subscription)の追加
dfl=pd.DataFrame(rows)
dfl.drop("Currency", axis=1, inplace=True)
dfl['Subscription'] = subname
# json形式への変換
rowl_key = ['UsageCost', 'Date', 'ResourceGroup', 'Subscription']
rowl_dict = [dict(zip(rowl_key,item)) for item in dfl.values]
# 保存するファイル名の生成
now = datetime.now()
# blob_name = './output/UsageCost_' + subname + '_' + now.strftime('%Y%m%d_%H%M%S') + '.json'
blob_name = now.strftime('%Y%m%d_%H%M%S') + '_' + subname + '.json'
try:
# Create a blob client using the blob_name as the name for the blob
blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=blob_name)
# Blob Upload
blob_client.upload_blob(json.dumps(rowl_dict))
except Exception as ex:
logging.info('Exception:')
logging.info(ex)
# Blob一覧の取得
def BlobList(container_client):
logging.info("\n\n === Blob 一覧の表示 ===")
try:
# List the blobs in the container
logging.info("\nListing blobs...")
blob_list = container_client.list_blobs()
for blob in blob_list:
logging.info("\t" + blob.name)
except Exception as ex:
logging.info('Exception:')
logging.info(ex)
def main(mytimer: func.TimerRequest) -> None:
# 日本時間の取得
JST = timezone(timedelta(hours=+9), 'JST')
today = datetime.now(JST)
if mytimer.past_due:
logging.info('■ ■ ■ The timer is past due! ■ ■ ■')
logging.info('■ ■ ■ Python timer trigger function (func-CostToBlob) ran at %s ■ ■ ■', today)
start = time.time()
GetSubscriptionCsotManagement()
generate_time = time.time() - start
logging.info("処理時間:{0}".format(generate_time) + " [sec]")
定义所需的Python软件包来执行。
azure-functions
azure-identity
azure-mgmt-resource
azure-mgmt-costmanagement
azure-storage-blob
tabulate
requests
pandas
设定执行计划(定时触发器)
每个月的第7号14:20:00(JST)(每个月的第7号5:20:00(UTC))执行
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "mytimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 20 5 7 * *"
}
]
}
在本地环境中测试 Azure Functions
执行程序
## Pythonパッケージのインストール
(.venv) (base)$ pip install -r requirements.txt
## Azure Functions 構成情報のローカルに設定
(.venv) (base)$ func azure functionapp fetch-app-settings func-CostToBlob
App Settings:
Loading FUNCTIONS_WORKER_RUNTIME = *****
Loading FUNCTIONS_EXTENSION_VERSION = *****
Loading AzureWebJobsStorage = *****
Loading WEBSITE_CONTENTAZUREFILECONNECTIONSTRING = *****
Loading WEBSITE_CONTENTSHARE = *****
Loading APPINSIGHTS_INSTRUMENTATIONKEY = *****
Loading AZURE_TENANT_ID = *****
Loading AZURE_CLIENT_ID = *****
Loading AZURE_CLIENT_SECRET = *****
Connection Strings:
## ローカル環境での実行
(.venv) (base)$ source ../.venv/bin/activate
(.venv) (base)$ func start --verbose
Found Python version 3.8.3 (python3).
%%%%%%
%%%%%%
@ %%%%%% @
@@ %%%%%% @@
@@@ %%%%%%%%%%% @@@
@@ %%%%%%%%%% @@
@@ %%%% @@
@@ %%% @@
@@ %% @@
%%
%
Azure Functions Core Tools
Core Tools Version: 3.0.3785 Commit hash: db6fe71b2f05d09757179d5618a07bba4b28826f (64-bit)
Function Runtime Version: 3.2.0.0
:
省略
:
[2021-10-02T03:48:00.398Z] Host started (526ms)
[2021-10-02T03:48:00.398Z] Job host started
[2021-10-02T03:48:04.946Z] Host lock lease acquired by instance ID '000000000000000000000000DDDDDDDD'.
:
省略
:
将部署至Azure
执行部署
## Pythonパッケージのインストール
(.venv) (base)$ func azure functionapp publish func-CostToBlob
Uploading 7.67 KB [###############################################################################]
Remote build in progress, please wait...
:
省略
:
Uploading built content /home/site/artifacts/functionappartifact.squashfs for linux consumption function app...
Resetting all workers for func-costtoblob.azurewebsites.net
Deployment successful.
Remote build succeeded!
Syncing triggers...
Functions in func-CostToBlob:
CostToBlobMontly - [timerTrigger]
在时间到达后,将按日对每个资源组进行汇总的 Azure 使用费用按订阅进行归档,并上传到指定的 Azure Blob 容器。
总结
在使用Azure Functions的过程中,我遇到了很多问题,但最终成功地让我自己编写的Python程序在其上运行。使用Azure Function确实需要花费一些功夫,我希望它能像AWS一样更加方便快捷。
请您参考以下的文章。
我参考了以下的文章。非常感谢。
安装和配置Mac上的Python3【最终版】