Microsoft Graph SDK非依存:REST API直叩きによるEntra ID自動化スクリプト

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

Microsoft Graph SDK非依存:REST API直叩きによるEntra ID自動化スクリプト

【導入:解決する課題】

巨大なGraph SDKのインストールを排除し、CI/CDやサーバー環境での認証・データ取得を軽量かつ高速に自動化します。

【設計方針と処理フロー】

本スクリプトは、クライアント資格情報フロー(Client Credentials Flow)を用いてMicrosoft Entra IDからアクセストークンを取得し、標準の Invoke-RestMethod でMicrosoft Graph APIエンドポイントへアクセスします。ページネーション(@odata.nextLink)の自動追跡と、APIのレートリミット(HTTP 429)に対する指数バックオフ処理を内蔵しています。

graph TD
    A[Start] --> B["OAuth 2.0 トークン要求"]
    B --> C{"トークン取得成功?"}
    C -->|No| D["エラー終了"]
    C -->|Yes| E["Graph API リクエスト送信"]
    E --> F{"HTTPステータス確認"}
    F -->|429 Throttled| G["Retry-After秒 待機"]
    G --> E
    F -->|200 OK| H["レスポンスデータ蓄積"]
    H --> I{"@odata.nextLink あり?"}
    I -->|Yes| J["次ページURLを設定"]
    J --> E
    I -->|No| K["結果オブジェクト出力"]
    K --> L[Finish]

【実装:コアスクリプト】

サードパーティ製モジュールを一切使用せず、PowerShell 5.1 / 7.x 双方で動作する堅牢な実装です。

function Get-GraphApiToken {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$TenantId,
        [Parameter(Mandatory = $true)]
        [string]$ClientId,
        [Parameter(Mandatory = $true)]
        [System.Security.SecureString]$ClientSecret
    )

    $TokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    $Bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ClientSecret)
    $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($Bstr)
    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($Bstr)

    $Body = @{
        client_id     = $ClientId
        client_secret = $UnsecureSecret
        scope         = "https://graph.microsoft.com/.default"
        grant_type    = "client_credentials"
    }

    try {
        $Response = Invoke-RestMethod -Uri $TokenEndpoint -Method Post -Body $Body -ContentType "application/x-www-form-urlencoded"
        return $Response.access_token
    }
    catch {
        Write-Error "認証トークンの取得に失敗しました: $_"
        throw $_
    }
}

function Invoke-GraphApiRequest {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$AccessToken,
        [Parameter(Mandatory = $true)]
        [string]$Uri,
        [ValidateSet("GET", "POST", "PATCH", "DELETE")]
        [string]$Method = "GET",
        [hashtable]$Body = @{},
        [int]$MaxRetryCount = 3
    )

    $Headers = @{
        "Authorization" = "Bearer $AccessToken"
        "Content-Type"  = "application/json"
    }

    $Results = [System.Collections.Generic.List[PSObject]]::new()
    $CurrentUri = $Uri

    do {
        $RetryCount = 0
        $Success = $false

        while (-not $Success -and ($RetryCount -lt $MaxRetryCount)) {
            try {
                $Params = @{
                    Uri     = $CurrentUri
                    Method  = $Method
                    Headers = $Headers
                }
                if ($Method -in @("POST", "PATCH") -and $Body.Count -gt 0) {
                    $Params.Body = ($Body | ConvertTo-Json -Depth 10)
                }

                $Response = Invoke-RestMethod @Params
                $Success = $true

                if ($Response.value) {
                    $Results.AddRange($Response.value)
                } else {
                    $Results.Add($Response)
                }

                # ページネーション処理

                $CurrentUri = $Response.'@odata.nextLink'
            }
            catch {
                $StatusCode = $_.Exception.Response.StatusCode.value__
                if ($StatusCode -eq 429) {
                    $RetryAfter = $_.Exception.Response.Headers['Retry-After']
                    $WaitSeconds = if ($RetryAfter) { [int]$RetryAfter } else { [Math]::Pow(2, $RetryCount + 1) }
                    Write-Warning "レート制限を検知 (429)。 $WaitSeconds 秒待機後に再試行します..."
                    Start-Sleep -Seconds $WaitSeconds
                    $RetryCount++
                }
                else {
                    Write-Error "APIリクエスト実行中にエラーが発生しました: $_"
                    throw $_
                }
            }
        }

        if (-not $Success) {
            throw "最大再試行回数 ($MaxRetryCount) を超えたため処理を中断しました: $CurrentUri"
        }

    } while ($CurrentUri)

    return $Results
}

# --- 実行例 ---


# $TenantId = "your-tenant-id"


# $ClientId = "your-client-id"


# $Secret = Read-Host -AsSecureString "Enter Client Secret"


# $Token = Get-GraphApiToken -TenantId $TenantId -ClientId $ClientId -ClientSecret $Secret


# $Users = Invoke-GraphApiRequest -AccessToken $Token -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,displayName,userPrincipalName"


# $Users | Select-Object displayName, userPrincipalName

【検証とパフォーマンス評価】

SDKモジュール(Microsoft.Graph)のインポート時と、本スクリプト(REST API直接呼び出し)の実行速度を Measure-Command にて比較。

# 実行時間計測の例

$Duration = Measure-Command {
    $Token = Get-GraphApiToken -TenantId $TenantId -ClientId $ClientId -ClientSecret $Secret
    $Data  = Invoke-GraphApiRequest -AccessToken $Token -Uri "https://graph.microsoft.com/v1.0/users?`$top=100"
}
Write-Output "総処理時間: $($Duration.TotalSeconds) 秒"
評価項目 Microsoft.Graph SDK利用 REST API直接実行 (本方式)
初期モジュールロード 3.5 ~ 10.0 秒以上 0 秒(依存なし)
コンテナ起動/関数実行時間 重厚(数百MBの容量) 超軽量(数KBのスクリプトのみ)
実行オーバーヘッド SDK内部ラッパーの処理遅延あり 最小(ダイレクトなHTTP通信)

【運用上の落とし穴と対策】

  • PowerShell 5.1 vs 7.x における Invoke-RestMethod の挙動差異
    PowerShell 5.1ではUTF-8レスポンスが文字化けすることがあります。必要に応じて [System.Text.Encoding]::UTF8.GetString($Response.RawContentStream.ToArray()) を用いるか、PowerShell 7系を標準実行環境に選定してください。

  • 429 Throttling(リクエスト制限)のハンドリング
    大規模テナントでバッチ処理を行う際、急激な並列リクエストはHTTP 429を引き起こします。Retry-After レスポンスヘッダーを解析し、適切なウェイト時間を設けるリトライ機構が必須です。

  • シークレットの安全な取り扱い
    クライアントシークレットを平文でハードコードすることは厳禁です。実行環境に応じてAzure Key Vault、環境変数、または Get-Credential を介してセキュアに取得してください。

【まとめ】

  1. 依存性排除:SDKのインストールや互換性問題から解放され、軽量コンテナやCI/CDパイプラインに最適。

  2. ページネーション&スロットリング対策@odata.nextLink の自動処理とHTTP 429の再試行を実装し、大量データ取得時の安定性を確保。

  3. セキュアな認証管理:OAuth 2.0 クライアント資格情報フローを用い、適切な権限スコープでの最小特権運用を徹底。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました