Azure Workbook を CI する工夫

本記事では Workbook を複数人で開発するときのチャレンジと工夫した方法を紹介します。特に複数人で複雑な Workbook を開発する際に参考になれば幸いです。

## Workbook とは

Workbook は Azure のメトリックやログ、リソース グラフ、また URL へのリクエストによる外部のデータのフェッチ、任意の JSON など Azure 以外のものも含めてプログラムを書かずに様々なソースを可視化できる Azure Monitor の一機能です。その表現力は多種多様でグラフやテーブルのようなビジュアルの種類、値に応じたカラーリング等柔軟性がとても高いことも特徴です。そのような点から、仮想マシンや AKS のリソースのメトリック、そのほかのさまざまなリソースのモニタリングの機能の一部として提供されているものもあります。

以下は[こちらのドキュメント](https://learn.microsoft.com/ja-jp/azure/azure-monitor/visualize/workbooks-visualizations)にある可視化の例です。
![](https://learn.microsoft.com/ja-jp/azure/azure-monitor/visualize/media/workbooks-overview/visualizations.png#lightbox)

Workbook は Azure のリソースとして保存されるため、権限管理やダッシュボードへの設定が可能でユーザー間の共有も簡単に出来ます。予め必要なメトリックやログ、障害対応手順等を Workbook としてまとめておくことで障害対応等にも役立ちます。

Workbook の作成や編集は Azure ポータルから行います。また Azure ポータルから作成した Workbook は JSON としてダウンロードが可能です。ARM テンプレートもダウンロードできるため、作成した Workbook を複数の環境に展開することが簡単にできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132347/972f2758-01fe-48f0-fe34-b10074ccb728.png)

## Workbook を開発するときの課題

このように非常に便利な Workbook ですが、複雑な Workbook を複数人で編集する場合いくつかの課題があります。
私が作成に関わっていた [Azure/reliability-workbook](https://github.com/Azure/reliability-workbook) の開発の中で、考慮が必要だったいくつかのポイントを元に紹介していきます。

### reliability-workbook とは

reliability-workbook はサブスクリプションに展開されているリソースのプロパティを Azure のリソース グラフを利用して取得し、可用性のスコアリングをしてくれる Workbook です。例えば、可用性ゾーンを使っていない VM やインスタンス数が 1 の Application Gateway があるとスコアが下がります。ペインを切り替えることでリソースごとの詳細が確認できます。

■ スコアの表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132347/8d88f0a2-86d4-645d-cce4-541081afa821.png)

■ リソースごとの詳細
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132347/736aa2de-7cb3-53ea-6d1f-5f72a18590c1.png)


### 課題1. 同じクエリを何回も書く必要がある

Workbook では複数のペインを作成でき、そのそれぞれにリソース グラフのクエリを記述できます。reliability-workbook では複数のタブがありリソースごとにそのペインが分かれているため、それぞれのペインにクエリを記述します。この時、リソース名やリソース タイプ、リソース グループ、また AZ の利用有無等リソースの種類に関係なく同じ列を持ったテーブルを表示しようとすると、同じクエリを書く必要が出てきます。

しかし、Workbook では今のところ関数やクラスのような複数のファイルに分割しモジュール化するための機能がなく、同じクエリをコピペしていくことになります。状態を持つこともできないため、取得したデータを引き渡すこともできません。

例えば、以下はゾーンの使用有無を取得するためのクエリですが、これをゾーンが利用できるすべてのリソースのペインで記述しなければいけません。当然、この一部に変更があった場合はすべて書き換えていく必要があります。

```kusto
| extend avZones = case(
    location !in~ ('brazilsouth', 'canadacentral', 'centralus', 'eastus', 'eastus2', 'southcentralus', 'usgovvirginia', 'westus2', 'westus3', 'francecentral', 'germanywestcentral', 'northeurope', 'norwayeast', 'uksouth', 'westeurope', 'swedencentral', 'switzerlandnorth', 'qatarcentral', 'uaenorth', 'southafricanorth', 'australiaeast', 'centralindia', 'japaneast', 'koreacentral', 'southeastasia', 'eastasia', 'chinanorth3'), 'Not Applicable',
    (type == 'microsoft.compute/virtualmachines'), coalesce(tostring(zones[0]), 'Not Configured'),
    (type == 'microsoft.compute/virtualmachinescalesets'), coalesce(tostring(array_length(parse_json(zones))), 'Not Configured'),
    (type == 'microsoft.containerservice/managedclusters' and AvZones <> ""), AvZones,
    (type == 'microsoft.containerservice/managedclusters' and isempty(AvZones)), "Not Configured",
    (type == 'microsoft.web/sites'), "Not Applicable",
    (type == 'microsoft.sql/servers/databases' and sku.tier <> 'DataWarehouse'), iif(isempty(properties.zoneRedundant) or properties.zoneRedundant == "false", 'Not Configured', 'Configured'),
    (type == 'microsoft.sql/servers/databases' and sku.tier == 'DataWarehouse'), "Not Applicable",
    (type == 'microsoft.documentdb/databaseaccounts'), iif(properties.locations[0].isZoneRedundant == "false", 'Not Configured', 'Configured'), 
    (type == 'microsoft.dbformysql/servers'), "Not Applicable",
    (type == 'microsoft.dbformysql/flexibleservers'), iif(properties.haEnabled == "Enabled", 'Configured', 'Not Configured'),
    (type == "microsoft.apimanagement/service"), coalesce(tostring(array_length(parse_json(zones))), 'Not Configured'),
    (type contains 'storageaccounts'), case(split(skuName, '_', 1)[0] =~ "zrs", "Configured", "Not Configured"),
    (type == "microsoft.network/azurefirewalls"), iif(isnotnull(zones), "Configured", "Not Configured"),
    (type == "microsoft.network/frontdoors"), "Not Applicable",
    (type == "microsoft.network/applicationgateways"), iif(isnotnull(zones), "Configured", "Not Configured"),
    (type == "microsoft.network/loadbalancers"), case( 
    skuName =~ "Basic", "Not Applicable",
    skuName =~ "Standard" and isnotnull(zones), "Configured",
    "Not Configured"
    ),
    (type == "microsoft.recoveryservices/vaults"), "Not Applicable",
    "Undefined"
    )
```

「同じものを複数の箇所に記述しなければならない」という制約は継続的に開発するための高いハードルでした。

### 課題2. 1 つのJSON ファイルとして表現されている

reliability-workbook では 8000 行以上の JSON ファイルから成っており、GUI によって自動生成されるため手動で書き換えることは現実的ではありません。
UI の変更だけでなく、1つのクエリの文字列を変えるだけであっても1人が更新している間は他の人はその1人がコミットするまで待つ必要がありシーケンシャルな更新が必須でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132347/c9ea0e91-110a-4599-efa1-55d77b6a0b71.png)


## 解決策: Terraform を利用したビルド

このような課題を解決するために、Terraform を利用することにしました。当初は独自のビルドツールを作成することを考えましたが、Terraform をビルドツールとして利用し開発・メンテの手間を減らすことにしました。

まず初めに行ったことは、Workbook の巨大な JSON からリソース グラフのクエリの書かれている部分を抜き出し、別のファイルに分割しました。さらにそのクエリファイルの中で共通な部分を別のファイルとし、Terraform を利用して読み込み＆1つの JSON ファイルにビルドすることにしました。

[main.tf はこちら](https://github.com/Azure/reliability-workbook/blob/main/build/main.tf)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132347/365a8bda-b3eb-0153-9bd0-1b2169c9b1ca.png)

テンプレートファイルとして Azure のポータルから Workbook の JSON ファイルをダウンロードし、`workbook.tpl.json`としておきます。このファイルの中でクエリ部分を変数化します。たとえば以下のような部分です。

```json
          {
            "type": 3,
            "content": {
              "version": "KqlItem/1.0",
              "query": ${kql_summary_workbook_reliability_score}, <-------- クエリを変数にする
              "size": 3,
              "title": "Workbook Reliability Score",
              "exportToExcelOptions": "all",
              "queryType": 1,
              "resourceType": "microsoft.resourcegraph/resources",
              "crossComponentResources": [
                "{Subscriptions}"
              ],
```

そして変数部分を Terraform で置き換えます。

```hcl
    "kql_summary_workbook_reliability_score"                    = jsonencode(local.kql_summary_workbook_reliability_score)
    "kql_summary_reliability_score_by_resource_environment"     = jsonencode(local.kql_summary_reliability_score_by_resource_environment)
    "kql_summary_reliability_score_by_resourceType_environment" = jsonencode(local.kql_summary_reliability_score_by_resourceType_environment)
```

クエリファイルの中に変数がある場合はさらにその変数を別のクエリで置き換えます。

```hcl
  //-------------------------------------------
  // Summary tab
  //-------------------------------------------
  // Workbook Reliability Score
  kql_summary_workbook_reliability_score = templatefile(
    "${path.module}/template_kql/summary/summary_workbook_reliability_score.kql",
    {
      "calculate_score" = local.kql_calculate_score
      "extend_resource" = local.kql_extend_resource
      "summarize_score" = local.kql_summarize_score
    }
  )
```

最後に Terraform の `local_file` リソースを使って JSON ファイルを作成します。

```hcl
// Generate workbook JSON
resource "local_file" "workbook" {
  filename = "${path.module}/ReliabilityWorkbook.json"
  content  = local.workbook_data_json
}
```

あとは GitHub Actions でビルドとリリースの作成をして完成です。

```yaml
      - name: Terraform Plan
        id: plan
        run: terraform plan -input=false -no-color -out tfplan -target local_file.armtemplate -target local_file.workbook

      - name: Terraform Apply
        run: terraform apply -no-color tfplan

      - name: Upload asset - azuredeploy.json
        uses: actions/upload-artifact@v3
        with:
          name: asset
          path: |
            build/azuredeploy.json
            build/ReliabilityWorkbook.json
          if-no-files-found: error
~~~
      - uses: release-drafter/release-drafter@v5
        id: release_drafter
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - uses: shogo82148/actions-upload-release-asset@v1
        with:
          upload_url: ${{ steps.release_drafter.outputs.upload_url }}
          asset_path: |
            asset/azuredeploy.json
            asset/ReliabilityWorkbook.json
          overwrite: true
```

## さいごに

クエリを細かく分割し、JSON ファイルのビルドをポータルに依存せずに行うことで複数人での開発と CI ができるようになりました。
クエリ部分ではなく、UI 部分を変更する場合はまだシーケンシャルな更新が必要ですが、頻度としてはそこまで多くないと考えられるため今後の機能追加に期待をしつつさらに良い方法を探っていきたいと思います。

## [おまけ]便利な Workbook の紹介

- [Azure Orphan Resources](https://github.com/dolevshor/azure-orphan-resources)
    - 接続されていないディスクやパブリック IP アドレスを見つけてくれます。
- [Cloud Roles and Operations Management](https://github.com/Azure/cloud-rolesandops)
    - [クラウド運用モデル](https://learn.microsoft.com/ja-jp/azure/cloud-adoption-framework/operating-model/)に基づいたタスクリストとスコアを表示してくれます。