Azureのコストアラートは、指定したスコープや条件でコストを監視し、閾値を超過した際に通知する機能です。
しかし、この通知は実際または予測コストが閾値を超えたタイミングに限られ、定期配信ではありません。さらに予算は単一スコープで動作するため、複数のリソースグループを横断して消費コストを一覧化・比較することはできません。通知に含まれる情報も閾値超過の事実とトータルコストが中心で、リソースグループごとの内訳やタグ情報まで、整った形で得ることは難しいのが実情です。
そこで、サブスクリプション内のリソースグループを横断して相対的に高コストなリソースグループを定期的に把握し、通知できる仕組みが必要だと考えました。
本記事では、Logic AppsとFunctionsによりCost Management APIの結果からリソースグループ別のコストをランキング化し、タグ情報を付与したHTMLレポートをメール配信するフローを紹介します。
概要
本手法では次の処理を自動化します。
- Azure Cost Management APIからリソースグループ別コストを取得
- Azure Resource Manager APIからタグ情報を取得
- Azure Functionsでコスト降順に整形し、HTMLテーブルを生成
- Logic Appsで整形済みHTMLをOffice 365 コネクタ経由でメール送信
- スケジュールトリガーで定期実行
必要な準備
Logic Appsリソースの準備
基本的には既定値で問題ないため、作成手順の詳細は本記事では割愛します。作成後はシステム割り当てマネージドIDを有効化し、サブスクリプションをスコープとして「コスト管理の閲覧者」ロールを付与します。
Functionsリソースの準備
HTMLレポートの作成をPythonで行うため、以下の設定値でリソースを作成します。
- オペレーティングシステム: Linux
- ランタイムスタック: Python
- バージョン: 3.12
実装手順
まずはフロー全体の完成イメージをお見せします。上から順にトリガー、HTTPコネクタ(リソースグループ別コスト取得用)、HTTPコネクタ(リソースグループ情報取得用)、HTTPコネクタ(Functions呼び出し用)、Outlookコネクタ(メール送信用)となっています。
各コネクタの設定とポイントを順に説明します。
トリガー
はじめにトリガーの設定についてです。
今回の実装ではRecurrenceトリガーにより毎週実行する設定にしていますが、取得頻度は要件に応じて設定してください。
リソースグループ別コスト取得用コネクタ
続いてはリソースグループ別コスト取得用コネクタの設定についてです。このコネクタはCost Management APIを呼び出し、サブスクリプション内のリソースグループごとの月初から当日までのコスト集計を取得します。
コネクタの設定項目には以下の値を入力します。
| 項目 | 値 |
|---|---|
| URI | https://management.azure.com/subscriptions/{サブスクリプションID}/providers/Microsoft.CostManagement/query?api-version=2023-03-01 |
| Method | POST |
| Headers | Content-Type: application/json |
| Body | 下のJSON参照 |
| Authentication Type | Managed identity |
| マネージドID | システム割り当てマネージドID |
BodyのJSONはこのようになります。
{ "type": "ActualCost", "timeframe": "MonthToDate", "dataset": { "aggregation": { "totalCost": { "name": "PreTaxCost", "function": "Sum" } }, "grouping": [ { "type": "Dimension", "name": "ResourceGroupName" } ] } }
リソースグループ情報取得用コネクタ
続いてはリソースグループ情報取得用のHTTPコネクタの設定についてです。このコネクタはAzure Resource ManagerのresourceGroupsエンドポイントにGETリクエストを送り、各リソースグループのタグなどのメタ情報を取得します。弊社Azure環境では所有者や用途を示すタグを付与しているため、取得したタグをCost Management APIの集計結果と突合してレポートに含めることで所有者や用途別の視点で分析できるようになります。
コネクタの設定項目には以下の値を入力します。
| 項目 | 値 |
|---|---|
| URI | https://management.azure.com/subscriptions/{サブスクリプションID}/resourceGroups?api-version=2021-04-01 |
| Method | GET |
| Authentication Type | Managed identity |
| マネージドID | システム割り当てマネージドID |
※メールに含める内容がリソースグループ名とコスト情報のみで十分な場合はこのコネクタを追加する必要はありません。
Functions呼び出し用コネクタ
続いてはFunctions呼び出し用コネクタの設定についてです。このコネクタは、前段の2つのHTTPコネクタで取得したコスト情報とリソースグループのタグ情報をもとにメール本文用のHTMLを作成するためのFunctionsを呼び出すコネクタです。まずはHTML整形を行う関数を用意します。以下はPythonによる実装の例です。
import azure.functions as func import logging import html from typing import Any, Dict, List app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) @app.route(route="http_trigger") def http_trigger(req: func.HttpRequest) -> func.HttpResponse: logging.info('Python HTTP trigger function processed a request.') try: data = req.get_json() except ValueError: return func.HttpResponse("Invalid JSON", status_code=400) if not isinstance(data, dict): return func.HttpResponse("Invalid JSON", status_code=400) rows: List[List[Any]] = data.get("rows") or [] rg_list: List[Dict[str, Any]] = data.get("rgTags") or [] try: rows_sorted = sorted( rows, key=lambda r: float(str(r[0]).replace(",", "")) if isinstance(r, list) and r and r[0] not in (None, "") else 0.0, reverse=True, ) except Exception: rows_sorted = rows tag_map: Dict[str, Dict[str, str]] = {} for rg in rg_list: if not isinstance(rg, dict): continue name = str(rg.get("name") or "").strip() if not name: continue tags = rg.get("tags") or {} usage = str((tags.get("Usage") or tags.get("usage") or "")).strip() owner = str((tags.get("Owner") or tags.get("owner") or "")).strip() tag_map[name.lower()] = {"usage": usage, "owner": owner} styles = ( "<style>table{border-collapse:collapse;width:100%;font-family:Segoe UI,sans-serif}" "th{background:#0F6CBD;color:#fff;padding:8px;text-align:left}" "td{border-bottom:1px solid #e1e1e1;padding:8px}" "tr:nth-child(even){background:#f9f9f9}</style>" ) header = ( "<h3>高額リソースグループ Top10</h3>" "<table><tr><th>#</th><th>リソースグループ名</th><th>コスト</th><th>用途</th><th>所有者</th></tr>" ) rows_html: List[str] = [] rank = 0 for r in rows_sorted: if not isinstance(r, list) or len(r) < 2: continue rg_name = str(r[1]).strip() if not rg_name: continue try: cost = float(str(r[0]).replace(",", "")) except Exception: cost = 0.0 info = tag_map.get(rg_name.lower(), {}) usage = info.get("usage", "") owner = info.get("owner", "") rank += 1 rows_html.append( "<tr>" f"<td>{rank}</td>" f"<td>{html.escape(rg_name)}</td>" f"<td>¥{cost:,.0f}</td>" f"<td>{html.escape(usage)}</td>" f"<td>{html.escape(owner)}</td>" "</tr>" ) if rank >= 10: break html_out = styles + header + "".join(rows_html) + "</table>" return func.HttpResponse(html_out, mimetype="text/html; charset=utf-8", status_code=200)
Logic Appsから受け取ったコストデータとリソースグループをもとに、コスト上位10件を抽出してHTMLテーブルを出力する実装となっています。こちらのコードをFunctionsへデプロイします。デプロイ手順は以下の公式ドキュメントを参照してください。
次に、Logic AppsのFunctions呼び出し用コネクタからこの関数を呼び出します。
コネクタの設定項目には以下の値を入力します。
| 項目 | 値 |
|---|---|
| URI | {Functionsの関数URL} |
| Method | POST |
| Headers | Content-Type: application/json |
| Body | {"rows": "@body('GetRGCost')?['properties']?['rows']", "rgTags": "@body('GetRG')?['value']"} |
Functionsの関数URLは関数の[関数のURLの取得] > [default (ファンクション キー)]から取得することができます。
ここまででHTML生成の処理が完了しました。
メール送信コネクタ
最後に、メール送信コネクタの設定についてです。このコネクタでは、Functions呼び出し用コネクタの出力を本文としたメールを送信する工程を担います。
コネクタの設定項目には以下の値を入力します。
| 項目 | 値 |
|---|---|
| 宛先 | {任意の宛先} |
| 件名 | {任意の件名} |
| 本文 | @{body('BuildHTML')} |
| 重要度 | {任意の重要度} |
宛先・件名・重要度は要件に合わせて設定してください。
以上がLogic Appsフローの作成手順でした。
実行結果
出力メールのイメージは次のとおりです。
表にはCost Managementの集計結果から抽出したリソースグループの上位10件を、用途・所有者タグと併せて一覧化しています。
まとめ
本記事では、Logic AppsとAzure Functionsを組み合わせて、リソースグループ別コストを定期配信するフローを紹介しました。
Azureのコストアラートでは設定できない定期配信、リソースグループごとのコスト詳細などをこのフローで補完することができるため、コストアラートと併用してこのフローをご活用いただければと思います。