我尝试将 Azure 使用费用发布到 Teams 的频道中【按月计费】
总结
将这篇文章(尝试使用Python在Azure资源组中按日获取使用费用)的Python程序注册到Azure Functions中进行自动执行,并将结果POST到我们公司信息共享工具Temas的频道上,如果能稍微降低Azure的使用费用就好了……
执行环境
macOS Big Sur 11.1
Python 3.8.3
Azure CLI 2.28.0
Azure Functions Core Tools 3.0.3785
Azure函数运行时版本:3.2.0.0
准备事前
创建用于执行程序的ServicePrincipal。
根据这篇文章,ServicePrincipal的客户机密有效期默认为1年,最大为2年。因此,我们可以使用Azure CLI按以下步骤创建一个有效期为29年的ServicePrincipal(目前还没有问题)。
## 使用するテナントのAzure環境へのログイン
$ az login --tenant <tenant_id>
## 最初は2年で作成します(ロール割当なしで)
$ az ad sp create-for-rbac --name <ServicePrincial名> --skip-assignment --years 2
## 作成された内容を取得できることを確認します(いきなり29年で作成した場合、取得できません、、、)
$ az ad sp list --display-name <ServicePrincial名>
## その後、29年で再作成します(ロール割当なしで)
$ az ad sp create-for-rbac --name <ServicePrincial名> --skip-assignment --years 29
{
"appId": "xxxxxxxx-xxxx-4633-8080-xxxxxxxxxxxx", --> 関数アプリの構成で AZURE_CLIENT_ID として登録
"displayName": "<ServicePrincial名>",
"name": "xxxxxxxx-xxxx-4633-8080-xxxxxxxxxxxx",
"password": "hogehogehogehogehogehogehogehogege", --> 関数アプリの構成で AZURE_CLIENT_SECRET として登録
"tenant": "zzzzzzzz-cccc-4645-5757-zzzzzzzzzzzz" --> 関数アプリの構成で AZURE_TENANT_ID として登録
}
## 必要なスコープに必要なロールを割り与えます
## 今回は複数Subscriptionの利用料金を取得したいので、スコープ:Subscription ロール:Reader とします
$ APP_ID=$(az ad sp list --display-name <ServicePrincial名> --query '[].{ID:appId}' --output tsv)
$ 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
设定Teams以通知程序的执行结果。
为了在Teams的频道中接收执行结果通知,请按照这篇文章中的”事前准备”步骤进行操作,并进行Webhook设置。在此过程中,请复制生成的Webhook URL。这个URL在配置函数应用程序时需要作为ENDPOINT_xxxx进行注册。
在Azure上创建所需的内容
## 使用サブスクリプションを定義します
$ 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
## Functions(関数アプリ)の作成
az functionapp create ---resource-group <ResourceGroup名> --name <Functions名> --storage-account <StorageAccount名> --runtime python --runtime-version 3.7 --consumption-plan-location japaneast --os-type Linux --functions-version 2
## 作成した関数アプリの構成設定
### 関数アプリ実行のための ServicePrincipal 情報の定義
az functionapp config appsettings set -n <Functions名> -g <ResourceGroup名> \
--settings "AZURE_TENANT_ID=zzzzzzzz-cccc-4645-5757-zzzzzzzzzzzz"
az functionapp config appsettings set -n <Functions名> -g <ResourceGroup名> \
--settings "AZURE_CLIENT_ID=xxxxxxxx-xxxx-4633-8080-xxxxxxxxxxxx"
az functionapp config appsettings set -n <Functions名> -g <ResourceGroup名> \
--settings "AZURE_CLIENT_SECRET=hogehogehogehogehogehogehogehogege"
### 関数アプリの実行結果をTeamsのチャネルにPOST(Webhook)するための定義
### サブスクリプション毎のPOSTを行なうので、その数分を定義します
az functionapp config appsettings set -n <Functions名> -g <ResourceGroup名> \
--settings "ENDPOINT_NSN=https://nnn.webhook.office.com/webhookb2/nnn/IncomingWebhook/sss/nnn"
:
:
az functionapp config appsettings set -n <Functions名> -g <ResourceGroup名> \
--settings "ENDPOINT_QND=https://nnn.webhook.office.com/webhookb2/qqq/IncomingWebhook/nnn/ddd"
### 定義情報の確認
az functionapp config appsettings list -n <Functions名> -g <ResourceGroup名> -o table
创建本地环境以用于功能。
## 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 CostDetail --python
Writing requirements.txt
Writing .funcignore
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostDetail/.vscode/extensions.json
## Functionの作成
(.venv) (base)$ cd CostDetail
(.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] CostSummaryMonthly
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostDetail/CostSummaryMonthly/readme.md
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostDetail/CostSummaryMonthly/__init__.py
Writing /Users/ituru/MyDevelops/AzureCostManagement/Functions/CostDetail/CostSummaryMonthly/function.json
The function "CostSummaryMonthly" was created successfully from the "Timer trigger" template.
## ディレクトリ構成の確認
(.venv) (base)$ tree -a
.
├── .funcignore
├── .gitignore
├── .vscode
│ └── extensions.json
├── CostSummaryMonthly
│ ├── __init__.py
│ ├── function.json
│ └── readme.md
├── getting_started.md
├── host.json
├── local.settings.json
└── requirements.txt
2 directories, 10 files
Python编程中的内嵌程序执行
执行程序
import logging
import azure.functions as func
import os
import time
import json
import itertools
from itertools import groupby
from datetime import datetime, timezone, timedelta
from azure.identity import DefaultAzureCredential
from azure.mgmt.resource import SubscriptionClient
from azure.mgmt.costmanagement import CostManagementClient
import requests
import pandas as pd
# Teams_endpoint = 'Microsoft Teamsの チャネルエンドポイント(Webhook)'
TEAMS_ENDPOINT = {
"NSG-01": os.environ['ENDPOINT_NSN'],
"PSG1-01": os.environ['ENDPOINT_PSP1'],
"PSG2-01": os.environ['ENDPOINT_PSP2'],
"SAG-01": os.environ['ENDPOINT_SAS'],
"SSG-01": os.environ['ENDPOINT_SSS'],
"STG1-01": os.environ['ENDPOINT_STS1'],
"STG2-01": os.environ['ENDPOINT_STS2'],
"WJT-01": os.environ['ENDPOINT_WJW'],
"cscedu-01": os.environ['ENDPOINT_CSC'],
"iapp-01": os.environ['ENDPOINT_IAPP'],
"market-sc-01": os.environ['ENDPOINT_SCC'],
"tech-share-01": os.environ['ENDPOINT_SHARE'],
"MixedTeam": os.environ['ENDPOINT_MIX'],
"Ondemand": os.environ['ENDPOINT_OND']
}
# 接続しているテナントのサブスクリプションを操作するオブジェクトを取得
def GetSubscriptionObject():
subscription_client = SubscriptionClient(
credential=DefaultAzureCredential()
)
return subscription_client
# CostManagement情報 を操作するオブジェクトを取得
def GetCostManagementObject():
costmgmt_client = CostManagementClient(
credential=DefaultAzureCredential()
)
return costmgmt_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(day0):
# サブスクリプションを操作するオブジェクトの取得
subscription_list = GetSubscriptionObject()
# CostManagementを操作するオブジェクトの取得
costmgmt_client = GetCostManagementObject()
# 取得コストの キーと値
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)
# リソースグループでグルーピングする
for key, resgp in groupby(rows, key=lambda x: x['ResourceGroup']):
# イテレータを複数回使用するための複製
resgp_list, resgp_cost = itertools.tee(resgp, 2)
# このリソースグループでのコスト合計の表示
TotalCost = sum(costList['UsageCost'] for costList in resgp_cost)
logging.info("\n\n ===== リソースグループ : {} =====".format(key))
logging.info("\t コスト合計:{0}".format(TotalCost) + " JPY")
# 取得したデータを DataFrame化し、必要な表示項目のみ抽出し、Index変更後、 markdown 形式に変換
df=pd.DataFrame(resgp_list)
df.drop(["ResourceGroup", "Currency"], axis=1, inplace=True)
df.set_index("Date", inplace=True)
teames_post_str = df.to_markdown()
# TeamsEndpointへのPOST
teams_endpoint_post_detail(teames_post_str, subs.display_name, key, '{:,}'.format(round(TotalCost)), day0)
costmgmt_client.close()
subscription_list.close()
# TeamsEndpointへのデータPOST(詳細)
def teams_endpoint_post_detail(teames_post_str, dept, key, TotalCost, day0):
# Microsoft Teams へ送信する下ごしらえ
request = {
'title': '【 Azure : ' + day0 + ' 】 リソースグループ: ' + key + ' 合計: ¥ ' + TotalCost,
'text': teames_post_str
}
# 環境変数の確認
try :
endpointurl = TEAMS_ENDPOINT[dept]
except KeyError :
logging.info("\n ENDPOINTの環境変数が定義されていません!")
return
# Microsoft Teams へ送信する
response = requests.post(endpointurl, json.dumps(request))
logging.info(response)
def main(mytimer: func.TimerRequest) -> None:
# 日本時間の取得
JST = timezone(timedelta(hours=+9), 'JST')
today = datetime.now(JST)
# 前月の日付(yyyy-mm)の取得
last_month = today - relativedelta(months=1)
day0 = last_month.strftime("%Y-%m")
if mytimer.past_due:
logging.info('■ ■ ■ The timer is past due! ■ ■ ■')
logging.info('■ ■ ■ Python timer trigger function (func-CostDetail) ran at %s ■ ■ ■', today)
start = time.time()
GetSubscriptionCsotManagement(day0)
generate_time = time.time() - start
logging.info("処理時間:{0}".format(generate_time) + " [sec]")
定义所需的Python包执行
azure-functions
azure-identity
azure-mgmt-resource
azure-mgmt-costmanagement
tabulate
requests
pandas
設定執行日程(定時觸發器)
每个月的第4天15:20:00(日本标准时间)(每个月的第4天6:20:00(协调世界时))执行。
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "mytimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 20 6 4 * *"
}
]
}
在本地环境中测试 Azure Functions。
执行程序 (zhí xù)
## Pythonパッケージのインストール
(.venv) (base)$ pip install -r requirements.txt
## Azure Functions 構成情報のローカルに設定
(.venv) (base)$ func azure functionapp fetch-app-settings <Functions名>
(.venv) (base)$ func azure functionapp fetch-app-settings func-CostDetail
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 = *****
Loading ENDPOINT_NSN = *****
Loading ENDPOINT_PSP1 = *****
Loading ENDPOINT_PSP2 = *****
Loading ENDPOINT_SAA = *****
Loading ENDPOINT_SSS = *****
Loading ENDPOINT_STS1 = *****
Loading ENDPOINT_STS2 = *****
Loading ENDPOINT_WJW = *****
Loading ENDPOINT_IAPP = *****
Loading ENDPOINT_SCC = *****
Loading ENDPOINT_SHARE = *****
Loading ENDPOINT_CSC = *****
Loading ENDPOINT_MIX = *****
Loading ENDPOINT_OND = *****
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-02T05:43:04.864Z] Host started (559ms)
[2021-10-02T05:43:04.864Z] Job host started
[2021-10-02T05:43:09.415Z] Host lock lease acquired by instance ID '000000000000000000000000EEEEEEEE'.
:
省略
:
部署到Azure
执行部署
(.venv) (base)$ func azure functionapp publish <Functions名>
Uploading 8.15 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-costdetail.azurewebsites.net
Deployment successful.
Remote build succeeded!
Syncing triggers...
Functions in func-CostDetail:
CostSummaryMonthly - [timerTrigger]
只需要确认每个月的第4天,Azure每个ResourceGroup的使用费用通知会发送到Teams的指定频道中。
总结
虽然遇到了很多困难,但我终于成功在Azure Functions上运行了自己编写的Python程序。 Azure Function确实需要花费很大的精力,我希望能拥有AWS那种轻松的感觉。
请参考下列文章。
我已参考以下文章并感谢:
安装和配置Mac上的Python3【决定版】