Copilot in Azureを使ってみた：タグのつけ忘れを検出せよ！

この記事では、Azureポータルから利用できる機能であるCopilot in Azureを試しに使ってみた結果を紹介します。今回は手始めに、「タグをつけ忘れたAzureサービスの検索」を手伝ってもらいました。

## 想定シナリオ：タグでAzureサービス利用者を管理
筆者は仕事柄、新しいAzureサービスの検証や、Azureサービスを用いたPoC開発に取り組んでいます。同様の業務に取り組んでいる同僚と、検証用のAzure環境を共有して、各自がいろいろなAzureサービスを使って検証・開発を進めています。

複数の開発者でAzure環境を共有している場合、Azure環境の運用管理者としては、Azureサービスについて利用者に問い合わせが必要になることがあります（例：使用料が異様に高い場合）。Azureサービスのタグとして利用者を明記する運用ルールにしておけば、誰が使っているAzureサービスなのか一目瞭然となり、便利です。

一方、Azureサービス作成時にタグをつけ忘れてしまい、そのままタグが存在しないままのAzureサービスが残ることがあります。時間が経つと、Azureサービスを作った本人すら作ったことを忘れてしまい、誰が作ったのかわからないAzureサービスが出てくる可能性もあります。

そのため、タグがついていないAzureサービスは、なるべく早めに把握し、Azure環境の利用者達に「このAzureサービスを作ったのは誰ですか？」と確認したいところです。Copilot in Azureには、Azureサービスの検索に活用できるAzure Resource Graphが組み込まれているので、サービス検索をCopilot in Azureに助けてもらうことにしました。

## 関連するAzure機能の紹介
### Azure Resource Managerとは
Azure Resource Managerは、Azureサービスの管理（作成、変更、削除など）に利用される機能です。Azure Resource Managerでは、各Azureサービスは「リソース」と呼ばれる要素から構成されます。

Azure Resource Managerは各リソースに関して、設定情報（リソース名、リージョン、料金プランなどの設定値）を取得・操作できるREST APIを提供します。例として、WebサイトをホスティングするサービスであるAzure Web Appsの設定情報をJSON形式で出力する際のイメージは以下のようになります（一部の設定情報だけ抜き出しています）。
```
{
    "id": "/subscriptions/<サブスクリプションID>/resourceGroups/<リソースグループ名>/providers/Microsoft.Web/sites/<リソース名>",
    "name": "<リソース名>",
    "type": "Microsoft.Web/sites",
    "kind": "app,linux",
    "location": "Japan East",
    "properties": {
        "state": "Running",
        (※省略)
    }
}
```

Azureポータル、Azure CLI、Azure PowerShellなどでリソースを操作するときも、裏ではこのREST APIが呼ばれているようです。Azure Resource Managerに関する詳細は以下のリンク先をご参照ください。

https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/management/overview

### Azure Resource Graphとは
Azure Resource Graphは、Azure Resource Managerから提供される設定情報の検索・活用に利用される機能です。クエリを実行することで、条件に基づいてリソースや設定情報を検索する際に活用できます。本記事では触れませんが、リソースの設定情報の変更履歴を検出する機能もあります。詳細は以下のリンク先をご参照ください。

https://learn.microsoft.com/ja-jp/azure/governance/resource-graph/overview

Azure Resource Graphはリソースや設定情報の検索に便利な一方、[Kusto Query Language (KQL)](https://learn.microsoft.com/ja-jp/kusto/query/?view=azure-data-explorer&preserve-view=true)というクエリ言語でクエリを記述する必要があります。筆者はこのクエリ言語に馴染みがなかったので、クエリ作成をCopilot in Azureに助けてもらおうと考えました。

### Copilot in Azureとは
Copilot in Azureは、AzureポータルまたはAzure mobile appを介して、Azureの運用管理を支援する機能です（Copilot **for** Azureと表記されていることもあります）。ユーザからのリクエストに応じて、Azure Resource Graphのクエリを生成してリソースを検索したり、そのリソースに対する操作（例：仮想マシンの電源オフ）をしたりなど、様々なタスクを実行できるようです。詳細は以下のリンク先をご参照ください。

https://learn.microsoft.com/ja-jp/azure/copilot/capabilities

Copilot in Azureは、2024年8月30日現在ではプレビュー段階です。少なくとも現状ではいくつかの制限があり、[こちらのリンク先](https://learn.microsoft.com/ja-jp/azure/copilot/capabilities#current-limitations)にまとめられています。例えば以下のような制限があります。

* 現状では英語のみに対応しています。チャットの質問・回答がいずれも英語のみである上に、Azureポータルの言語設定自体を英語に設定する必要があります。
* 投稿できるリクエストの数に制限があります。具体的には、各チャット（ユーザのリクエストと応答の一連のやり取り）で行える投稿は15件までです。また、24時間中に作成できるチャットは10件のみです。

## 本題：タグをつけ忘れたAzureサービスを探せ！
### 前提とする環境
今回は、検証用にAzure Web Appsを4個用意しました。例えばAzureポータルでApp Serviceを一覧表示して、タグを表示させることもできますが、目視でタグの有無を確認すると見落としが発生しやすくなります。複数人でAzure環境を共有していると、リソースの数は4個よりかなり多くなるので、管理がさらに大変になります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/6495a14b-7534-92eb-8fc7-62b4528a6c1d.jpeg" width=90% style="display: block; margin: auto;">

各リソースに付与したタグは以下の通りです。リソースのうち、`TestWebApps02`というリソースはタグをつけ忘れた想定なので、タグを何も付与していません。他の3個には、利用者名を表すために`user`をキーとするタグを付与してあります。

|リソース名|タグ|
|---|---|
|TestWebApps01|user: mitsu|
|TestWebApps02|※タグなし|
|TestWebApps03|user: mitsu|
|WebAppDevelABC|user: bishi|

Copilot in Azureは生成AIらしく、同じ質問でも回答が毎回変わったり、質問に対して誤った回答をしたりします。そのため今回は、正解となる検索対象（タグ未付与のリソース）が明らかになっている状態で、クエリ作成を進めました。

### 対象のAzureサービスを検索できるまでの軌跡
それでは、Copilot in Azureを使って、タグをつけ忘れたリソースを探すクエリを作成した流れを紹介します。

#### 準備：リソース一覧を取得する
まだリソース検索用のクエリを一度も作ったことがなかったので、まずは試しに、Azure環境にあるAzure Web Appsを一覧化するクエリを作ってもらいました。生成されたクエリは以下の通りです。2行目でAzure Web Appsを絞り込み、3行目でリソース名とタグを抽出してくれそうな記述になっています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/9e8e3baa-ad3d-4dcd-2db3-bcb5f01a4412.jpeg" width=60% style="display: block; margin: auto;">

上図に`Run`というボタンがあり、このボタンをクリックすると、Azureポータル上でAzure Resource Graph エクスプローラーという機能が起動するので、クエリの編集や実行ができます。ただ、Copilot in Azureもクエリ実行・結果表示をしてくれます。Copilot in Azureにクエリを実行してもらった結果は以下の通りです。クエリをもとに、4個のAzure Web Appsの名前とタグを検索できました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/32316e68-ee31-4645-2f90-c33561d0e677.jpeg" width=60% style="display: block; margin: auto;">

今回作ってもらったクエリは非常にシンプルでしたが、`=~`という演算子の意味を知らなかったので、聞いてみました。以下の通り、参考文献つきで詳しく教えてくれました。便利ですね。回答内容は誤りを含んでいるかもしれないので、参考文献を合わせて確認すると確実ですね（もしかしたら、参考文献へのリンクが誤っている可能性もあるかもしれませんが）。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/361fb622-d9b0-562f-0062-670c9cceedab.jpeg" width=60% style="display: block; margin: auto;">

#### タグ未付与リソースの検索クエリを作る
リソースの数が少なければ、先ほど作成したクエリで表示されるタグ一覧からタグが付いていないリソースを探すこともできます。ただ、リソースの数が多くなると少し面倒なので、本題の「タグが付与されていないリソース」を検索するクエリを作ることにしました。

Copilot in Azureでは1つのチャットにつき、リクエストを15回までしか送れないので、一応新しいチャットを立ち上げます。以下のように`New chat`を選べば新しいチャットを開始できます。（※ここで以前のチャットは削除されますので、必要なチャット履歴は控えておくとよいと思います）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/1b45a207-7b5d-68b1-631f-dd0a1a42c6e9.jpeg" width=60% style="display: block; margin: auto;">

最初に生成したクエリは以下の通りです。クエリの知識がない状態の筆者は、`isnull(tags)`と`isempty(tags)`という記述によって、`tags`としてタグが取得されており、タグが`null`もしくは空ということを判別できるのかな...？と考えていました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/3f2a79c0-abbe-8138-17cf-db94ad7aaa00.jpeg" width=60% style="display: block; margin: auto;">

クエリを実行してもらった結果がこちらです。回答としては「タグがついていないAzure Web Appsはありません」とのこと。本当は、タグが付いていないリソースが1個存在するはずなので、クエリに誤りがありそうです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/877c8053-daa5-8827-3752-bb7db3e6ad13.jpeg" width=60% style="display: block; margin: auto;">

#### タグ未付与リソースの検索クエリを改善
生成されたクエリには`where isnull(tags) or isempty(tags)`とあり、タグがnullか空の場合に条件に合致しそうに見えます。ここで気になるのは、`tags`には実際に何が設定されているのか？ということです。

最初に作成したリソース一覧取得のクエリの出力結果を再掲すると、以下の通りです。実は、タグ未付与のリソースのTags列には`{}`と表示されています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/b821e656-5d04-6e25-1fbe-12c097f4e781.jpeg" width=40% style="display: block; margin: auto;">

ここが怪しいような気がしたので、以下のようなリクエストを作ってみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/f4bba904-2d13-48bb-1c51-41108147786f.jpeg" width=60% style="display: block; margin: auto;">

以下の回答があり、タグの絞り込みの条件が`tags == '{}'`に修正されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/ec03b771-f33b-3e0c-b899-c0ac584f0b76.jpeg" width=60% style="display: block; margin: auto;">

クエリを実行してもらうと、以下の通り、タグ未付与のリソースを正しく抽出できるようになりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/9e5c4211-918e-5ebf-0973-136ff4e96981.jpeg" width=60% style="display: block; margin: auto;">

### 前提とする環境（その2）
先ほどの環境では、Azure Web Appsに付与されているタグのキーは`user`のみでした。しかし、実際の環境では`user`以外にもタグが付与されているかもしれません。ということで、Azure Web Appsの想定用途を表す`env`というキーを追加してみました。開発環境であれば値は`devel`、試験環境であれば値は`test`と設定します。

|リソース名|タグ(キー：env)|タグ(キー：user)|
|---|---|---|
|TestWebApps01|env: devel|user: mitsu|
|TestWebApps02|env: test|※なし|
|TestWebApps03|env: devel|user: mitsu|
|WebAppDevelABC|env: devel|user: bishi|

この前提で何が問題になるかというと、`TestWebApps02`は依然として`user`タグをつけ忘れているのですが、`env`タグはついています。つまり、先ほど取り組んでいた「タグが付与されていない（空の）リソースを探す」というクエリではなく、「`user`タグがないリソースを探す」というクエリにしなければなりません。

#### リソース一覧の取得とタグの確認
念のため、再度各リソースの設定状況を確認するクエリを実行します。ここでは、`Can you generate and run...`というリクエストに変えて、クエリを作ったらそのまま実行してもらいました。実行結果は以下の通りで、`env`タグが正しく追加されています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/1d93635b-ebd4-21d3-05f7-ffc4be9ff876.jpeg" width=60% style="display: block; margin: auto;">

ちなみに、先ほどのクエリでは見られませんでしたが、今回のクエリでは`subscriptionId`に基づいた絞り込みも行っています。やはり生成AIらしく、似たようなリクエストでも出力が若干異なるのでしょう。

タグ追加後、先ほどの前提で作ったタグ未付与リソース検索クエリを、Azure Resource Graphエクスプローラーで実行してみました。すると、検索対象の`TestWebApps02`は抽出されませんでした。もはや`TestWebApps02`のタグは`{"env":"test"}`になっているので、絞り込みの条件が合致しなくなってしまったのでしょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/0cfdd964-dc05-1b38-df3f-0b477b2bdef1.jpeg" width=75% style="display: block; margin: auto;">

#### 新しい前提向けのクエリ作成
新しい前提に合わせたクエリを作るために、Copilot in Azureにリクエストを投げてみました。すると、「今のAzure Resource Graphでは所望のクエリは作成できない」というような回答が返ってきました...。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/2d1dfd87-2d76-e3e5-a8d5-9caa83099978.jpeg" width=60% style="display: block; margin: auto;">

生成AIでは一発で所望の結果が得られることは期待できないので、ちょっとアプローチを変えてみました。ひとまず「リソースのタグのキーだけを抽出する」ことを目的とし、以下のようなリクエストを作り、回答を得ました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/ab9409cb-ccb2-ac63-3998-d4d3a6a62d69.jpeg" width=60% style="display: block; margin: auto;">

何やら`mv-expand`という見慣れない記述はありますが、クエリを実行してみると、以下のようにキーごとに分解した一覧表示が得られました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/80360c98-8b76-8d5e-56bb-0d416d88790f.jpeg" width=75% style="display: block; margin: auto;">

今はタグのキーだけに着目したいので、以下のようなリクエストを送ると、クエリを改善してくれました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/02780fdd-ce8b-17c9-f41c-555f24d92d24.jpeg" width=60% style="display: block; margin: auto;">

`bag_keys`という記述がよくわかりませんが、クエリを実行してみると、以下のようにキーだけが抽出されました。これを見ると、各リソースに紐づいているタグのキーがわかるので、`user`タグがついていないリソースの検索は可能なのではないか？と期待できますね。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/369a8726-79a2-496f-1c6f-d18477419dd3.jpeg" width=75% style="display: block; margin: auto;">

ちなみに`mv-expand`と`bag_keys`について意味を質問すると、以下のように教えてくれました。やはり便利ですね。ただし、今回は参考文献へのリンクは付与されていませんでした。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/2ea67484-1662-1c3f-e723-977d66fafc12.jpeg" width=60% style="display: block; margin: auto;">

今までのリクエストと回答のやり取りで絞り込みの情報が得られているので、今回はクエリを作ってくれると期待し、改めて以下のようなリクエストを送ってみました。回答結果は...、クエリを作ってくれました！しかし、このクエリには先ほど登場した`mv-expand`と`bag_keys`は両方とも使われていませんでした。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/19dae260-0aaf-e1ed-54bf-02c6bec73de7.jpeg" width=60% style="display: block; margin: auto;">

Azure Resource Graphエクスプローラーでクエリを実行してみましたが、以下のような結果となり、このクエリで`user`タグが存在しないリソースを抽出できることがわかりました。途中の`mv-expand`や`bag_keys`のやり取りの効果があったのかはわかりませんが...、所望のクエリができたので良しとしましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850264/b503857c-efbb-0b7e-f24f-08a4e75e7f7a.jpeg" width=60% style="display: block; margin: auto;">

## 補足：GPT-4oにも聞いてみた
Copilot in Azure以外に聞いてみるとどうなるのか気になったので、Azure OpenAIのGPT-4oにもAzure Resouce Graphのクエリを作ってもらいました。

今回はGPT-4oのバージョン2024-05-13を使用しました。システムプロンプトとしては以下を使用しました（これ以外のシステムプロンプトでどのように生成結果が変わるのかは確認していません）。
```
You are an expert in Azure Resource Graph. You help engineers understand and create queries for Azure Resource Graph effectively. Please check the accuracy of the information that you provide.
```

### 前提1
#### 最初のプロンプトと回答
まずは、Copilot in Azureに最初に質問したものとほぼ同じ質問をしてみました。入力したプロンプトは以下の通りです。
```
Can you generate a query to list Azure Web Apps without tags?
```

回答には以下の2つのクエリが含まれていましたが、いずれも検索対象を正しく抽出できないものでした。クエリ1は、3行目が`tags == '{}'`なら正しいですが、そうなっていません。クエリ2は、Copilot in Azureで最初に得られたクエリと同じであり、検索対象を正しく抽出できません。

* クエリ1

    ```
    Resources  
    | where type == "microsoft.web/sites"  
    | where tags == ''  
    | project name, resourceGroup, subscriptionId 
    ```
* クエリ2

    ```
    Resources  
    | where type == "microsoft.web/sites"  
    | where isnull(tags) or isempty(tags)  
    | project name, resourceGroup, subscriptionId 
    ```

#### クエリの改善
Copilot in Azureに指摘したのと同様に、`tags`が空の場合は`{}`という値になることを指摘してみました。入力したプロンプトは以下の通りです。
```
These commands will return a list of Azure Web Apps without tags.
I think the property 'tags' is set to '{}' if it is empty. Can you update the query?
```

回答は以下の通りでした。Copilot in Azureの回答では見られなかった`array_length(bag_keys(tags)) == 0`という表現が追記されました。
```
resources  
| where type == 'microsoft.web/sites'  
| where isnull(tags) or tags == '{}' or array_length(bag_keys(tags)) == 0  
| project name, resourceGroup, location, tags  
```

今回のケースでは、3行目にある`tags == '{}'`と`array_length(bag_keys(tags)) == 0`は機能的には同じようですし、どちらかを選べばよさそうです。`isnull(tags)`も、Copilot in Azureのときに試した通り無意味なので、省略できますね。

Copilot in Azureで生成したものと結果は同じですが、違う書き方のクエリとして、最終的に以下を得ました。Azure Resource Graphエクスプローラーで実行したところ、正常に動作しました。
```
resources  
| where type == 'microsoft.web/sites'  
| where array_length(bag_keys(tags)) == 0  
| project name, resourceGroup, location, tags  
```

### 前提2
`env`タグを追加した場合も試してみました。入力したプロンプトは以下の通りです。
```
Can you generate a query to list Azure Web Apps missing the key 'user' in their tags?
```

実はこちらについては、一発で所望のクエリを得ることができました。生成されたクエリは、Copilot in Azureで最終的に得られたものと同じで、正常に動作します。
```
Certainly! To list Azure Web Apps that are missing the key 'user' in their tags using Azure Resource Graph, you can use the following Kusto Query Language (KQL) query:
Resources  
| where type == 'microsoft.web/sites'  
| where isnull(tags['user'])  
| project id, name, location, tags  
```

ちなみにお気づきの方がいるかもしれませんが、Azure OpenAIが作った方のクエリでは`type`の絞り込みが`type == 'microsoft.web/sites'`となっています。Copilot in Azureが生成したクエリでは`type =~ 'microsoft.web/sites'`になっており、演算子が違います。結局どちらも動作したのですが、どちらの演算子の方がよいかは別途調べたいと思います。

## おわりに
今回は、Copilot in Azureを使ったAzure運用支援の一例として、タグをつけ忘れたリソースを探すためのAzure Resource Graphのクエリ作成を支援してもらいました。筆者はAzure Resource Graphのクエリについてほぼ知見がない状態でしたが、Copilot for Azureの支援のおかげで効率的にクエリを作成できました。

Copilot in Azureが生成するクエリの精度が高まってくると、さらに使い勝手が良くなりそうです。参考までにGPT-4oでもクエリを生成してみましたが、正直今回の結果だけでは、Copilot in AzureとGPT-4oで生成結果の違いはわかりませんでした。Azureポータルに統合されている分、今回のようなリソース検索向けにはCopilot in Azureの方が便利に使えそうです。

このクエリを定期的に自動実行するような仕組みを作れば、誰かがAzureサービスにタグをつけ忘れたとしても、そのAzureサービスを自動で検知できるようになりますね。例えば、Logic Appsを使って自動化する場合、以下のリンク先が参考になりそうです。

https://learn.microsoft.com/ja-jp/azure/governance/resource-graph/tutorials/logic-app-calling-arg

今回はちょっとした運用管理の困りごとの解決をCopilot in Azureに手伝ってもらいましたが、Copilot in Azureのとてもシンプルな適用例に過ぎません。今後は、もう少し複雑な運用管理のタスクを支援してもらえないか検証してみます。