Azureコストとサービス毎の内訳をTeams通知する

## 0. はじめに
Azureのコストは[Azure portal](https://portal.azure.com/)の[コスト管理](https://portal.azure.com/#view/Microsoft_Azure_CostManagement/Menu)で詳しく分析できますが、毎日ログインして確認するのはなかなか大変です。

そこで、Azureでどのサービスをいくら使っているか、今月と今年度のコストをTeamsに毎日通知するようにしてみました。

![teams_post.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/c85f2843-2a1f-53d8-4e03-66b28be9f338.png "通知イメージ")

## 1. 手順の流れ
```Automation アカウント```の```Runbook```でコストを取得し、```ロジック アプリ```の```ワークフロー```で```Teams```に投稿します。（図中の番号は以下の章番号に対応しています。）

```mermaid
graph LR
    subgraph AUTO["`**Automation アカウント**`"]
        direction TB
        AUTO1[2.Automationアカウントの作成]
        AUTO2[3.Runbookの作成]
    end
    subgraph LOGIC["`**ロジック アプリ**`"]
        direction TB
        LOGIC1[4.ロジックアプリの作成]
        LOGIC2[5.ワークフローの作成]
    end
    subgraph TEAMS["`**Teams**`"]
        direction TB
        TEAMS1[6.Teamsで通知確認]
    end
    AUTO --> LOGIC --> TEAMS
    AUTO1 --> AUTO2
    LOGIC1 --> LOGIC2
```

## 2. Automationアカウントの作成
#### 2.1. Automationアカウントを作成する
[Azure portal](https://portal.azure.com)にサインインし、ナビゲーションバーで```Automation アカウント```に移動します。

```作成```を選択し、```サブスクリプション```、```リソースグループ```、```Automation アカウント名```、```リージョン```を入力してアカウントを```作成```します。

#### 2.2. Automationアカウントにコストを閲覧する権限を付与する
コストを参照できるよう、AutomationアカウントのシステムマネジメントIDに```コスト管理の閲覧者```ロールを割り当てます。

```作成したAutomationアカウント```を選択し、```アカウント設定```>```ID```>```システム割り当て済み```>```Azure ロールの割り当て```をクリックします。

![automation_add_role.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/c885b8fe-795d-3455-adaa-ffbef59be58c.png)

```ロールの割り当ての追加```を選択し、```スコープ```を```サブスクリプション```にし、```サブスクリプション```を入力し、```役割```で```コスト管理の閲覧者```を選んで```保存```します。

![automation_add_role_costmanagementreader.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/a67bf710-22b4-816b-2d6e-558a5fcaa6c1.png)

## 3. Runbookの作成
#### 3.1. AutomationアカウントにRunbookを作成する
Azureコストを取得し、月次コストおよび年次コストとそのサービス内訳を抽出して、ロジックアプリへHTTPリクエストを送信するRunbookを作成します。

```作成したAutomationアカウント```を選択し、```プロセス オートメーション```>```Runbook```>```Runbook の作成```をクリックし、Runbookの```名前```、```Runbook の種類```、```ランタイム バージョン```を入力して```作成```します。今回は以下を選択しました。
| 項目 | 設定値 |
|:-|:-|
| 名前 | （任意） |
| Runbook の種類 | PowerShell |
| ランタイム バージョン | 7.2 |

```PowerShell Runbook の編集```で以下のコードを入力し```保存```し```公開```します。

:::note info
Runbookを作成（修正）したら、必ず```公開```して```状態```が```発行済み```になっていることを確認してください。保存だけしても公開しないと有効になりません。
:::
:::note warn
リソースグループの利用実績が無い場合コストがnullとなり正しく動かない場合があります。必要ならエラー処理などを追加してください。
:::

```PowerShell:Runbook
$SubscriptionID = Get-AutomationVariable -Name "SubscriptionID"
$RGName = Get-AutomationVariable -Name "ResourceGroupName"
$LogicAppsUri = Get-AutomationVariable -Name "LogicAppsUri"

if ((Get-Date).Month -ge 4) {
    $FiscalYear = (Get-Date).Year
} else {
    $FiscalYear = (Get-Date).Year - 1
}
$FYStartDay = (Get-Date -Year $FiscalYear -Month 4 -Day 1)
$Today = (Get-Date).Date

Connect-AzAccount -Identity
Set-AzContext -SubscriptionId $SubscriptionID

$MonthlyUsageDetail = Get-AzConsumptionUsageDetail -ResourceGroup $RGName
$MonthlyCost = [Math]::Round($($MonthlyUsageDetail.PretaxCost | Measure -Sum).Sum, [MidpointRounding]::AwayFromZero).ToString("#,#")
$MonthlyServicesCost = $MonthlyUsageDetail | Group-Object -Property ConsumedService | Sort-Object -Property {@($_.Group.PretaxCost | Measure -Sum).Sum} -Descending | ForEach-Object {
    [PSCustomObject]@{
        Service = $_.Name -replace '^Microsoft\.', ''
        Cost = [Math]::Round(($_.Group.PretaxCost | Measure -Sum).Sum, [MidpointRounding]::AwayFromZero).ToString("#,#")
    }
}

$AnnualUsageDetail = Get-AzConsumptionUsageDetail -ResourceGroup $RGName -StartDate $FYStartDay -EndDate $Today
$AnnualCost = [Math]::Round($($AnnualUsageDetail.PretaxCost | Measure -Sum).Sum, [MidpointRounding]::AwayFromZero).ToString("#,#")
$AnnualServicesCost = $AnnualUsageDetail | Group-Object -Property ConsumedService | Sort-Object -Property {@($_.Group.PretaxCost | Measure -Sum).Sum} -Descending | ForEach-Object {
    [PSCustomObject]@{
        Service = $_.Name -replace '^Microsoft\.', ''
        Cost = [Math]::Round(($_.Group.PretaxCost | Measure -Sum).Sum, [MidpointRounding]::AwayFromZero).ToString("#,#")
    }
}

$Body = @{
    resourcegroup = $RGName
    monthlycost = $MonthlyCost
    monthlyservicescost = $MonthlyServicesCost
    annualcost = $AnnualCost
    annualservicescost = $AnnualServicesCost
}
Invoke-RestMethod -Uri $LogicAppsUri -Method Post -Body ($Body | ConvertTo-Json -Depth 3) -ContentType "application/json"
```

#### 3.2. Runbookにスケジュールを設定する
```作成したAutomationアカウント```を選択し、```共有リソース```>```スケジュール```>```スケジュールの追加```をクリックし、```名前```、```開始時```、```繰り返し```、```間隔```、```有効期限```を入力して```作成```します。
今回は月曜日から金曜日の午前9時に実行するスケジュールを作成しました。

![runbook_schedule_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/c5e61ccb-509a-6453-2107-c752de277352.png)

```作成したAutomationアカウント```を選択し、```プロセス オートメーション```>```Runbook```>```作成したRunbook名```を選択し、```スケジュールをリンク```をクリックします。

![runbook_schedule_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/1150c8f2-e989-3cb1-b877-b1342b06d079.png)

```スケジュールを Runbook にリンクします```>```作成したスケジュール```を選択し、```OK```をクリックします。

![runbook_schedule_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/2095d146-fa0d-b02d-b4f3-fb1d1556e13c.png)

#### 3.3. Runbookの変数を設定する
Runbookから呼び出す変数を設定します。

```作成したAutomationアカウント```を選択し、```共有リソース```>```変数```>```変数の追加```をクリックし、以下の変数を```作成```します。

| 名前 | タイプ | 値 |
|:-|:-|:-|
| LogicAppsUri | 文字列 | コスト情報をRunbookからロジックアプリへ送信するためのロジックアプリのアドレス<br>（```5.9. ロジックアプリのアドレスをRunbook変数に設定```参照） |
| ResourceGroupName | 文字列 | コストを取得するリソースグループ名 |
| SubscriptionID | 文字列 | サブスクリプションID |

## 4. ロジックアプリの作成
[Azure portal](https://portal.azure.com)のナビゲーションバーで```ロジック アプリ```に移動します。

```追加```を選択し、```ホスティング オプションの選択```で```マルチテナント```を選択して```選択```をクリックします。

![logicapps_create_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/69c7ee61-f842-d334-dba0-efd9b4232619.png)

```ロジック アプリの作成（マルチテナント）```で```サブスクリプション```、```リソース グループ```、```ロジック アプリ名```、```リージョン```を入力して```作成```します。

## 5. ワークフローの作成
Runbookからコスト情報を受信し、月次コストおよび年次コストを取得して、Teamsチャネルに投稿するロジックアプリワークフローを作成していきます。

![workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/3b887c56-4aa7-8d39-ff82-605a8d79168e.png)

```作成したロジックアプリ名```を選択し、```開発ツール```>```ロジック アプリ デザイナー```を選択します。

#### 5.1. HTTPリクエストの受信
Runbookからコスト情報を含むHTTPリクエストを受信します。

```ロジック アプリ デザイナー```で```トリガーの追加```を選択し、```Request```>```When a HTTP request is received```をクリックします。

![workflow_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/00129bee-afbe-a10a-0189-e455b99b313d.png)

トリガー名を変更します。

| トリガー名 |
|:-|
| HTTPリクエストの受信 |

```パラメーター```に以下の項目を入力し、```>>```でトリガーを折りたたみます。

| パラメーター | 入力内容 |
|:-|:-|
| Method | POST |
| Request Body JSON Schema | 以下参照 |

```json: Request Body JSON Schema
{
    "properties": {
        "resourcegroup": "string",
        "monthlycost": "string",
        "monthlyservicescost": {
            "type": "array",
            "items": {
                "properties": {
                    "Service": "string",
                    "Cost": "string"
                },
                "type": "object"
            }
        },
        "annualcost": "string",
        "annualservicescost": {
            "type": "array",
            "items": {
                "properties": {
                    "Service": "string",
                    "Cost": "string"
                },
                "type": "object"
            }
        }
    },
    "type": "object"
}
```

#### 5.2. 月次コストの初期化
月次コストを出力するに当たって、変数を初期化します。

```HTTPリクエストの受信```トリガーの下の```＋```>`アクションの追加`を選択し、```Variables```>```Initialize variable```をクリックします。

アクション名を変更します。

| アクション名 |
|:-|
| 月次コスト変数の初期化 |

```パラメーター```に以下の項目を入力し、```>>```でアクションを折りたたみます。

| パラメーター | 入力内容 |
|:-|:-|
| Name | monthlyServicesCostFormatted |
| Type | String |
| Value | （空白） |

#### 5.3. 月次コストの出力
サービス毎の月次コストを取得するため、繰り返し制御を行います。

```月次コスト変数の初期化```アクションの下の```＋```>`アクションの追加`を選択し、```Control```>```For each```をクリックします。

アクション名を変更します。

| アクション名 |
|:-|
| 月次コストの出力 |

```パラメーター```に以下の項目を入力します。

| パラメーター | 入力内容 |
|:-|:-|
| Select An Output From Previous Steps | ![workflow_insert_content.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/a463c239-8212-b84b-713c-8f47c947601c.png) monthlyservicescost |

```設定```に以下の項目を入力し、```>>```でアクションを折りたたみます。
| 設定 | 設定内容 |
|:-|:-|
| Concurrency control | オン |
| Degree of parallelism | 1 |

#### 5.4. 月次コスト変数にサービスコストを追加
繰り返し処理の中で、サービス毎の月次コストを取得します。

```月次コストの出力```アクションの中の```＋```>```アクションの追加```を選択し、```Variables```>```AppendToStringVariable```をクリックします。

アクション名を変更します。

| アクション名 |
|:-|
| 月次コスト変数にサービスコストを追加 |

```パラメーター```に以下の項目を入力します。

| パラメーター | 入力内容 |
|:-|:-|
| Name | monthlyServicesCostFormatted |
| Value | ![workflow_insert_expression.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/84b12f8b-13c6-7548-68c9-aba53cd2c967.png) 以下参照 |

```json:Value
concat('- ', items('月次コストの出力')?['Service'], '：', if(empty(items('月次コストの出力')?['Cost']), '0', items('月次コストの出力')?['Cost']), '円<br>')
```

#### 5.5. 年次コストの初期化
年次コストを出力するに当たって、変数を初期化します。

```HTTPリクエストの受信```トリガーの下の```＋```>`並列分岐の追加`を選択し、```Variables```>```Initialize variable```をクリックします。

アクション名を変更します。

| アクション名 |
|:-|
| 年次コスト変数の初期化 |

```パラメーター```に以下の項目を入力し、```>>```でアクションを折りたたみます。

| パラメーター | 入力内容 |
|:-|:-|
| Name | annualServicesCostFormatted |
| Type | String |
| Value | （空白） |

#### 5.6. 年次コストの出力
サービス毎の年次コストを取得するため、繰り返し制御を行います。

```年次コスト変数の初期化```アクションの下の```＋```>`アクションの追加`を選択し、```Control```>```For each```をクリックします。

アクション名を変更します。

| アクション名 |
|:-|
| 年次コストの出力 |

```パラメーター```に以下の項目を入力します。

| パラメーター | 入力内容 |
|:-|:-|
| Select An Output From Previous Steps | ![workflow_insert_content.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/a463c239-8212-b84b-713c-8f47c947601c.png) annualservicescost |

```設定```に以下の項目を入力し、```>>```でアクションを折りたたみます。
| 設定 | 設定内容 |
|:-|:-|
| Concurrency control | オン |
| Degree of parallelism | 1 |

#### 5.7. 年次コスト変数にサービスコストを追加
繰り返し処理の中で、サービス毎の年次コストを取得します。

```年次コストの出力```アクションの中の```＋```>```アクションの追加```を選択し、```Variables```>```AppendToStringVariable```をクリックします。

アクション名を変更します。

| アクション名 |
|:-|
| 年次コスト変数にサービスコストを追加 |

```パラメーター```に以下の項目を入力します。

| パラメーター | 入力内容 |
|:-|:-|
| Name | annualServicesCostFormatted |
| Value | ![workflow_insert_expression.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/84b12f8b-13c6-7548-68c9-aba53cd2c967.png) 以下参照 |

```json:Value
concat('- ', items('年次コストの出力')?['Service'], '：', if(empty(items('年次コストの出力')?['Cost']), '0', items('年次コストの出力')?['Cost']), '円<br>')
```

#### 5.8. チャネルにコストを投稿
月次コストおよび年次コストをTeamsチャネルに投稿します。

```月次コストの出力```アクションの下の```＋```>`アクションの追加`を選択し、```Microsoft Teams```>```チャットまたはチャネルでメッセージを投稿する```をクリックします。

```サインイン```を選択し、```Teams投稿に使用するアカウント```を選択して認証します。

アクション名を変更します。

| アクション名 |
|:-|
| チャネルにコストを投稿 |

```設定```に以下の項目を入力します。
| 設定 | 設定内容 |
|:-|:-|
| Run after | 月次コストの出力<br>年次コストの出力 |

```パラメーター```に以下の項目を入力し、```>>```でアクションを折りたたみます。

| パラメーター | 入力内容 |
|:-|:-|
| 投稿者 | フロー ボット |
| 投稿先 | Channel |
| Team | 任意のTeamsチーム |
| Channel | 任意のTeamsチャネル |
| Message | 以下参照 |

```html:Message
<p class="editor-paragraph"><u><b><strong class="editor-text-bold editor-text-underline">今月の利用料金</strong></b></u><br>■合計<br>@{triggerBody()?['resourcegroup']}：@{triggerBody()?['monthlycost']}円<br>■内訳<br>@{variables('monthlyServicesCostFormatted')}<br><u><b><strong class="editor-text-bold editor-text-underline">今年度の利用料金</strong></b></u><br>■合計<br>@{triggerBody()?['resourcegroup']}：@{triggerBody()?['annualcost']}円<br>■内訳<br>@{variables('annualServicesCostFormatted')}</p><br>
```

最後に、```保存```をクリックしてロジックアプリを保存します。

#### 5.9. ロジックアプリのアドレスをRunbook変数に設定
ロジックアプリを保存するとRunbookからHTTPリクエストを受信するためのアドレスが割り当てられるので、Runbookの変数に値を設定します。

```HTTPリクエストの受信```トリガーを選択し、```パラメーター```>```HTTP URL```をコピーします。

![logicappsuri.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/b41cd547-765a-e88c-03bf-c575530e3ef6.png)

ナビゲーションバーで```Automation アカウント```に移動します。

```作成したAutomationアカウント```>```共有リソース```>```変数```>```LogicAppsUri```を選択し、```値```にコピーしたアドレスをペーストして```保存```します。

![runbook_variable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/030921a8-dee6-4ed9-0979-e13b3d8dd9a9.png)

## 6. Teamsで通知確認
想定通りにAzureコストとサービス内訳をTeams通知できることを確認するため、Runbookを手動実行します。

[Azure portal](https://portal.azure.com)のナビゲーションバーで```Automation アカウント```に移動します。

```作成したAutomationアカウント```>```プロセス オートメーション```>```Runbook```>```作成したRunbook```を選択し、```開始```>```はい```をクリックします。

![runbook_start.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/a9dffe95-869f-54bb-b30e-98ac95a66ad1.png)

Teamsチャネルにコストが通知されていることを確認します。
サービスはコストの高いものから降順に表示されます。

![teams_post.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899718/c85f2843-2a1f-53d8-4e03-66b28be9f338.png "通知イメージ")

## 7. まとめ
Azureコストとサービス毎の内訳をTeams通知する方法をご紹介しました。

注意点として、法人でCSP（Cloud Solution Provider）やEA（Enterprise Agreement）のライセンス形態を採用している場合、販売代理店ごとの割引や保守費用が発生するため、Azure portalで表示されるコストと実際の請求料金が一致しないことがあります。

ご利用の環境に合わせて算出式を変更するなどカスタマイズして取り入れていただければ幸いです。

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。
※ 2024年10月 現在