久しぶりに触った Azure(と Graph API) でハマったことをすべて話そう

本記事はリンクアンドモチベーション Advent Calender 2023 の最終日の記事です。

https://qiita.com/advent-calendar/2023/lmi

# はじめに
メリークリスマス！みなさんプレゼントはもらいましたか？まぁほとんどの人はあげる側ですね。
え？私ですか？嫁さんに高級焼肉ですが、それがなにか？

# 冗談は置いておいて
最近以下のような流れで CRE エンジニアから新米データエンジニアに異世界転生しました。

I部長「Microsoft Azure で次世代データ分析基盤作ってよ。データの流入元は Teams とかの Graph API を想定しておいて。」
わい「あっ、はい（前職で Microsoft Azure 使ってたし、なんとなるだろ）」
I部長「おｋ。じゃぁ、他のメンバーは割り当てられないから、当分一人でお願いね」
わい「あっ、はい（他のエンジニアは AWS か GCP しかやったことないもんな…）」

そんなわけで約3年ぶりくらいに Microsoft Azure（と Graph API）に向き合ったのですが、色々なことでハマって悲しい思いをしたので、そんな悲しみの再生産を防ぐべく、筆をとりました。

同じことで悩む人が一人でも減ったらこれに勝る喜びはありません。

:::note info
正確には「I 部長」ではなく「I マネージャー」です。もちろん元ネタは「T 部長」です。
:::

# Azure Portal でハマる
## Active Directory is ドコ？
Microsoft でユーザー/グループ管理といえば Active Directory です。
久しぶりに Azure Portal にログインして、諸々の設定をするために Active Directory のページを開こうとしたら…あれ？Active Directory の霊圧が消えた？そんな馬鹿な…。

しばし電脳の海を彷徨ってみると、なんと**名前が変わって**いました。

https://www.microsoft.com/ja-jp/security/business/identity-access/microsoft-entra-id

まるで `X(旧 Twitter)` みたいなことになってました。なぜ変えたし。

:::note info
一応、Portal 上に「名前が変わったよ」という注意喚起のメッセージは出てたんですが、筆者はしばらく気が付きませんでした。
:::

まだドキュメントは完全に対応しきってないようで、まだ Active Directory という表記がチラホラ出てきます。

# 開発環境の作成でハマる
## Azurite を HTTPS で動かしたら Storage Explorer から接続できない
Azure Storage Account を利用するサービスをローカルで開発をする場合、もちろん Azure 上にローカルテスト用途の Storage Account を作ってテストしてもいいんですが、便利なエミュレータがあるのでこちらを使ったほうが捗ります（単体テストがしやすくなる）

ただし、本番 Storage Account と同様に接続をする場合はエミュレーターに HTTPS で動く設定をしないといけません（デフォルトは HTTP）
なので[公式のチュートリアル](https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#connect-to-azurite-using-https)に従って自己署名証明書の作成と設定を行ったのですが…何故か以下のようなエラーが出て Storage Exploer（Storage の内容を閲覧できるツール）から接続できず。

```
Unable to retrieve child resources

ProducerError:{
  "name": "Node Fetch Error",
  "message": "unable to verify the first certificate",
  "stack": "Error: unable to verify the first certificate\n    at TLSSocket.onConnectSecure (node:_tls_wrap:1543:34)\n    at TLSSocket.emit (node:events:513:28)\n    at TLSSocket._finishInit (node:_tls_wrap:962:8)\n    at TLSWrap.onhandshakedone (node:_tls_wrap:746:12)",
  "cause": {
    "cause": {
      "code": "UNABLE_TO_VERIFY_LEAF_SIGNATURE"
    }
  },
  "code": "UNABLE_TO_VERIFY_LEAF_SIGNATURE"
}
```

とりあえず TLS 接続ができないことはわかったものの、それ以上がわからなかったのでまた電脳の海を彷徨っていると以下のような記事にたどり着きました。

https://github.com/microsoft/AzureStorageExplorer/issues/6038#issuecomment-1221094583

このコメントにある通り Storage Exploer の設定で「System Proxy を使う」という設定があって、それを有効にすると無事接続できました。えぇ…なにそれ…。
一応以下の通り「System Proxy を使う場合」の説明自体はあるんですが、これだけ読んでもわからんて…。

https://learn.microsoft.com/en-us/azure/storage/common/storage-explorer-network#use-system-proxy

ちなみに Azurite を起動する際にはこんな風に指定して起動させるといろいろと幸せになれます。
お試しあれ。
```PowerShell:hllspd.ps 
 azurite --oauth basic --cert .\azurite-emu.pfx --pwd hogefuga --skipApiVersionCheck `
 --location "$HOME\.azurite" --debug "$HOME\.azurite\debug.$(Get-Date -UFormat "%Y%m%d").log"
```

# Graph API でハマる
そもそも「Graph API ってなに？」という方は以下の記事を見てみましょう。とても良くまとめられた記事です。

https://qiita.com/massie_g/items/fe7540161aa4a5f86bf5

## ChannelMessage を取得しようとしたら簡単には取得できなかった
ふと「Slack みたいに Teams から Message をとれるのかな？」と試してみたくなったので試してみたらドハマリしました。
さすがセキュリティベンダーの側面をもつ Microsoft 様。そんな簡単には取らせてくれません。
方法は2つであるとわかりました。

- Export API を利用する（有料）
  - [こちら](https://zenn.dev/ymasaoka/articles/get-study-teams-export-api-license)が大変詳しいので読んでみてください
- 特別な Permisson を付与するための Consent を用意し、通常の API でアクセスをする
  - https://learn.microsoft.com/ja-jp/microsoftteams/platform/graph-api/rsc/grant-resource-specific-consent
  - 要するに Teams アプリを用意する必要がある

というわけで、安易に手を出してはいけません。それ相応の覚悟を決めてから取り掛かりましょう。
Slack と同じだと思っていたら火傷します。

## OData Query Parameter を利用しようとしたら使えない？
Graph API には「OData Query Parameter」という便利な拡張機能的なものがあります。

https://learn.microsoft.com/ja-jp/graph/query-parameters?tabs=http#odata-system-query-options

単なる REST API に SQL 的な機能がちょっと追加されてるようなイメージですね。
例えば `$expand` は Left Outer Join 的な動きをしてくれるので REST API でよくある
「上位のリソースからリストしてって、ID 探して、更に子リソースをリストして、ID 探して…次は孫リs(ry)」という苦行をしなくてすみます。

…なんですが、これ、とても残念なことに API によって使えるものが限定されていたりします。
例えば [Channel を取得する API](https://learn.microsoft.com/ja-jp/graph/api/channel-get?view=graph-rest-1.0&tabs=http#optional-query-parameters) がサポートしているのは
`$filter` および `$select` のみとなっています。
よく確認せずに利用不能な Parameter を使うと「この API は指定された Query Parameter はサポートしてないんだ、ごめんね」という旨のエラーレスポンスが返ってきて悲しい気持ちになります。
無駄に悲しまないで済むように、利用予定の API の対応 Parameter はちゃんと確認しましょう。

## 削除済みのユーザーを取得しようとしたら…
Azure Entra ID から過去に削除されてしまったユーザーを探そうとしたんですが…基本的には30日以上経過すると完全削除されてしまい、どうにもならなくなります。

https://learn.microsoft.com/ja-jp/graph/api/resources/directory?view=graph-rest-1.0

>アイテムが削除されると、削除されたアイテム "コンテナー" に追加されます。 削除されたアイテムは、最大 30 日間復元できます。 
>30 日が経過すると、アイテムは完全に削除されます。

なので、通常の API では削除後30日以上経過したユーザーの情報は（Display Name すらも）全く取得できません。
え？じゃあどうにもならないの？というと一応救済手段はあります。[デルタクエリ](https://learn.microsoft.com/ja-jp/graph/delta-query-overview)です。
本来の用途は「変更追跡」なんですが、こちらを使うと「限定的に」削除済みユーザーの情報がわかります。
例えば削除済みユーザーであればこんな感じ。

```json
{
    "id": "b47b2c98-fc6f-4ba2-8437-d759f0ee9738",
    "@removed": {
        "reason": "changed"
    },
    "manager@delta": [
        {
            "@odata.type": "#microsoft.graph.user",
            "id": "d694249b-64eb-4b8a-81ba-9239e6f8481b",
            "@removed": {
                "reason": "deleted"
            }
        }
    ]
}
```

ご覧の通り、Id だけです。まぁ、これでも全く無いより何倍もマシなのですが。
こんな感じなので過去に遡って Graph API からデータを取得する場合に「過去に在籍していたユーザーの情報は取れない」という前提で考えないといろいろ困ったことになります。
なんでも Slack と同じだと思っていたら火傷します。

# Azure Functions でハマる
## .NET 8 Isolated で動かそうとしたらローカルで動かない
最初は In-Process でもいいかな？とか思ったんですが、[そう遠くないうちに Isolated に移行しなきゃいけなくなりそう](https://blog.shibayan.jp/entry/20230912/1694509574)と考えたので Isolated を選択しました。
Isolated で動かすためには Program.cs とその中で HostBuilder の処理を用意する必要があります（ASP.NET 的なお作法）
ということで公式のチュートリアルを参考に作って起動したら起動時にエラーが…。
んでいろいろ調べてみるとこんなこと記事が

https://stackoverflow.com/questions/77544110/migrating-azure-functions-to-net8-isolated-function-hangs-when-triggered

え？ `ConfigureFunctionsWorkerDefaults` 使っちゃだめなの？
そんなわけ無いでしょと思って、もう一度[公式ドキュメント](https://learn.microsoft.com/ja-jp/azure/azure-functions/dotnet-isolated-process-guide#start-up-and-configuration)を見ると…
>ASP.NET Core 統合を使用している場合は ConfigureFunctionsWebApplication() を呼び出し、
>それ以外の場合は ConfigureFunctionsWorkerDefaults() を呼び出します。 これらのオプションの詳細については、「HTTP トリガー」を参照してください。

はい、私の場合はこの「ASP.NET 統合」を利用していたので `ConfigureFunctionsWorkerDefaults` は使っちゃだめだったというしょうもない理由でした。
ちなみに記事にちょっと言及がある通り `ConfigureFunctionsWorkerDefaults` は gRPC の設定をいじるんですが
それがローカル起動及びテストに必要な gRPC の設定まで変えてしまうようで、それが原因で起動しなくなっていたようです。
oh…

# Microsoft Fabric でハマる
## 課金を Azure Subscription と紐付けたいけどどうやんの？

Microsoft Fabric とは今、データ分析の世界で最も熱い（主観込み）ソリューションです。
詳しい説明はぜひ以下の記事を読んでみてください。

https://qiita.com/yangjiayi/items/605d9679e1e35d391a1a

さて、データエンジニア的には使って当然の流れなので、利用を開始…あれ？これ課金どうなるんだ？

https://learn.microsoft.com/ja-jp/fabric/enterprise/buy-subscription#buy-an-azure-sku

上記を読むと「Capacity を Azure 上に作る」ことは書いてあるのですが、その先の「Azure 上で作った Capacity と Microsoft Fabric との紐づけ」をどうしたら良いかよくわからない…。
色々探した結果、以下の流れで設定ができることが分かりました。

1.ワークスペースタブ＞利用しているワークスペースの `ワークスペースの設定` を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143870/8ff17e78-2358-2b77-65a5-2a09b463d5ce.png)

2.プレミアム＞ライセンスモードで `ファブリック容量` にて Azure 上で作成した Fabric Capacity を選択する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143870/a38ec012-2188-632a-735f-8d5cbb55b1ef.png)

同じように困った方は参考にしてみてください。

# Azure Data Factory でハマる
データエンジニアリングといえば ETL。Azure で ETL といえば Azure Data Factory ですね？
というわけで私も早速利用しました。
マルチテナント的に利用したいと思っていたので、データ変換を行う Pipline にテナント識別子を Parameter として渡せるように設定し、その Parameter を各 Activity で利用するように設定をしてみると Dataset の Config で以下の Warning が突如として現れました。

```
Pipeline parameters can only be used within their defining pipeline.

Define dataset parameters to accept the pipeline parameter values instead.
```

ふむふむ。Dataset parameter を経由して Pipeline parameter を渡した方がいいとな？
Warning をシカトするのは良くないので、公式ドキュメントを探してみるのですがいまいち方法がわからない…
と思いながら再び電脳の海をさまよっているとこんな神記事が。

https://qiita.com/yomatsum/items/683d8ed8a09cea6a54f3

>docs 上であまり明記されていない気がしたので、データセットのパラメーターを使用したパイプラインの作成について、簡単にまとめてみたいと思います。

はい、全く持ってその通りでございます。本当に助かりました！

# 最後に
最後までお読みいただき、ありがとうございました！
こうやって書き出してみるとめちゃくちゃハマってますねw
このやらかしが誰かの助けになると信じ、筆を置かせていただきます。