PowerShellでActive Directoryグループから無効ユーザーを一括削除する方法

Active Directory のグループに無効(Disabled)ユーザーが大量に残ると、アクセス権や配布リスト、ライセンス付与の運用で思わぬ事故につながります。本記事では PowerShell で無効ユーザーだけを抽出し、安全に一括削除する実践手順をまとめます。

目次

よくある状況:無効ユーザーがグループに残り続ける

退職や異動、アカウント棚卸しの結果として、ユーザーアカウントを「無効化」しただけで、所属グループからの削除が追いつかないことは珍しくありません。例えば、AD グループに無効ユーザーが 300 名以上残っていると、GUI(Active Directory ユーザーとコンピューター)での手作業は現実的ではなく、削除漏れも起きやすくなります。

特に次のようなグループで、無効ユーザーが残存すると影響が出やすくなります。

  • ファイルサーバーやアプリのアクセス権に直結するセキュリティグループ
  • Microsoft 365 / Exchange の配布グループ(静的なメンバー管理)
  • ライセンス付与や条件付きアクセスの対象として利用している運用グループ

「ログオンできないから問題ない」と見過ごすと、監査で指摘を受けたり、グループを条件にした処理(ライセンス割り当て、スクリプト、連携)で意図しない挙動が起きることがあります。定期的に棚卸しできる形にしておくと安心です。

結論:メンバー取得 → ユーザーに限定 → Enabled=False を抽出 → Remove-ADGroupMember

PowerShell(ActiveDirectory モジュール)での定番手順は次の流れです。ポイントは「いきなり削除せず、必ず抽出結果を確認する」ことです。

手順やること狙い
1Get-ADGroupMember でメンバー一覧を取得対象を「グループの中身」に限定
2objectClass=user に絞るPC/グループ/連絡先などの混入を排除
3Get-ADUser -Properties Enabled で状態を取得無効(Enabled=False)を判定可能に
4Enabled=False のユーザーだけ抽出削除対象を最小化
5Remove-ADGroupMember で削除(最初は -WhatIf)事故なく一括削除

事前準備:実行環境と権限を確認する

スクリプトが正しくても、環境条件が揃っていないと失敗します。実行前に次を確認してください。

  • ActiveDirectory モジュールが利用できる(RSAT 導入済み、または管理サーバー/管理端末)
  • 対象グループのメンバーを削除できる権限がある(委任、または適切な管理者権限)
  • 初回は必ず-WhatIf でドライランし、レビューしてから本番実行する
項目確認内容補足
実行ホストAD へ接続できる管理端末/管理サーバーDC 上で実行する必要はありません
PowerShellWindows PowerShell 5.1 が無難PowerShell 7 では互換機能が必要になる場合あり
権限対象グループの「メンバーの書き込み」OU/グループ単位で委任されているケースもあります

基本例:削除対象の確認 → 問題なければ削除

まずは「どれが消えるか」を見える化し、想定どおりであることを確認してから削除します。以下はもっとも分かりやすい基本形です(グループ名は例として Group1)。

Import-Module ActiveDirectory

$group = "Group1"

# 1) グループメンバー取得(ユーザーのみ)
$users = Get-ADGroupMember -Identity $group -Recursive |
  Where-Object { $_.objectClass -eq "user" } |
  ForEach-Object { Get-ADUser $_ -Properties Enabled }

# 2) 無効ユーザーだけ抽出
$disabledUsers = $users | Where-Object { $_.Enabled -eq $false }

# 3) まず一覧で確認(必要ならCSV出力など)
$disabledUsers | Select-Object SamAccountName, DistinguishedName

# 4) 削除(最初は -WhatIf 推奨)
Remove-ADGroupMember -Identity $group -Members $disabledUsers -Confirm:$false -WhatIf

# 5) 問題なければ -WhatIf を外して本実行
# Remove-ADGroupMember -Identity $group -Members $disabledUsers -Confirm:$false

このままでも実用的ですが、運用でよくある「例外」を想定すると、次の改善が効きます。

  • 削除対象を CSV に出してレビュー・証跡を残す
  • 削除はバッチ(例:50件)に分けて安定性を上げる
  • 必要に応じて -Recursive を外し、「直接メンバーのみ」を対象にする

-Recursive の注意点:ネストされたグループ経由のメンバーは“直接”削除できないことがある

Get-ADGroupMember に -Recursive を付けると、ネストされたグループ(グループの中にグループ)経由で含まれるユーザーも拾えます。ここで重要なのは、拾えたユーザーが必ずしも「そのグループの直接メンバー」とは限らない点です。

  • 直接メンバー:Group1 にユーザーが直接追加されている
  • 間接メンバー:Group1 の中の GroupA に所属しているため、結果的に Group1 に含まれている

Remove-ADGroupMember は基本的に「そのグループの直接メンバー」を削除します。-Recursive で拾ったユーザーが間接メンバーだった場合、削除しようとしても「メンバーではない」旨のエラーになることがあります。

目的推奨の取得方法理由
Group1 の直接メンバーだけ掃除したいGet-ADGroupMember -Identity Group1(-Recursive なし)削除対象=直接メンバーに揃えられる
ネストも含めてどこかに無効ユーザーが紛れているのを見つけたいGet-ADGroupMember -Identity Group1 -Recursive棚卸し用途に向く(削除は所属元のグループで実施)

「Group1 を起点に棚卸ししたい」のか、「Group1 の直接メンバーだけ減らしたい」のかで、-Recursive の有無を決めるのがコツです。まずは直接メンバーのみを掃除し、それでも残る場合にネスト側の整理へ進むと安全です。

事故防止:削除前に“証跡”を残す(CSV出力と差分確認)

運用で安心なのは、削除前後で「何を消したか」を追える状態にすることです。最低限、削除対象の一覧を CSV に出しておくと、監査対応やロールバック検討がしやすくなります。

$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$csvPath   = ".\\${group}_DisabledMembers_$timestamp.csv"

$disabledUsers |
  Select-Object SamAccountName, Name, Enabled, DistinguishedName |
  Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8

Write-Host "Exported: $csvPath"

さらに安全にしたい場合は、削除前の「グループ現状メンバー」もバックアップしておきます。あとで差分確認するだけでなく、復元手順を検討する材料にもなります。

$beforePath = ".\\${group}_Members_Before_$timestamp.csv"

Get-ADGroupMember -Identity $group |
  Select-Object Name, SamAccountName, objectClass, DistinguishedName |
  Export-Csv -Path $beforePath -NoTypeInformation -Encoding UTF8

除外ルールを入れる:サービスアカウントや例外ユーザーを守る

環境によっては、無効化されたサービスアカウントや、監査目的で残しているアカウントが存在します。そうした例外を運用上守りたい場合は、削除対象に「除外条件」を追加します。

よくある除外例考え方実装例(SamAccountName)
svc_ / app_ などのサービス系無効でもグループに残す設計の場合がある-notmatch '^(svc_|app_)'
特定 OU 配下のユーザー例外 OU に隔離して管理しているDistinguishedName -notlike '*OU=Exceptions,*'
特定の担当者だけ棚卸し対象外の管理者などSamAccountName -notin @('admin1','admin2')

例えばサービスアカウント名を除外するなら、抽出の直前に次のようにフィルターします。

$disabledUsers = $users |
  Where-Object { $_.Enabled -eq $false } |
  Where-Object { $_.SamAccountName -notmatch '^(svc_|app_)' }

実運用向け:パラメータ化した安全スクリプト例

毎回コピペで実行するよりも、「対象グループ」「直接/再帰」「ドライラン/本実行」「出力先」を切り替えられる形にしておくと、運用事故が減ります。以下は 1 ファイルで完結する例です。

param(
  [Parameter(Mandatory)]
  [string]$Group,

  [switch]$Recursive,

  # 既定はドライラン(-Execute を付けたときだけ削除する)
  [switch]$Execute,

  # ログ・CSVの保存先
  [string]$OutDir = ".",

  # 除外プレフィックス(例:svc_、app_)
  [string[]]$ExcludePrefix = @("svc_", "app_"),

  # バッチサイズ(大きすぎると失敗時の切り戻しが大変)
  [int]$BatchSize = 50
)

Import-Module ActiveDirectory

$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
New-Item -ItemType Directory -Path $OutDir -ErrorAction SilentlyContinue | Out-Null

# メンバー取得(必要に応じて再帰)
$members = if ($Recursive) {
  Get-ADGroupMember -Identity $Group -Recursive -ErrorAction Stop
} else {
  Get-ADGroupMember -Identity $Group -ErrorAction Stop
}

# ユーザーだけ取り出し、Enabled を取得
$users = foreach ($m in $members) {
  if ($m.objectClass -ne "user") { continue }

  try {
    Get-ADUser -Identity $m -Properties Enabled -ErrorAction Stop
  } catch {
    # 参照できないオブジェクトが混ざっていても処理を止めない
    Write-Warning "Failed to read user: $($m.DistinguishedName)"
  }
}

# 無効ユーザー抽出(除外条件つき)
$disabledUsers = $users | Where-Object { $_.Enabled -eq $false }

if ($ExcludePrefix.Count -gt 0) {
  $pattern = '^(?:' + (($ExcludePrefix | ForEach-Object { [Regex]::Escape($_) }) -join '|') + ')'
  $disabledUsers = $disabledUsers | Where-Object { $_.SamAccountName -notmatch $pattern }
}

# 証跡出力
$reportPath = Join-Path $OutDir "${Group}_DisabledMembers_${timestamp}.csv"
$disabledUsers |
  Select-Object SamAccountName, Name, Enabled, DistinguishedName |
  Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8

Write-Host "Disabled members: $($disabledUsers.Count)"
Write-Host "Report: $reportPath"

if (-not $Execute) {
  Write-Host "Dry-run mode. No changes are made."
  Write-Host "To execute removal, rerun with -Execute"
  return
}

# 削除:バッチに分割
for ($i = 0; $i -lt $disabledUsers.Count; $i += $BatchSize) {
  $end = [Math]::Min($i + $BatchSize - 1, $disabledUsers.Count - 1)
  $batch = $disabledUsers[$i..$end]

  Remove-ADGroupMember -Identity $Group -Members $batch -Confirm:$false -ErrorAction Stop
  Write-Host "Removed: $($batch.Count) (index $i-$end)"
}

Write-Host "Removal completed."

実行例:

  • 確認だけ(ドライラン):.\Remove-DisabledMembers.ps1 -Group Group1 -OutDir .\\reports
  • 直接メンバーを削除:.\Remove-DisabledMembers.ps1 -Group Group1 -Execute -OutDir .\\reports
  • 再帰で棚卸し(削除は所属元に注意):.\Remove-DisabledMembers.ps1 -Group Group1 -Recursive -OutDir .\\reports

削除後の確認:本当に無効ユーザーが消えたかをチェックする

削除後は「空振りしていないか」「思ったより消えていない/消えすぎていないか」を簡単に確認しておくと安心です。例えば次のように再チェックできます。

$group = "Group1"

$remainingDisabled = Get-ADGroupMember -Identity $group |
  Where-Object { $_.objectClass -eq "user" } |
  ForEach-Object { Get-ADUser $_ -Properties Enabled } |
  Where-Object { $_.Enabled -eq $false }

$remainingDisabled | Select-Object SamAccountName, DistinguishedName

ここで残っている場合、ネスト経由(間接メンバー)か、除外条件に該当している可能性があります。原因を切り分けて、必要なら所属元のグループ側を整理します。

大量メンバーのときの考え方:速度と負荷を意識する

無効ユーザーが 300 名程度であれば、1 ユーザーずつ Get-ADUser で状態を取っても現実的な時間で終わることが多いです。一方、数千〜数万規模のグループになると、AD への問い合わせ回数が増えて時間がかかり、運用時間帯によっては負荷にもなり得ます。

大規模環境では、次のような方針が有効です。

  • 実行時間をずらす(夜間・メンテ時間)
  • 削除はバッチ(50〜200件)に分ける
  • ログを残し、途中で止まっても再実行しやすくする
  • 対象を「直接メンバー」に限定して無駄な探索を減らす
状況おすすめ理由
無効ユーザーが数十〜数百シンプルな ForEach + Get-ADUser読みやすく、保守しやすい
メンバーが数千以上直接メンバーに限定+バッチ削除不要な探索を減らし、実行を安定させる
ネストが多い棚卸しは -Recursive、削除は所属元グループで間接メンバーを誤って「消えないのに消そうとする」事態を防ぐ

トラブルシューティング:よくあるエラーと対処

症状主な原因対処
Access is denied権限不足(グループ変更権限がない)委任設定を見直す/実行アカウントを変更する
Cannot find an object with identity…オブジェクト参照の不整合、または削除済み対象ユーザーが存在するか再確認し、リストを更新する
The specified account name is not a member of the group-Recursive で拾った間接メンバーを、親グループから削除しようとしている直接メンバーで取得し直す/所属元のネストグループで削除する
Server is not operationalDC/ネットワーク障害、名前解決、ADWS の問題接続先 DC を指定(-Server)する/ネットワークを確認する
処理が途中で止まる一部ユーザーの参照失敗、または削除失敗try/catch とバッチ削除で継続可能にし、失敗分だけ再実行する

運用のコツ:定期実行の前に“条件”を固める

無効ユーザーの削除を定期化する場合、技術より先に「運用ルール」を決めておくと揉めません。

  • 削除対象は「Enabled=False のユーザーのみ」に限定する(不明オブジェクトは除外)
  • 削除前に必ず CSV を出す(監査・復元検討のため)
  • ネストグループを掃除するかどうか(-Recursive の扱い)を決める
  • 実行頻度(週次/月次)と、レビュー担当(承認フローの有無)を決める
  • 本番実行はメンテ時間に行い、実行ログを保管する

特に、アクセス権の中核となるグループでは「ドライラン→レビュー→本番実行」の流れを残しておくと、属人化を防ぎつつ安全に回せます。

よくある質問

無効ユーザーは削除せず、グループから外すだけで大丈夫?

多くの環境では「無効化=ログオン不可」ですが、グループに残っていること自体が監査指摘や設定ミスの温床になります。アカウントの扱い(削除/保持)は組織ポリシーに従いつつ、グループメンバーとして残す必然性がなければ外す方が安全です。

Remove-ADGroupMember を実行するときに -Confirm:$false は必須?

必須ではありませんが、対象が多いと確認プロンプトが大量に出ます。だからこそ、最初は -WhatIf で必ず確認し、問題ないと確信できた段階で -Confirm:$falseを付ける、という順序が現実的です。

無効ユーザーの判定は Enabled 以外でもできる?

Enabled は分かりやすく、PowerShell で扱いやすい代表的なプロパティです。より厳密に扱うなら userAccountControl のフラグや、組織独自の属性(退職日属性など)と組み合わせる運用もあります。まずは Enabled での整理を確実にし、その後に要件に応じて拡張するとスムーズです。

まとめ:まずは“確認してから削除”を徹底する

Active Directory グループから無効ユーザーを一括削除する最短ルートは、グループメンバーを取得して Enabled=False を抽出し、Remove-ADGroupMember で削除することです。ただし、-Recursive の扱い(直接メンバーか、ネスト経由か)と、証跡(CSV)を残す運用が重要です。小さく安全に回しながら、環境に合わせてスクリプトを育てていきましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次