0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GUI運用を卒業する:Microsoft Graph PowerShell実践レシピ6選【M365管理者向け】

0
Posted at

はじめに

こんな業務を毎月手作業でやっていませんか。

  • ライセンス未割り当てユーザーをGUIで1件ずつ確認している
  • 外部ゲストの最終サインイン確認をスプレッドシートに手転記している
  • メッセージセンターの通知を毎回GUIで読んで重要度を判断している
  • 条件付きアクセスポリシーをExcelに手作業でまとめている

外資系ITコンサルとしてModern Workplace案件を複数担当している筆者は、これらの作業をすべてMicrosoft Graph PowerShell SDKで自動化しています。数千ユーザー規模のテナントではGUI中心の運用は限界があり、属人化・ミス・工数増加の温床になります。

本記事では現場でそのまま使っているスクリプトを6本紹介します。Graph APIの概念的な解説はしがないblogに書いているので、ここはコードと実務知見に集中します。


前提:環境と接続

本記事のスクリプトは PowerShell 7.4以上 での実行を推奨します。

# Microsoft Graph PowerShell SDK v2(2026年5月時点)
Install-Module Microsoft.Graph -Scope CurrentUser -Force -AllowClobber
Update-Module Microsoft.Graph -Force

# 必要なスコープを指定して接続(-NoWelcomeで出力をスッキリ)
Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All", "AuditLog.Read.All", "Organization.Read.All" -NoWelcome

権限について重要な注意点User.ReadWrite.AllDirectory.ReadWrite.Allは非常に強い権限です。必ず検証環境で動作確認してから本番適用してください。本番環境ではPrivileged Identity Management(PIM)や一時昇格の利用を推奨します。


レシピ①:ライセンス未割り当てユーザーを高速取得してCSV出力

実務での使いどころ:約2,000ユーザー規模のテナントで毎月のライセンス棚卸しに使っています。GUI確認では30分以上かかっていた作業が、CSV出力込みで1分以内に短縮できました。

Where-Objectでフィルタリングする方法は大規模テナントで非常に遅くなります。Microsoftが推奨するserver-side filteringを使います。

Connect-MgGraph -Scopes "User.Read.All" -NoWelcome

# サーバーサイドフィルタリング(大規模テナントでも高速)
$users = Get-MgUser -All `
    -Filter "assignedLicenses/`$count eq 0 and accountEnabled eq true" `
    -ConsistencyLevel eventual `
    -Property DisplayName, UserPrincipalName, AccountEnabled

$users | Select-Object DisplayName, UserPrincipalName |
    Export-Csv -Path ".\unlicensed_users.csv" -Encoding UTF8 -NoTypeInformation

Write-Host "出力完了: $($users.Count) 件"

ゲストを除いてメンバーのみに絞る場合:

$users = Get-MgUser -All `
    -Filter "assignedLicenses/`$count eq 0 and userType eq 'Member' and accountEnabled eq true" `
    -ConsistencyLevel eventual `
    -Property DisplayName, UserPrincipalName

レシピ②:ライセンスをCSVから一括付与する

実務での使いどころ:部門異動やプロジェクト参加に伴うライセンス一括付与で使います。人事異動のタイミングで50〜100件まとめて処理する場面が多いです。

ステップ1:テナントのSKU一覧を確認

Connect-MgGraph -Scopes "Organization.Read.All" -NoWelcome

Get-MgSubscribedSku | Select-Object SkuPartNumber, SkuId, @{
    Name = "利用可能数"
    Expression = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits }
} | Format-Table

ステップ2:CSVから一括付与

Connect-MgGraph -Scopes "User.ReadWrite.All" -NoWelcome

# users.csvの形式: UserPrincipalName列のみ
$targets = Import-Csv ".\users.csv"
$skuId = "06ebc4ee-1bb5-47dd-8120-11324bc54e06"  # E5のSkuId

foreach ($user in $targets) {
    try {
        Set-MgUserLicense -UserId $user.UserPrincipalName `
            -AddLicenses @(@{ SkuId = $skuId }) `
            -RemoveLicenses @()
        Write-Host "付与成功: $($user.UserPrincipalName)"
    }
    catch {
        Write-Warning "付与失敗: $($user.UserPrincipalName) - $($_.Exception.Message)"
    }
}

※ Microsoft.Graph 2.29.0以降を使用してください。それ以前のバージョンではSet-MgUserLicenseに不具合報告がありました。


レシピ③:メッセージセンターの対応必須MCを自動抽出

実務での使いどころ:月次のMC棚卸しで毎回使っています。「planForChange」タグ(対応必須系)を自動抽出してCSVに出力し、「対応必須/確認推奨/無視OK」の判定をつけてチームに展開するワークフローの元データとして使います。GUIで毎回読む作業がなくなり、判断作業に集中できるようになりました。

Connect-MgGraph -Scopes "ServiceMessage.Read.All" -NoWelcome

$messages = Get-MgServiceAnnouncementMessage -All -Property `
    Id, Title, Severity, StartDateTime, EndDateTime, Tags, Services

# planForChange(対応必須系)で絞り込み
$planForChange = $messages | Where-Object {
    $_.Tags -contains "planForChange"
} | ForEach-Object {
    [PSCustomObject]@{
        MCId       = $_.Id
        タイトル    = $_.Title
        重要度      = $_.Severity
        開始日      = $_.StartDateTime
        終了日      = $_.EndDateTime
        対象サービス = ($_.Services -join ", ")
    }
}

$planForChange | Export-Csv -Path ".\mc_planforchange.csv" -Encoding UTF8 -NoTypeInformation
Write-Host "対応必須MC数: $($planForChange.Count) 件"

stayInformed(確認推奨)タグで絞る場合は"planForChange""stayInformed"に変えるだけです。


レシピ④:テナント内のゲストユーザーを最終サインイン日時付きで一覧取得

実務での使いどころ:四半期ごとのゲスト棚卸しで使います。最終サインインから180日以上経過しているゲストをアカウント削除の候補として抽出し、業務部門に確認を取るフローに組み込んでいます。

Connect-MgGraph -Scopes "User.Read.All", "AuditLog.Read.All" -NoWelcome

$guests = Get-MgUser -All `
    -Filter "userType eq 'Guest'" `
    -Property DisplayName, UserPrincipalName, Mail, CreatedDateTime, SignInActivity, ExternalUserState `
    -PageSize 999

$result = $guests | ForEach-Object {
    $lastSignIn = $_.SignInActivity?.LastSignInDateTime
    $daysSince = if ($lastSignIn) {
        [int](New-TimeSpan -Start $lastSignIn -End (Get-Date)).TotalDays
    } else { 9999 }

    [PSCustomObject]@{
        DisplayName        = $_.DisplayName
        UPN                = $_.UserPrincipalName
        Mail               = $_.Mail
        作成日             = $_.CreatedDateTime
        最終サインイン      = $lastSignIn
        未サインイン日数    = $daysSince
        状態               = $_.ExternalUserState
        削除候補           = if ($daysSince -ge 180) { "要確認" } else { "" }
    }
}

$result | Sort-Object 未サインイン日数 -Descending |
    Export-Csv -Path ".\guest_users.csv" -Encoding UTF8 -NoTypeInformation

Write-Host "ゲスト総数: $($result.Count) 件 / 要確認: $(($result | Where-Object {$_.削除候補 -eq '要確認'}).Count) 件"

SignInActivityはEntra ID P1/P2ライセンスが必要です。


レシピ⑤:失敗したサインインをフィルタリングしてCSVエクスポート

実務での使いどころ:インシデント対応や不審なサインインの調査で使います。特定のエラーコード(50126=パスワード誤り、53003=条件付きアクセスブロックなど)で絞り込むと原因特定が速くなります。

Connect-MgGraph -Scopes "AuditLog.Read.All" -NoWelcome

$startDate = (Get-Date).AddDays(-7).ToString("yyyy-MM-ddTHH:mm:ssZ")

$failedSignIns = Get-MgAuditLogSignIn -All `
    -Filter "status/errorCode ne 0 and createdDateTime ge $startDate" `
    -Property UserDisplayName, UserPrincipalName, CreatedDateTime, Status, `
              IpAddress, Location, AppDisplayName, ConditionalAccessStatus

$result = $failedSignIns | ForEach-Object {
    [PSCustomObject]@{
        ユーザー名       = $_.UserDisplayName
        UPN             = $_.UserPrincipalName
        日時            = $_.CreatedDateTime
        エラーコード     = $_.Status.ErrorCode
        失敗理由        = $_.Status.FailureReason
        IPアドレス      = $_.IpAddress
                     = $_.Location.CountryOrRegion
        アプリ          = $_.AppDisplayName
        CAステータス    = $_.ConditionalAccessStatus
    }
}

$result | Export-Csv -Path ".\failed_signins.csv" -Encoding UTF8 -NoTypeInformation
Write-Host "失敗ログ: $($result.Count) 件"

よく使うエラーコードの対応表:

エラーコード 意味
50126 パスワード誤り
50074 MFAが必要(未完了)
53003 条件付きアクセスによるブロック
70011 無効なスコープ
90095 管理者の同意が必要

レシピ⑥:条件付きアクセスポリシーの設計棚卸し

実務での使いどころ:テナントの条件付きアクセス設計レビューで使います。ポリシー数が20を超えると全体像の把握がGUIでは難しくなります。CSV出力してExcelで整理するだけで、重複・漏れ・過剰制御が見えやすくなります。

Connect-MgGraph -Scopes "Policy.Read.All" -NoWelcome

$policies = Get-MgIdentityConditionalAccessPolicy -All

$result = $policies | ForEach-Object {
    [PSCustomObject]@{
        DisplayName      = $_.DisplayName
        State            = $_.State
        作成日           = $_.CreatedDateTime
        最終更新         = $_.ModifiedDateTime
        対象ユーザー     = ($_.Conditions.Users.IncludeUsers -join ", ")
        除外ユーザー     = ($_.Conditions.Users.ExcludeUsers -join ", ")
        対象グループ     = ($_.Conditions.Users.IncludeGroups -join ", ")
        除外グループ     = ($_.Conditions.Users.ExcludeGroups -join ", ")
        付与制御         = ($_.GrantControls.BuiltInControls -join ", ")
    }
}

$result | Export-Csv -Path ".\ca_policies.csv" -Encoding UTF8 -NoTypeInformation
Write-Host "ポリシー数: $($result.Count) 件"

よくあるエラーと対処

Insufficient privileges
Connect-MgGraphのScopesに必要な権限を追加して再接続してください。

400 Bad Request(filterとcountを組み合わせた場合)
-ConsistencyLevel eventualを追加してください。$count$orderbyとの組み合わせ時に必要です。

SignInActivityプロパティが返ってこない
-Property SignInActivityを明示的に指定してください。デフォルトでは返ってきません。

大量データ取得でタイムアウト
-PageSize 999を追加してページング制御してください。-Allだけでは不安定になる場合があります。


おわりに

M365運用は「GUIで頑張る管理」から「Graph APIで運用をコード化する管理」へ確実に移行しています。数百〜数千ユーザー規模になると、GUI中心の運用は人的ミス・属人化・工数増加の原因になります。

本記事のスクリプトはすべて実案件で使っているものです。コピーしてそのまま動かし、自テナントの環境に合わせてフィルター条件を調整してみてください。

各レシピの設計判断や概念的な解説はしがないblogでも書いています。Graph Explorerを使ったインタラクティブな動作確認方法はこちらを参照してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?