PowerShellでユーザーのOU移動とエラーリスト出力を行う方法

PowerShellでユーザーのOU移動とエラーリスト出力を行う方法では、全userとtarget OUをGUIDで解決し、WhatIf結果を確認後、明示承認されたrequestだけ一件ずつ移動してCSVへ結果を残す。Move-ADObjectはdirectory objectをtarget pathへ移動し、権限、保護、domain controller、target OU状態で失敗し得る。display nameやSamAccountNameの部分一致で対象を選ばない。この記事はrequest ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにするを判断軸にして、記事固有のコード、合否、停止条件、復元を順序立てて説明します。

AD userの作成や属性変更ではなく、OU parent変更と行単位error listを安全に実装する。完了は「各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがある」です。結果が空なら「input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする」として調べ、エラーを0件へ置き換えません。

目次

input CSVとtarget OUを事前検証

全userとtarget OUをGUIDで解決し、WhatIf結果を確認後、明示承認されたrequestだけ一件ずつ移動してCSVへ結果を残す。ADユーザーOU移動とエラーCSVでは、単にコマンドが終了したことではなく「各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがある」を完了条件にします。AD userの作成や属性変更ではなく、OU parent変更と行単位error listを安全に実装する。

input CSVとtarget OUを事前検証に入る前に、対象、実行場所、権限、入力の由来を確認します。input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする。判定不能を成功へ丸めません。

userをObjectGUIDで一意にする

userをObjectGUIDで一意にするでは「request ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにする」という粒度で対象を特定します。Move-ADObjectはdirectory objectをtarget pathへ移動し、権限、保護、domain controller、target OU状態で失敗し得る。display nameやSamAccountNameの部分一致で対象を選ばない。表示名や先頭候補だけを採用しません。

ADユーザーOU移動とエラーCSVの対象が複数なら、候補数と除外理由を残します。ADユーザーOU移動とエラーCSVでは実行ユーザー、OS・製品版、locale、カレントディレクトリも結果の解釈へ影響するため同時に記録します。

移動前DNとOU GUIDを保存

移動前DNとOU GUIDを保存は変更や出力生成より先に行う観測です。request ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにするを含む形で現状を保存し、後段のコードが同じ対象へ向くか確認します。

Import-Module ActiveDirectory
$inputPath = 'C:\Ops\Requests\ou-move.csv'
$planPath = 'C:\Ops\Reports\ou-move-plan-20260717.csv'
$targetOuDn = 'OU=Sales,DC=corp,DC=example'
if (-not (Test-Path -LiteralPath $inputPath -PathType Leaf)) { throw "Input not found: $inputPath" }
if (Test-Path -LiteralPath $planPath) { throw "Plan already exists: $planPath" }
$rows = @(Import-Csv -LiteralPath $inputPath)
if ($rows.Count -eq 0) { Write-Host 'No requests; no changes.'; return }
$targetOu = @(Get-ADOrganizationalUnit -Identity $targetOuDn -Properties ObjectGUID,DistinguishedName -ErrorAction Stop)
if ($targetOu.Count -ne 1) { throw 'Target OU could not be resolved uniquely.' }
$plan = foreach ($row in $rows) {
    if ([string]::IsNullOrWhiteSpace($row.RequestId) -or [string]::IsNullOrWhiteSpace($row.SamAccountName)) { throw 'RequestId or SamAccountName is empty.' }
    $user = Get-ADUser -Identity $row.SamAccountName -Properties DistinguishedName,ObjectGUID,Enabled -ErrorAction Stop
    [pscustomobject]@{RequestId=$row.RequestId;SamAccountName=$user.SamAccountName;ObjectGuid=$user.ObjectGUID.Guid;OldDn=$user.DistinguishedName;OldParentDn=($user.DistinguishedName -replace '^(?:\\.|[^,])+,','');TargetOuDn=$targetOu[0].DistinguishedName;TargetOuGuid=$targetOu[0].ObjectGUID.Guid;Status='Validated'}
}
if (($plan.RequestId | Sort-Object -Unique).Count -ne $plan.Count) { throw 'Duplicate RequestId.' }
$plan | Export-Csv -LiteralPath $planPath -NoTypeInformation -Encoding UTF8
$plan | ForEach-Object { Move-ADObject -Identity ([guid]$_.ObjectGuid) -TargetPath $_.TargetOuDn -WhatIf -Confirm:$false -ErrorAction Stop }
$plan | Format-Table RequestId,SamAccountName,ObjectGuid,OldParentDn,TargetOuDn,TargetOuGuid

Move-ADObjectはdirectory objectをtarget pathへ移動し、権限、保護、domain controller、target OU状態で失敗し得る。display nameやSamAccountNameの部分一致で対象を選ばない。ADユーザーOU移動とエラーCSVでは取得不能、対象なし、値が空という三状態を分け、stderrや終了コードを捨てません。

全件WhatIfでpreview

全件WhatIfでpreviewでは全userとtarget OUをGUIDで解決し、WhatIf結果を確認後、明示承認されたrequestだけ一件ずつ移動してCSVへ結果を残す。ADユーザーOU移動とエラーCSVのサンプルにあるパス、セル、ユーザー、時刻は検証用なので、直前に確認した承認値へ置き換えます。

Import-Module ActiveDirectory
$planPath = 'C:\Ops\Reports\ou-move-plan-20260717.csv'
$resultPath = 'C:\Ops\Reports\ou-move-results-20260717.csv'
if (Test-Path -LiteralPath $resultPath) { throw "Result already exists: $resultPath" }
$plan = @(Import-Csv -LiteralPath $planPath)
if ($plan.Count -eq 0) { throw 'Approved plan is empty.' }
if (($plan.RequestId | Sort-Object -Unique).Count -ne $plan.Count) { throw 'Plan has duplicate RequestId.' }
$targetBindings = @{}
foreach ($row in $plan) {
    if ($row.Status -ne 'Validated' -or [string]::IsNullOrWhiteSpace($row.TargetOuDn) -or $row.TargetOuGuid -notmatch '^[0-9A-Fa-f-]{36}$') { throw "Invalid plan row: $($row.RequestId)" }
    $target = @(Get-ADOrganizationalUnit -Identity $row.TargetOuDn -Properties ObjectGUID,DistinguishedName -ErrorAction Stop)
    if ($target.Count -ne 1 -or $target[0].ObjectGUID.Guid -ne $row.TargetOuGuid -or $target[0].DistinguishedName -ne $row.TargetOuDn) { throw "Target OU DN/GUID binding changed before first move: $($row.RequestId)" }
    $targetBindings[$row.RequestId] = $target[0].ObjectGUID.Guid
}
$approval = Read-Host 'Type MOVE-APPROVED-USERS after reviewing every WhatIf line and target OU GUID'
if ($approval -cne 'MOVE-APPROVED-USERS') { throw 'Approval token did not match.' }
$results = [System.Collections.Generic.List[object]]::new()
foreach ($row in $plan) {
    try {
        $liveTarget = @(Get-ADOrganizationalUnit -Identity $row.TargetOuDn -Properties ObjectGUID,DistinguishedName -ErrorAction Stop)
        if ($liveTarget.Count -ne 1 -or $liveTarget[0].ObjectGUID.Guid -ne $row.TargetOuGuid -or $liveTarget[0].DistinguishedName -ne $row.TargetOuDn) { throw 'Target OU DN/GUID changed immediately before Move-ADObject.' }
        $user = Get-ADUser -Identity ([guid]$row.ObjectGuid) -Properties DistinguishedName,ObjectGUID -ErrorAction Stop
        $parent = $user.DistinguishedName -replace '^(?:\\.|[^,])+,',''
        if ($parent -eq $liveTarget[0].DistinguishedName) { $status = 'AlreadyAtTarget' } else {
            if ($user.DistinguishedName -ne $row.OldDn) { throw 'Current DN differs from the approved plan.' }
            Move-ADObject -Identity $user.ObjectGUID -TargetPath $liveTarget[0].DistinguishedName -Confirm -ErrorAction Stop
            $moved = Get-ADUser -Identity $user.ObjectGUID -Properties DistinguishedName,ObjectGUID -ErrorAction Stop
            if (($moved.DistinguishedName -replace '^(?:\\.|[^,])+,','') -ne $liveTarget[0].DistinguishedName) { throw 'Moved user did not rebind to the approved target OU.' }
            $status = 'Moved'
        }
        $results.Add([pscustomobject]@{RequestId=$row.RequestId;SamAccountName=$row.SamAccountName;ObjectGuid=$row.ObjectGuid;OldDn=$row.OldDn;OldParentDn=$row.OldParentDn;TargetOuDn=$row.TargetOuDn;TargetOuGuid=$row.TargetOuGuid;Stage='Move';Status=$status;Error=$null})
    } catch {
        $results.Add([pscustomobject]@{RequestId=$row.RequestId;SamAccountName=$row.SamAccountName;ObjectGuid=$row.ObjectGuid;OldDn=$row.OldDn;OldParentDn=$row.OldParentDn;TargetOuDn=$row.TargetOuDn;TargetOuGuid=$row.TargetOuGuid;Stage='Move';Status='Failed';Error=$_.Exception.Message})
    }
}
$results | Export-Csv -LiteralPath $resultPath -NoTypeInformation -Encoding UTF8
$results | Format-Table RequestId,SamAccountName,TargetOuGuid,Stage,Status,Error -Wrap

無条件bulk move、保護object、管理account、別domain target、remote DC不整合があれば止める。ADユーザーOU移動とエラーCSVで変更が発生する場合は、新規出力、no-clobber、WhatIf、下書き表示など利用可能な安全機構を先に使います。

承認token後に一件ずつMove-ADObject

承認token後に一件ずつMove-ADObjectでは入力と出力を別々に再取得します。ADユーザーOU移動とエラーCSVの合格は、各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがあることです。件数だけでなく識別値と内容も照合します。

Import-Module ActiveDirectory
$resultPath = 'C:\Ops\Reports\ou-move-results-20260717.csv'
$verificationPath = 'C:\Ops\Reports\ou-move-verification-20260717.csv'
if (Test-Path -LiteralPath $verificationPath) { throw "Verification already exists: $verificationPath" }
$verification = foreach ($row in (Import-Csv -LiteralPath $resultPath)) {
    if ($row.Status -notin 'Moved','AlreadyAtTarget') { [pscustomobject]@{RequestId=$row.RequestId;ObjectGuid=$row.ObjectGuid;CurrentDn=$null;TargetOuGuid=$row.TargetOuGuid;OldParentDn=$row.OldParentDn;Status='NotMoved';RollbackPreview=$null}; continue }
    try {
        $target = @(Get-ADOrganizationalUnit -Identity $row.TargetOuDn -Properties ObjectGUID,DistinguishedName -ErrorAction Stop)
        if ($target.Count -ne 1 -or $target[0].ObjectGUID.Guid -ne $row.TargetOuGuid) { throw 'Verification target OU GUID differs from the approved GUID.' }
        $user = Get-ADUser -Identity ([guid]$row.ObjectGuid) -Properties DistinguishedName,ObjectGUID -ErrorAction Stop
        $parent = $user.DistinguishedName -replace '^(?:\\.|[^,])+,',''
        $ok = $parent -eq $target[0].DistinguishedName
        [pscustomobject]@{RequestId=$row.RequestId;ObjectGuid=$user.ObjectGUID.Guid;CurrentDn=$user.DistinguishedName;TargetOuGuid=$row.TargetOuGuid;OldParentDn=$row.OldParentDn;Status=$(if ($ok) {'Verified'} else {'WrongParent'});RollbackPreview="Move-ADObject -Identity $($user.ObjectGUID.Guid) -TargetPath '$($row.OldParentDn)' -WhatIf"}
    } catch { [pscustomobject]@{RequestId=$row.RequestId;ObjectGuid=$row.ObjectGuid;CurrentDn=$null;TargetOuGuid=$row.TargetOuGuid;OldParentDn=$row.OldParentDn;Status='VerifyFailed';RollbackPreview=$_.Exception.Message} }
}
$verification | Export-Csv -LiteralPath $verificationPath -NoTypeInformation -Encoding UTF8
$verification | Format-Table RequestId,ObjectGuid,TargetOuGuid,OldParentDn,Status,CurrentDn -Wrap

input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする。ADユーザーOU移動とエラーCSVの結果が期待と違えば追加変更を重ねず、入力、対象範囲、locale・時刻、権限、製品仕様の順に戻って調べます。

成功・失敗をresult listへ追加

無条件bulk move、保護object、管理account、別domain target、remote DC不整合があれば止める。成功・失敗をresult listへ追加に当てはまるときは中止理由、対象識別子、終了コードまたはErr.Number、直前に成功した段階を保存します。

input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする。ADユーザーOU移動とエラーCSVの再試行は原因を直し、同じ入力と対象を再確認してから行います。警告抑止や強制上書きで通しません。

再取得DNで移動を検証

保存したold parent DNへObjectGUID単位でWhatIf・承認後に戻し、group policyとapplication影響を確認する。復元にも「request ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにする」を用い、類似名の別対象へ処理しません。

  • ADユーザーOU移動とエラーCSVの開始前状態
  • 採用対象: request ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにする
  • 復元後の確認: 各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがある
  • 復元を止める条件: 無条件bulk move、保護object、管理account、別domain target、remote DC不整合があれば止める

rollbackは旧ParentDNへ個別実行

request IDで再実行を冪等化し、既にtargetにいるuserをMovedとして二重処理しない。ADユーザーOU移動とエラーCSVを反復するときは、正常、差分なし、対象なし、要承認、失敗を別の状態として記録します。

rollbackは旧ParentDNへ個別実行の主キーrequest ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにする
採用条件各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがある
空結果input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする
中止条件無条件bulk move、保護object、管理account、別domain target、remote DC不整合があれば止める

input CSVとtarget OUを事前検証から証跡化する移動前DNとOU GUIDを保存

ADユーザーOU移動とエラーCSVの証跡は「request ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにする」を主キーにします。input CSVとtarget OUを事前検証で確認した値と、実行直前・実行直後の値を同じ作業番号に保存し、表示名が似ている別対象や前回の結果を混ぜません。

userをObjectGUIDで一意にするで空結果を判定する全件WhatIfでpreview

ADユーザーOU移動とエラーCSVの空結果は「input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする」として扱います。userをObjectGUIDで一意にするで入力自体が存在するか、権限で見えていないか、条件に一致しないだけかを分け、0件という表示だけで成功・失敗を決めません。

rollbackは旧ParentDNへ個別実行から復旧可否を測るinput CSVとtarget OUを事前検証

ADユーザーOU移動とエラーCSVの復旧判断では「保存したold parent DNへObjectGUID単位でWhatIf・承認後に戻し、group policyとapplication影響を確認する」を採用します。rollbackは旧ParentDNへ個別実行を再確認し、復旧後に「各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがある」へ戻ったかを別の読み取り処理で測定します。

ADユーザーOU移動とエラーCSVの事前確認では、request ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにするを画面表示や標準出力だけで済ませず、実行日時と一緒に作業記録へ写します。Move-ADObjectはdirectory objectをtarget pathへ移動し、権限、保護、domain controller、target OU状態で失敗し得る。display nameやSamAccountNameの部分一致で対象を選ばないという仕様があるため、似た名前の別対象、前回実行時の値、キャッシュされた表示を今回の対象と取り違えないことが重要です。

ADユーザーOU移動とエラーCSVのコードを実行した直後は、まず終了状態を保存し、その後に別の読み取り処理で「各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがある」を確認します。ADユーザーOU移動とエラーCSVでは同じコードの表示だけを合否判定に使うと部分成功や遅延反映を見逃すため、識別値、件数、内容の三点を照合します。

ADユーザーOU移動とエラーCSVで結果が得られない場合は、input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする。ADユーザーOU移動とエラーCSVではこの状態と、権限拒否、入力形式の不一致、接続先や時刻の違いを一緒にしません。ADユーザーOU移動とエラーCSVの対象候補数、除外された候補、最後に成功した確認処理を残すと、再試行で同じ失敗を重ねずに済みます。

ADユーザーOU移動とエラーCSVを元へ戻す必要があるときは、保存したold parent DNへObjectGUID単位でWhatIf・承認後に戻し、group policyとapplication影響を確認する。ADユーザーOU移動とエラーCSVの復元前にも変更後の識別値を再取得し、別担当者の更新が入っていないか確認します。ADユーザーOU移動とエラーCSVの復元結果も通常処理と同じ完了条件で測り、戻したつもりという報告だけで閉じません。

ADユーザーOU移動とエラーCSVを引き継ぐ記録には、request IDで再実行を冪等化し、既にtargetにいるuserをMovedとして二重処理しない。特に「無条件bulk move、保護object、管理account、別domain target、remote DC不整合があれば止める」に該当した場合は、実行を止めたこと自体を正しい結果として扱います。ADユーザーOU移動とエラーCSVの次回担当者が承認範囲と未処理対象を区別できるよう、作業番号と対象識別子を対応付けます。

ADユーザーOU移動とエラーCSVの作業記録には、開始前の対象候補、採用した識別値、実行したコード、終了後の実測、除外理由を同じ作業番号で保存します。「request ID、user ObjectGUID、old DistinguishedName、target OU ObjectGUID、status、errorを一移動recordにする」を省くと別対象との比較になり得るため、日時、実行場所、製品版と一緒に残します。

PowerShellでユーザーのOU移動とエラーリスト出力を行う方法を定期運用へ組み込む場合も初回は対話的に確認します。正常は「各成功userのObjectGUIDが同じままnew DistinguishedNameのparentがtarget OUとなり、失敗行にはstageとmessageがある」、空結果は「input 0件なら変更なしで終了し、user未検出・複数・target OUなしを別error codeにする」、停止は「無条件bulk move、保護object、管理account、別domain target、remote DC不整合があれば止める」として報告し、次の担当者が同じ条件で追試できるようにします。

公式情報・参考資料

ADユーザーOU移動とエラーCSVのコマンド、API、対応範囲は次の公式一次資料で確認しました。確認日は2026年7月17日です。ADユーザーOU移動とエラーCSVの実行環境にあるman、–help、VBA Object Browser、Get-Helpも併用してください。

この記事を書いた人

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

コメント

コメントする

目次