MS Graph PowerShell SDKを用いたM365ライセンス管理の非同期・並列自動化戦略

Tech
---
subject: "MS Graph PowerShell SDKを使用したM365ライセンス管理の自動化"
author: "Senior PowerShell Engineer"
date: "2024-06-25"
version: "1.0"
security: "Azure AD, Graph API Scopes"
tags:

  - "PowerShell"

  - "MSGraph"

  - "M365"

  - "LicenseManagement"

  - "ParallelProcessing"
---

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

MS Graph PowerShell SDKを用いたM365ライセンス管理の非同期・並列自動化戦略

【導入:解決する課題】

数百人規模のユーザーに対するライセンスプロビジョニングと解除にかかる手動作業時間を削減し、コンプライアンス遵守を自動化によって強化します。

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

大規模なライセンス操作は、同期処理ではボトルネックになりがちです。本設計では、PowerShell 7以降で利用可能な ForEach-Object -Parallel を活用し、Graph APIへのリクエストを並列化することで、処理時間を劇的に短縮します。エラー発生時には処理を中断せず、詳細なログに記録する「フォールトトレラント」な設計を優先します。

Mermaid図解

graph TD
    A[Start] --> B["Connect-MgGraph & Scope Check"]
    B --> C["Read Target Users (CSV)"]
    C --> D{"ForEach-Object -Parallel"}
    D --> E["Get UserPrincipalName / ObjectID"]
    E --> F["Construct License Object(\"SKU IDs\")"]
    F --> G{"Set-MgUserLicense (Try/Catch)"}
    G -->|Success| H["Log Success"]
    G -->|Failure| I["Log Error Details"]
    H & I --> J["End Parallel Thread"]
    J --> K["Generate Summary Report"]
    K --> L[Finish]

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

以下に示す Invoke-MgLicenseProvisioning 関数は、CSVファイルからユーザーリストと操作種別(割り当て/解除)を読み込み、並列処理を用いて高速にライセンス操作を実行します。

前提条件と認証スコープ

この操作には、User.ReadWrite.All および Directory.AccessAsUser.All (または適切な委任/アプリケーション権限) スコープが必要です。

# ----------------------------------------------------------------


# 01. モジュールと認証


# ----------------------------------------------------------------

$RequiredModules = @("Microsoft.Graph.Users", "Microsoft.Graph.Identity.DirectoryManagement")
foreach ($Module in $RequiredModules) {
    if (-not (Get-Module -Name $Module -ListAvailable)) {
        Write-Warning "Required module '$Module' not found. Installing..."
        Install-Module -Name $Module -Scope CurrentUser -Force
    }
}
Import-Module Microsoft.Graph.Users, Microsoft.Graph.Identity.DirectoryManagement

# M365接続(必要なスコープを指定)

$Scopes = @("User.ReadWrite.All", "Directory.AccessAsUser.All")
try {
    Write-Host "Connecting to MS Graph with required scopes..." -ForegroundColor Cyan
    Connect-MgGraph -Scopes $Scopes
} catch {
    Write-Error "Failed to connect to MS Graph. Check permissions and network connectivity."
    exit 1
}

# ----------------------------------------------------------------


# 02. 設定定義


# ----------------------------------------------------------------


# 操作対象のライセンスSKU ID (例: E3)


# SKU IDはテナント固有であり、事前に Get-MgSubscribedSku で確認が必要です。

$TargetSkuId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # 仮のGUID
$LogFilePath = "$PSScriptRoot\LicenseOperation_$(Get-Date -Format yyyyMMdd_HHmmss).log"

# CSV入力ファイルのフォーマット例:


# UserPrincipalName,Action,RemovePlans (Actionは 'Assign' または 'Remove')


# user.a@contoso.com,Assign,


# user.b@contoso.com,Remove,

$InputCsvPath = "$PSScriptRoot\TargetUsers.csv"

# ----------------------------------------------------------------


# 03. コア関数:ライセンスプロビジョニングの実行


# ----------------------------------------------------------------

function Invoke-MgLicenseProvisioning {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path,

        [Parameter(Mandatory=$true)]
        [string]$SkuId
    )

    Write-Host "Processing users from $Path (SKU ID: $SkuId)..." -ForegroundColor Yellow

    # 並列処理の準備

    $Users = Import-Csv -Path $Path

    # Parallel処理の実行 (ThrottleLimit: 環境とGraph API制限に応じて調整)

    $Results = $Users | ForEach-Object -Parallel {
        param($User, $SkuId, $LogFilePath) # スクリプトブロック変数

        $upn = $User.UserPrincipalName
        $action = $User.Action
        $removePlans = $User.RemovePlans
        $threadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId

        $ResultObject = [PSCustomObject]@{
            ThreadID = $threadId
            UserPrincipalName = $upn
            Action = $action
            Status = "Pending"
            Message = ""
        }

        try {

            # ユーザーのObjectIDを取得 (Graph SDKではUPNまたはIDが必要)

            $mgUser = Get-MgUser -UserId $upn -Select Id, UserPrincipalName -ErrorAction Stop
            $userId = $mgUser.Id

            # ------------------------------------------------------


            # ライセンス操作オブジェクトの構築


            # ------------------------------------------------------

            $LicenseChanges = @()
            if ($action -ceq "Assign") {

                # ライセンス割り当て

                $LicenseChanges = @{
                    AddLicenses = @(
                        @{ SkuId = $SkuId; DisabledPlans = @() }
                    )
                    RemoveLicenses = @()
                }
            } elseif ($action -ceq "Remove") {

                # ライセンス解除

                $LicenseChanges = @{
                    AddLicenses = @()
                    RemoveLicenses = @($SkuId)
                }
            } else {
                 throw "Invalid action specified: $($action). Must be 'Assign' or 'Remove'."
            }

            # Set-MgUserLicenseの実行

            Set-MgUserLicense -UserId $userId -Body $LicenseChanges -ErrorAction Stop

            $ResultObject.Status = "Success"
            $ResultObject.Message = "License operation '$action' completed successfully."

        } catch {
            $errorMessage = $_.Exception.Message -replace "`n", " "
            $ResultObject.Status = "Error"
            $ResultObject.Message = "Failed operation '$action': $errorMessage"
        }

        # 結果をロギングし、メインスレッドに戻す

        $logEntry = "$(Get-Date -Format 'HH:mm:ss') [T:$threadId] $($ResultObject.Status) - $($ResultObject.UserPrincipalName): $($ResultObject.Message)"
        Add-Content -Path $LogFilePath -Value $logEntry

        return $ResultObject

    } -ThrottleLimit 50 -LogFile $LogFilePath -ArgumentList $SkuId, $LogFilePath

    # 処理結果のサマリー

    $SuccessCount = ($Results | Where-Object { $_.Status -eq "Success" }).Count
    $ErrorCount = ($Results | Where-Object { $_.Status -eq "Error" }).Count

    Write-Output "`n--- Operation Summary ---"
    Write-Output "Total Processed: $($Users.Count)"
    Write-Output "Success Count: $SuccessCount"
    Write-Output "Error Count: $ErrorCount"
    Write-Output "Full log path: $LogFilePath"
}

# ----------------------------------------------------------------


# 04. 実行部


# ----------------------------------------------------------------

# 事前に $InputCsvPath を作成し、実行する


# Invoke-MgLicenseProvisioning -Path $InputCsvPath -SkuId $TargetSkuId

Disconnect-MgGraph

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

並列処理 (ForEach-Object -Parallel) を使用することで、Graph APIのレイテンシとネットワーク遅延を効果的に隠蔽できます。

計測例

(※この例はローカル環境での計測をシミュレートしたものです。実際の値はAPIの応答速度に依存します。)

環境 処理内容 ユーザー数 (N) スレッド数 処理時間 (秒)
Pwsh 7.3 同期処理 (ForEach) 100 1 45.2秒
Pwsh 7.3 並列処理 (Parallel) 100 50 5.8秒
# 100ユーザーを対象とした場合の計測例

Measure-Command {

    # 実際にはテスト用のCSVを用意し、上記の Invoke-MgLicenseProvisioning を実行


    # Invoke-MgLicenseProvisioning -Path '.\Test100Users.csv' -SkuId $TargetSkuId

}

# TotalSeconds : 5.8123 (並列処理の場合)

大規模環境での期待値: スロットリング制限(Rate Limiting)に達しない範囲であれば、並列スレッド数(ThrottleLimit)を増やすほど処理速度は向上します。しかし、一般的にスレッド数を50〜100程度に設定するのが最も安定して高速なパフォーマンスを得る実戦的なアプローチです。

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

課題 概要 対策
P5.1 vs Pwsh 7 ForEach-Object -Parallel はPowerShell 7以降でしか使用できません。P5.1環境ではカスタムRunspaceプール構築が必要です。 運用環境をPowerShell 7以上に統一するか、P5.1用にRunspaceを活用した同期処理を設計し直す。
Graph API認証スコープ 必要な権限(例: Directory.AccessAsUser.All)が不足していると、Set-MgUserLicense が403 Forbiddenエラーを返します。 接続前に Connect-MgGraph -Scopes ... の引数に漏れがないか確認し、アプリケーション登録の場合は必要なGraph権限が付与されていることを確認します。
文字コード問題 Import-Csv が使用する既定のエンコーディング(通常ANSIまたはUTF8/BOMなし)が、CSVファイルの保存形式と合致しないと、ユーザー名が正しく読み取れません。 Import-Csv -Encoding UTF8 のように、明示的にエンコーディングを指定するか、入力CSVをUTF8(BOM付き)で保存する運用を標準化します。
並列処理中のログ競合 複数のスレッドが同時に同じログファイルに書き込もうとすると、IO競合が発生する可能性があります。 本稿のコードのように Add-Content を使用する場合、内部でロックが確保されますが、さらに堅牢性を求めるなら、System.Management.Automation.Runspaces.InitialSessionState を利用してログ操作専用のセッション状態を作成します。

【まとめ】

M365ライセンス管理の自動化は、PowerShellとMS Graph SDKの並列処理能力を活用することで、迅速かつ正確に実行可能です。安全に運用するための3つのポイントを再確認します。

  1. 粒度の高いエラーロギングの徹底: try/catch ブロック内で発生した例外情報を必ず捕捉し、ユーザー単位で成功/失敗のステータスとエラーメッセージを詳細にログに出力することで、未処理の対象を迅速に特定できるようにします。

  2. 適切なスコープと認証の維持: 操作に必要な最小権限のGraphスコープ(例: User.ReadWrite.All)を特定し、定期的に認証トークンの有効期限や、登録されたサービスプリンシパルの権限変更がないか監視します。

  3. ThrottleLimit の慎重な設定: 並列処理は高速ですが、APIスロットリングを引き起こすと全体の処理が遅延または失敗します。本番環境の規模に応じて、初期は低い値(例: 20)から始め、計測に基づいて徐々に値を調整する戦略を採用してください。

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

コメント

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