AzureコストをSlackに通知する

pexels-photo-3943748.jpeg

Microsoft Sentinel等を利用していると、ログが毎月増加していくためそれに伴って費用も増加します。
確認のためにいちいちAzureポータルを見に行くのは面倒くさい&いつもどこから見るんだ?って迷うので、1ヶ月に1度前月分の金額をSlackに通知させてみたいと思います。

目次

課金データ閲覧ユーザーを作成する

すべてのロールをもたせる必要はないので、請求データが閲覧できる専用ユーザーを作成します。
そのほうがセキュリティ的にもいいですしね!
ここでは、AZ CLIを利用してコマンドラインから作成します。
サブスクリプションIDでは請求データを確認したいサブスクリプションIDを入力してください。

az login
az ad sp create-for-rbac --name slack-cost --role "Billing Reader" --scopes /subscriptions/サブスクリプションID

以下のような結果が返ってくれば成功です。
ここで表示されたパスワードを後ほど利用しますので、控えておいてください。

{
  "appId": "XXXXX-XXXXXXX-XXXXXXX-XXXX",
  "displayName": "slack-cost",
  "password": "***********************",
  "tenant": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX"
}

CostManagement APIを実行する

アクセストークンを取得する

上記で作成した情報を利用し、APIを実行するためのアクセストークンを取得します。
テナントIDや、クライアントID、パスワードを変更し実行してください。

curl -X POST \
https://login.microsoftonline.com/${tenant}/oauth2/token \
-F grant_type=client_credentials \
-F resource=https://management.core.windows.net/ \
-F client_id=${appId} \
-F client_secret=${password} \
| jq -r .access_token

APIを実行する

まずテストでPostmanを利用して、リクエストを送信し期待する値が取得できるかを確認します。
メソッドやリクエスト先は以下のとおりです。

リクエスト先https://management.azure.com/subscriptions/サブスクリプションID/providers/Microsoft.CostManagement/query?api-version=2022-10-01
メソッドPOST
ヘッダーContent-Type : application/json
Authorization Bearer 発行したアクセストークン


今回は先月の合計金額を出力したいので、リクエスト本文(Body)には以下のように入力します。
その他色々な形式で出力が可能なので、Microsoftのドキュメントを参照してみてください。

{
    "dataset": {
            "aggregation": {
                "totalCost": {
                    "function": "Sum",
                    "name": "PreTaxCost"
                }
            },
            "granularity": "Month"
        },
        "timeframe": "TheLastBillingMonth",
        "type": "Usage"
  }

リクエストが成功すると以下のような応答が返ってきます。

{
    "id": "subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.CostManagement/query/e4694e2b-58c9-48fd-8a43-1b1edc864330",
    "name": "XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "type": "Microsoft.CostManagement/query",
    "location": null,
    "sku": null,
    "eTag": null,
    "properties": {
        "nextLink": null,
        "columns": [
            {
                "name": "PreTaxCost",
                "type": "Number"
            },
            {
                "name": "Currency",
                "type": "String"
            }
        ],
        "rows": [
            [
                50000.02341900055,
                "JPY"
            ]
        ]
    }
}

請求金額が取得できたので、後はこれを呼び出すようなスクリプトを作成し通知させればOKです。

Slackへ通知する(GAS利用)

簡単に使えるGASで今回はSlackへ通知させてみます。

  1. スプレッドシートを新規作成します
  2. 「拡張機能」→【Apps Script】をクリックします。

今回は以下のようなコードを作成し、通知させてみました。
スクリプトプロパティに各種値を登録してください

var PS = PropertiesService.getScriptProperties()
var TENANT_ID = PS.getProperty('TENANT_ID')
var CLIENT_ID = PS.getProperty('CLIENT_ID')
var CLIENT_SECRET = PS.getProperty('CLIENT_SECRET')
var SUBSCRIPTION_ID = PS.getProperty('SUBSCRIPTION_ID')
var SLACK_WEBHOOK_URL = PS.getProperty('SLACK_WEBHOOK_URL')

const ss = SpreadsheetApp.getActiveSpreadsheet() //書き込み先のスプレッドシートファイル
const sheet = ss.getActiveSheet() //書き込み先のシート

function getCost(){
  let token = getToken()
  
  //先月の合計請求金額を取得する
  let queryJson = {
    "dataset": {
            "aggregation": {
                "totalCost": {
                    "function": "Sum",
                    "name": "PreTaxCost"
                }
            },
            "granularity": "Month"
        },
        "timeframe": "TheLastBillingMonth",
        "type": "Usage"
  }


  let response = fetchCostManagementRawJSON(token,queryJson)
  let totalCost = response.properties.rows[0][0]
  

  //先月を取得する
  let date = new Date();
  let month = date.getMonth()+1
  date.setMonth(month-2)
  let onemonthbefore = Utilities.formatDate(date, 'JST', 'yyyy/MM');

  //四捨五入し、スプシにデータを入れた後、カンマ区切り数値にする
  totalCost = Math.round(totalCost)
  let append = [ onemonthbefore,totalCost]
  sheet.appendRow(append)
  totalCost = totalCost.toLocaleString('ja-JP')
  
  //Slack Block Kit組み立て
  const blockKitJson = {
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "Azure" + onemonthbefore + "の請求金額"
        }
      },
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*" + totalCost + "円*"
        },
        "accessory": {
          "type": "button",
          "text": {
            "type": "plain_text",
            "text": "Azure Portal",
            "emoji": true
          },
          "value": "Azure",
          "url": "https://portal.azure.com/#@example.onmicrosoft.com/resource/subscriptions/2051cec7-f215-4c24-aed0-ffba0c22eb16/costByResource",
          "action_id": "button-action"
        }
      }
    ]
  }

  notifySlack(SLACK_WEBHOOK_URL,blockKitJson)

}


function fetchCostManagementRawJSON(token, query) {
    var options = {
        muteHttpExceptions: true,
        contentType: 'application/json',
        headers: { Authorization: "Bearer " + token },
        method: 'post',
        payload: JSON.stringify(query)
    };
    var url = 'https://management.azure.com/subscriptions/' +
        SUBSCRIPTION_ID +
        '/providers/Microsoft.CostManagement/query?api-version=2022-10-01';
    var res = UrlFetchApp.fetch(url, options).getContentText();
    return JSON.parse(res);
}


function getToken() {
    var payload = 'client_id=' +
        CLIENT_ID +
        '&client_secret=' +
        CLIENT_SECRET +
        '&grant_type=client_credentials' +
        '&resource=https%3A%2F%2Fmanagement.core.windows.net%2F';
    var options = {
        headers: { contentType: 'application/x-www-form-urlencoded' },
        method: 'post',
        payload: payload
    };
    var url = 'https://login.microsoftonline.com/' + TENANT_ID + '/oauth2/token';
    var res = UrlFetchApp.fetch(url, options);
    var token = JSON.parse(res.getContentText()).access_token;
    return token;
}
function notifySlack(url,payload) {

  var payload = JSON.stringify(payload)

  let headers = {
    "Content-Type" : "application/json; charset=UTF-8"
  }

  let params = {
    "method" : "post",
    "contentType" : "application/json",
    "headers" : headers,
    "payload" : payload
  }

  let ret = UrlFetchApp.fetch(url,params)
  console.log(ret)
  return ret.getContentText()
  
}

実行結果

手動でテスト実行してみるとSlackに以下のような形で通知がきます。
横にAzure Portalというリンクをつけていますが、不要な方はBlock Kitから削除してください。

まとめ

CostManagement APIは色々できるのですが、リクエスト本文を組み立てるのが結構大変な気がします。
Azureのポータルサイトもどこからコスト見るんだったっけ?となりがち(?)な気がするので、毎月1回Slackに通知を飛ばして、請求金額の増減を確認できるようにするだけでもそれなりの効果はありそうです。
今回のコードではスプレッドシートに毎月金額行を追加する形にしているので、データがたまってきたらLoockerStudioなどで可視化したりするとおもしろいかもしれません。

pexels-photo-3943748.jpeg

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次