【GraphAPI】リソースの変更を検知する (2)：デルタ クエリ

# はじめに

Graph API のデルタ クエリを利用すると、ポーリング処理によってテナント内のリソースが変更されたことを検知することができます。この記事ではデルタ クエリについてまとめてみました。

リソースの変更を検知するもう 1 つの方法として変更通知があります。そちらは以下の記事で紹介しています。


https://qiita.com/atsmin77/private/8379b5328ae6e5a09e53

# デルタ クエリ

## デルタ クエリをサポートしているリソース

デルタ クエリをサポートしているリソースは、以下の公開情報に記載されています。

[変更通知](https://qiita.com/atsmin77/private/8379b5328ae6e5a09e53#%E5%A4%89%E6%9B%B4%E9%80%9A%E7%9F%A5%E3%82%92%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9) よりもサポートしているリソースが少ないですが、デルタ クエリでは Entra ID アプリケーションや [サービスプリンシパル](https://qiita.com/atsmin77/items/6bcc9fb88b305e124399#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9-%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B7%E3%83%91%E3%83%AB%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6) といったリソースもサポートされています。

(参考：サポートされているリソース)
https://learn.microsoft.com/ja-jp/graph/delta-query-overview#supported-resources

## デルタ クエリの利用方法

デルタ クエリを使用してポーリングによってリソースの変更を検知するには、以下のように Graph API を実行する必要があります。

1. 対象のリソースの delta API (/delta) に対してリクエストを送信する
1. **@odata.nextLink** が返されなくなるまで繰り返しリクエストを送信する
   現在のリソースの情報をすべて取得 (同期) します
   情報がすべて取得されると @odata.deltaLink が返されます
1. **@odata.deltaLink** を使用してリクエストを送信する
1.  **@odata.nextLink** が返されなくなるまで繰り返しリクエストを送信する
   前回から変更されたリソースの情報をすべて取得 (同期) します
   情報がすべて取得されると @odata.deltaLink が返されます
1.  一定時間待機し、3 に戻る 

::: note
変更されたリソースの情報は応答に含まれる **@odata.deltaLink** に対してリクエストを送信することで取得できます。
@odata.deltaLink を取得するためには、[ページング](https://qiita.com/atsmin77/items/a1d53031b120da5d9697#%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%B3%E3%82%B0%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6) によって @odata.nextLink が返されなくなるまで、すべての情報を取得する必要があります。
:::

(参考：デルタ クエリを使用して、Microsoft Graph データの変更を追跡する)
https://learn.microsoft.com/ja-jp/graph/delta-query-overview

## ポーリング処理のフローチャート

デルタ クエリを利用する手順のフローチャートを作ってみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/a87319f6-2dd3-44a7-818f-ef5edc79b84e.png)

::: note
ユーザーやグループなどのリソースの場合は、1 のリクエストに "$deltaToken=latest" を付けて実行すると 2 の手順をスキップすることができます。
:::

(参考：デルタ クエリを使用して、Microsoft Graph データの変更を追跡する)
https://learn.microsoft.com/ja-jp/graph/delta-query-overview
>ユーザーやグループなどのMicrosoft Entra IDリソースでは、"今からの同期" シナリオがサポートされています。 この機能を使用すると、手順 1 と手順 2 をスキップし (リソースの完全な状態を取得しない場合)、代わりに最新の @odata.deltaLink を要求できます。

## @odata.deltaLink の有効期限

対象のリソースによっては @odata.deltaLink に有効期限があり、期限が切れた場合には @odata.deltaLink へリクエストを送信した際に 40x のエラーが返されます。また公開情報にはクラウド側のメンテナンスでも同様にエラーが返されると記載されていました。

このような場合には 1 から再度処理を行う必要があります。

(参考：トークン期間)
https://learn.microsoft.com/ja-jp/graph/delta-query-overview#token-duration

## ユーザー情報の変更

例として、指定した間隔でポーリングを行ってユーザー情報の変更を検知する PowerShell スクリプトを作ってみました。

PowerShell へコピー & ペーストして実行した状態で、ユーザーの情報が変更されると内容が表示されます。

::: note warn
ユーザーに割り当てられたライセンスの変更はデルタ クエリでまだ検知できないようです。
:::

(参考：user: delta)
https://learn.microsoft.com/ja-jp/graph/api/user-delta?view=graph-rest-1.0&tabs=http
>現在、licenseAssignmentStates プロパティの変更は追跡されません。

### スクリプト例

```powershell
# 対象のリソースの Delta API を指定してください
$DeltaAPI = "/users/delta"
# 特定のプロパティの変更のみを検知したい場合は $select にプロパティを指定してください
# $DeltaAPI = "/users/delta?$select=displayName,jobTitle,mobilePhone"
# 必要なアクセス許可を指定してください
$Scopes = "User.ReadBasic.All" 
# ポーリング間隔 (秒) を指定してください
$Interval = 10

# Invoke-MgGraphRequest で'@odata.deltaLink' を取得するためにページング処理を行うコマンド(関数) です
Function Invoke-MgGraphRequestWithPaging($Method, $Uri, $OutputType) {
    $responses = @()
    $response = Invoke-MgGraphRequest -Method $Method -Uri $Uri -OutputType $OutputType
    $responses += $response
    # ページング処理を行います
    # '@odata.nextLink' が返されなくなるまで情報を取得します
    while($response.'@odata.nextLink' -ne $null){
        $nextLink = $response.'@odata.nextLink'
        $response = Invoke-MgGraphRequest -Method "GET" -Uri $nextLink -OutputType $OutputType
		$responses += $response
    }
	# 取得したレスポンスをすべて返します
    return $responses
}

# ダイアログが表示されたらサインインしてください
Connect-MgGraph -Scopes $Scopes
# delta API でページングを行いすべての情報を取得します
$responses = Invoke-MgGraphRequestWithPaging -Method "GET" -Uri "/v1.0/$DeltaAPI" -OutputType "PSObject"
# 最後のレスポンスから deltaLink を取得します
$deltaLink = $responses[-1].'@odata.deltaLink'

# ポーリング処理を開始します (Ctrl-C で停止してください)
while($true){
    # deltaLink を使用して情報を取得します
    $responses = Invoke-MgGraphRequestWithPaging -Method "GET" -Uri $deltaLink -OutputType "PSObject"
    # 変更されたリソースがあれば表示します
    $responses | % {echo $_.value | fl}
    # deltaLink を新しい URL に更新します
    $deltaLink = $responses[-1].'@odata.deltaLink'
    # 指定された間隔でポーリングします
    Sleep $Interval 
}
```

### ユーザーの新規作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/34a69879-d14d-4631-a5ee-92814a659985.png)

```
displayName       : デルタクエリ テストユーザー
givenName         : テストユーザー
surname           : デルタクエリ
userPrincipalName : deltatestuser@atsmin.net
id                : 9d9c313a-e80e-44fd-a180-0c35d3c4c559
```

### ユーザー情報の変更

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/39bef205-c4ef-4f1d-b38b-ce7b90c78c9f.png)

::: note
デルタ クエリの応答には変更された属性の情報が含まれています。
:::

```
displayName       : デルタクエリ テストユーザー
givenName         : テストユーザー
jobTitle          : 変更テスト
surname           : デルタクエリ
userPrincipalName : deltatestuser@atsmin.net
id                : 9d9c313a-e80e-44fd-a180-0c35d3c4c559
```

### ユーザーの削除

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/95a74db2-776c-449e-af98-fe70881b14d6.png)

```
id       : 9d9c313a-e80e-44fd-a180-0c35d3c4c559
@removed : @{reason=changed}
```

::: note
ユーザーが復元できる状態でも削除されたことが検知されました。ユーザーを復元すると、ユーザーを新規に作成した場合と同様に検知されました。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/0299ff58-aeff-4b98-b8be-808e90b756a2.png)

```
displayName       : デルタクエリ テストユーザー
givenName         : テストユーザー
jobTitle          : 変更テスト
surname           : デルタクエリ
userPrincipalName : deltatestuser@atsmin.net
id                : 9d9c313a-e80e-44fd-a180-0c35d3c4c559
```