AD ユーザーの一括登録|CSV と PowerShell の手順と注意点

  • URLをコピーしました!
目次

はじめに

組織変更や年度の入れ替わりで、オンプレミスの Active Directory Domain Services(AD DS)へユーザーをまとめて登録する場面があります。管理ツールから 1 件ずつ作成する方法は、件数が増えるほど入力ミスが起きやすく、作業時間もかかります。

一括登録の手段としては csvdeldifde、PowerShell が挙がります。ただし、この 3 つはパスワードの扱いと変更操作への対応が大きく異なります。選び違えると、アカウントは作成できてもパスワードが設定できず、有効化まで進まないという状況になります。

本記事は、オンプレミスの AD DS に CSV を使って複数のユーザーを登録する手順を扱います。Microsoft Entra ID や Microsoft 365 のユーザー一括登録は、管理センターの CSV インポートや Microsoft Graph を使う別の手順のため、本記事の対象には含みません。AD DS そのものの仕組みは『Active Directory 基礎|AD DS の仕組みと Server 2025 の変更点』で解説しています。

この記事でわかること
  • csvde・ldifde・PowerShell の違いと、今回の作業に適した方法の選び方
  • 登録用 CSV と登録先 OU の準備、必要な権限の考え方
  • 入力ミスと既存アカウントの重複を、登録前に確認する手順
  • 無効状態での作成からパスワード設定、有効化までの流れと各段階の意味
  • 一部が失敗したときに、既存ユーザーを変更せず実行ログから再開する方法

先に結論を示します。AD DS のユーザー一括登録は、作成とパスワード設定を一連の処理としてまとめられる PowerShell の New-ADUser を中心に組み立てる方法が扱いやすくなります。csvde はパスワードを設定できず、ldifde は暗号化された接続と unicodePwd の書式という条件が加わるためです。

実行は「登録前チェック → 無効状態で作成 → パスワード設定 → 初回変更の要求 → 有効化 → 結果ログの保存」の順に分けます。段階を分けたうえで、作成に成功したオブジェクトだけを後続の対象にすると、一部のユーザーで失敗してもどこまで進んだかをユーザー単位で切り分けられ、既存ユーザーを巻き込むこともありません。

csvde・ldifde・PowerShell の違いと選び方

3 つの方法は、入力形式だけでなく、既存オブジェクトを変更できるか、パスワードを設定できるかという点で性質が分かれます。まず違いを整理してから、本記事で PowerShell を中心に扱う理由を示します。

3 つの方法の比較

項目csvdeldifdePowerShell(New-ADUser)
入力形式CSVLDIFCSV ほか任意(Import-Csv 経由)
オブジェクトの作成可能可能可能
既存オブジェクトの変更公式資料にインポートとエクスポートの説明があり、変更操作の指定は用意されていない可能(changeType に add / modify / delete を指定)可能(Set-ADUser)
パスワード設定不可条件付きで可能(暗号化された接続と unicodePwd の書式が必要)可能(AccountPassword、Set-ADAccountPassword)
失敗時の制御-k で一部のエラーを無視-k で一部のエラーを無視try / catch と ErrorAction で行単位に制御
主な用途既存データの書き出し、他ディレクトリとの受け渡し属性の追加・変更・削除、スキーマ拡張ユーザー作成・更新の日常的な自動化

csvde は AD DS のデータを CSV として書き出す用途で扱いやすく、登録内容の棚卸しや他システムへの受け渡しに向きます。ldifde は属性単位の変更やスキーマ拡張まで扱えるため、既存オブジェクトへ属性を追加したい場合の選択肢になります。

csvde でパスワードを設定できない理由

csvde の公式資料には、パスワードのインポートができない旨が明記されています。パスワードは暗号化されたチャネルで送る必要があるのに対し、csvde は SSL や暗号化された LDAP 通信に対応していないためです。

参考: Csvde(Microsoft Learn)
“You cannot import user passwords by using Csvde”
(csvde を使用してユーザーのパスワードをインポートすることはできません)
https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/cc732101(v=ws.11)

この制約があるため、csvde の公式サンプルでは userAccountControl に 514 を指定し、無効状態でアカウントを作成しています。ここで userAccountControl の値の意味を取り違えやすいため、代表的な値を整理します。

値(10 進数)内訳意味
512NORMAL_ACCOUNT一般ユーザーアカウントの既定値。有効な状態
514NORMAL_ACCOUNT + ACCOUNTDISABLE一般ユーザーアカウントで、無効な状態

値の内訳は Microsoft Learn の UserAccountControl property flags にまとまっており、NORMAL_ACCOUNT は一般的なユーザーを表す既定のアカウント種別と説明されています。つまり 512 はアカウントの種別と有効状態を表す値であり、次回ログオン時のパスワード変更を要求する設定ではありません。初回ログオン時の変更要求は pwdLastSet を 0 にすることで表現され、PowerShell では ChangePasswordAtLogon パラメーターが対応します。

ldifde でパスワードを扱う場合の条件

csvde の制約を、そのまま ldifde に当てはめることはできません。ldifde ではパスワード属性 unicodePwd を操作できますが、いくつかの条件が付きます。

参考: How to set a user’s password with Ldifde(Microsoft Learn)
“This attribute can only be modified, not added on object creation or read by a search.”
(この属性は変更のみが可能で、オブジェクト作成時の追加や検索による読み取りはできません)
https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/set-user-password-with-ldifde

同資料では、この属性を変更するために 128 ビットの SSL / TLS または SASL による暗号化された接続が必要であること、値は引用符で囲んだうえで Base64 でエンコードして記述することが説明されています。SSL / TLS を使う場合は -t 636、SASL を使う場合は -h を指定します。

注意したいのは、Base64 は文字列を転送可能な形式へ変換する符号化であり、暗号化ではないという点です。ファイル内の値は復元できるため、LDIF ファイルの取り扱いは平文パスワードと同等に管理する必要があります。実際の変更手順とエラー時の挙動は上記の公式資料に詳しくまとまっているため、本記事ではこれ以上は展開しません。

本記事で PowerShell を中心に扱う理由

新規ユーザーの一括登録では、アカウント作成とパスワード設定、初回変更の要求、有効化までを続けて実施します。PowerShell であれば、これらを 1 つのスクリプト内で行単位に制御でき、失敗した行だけを結果として残せます。以降の手順は New-ADUser を中心に構成し、csvde と ldifde は用途に応じて使い分ける前提とします。

実行前に準備するもの

スクリプトを書く前に、実行環境、権限、登録先 OU の 3 点を確定させます。ここが曖昧なまま進めると、意図しないコンテナーにユーザーが作成されるといった手戻りにつながります。

実行環境と ActiveDirectory モジュール

掲載するスクリプトは、ドメインコントローラーまたは RSAT を導入した管理端末上の Windows PowerShell 5.1 を想定環境としています。コマンドと仕様は Microsoft Learn の Windows Server 2025 向けリファレンスで確認していますが、筆者の実機で動作を検証したものではありません。想定環境と検証済み環境は別のものとして扱ってください。適用前に、検証環境での確認を推奨します。

ドメインコントローラーには ActiveDirectory モジュールが導入済みですが、メンバーサーバーから実行する場合は機能の追加が必要です。

Install-WindowsFeature -Name RSAT-AD-PowerShell
Import-Module ActiveDirectory

ここで、Windows Server の版と PowerShell の版を混同しないよう注意します。ドメインコントローラーの OS バージョンと、管理端末で使う PowerShell のバージョンは別の話です。ActiveDirectory モジュールは Windows PowerShell 向けに提供されており、PowerShell 7 で利用する場合は互換性の扱いが別途整理されています。Microsoft Learn のモジュール一覧ページから PowerShell 7 module compatibility が案内されているため、PowerShell 7 で実行する場合は事前に確認することを推奨します。

必要な権限は Domain Admins だけではない

ユーザーの一括登録に必要なのは、対象 OU に対してユーザーオブジェクトを作成し、パスワードをリセットできる権限です。これは Domain Admins でなくても、制御の委任によって付与できます。Microsoft Learn の Delegation of control in Active Directory Domain Services では、委任のウィザードで指定できる一般的なタスクとして、ユーザーアカウントの作成・削除・管理と、パスワードのリセットおよび次回ログオン時の変更要求が挙げられています。

作業アカウントを決めるときは、次の観点で確認します。

  • 登録先 OU に対して、ユーザーオブジェクトの作成権限があるか
  • 作成したユーザーのパスワードをリセットできるか
  • アカウントの有効化に必要な userAccountControl の書き込み権限があるか
  • 権限の付与先が個人アカウントではなく、運用管理用のグループになっているか

委任の範囲は、今回の作業で使う OU に限定しておくと、スクリプトの記述ミスで別の OU へ書き込んでしまう事故を抑えられます。

登録先 OU と接続先ドメインコントローラーの確認

New-ADUserPath パラメーターを省略すると、公式リファレンスの記載どおり既定の Users コンテナーへユーザーが作成されます。一括登録では、登録先 OU の識別名(DN)を必ず明示します。あわせて、複数のドメインコントローラーがある環境では Server パラメーターで接続先を固定し、作成直後の確認が別のドメインコントローラーに向かわないようにします。

Get-ADDomain | Select-Object DNSRoot, DistinguishedName, PDCEmulator
Get-ADOrganizationalUnit -Identity "OU=Sales,OU=Users,DC=example,DC=local" | Select-Object Name, DistinguishedName

登録用 CSV の作り方

CSV はスクリプトの入力そのものです。列名はスクリプト内での参照名とそろえ、後から列を足すときもスクリプト側を同時に更新します。

サンプル CSV と列の意味

ここでは架空のユーザー 3 件を例にします。ファイル名は users.csv とします。

Surname,GivenName,DisplayName,SamAccountName,UserPrincipalName,OUPath
山田,太郎,山田 太郎,t.yamada,t.yamada@example.local,"OU=Sales,OU=Users,DC=example,DC=local"
鈴木,花子,鈴木 花子,h.suzuki,h.suzuki@example.local,"OU=Sales,OU=Users,DC=example,DC=local"
佐藤,一郎,佐藤 一郎,i.sato,i.sato@example.local,"OU=Corp,OU=Users,DC=example,DC=local"

各列の意味は次のとおりです。

Surname / GivenName

姓と名です。それぞれ LDAP 属性の sngivenName に対応します。

DisplayName

管理ツールの一覧に表示される名前です。本記事のスクリプトでは、この値を Name パラメーターにも使うため、同じ OU 内で重複しない値にします。Name はオブジェクトの CN になります。

SamAccountName

従来形式のログオン名です。公式リファレンスでは、古い OS との互換性を考慮する場合に 20 文字以下で作成するよう案内されています。ドメイン内で一意である必要があります。

UserPrincipalName

ユーザー名とドメイン名をアットマークでつないだ形式のログオン名です。オブジェクトの DN とは独立しているため、ユーザーを別の OU へ移動しても値は変わりません。

OUPath

登録先 OU の DN です。New-ADUserPath パラメーターへ渡します。

移動ユーザープロファイルやホームフォルダーを併用する環境では、ProfilePathHomeDirectory に相当する列を追加する構成も考えられます。プロファイルの仕組みは『移動ユーザープロファイルとフォルダリダイレクトの違いと運用の落とし穴』で解説しています。

DN 内のカンマと引用符、日本語を含む場合の文字コード

OU の DN は OU=Sales,OU=Users,DC=example,DC=local のようにカンマを含みます。CSV の区切り文字と衝突するため、DN の列はダブルクォートで囲みます。上のサンプルで OUPath だけ引用符が付いているのはこのためです。

文字コードにも注意が必要です。Import-CsvEncoding パラメーターの既定値は、PowerShell 7 系のリファレンスでは utf8NoBOM と記載されています。版によって既定の扱いが異なるため、氏名などに日本語を含む CSV では Encoding を明示します。本記事では想定環境を Windows PowerShell 5.1 とし、CSV を UTF-8 で保存したうえで -Encoding UTF8 を明示する形に統一しています。以降のスクリプトもすべてこの指定です。

Import-Csv -Path .\users.csv -Encoding UTF8 | Format-Table -AutoSize

読み込み結果を先に表示し、氏名が文字化けしていないか、列がずれていないかを目視で確認してから次へ進みます。

CSV にパスワード列を作らない

一括登録の解説では、CSV にパスワード列を持たせる構成が紹介されることがあります。ただし、この方法では平文のパスワードがファイルとして残り、共有フォルダーやメールの添付を経由して広がる可能性があります。全員に同じ初期パスワードを与える方法も、1 件が漏れた時点で全アカウントに影響するため、標準の手順には向きません。

本記事では、CSV にパスワードを含めず、作成後にユーザー単位でパスワードを設定する構成を採ります。組織の運用に合わせて、乱数で生成した個別パスワードを別経路で配布する、初回のみ対面で設定するなど、配布方法は差し替えられる形にしています。

もう 1 点、既存ユーザーの全属性をエクスポートして、そのまま新規登録の入力に流用する方法も避けます。objectSIDobjectGUID のようにシステムが払い出す属性、権限やグループ所属に関わる属性が混ざり、意図しない設定を引き継ぐためです。新規登録に必要な列だけを定義した CSV を用意します。

登録前チェックと一括登録の実行

チェックと登録を別のスクリプトに分けると、チェックした入力と実際に登録する入力が別物になり得ます。ここでは、入力の読み込み、登録前チェック、登録を 1 本のスクリプトにまとめ、チェックを通過したデータだけをそのまま登録へ渡す構成にします。

登録前チェックで確認すること

チェックは次の観点で行い、1 件でも問題が見つかった場合は登録処理を開始しません。

  • CSV にデータ行があるか、必須列がそろっているか
  • 必須値に空欄がないか、SamAccountName が 20 文字以下か
  • CSV 内で SamAccountNameUserPrincipalName、同一 OU 内の Name が重複していないか
  • 同じログオン名や UPN のユーザーが AD に既に存在しないか
  • 登録先 OU が AD 上に存在するか

AD への問い合わせでは、「検索は成功したが該当なし」と「接続や権限、検索式のエラーで確認できなかった」を必ず区別します。後者を「問題なし」として扱うと、確認していない状態のまま登録が始まります。スクリプトでは前者を通過扱い、後者を確認未完了として停止対象にしています。

もう 1 点、CSV の値を検索条件へ渡す箇所も注意が必要です。値を文字列の検索式へそのまま埋め込むと、括弧やバックスラッシュを含む入力で検索式が壊れます。ここでは LDAP フィルターを使い、値の中の \*() をエスケープしてから組み立てます。

-WhatIf でできることとできないこと

New-ADUser には -WhatIf が用意されており、公式リファレンスでは「コマンドレットを実行した場合に何が起きるかを表示し、コマンドレット自体は実行しない」と説明されています。対象と件数の取り違えに気付く用途では有効です。

一方で、コマンドレットが実行されない以上、実際に作成が成功するか、指定したパスワードがドメインのパスワードポリシーを満たすかまでは -WhatIf では判定できません。登録前チェックについても同じことが言えます。チェックの時点と作成の時点には時間差があるため、他の管理者が同じログオン名のユーザーを作成するといった競合までは防げません。事前チェックは事故の確率を下げる手段であり、作成時に失敗した行が後続の変更処理へ進まない構成と併用することが前提になります。

登録の流れと各段階の意味

手順
入力を読み込み、登録前チェックを実行する

読み込んだデータをそのままチェックし、問題があれば登録へ進みません。確認できなかった項目も停止対象として扱います。

手順
無効状態でアカウントを作成する

パスワードを指定せずに作成し、-PassThru で作成されたオブジェクトを受け取ります。作成に失敗した行は結果を記録してそこで終了し、後続の変更処理へは進みません。

手順
パスワードを設定する

手順 2 で受け取ったオブジェクトを対象に Set-ADAccountPassword-Reset 付きで実行します。ログオン名での再検索を挟まないため、同名の既存ユーザーを対象にしてしまう余地がありません。

手順
初回ログオン時の変更を要求する

運用方針に応じて、同じオブジェクトに ChangePasswordAtLogon を設定します。この設定は PasswordNeverExpires と同時に有効にできません。

手順
アカウントを有効化する

パスワード設定まで成功した行だけを Enable-ADAccount で有効化します。途中で失敗した行は無効のまま残ります。

手順
結果ログを保存する

今回作成した ObjectGUID、想定したログオン名と UPN と OU、到達した段階、エラー内容を CSV へ保存します。パスワードは記録しません。ファイル名に実行時刻を入れ、前回分を上書きしません。

この流れの前提として、パスワードを指定しない New-ADUser はアカウントを無効な状態で作成します。

参考: Set-ADUser(Microsoft Learn)
“Accounts created with the New-ADUser cmdlet are disabled if no password is provided.”
(パスワードを指定しない場合、New-ADUser で作成したアカウントは無効になります)
https://learn.microsoft.com/en-us/powershell/module/activedirectory/set-aduser?view=windowsserver2025-ps

一括登録を通すために、ドメインのパスワード最小長を 0 にしたり複雑さの要件を無効にしたりする対処は採りません。ポリシーを満たす値を設定する側で調整します。

一括登録のスクリプト

次のスクリプトは、ユーザーごとに初期パスワードをコンソールから入力する対話式のサンプルです。無人実行は想定していません。件数が多い環境では、この入力部分を、乱数で生成したパスワードを別経路で配布する処理などに差し替えて使用します。想定環境は Windows PowerShell 5.1、入力ファイルは UTF-8 で保存した users.csv です。

#Requires -Version 5.1
#Requires -Modules ActiveDirectory

$Server    = 'dc01.example.local'
$CsvPath   = '.\users.csv'
$Required  = 'Surname','GivenName','DisplayName','SamAccountName','UserPrincipalName','OUPath'
$Stamp     = Get-Date -Format 'yyyyMMdd-HHmmss'
$ResultCsv = ".\bulk-create-$Stamp.csv"   # 実行ごとに別ファイル。前回分を上書きしない

function ConvertTo-LdapValue {
    param([string]$Value)
    $V = $Value -replace '\\', '\5c'
    $V = $V -replace '\*', '\2a'
    $V = $V -replace '\(', '\28'
    $V = $V -replace '\)', '\29'
    return $V
}

# ---------- 1. 入力の読み込み ----------
$Csv = @(Import-Csv -Path $CsvPath -Encoding UTF8)
if ($Csv.Count -eq 0) { throw 'CSV にデータ行がありません。' }

$Columns = $Csv[0].PSObject.Properties.Name
$Missing = $Required | Where-Object { $Columns -notcontains $_ }
if ($Missing) { throw "必須列がありません: $($Missing -join ', ')" }

# ---------- 2. 登録前チェック ----------
$Issues = New-Object System.Collections.Generic.List[string]

foreach ($Key in 'SamAccountName','UserPrincipalName') {
    foreach ($G in ($Csv | Group-Object $Key | Where-Object { $_.Count -gt 1 })) {
        $Issues.Add("CSV 内で $Key が重複しています: $($G.Name)")
    }
}
foreach ($G in ($Csv | Group-Object OUPath, DisplayName | Where-Object { $_.Count -gt 1 })) {
    $Issues.Add("CSV 内で同一 OU の Name が重複しています: $($G.Name)")
}

$Index = 0
foreach ($Row in $Csv) {
    $Index++
    $Label = "行 $Index"
    if (-not [string]::IsNullOrWhiteSpace($Row.SamAccountName)) { $Label = $Row.SamAccountName }

    foreach ($Column in $Required) {
        if ([string]::IsNullOrWhiteSpace($Row.$Column)) {
            $Issues.Add("$Label : 列 $Column が空です")
        }
    }
    if (-not [string]::IsNullOrWhiteSpace($Row.SamAccountName) -and $Row.SamAccountName.Length -gt 20) {
        $Issues.Add("$Label : SamAccountName が 20 文字を超えています")
    }

    try {
        Get-ADOrganizationalUnit -Server $Server -Identity $Row.OUPath -ErrorAction Stop | Out-Null
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
        $Issues.Add("$Label : 登録先 OU が見つかりません ($($Row.OUPath))")
    }
    catch {
        $Issues.Add("$Label : OU を確認できませんでした(確認未完了): $($_.Exception.Message)")
    }

    foreach ($Check in @(
        @{ Attr = 'sAMAccountName';    Value = $Row.SamAccountName },
        @{ Attr = 'userPrincipalName'; Value = $Row.UserPrincipalName }
    )) {
        try {
            $Filter = "($($Check.Attr)=$(ConvertTo-LdapValue $Check.Value))"
            $Hit = Get-ADUser -Server $Server -LDAPFilter $Filter -ErrorAction Stop
            if ($Hit) { $Issues.Add("$Label : 同じ $($Check.Attr) のユーザーが AD に存在します") }
        }
        catch {
            $Issues.Add("$Label : $($Check.Attr) を確認できませんでした(確認未完了): $($_.Exception.Message)")
        }
    }
}

if ($Issues.Count -gt 0) {
    $Issues
    throw "登録前チェックで $($Issues.Count) 件の問題を検出したため、登録処理は開始しません。"
}

# ---------- 3. 登録 ----------
$Log = New-Object System.Collections.Generic.List[object]

foreach ($Row in $Csv) {
    $Record = [ordered]@{
        SamAccountName    = $Row.SamAccountName
        UserPrincipalName = $Row.UserPrincipalName
        OUPath            = $Row.OUPath
        ObjectGUID        = ''
        DistinguishedName = ''
        Stage             = 'NotStarted'
        Status            = ''
        Message           = ''
        TimestampUtc      = (Get-Date).ToUniversalTime().ToString('s')
    }

    $New = $null
    try {
        $New = New-ADUser -Server $Server -PassThru `
            -Name              $Row.DisplayName `
            -DisplayName       $Row.DisplayName `
            -Surname           $Row.Surname `
            -GivenName         $Row.GivenName `
            -SamAccountName    $Row.SamAccountName `
            -UserPrincipalName $Row.UserPrincipalName `
            -Path              $Row.OUPath `
            -Enabled           $false `
            -ErrorAction Stop
    }
    catch {
        # 作成に失敗した行は、以降の変更処理へ進まない
        $Record.Status  = 'CreateFailed'
        $Record.Message = $_.Exception.Message
        $Log.Add([pscustomobject]$Record)
        continue
    }

    if ($null -eq $New -or $null -eq $New.ObjectGUID) {
        $Record.Status  = 'NeedsReview'
        $Record.Message = '作成結果のオブジェクトを取得できませんでした。AD 上の状態の確認が必要です。'
        $Log.Add([pscustomobject]$Record)
        continue
    }

    $Record.ObjectGUID        = $New.ObjectGUID.ToString()
    $Record.DistinguishedName = $New.DistinguishedName
    $Record.Stage             = 'Created'
    $Record.Status            = 'Created'

    $Password = Read-Host -Prompt "$($Row.SamAccountName) の初期パスワード" -AsSecureString
    try {
        Set-ADAccountPassword -Server $Server -Identity $New -Reset -NewPassword $Password -ErrorAction Stop
        $Record.Stage = 'PasswordSet'

        Set-ADUser -Server $Server -Identity $New -ChangePasswordAtLogon $true -ErrorAction Stop
        $Record.Stage = 'ChangeAtLogonSet'

        Enable-ADAccount -Server $Server -Identity $New -ErrorAction Stop
        $Record.Stage  = 'Enabled'
        $Record.Status = 'Completed'
    }
    catch {
        $Record.Status  = 'Incomplete'
        $Record.Message = $_.Exception.Message
    }
    finally {
        $Password = $null
    }

    $Log.Add([pscustomobject]$Record)
}

$Log | Export-Csv -Path $ResultCsv -NoTypeInformation -Encoding UTF8
$Log | Format-Table SamAccountName, Stage, Status, Message -AutoSize
Write-Host "結果ログ: $ResultCsv"

作成後の 3 つの操作は、いずれも -PassThru で受け取ったオブジェクトを Identity に渡しています。ログオン名で検索し直して対象を決める処理を挟まないため、同名の既存ユーザーを変更する経路がありません。作成に失敗した行は CreateFailed として記録され、パスワード設定へは進みません。オブジェクトを受け取れなかった行は NeedsReview として残し、後述の方法で状態を確認します。

作成エラーの原因を調べるために AD 上の状態を確認することはあります。ただしそれは読み取りにとどめ、ユーザーが存在したという理由だけで、パスワードの再設定や有効化を自動で実行しないようにします。

結果確認と一部失敗時の対処

スクリプトが最後まで流れたことと、意図したアカウントが意図した状態で存在することは別の話です。確認は、実行ログと AD から取得した現在の属性を突き合わせて行います。

実行ログと AD の状態を突き合わせる

実行ログは「どの段階まで処理が成功したか」の記録であり、AD の現在の状態そのものではありません。ログに Completed と残っていても、その後に別の操作で状態が変わっている可能性があります。両方を並べて確認します。

#Requires -Version 5.1
#Requires -Modules ActiveDirectory

$Server    = 'dc01.example.local'
$ResultCsv = '.\bulk-create-20260915-101500.csv'   # 登録時に出力されたログ

Import-Csv -Path $ResultCsv -Encoding UTF8 | ForEach-Object {
    $Record = $_
    $View = [ordered]@{
        SamAccountName = $Record.SamAccountName
        LogStage       = $Record.Stage
        LogStatus      = $Record.Status
        AdState        = ''
        SamMatch       = ''
        UpnMatch       = ''
        OuMatch        = ''
        PwdLastSetZero = ''
    }

    if ([string]::IsNullOrWhiteSpace($Record.ObjectGUID)) {
        # 今回の作成対象として特定できない行。自動で変更せず、個別に確認する
        $View.AdState = '作成記録なし(要確認)'
        [pscustomobject]$View
        return
    }

    try {
        $User = Get-ADUser -Server $Server -Identity $Record.ObjectGUID `
            -Properties Enabled, pwdLastSet, whenCreated, UserPrincipalName -ErrorAction Stop

        if ($User.Enabled) { $View.AdState = '有効' } else { $View.AdState = '無効' }
        $View.SamMatch       = ($User.SamAccountName -eq $Record.SamAccountName)
        $View.UpnMatch       = ($User.UserPrincipalName -eq $Record.UserPrincipalName)
        $View.OuMatch        = $User.DistinguishedName.EndsWith(',' + $Record.OUPath)
        $View.PwdLastSetZero = ($User.pwdLastSet -eq 0)
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
        $View.AdState = '未作成または削除済み'
    }
    catch {
        $View.AdState = "確認エラー: $($_.Exception.Message)"
    }

    [pscustomobject]$View
} | Format-Table -AutoSize

ObjectGUID で照会しているため、ログオン名が同じ別のユーザーを見てしまうことがありません。OuMatch は、識別名が登録先 OU の配下にあるかどうかの確認です。取得結果は次のように読み分けます。

AdState の表示意味次の動き
有効 / 無効オブジェクトを取得できた。現在の状態がそのまま表示されるLogStage と併せて、どこまで完了したかを判断する
未作成または削除済み検索は成功したが、その GUID のオブジェクトが存在しないログの Status と Message から、作成失敗か事後の削除かを切り分ける
確認エラー接続、権限、パラメーターなどの理由で取得できなかった未作成と扱わず、原因を解消してから再確認する
作成記録なし(要確認)今回の実行で作成できた記録がない自動処理の対象にせず、個別に AD を確認する

管理ツールの画面を再表示して一覧に出たかどうかで判断すると、これらの区別は付きません。画面の更新はあくまで表示の更新であり、パスワードポリシーの適用手段でもありません。

pwdLastSet が 0 でも完了とは判断できない

初回ログオン時のパスワード変更要求は pwdLastSet が 0 の状態として表れます。ただし、この値が 0 になる条件は 1 つではありません。

参考: Review the accounts whose attribute “pwdlastset” has a zero value(Microsoft Learn)
“Where an account has been created but a password has not been assigned.”
(アカウントが作成され、パスワードが割り当てられていない場合)
https://learn.microsoft.com/en-us/services-hub/microsoft-engage-center/health/remediation-steps-ad/review-accounts-whose-attribute-pwdlastset-has-a-zero-value

同資料では、この条件に加えて、パスワードを設定したうえで次回ログオン時の変更を選んだ場合と、パスワードのリセット時に変更を要求した場合の合計 3 通りが挙げられています。つまり、パスワードを設定できずに無効のまま残ったアカウントでも pwdLastSet は 0 になります。この値だけでパスワード設定と初回変更要求の完了を判断せず、実行ログの StageChangeAtLogonSet または Enabled に到達しているかと組み合わせて判断します。

New-ADUser にパスワードを渡す場合の注意

本記事では作成とパスワード設定を分けていますが、New-ADUserAccountPassword にパスワードを渡す構成を採る場合は、次の挙動を把握しておく必要があります。

参考: New-ADUser(Microsoft Learn)
“The account is created if the password fails for any reason.”
(何らかの理由でパスワードの設定に失敗した場合も、アカウントは作成されます)
https://learn.microsoft.com/en-us/powershell/module/activedirectory/new-aduser?view=windowsserver2025-ps

同リファレンスでは、この場合に Set-ADAccountPassword で設定できること、有効なパスワードが設定されない限りアカウントは有効にならないことも説明されています。つまり、コマンドがエラーを返したからといって「何も作成されていない」とは限りません。

ただし、この記述はパスワードを New-ADUser に渡した場合の説明です。作成が失敗したあらゆる場面で、存在するユーザーに対して変更処理を続けてよい根拠にはなりません。権限不足や属性の制約でオブジェクト自体が作成されなかった場合、同じログオン名で見つかったユーザーは別人である可能性があります。本記事の構成でパスワード設定を分離しているのは、この判断を挟まずに済ませるためです。

途中で失敗した行の再開

再開の話をする前に、2 つの作業を区別します。

新規登録スクリプトの再実行

まだ 1 件も作成していない状態からやり直す場合の操作です。既に作成済みのユーザーが CSV に含まれていると、登録前チェックで検出されて処理は開始しません。作成済みの行を CSV から除いてから実行します。

途中まで作成されたユーザーへの再開作業

作成は成功したが、パスワード設定や有効化で止まった行への対応です。これは新規登録スクリプトの仕事ではなく、実行ログを見ながらの個別作業として扱います。

後者の判断材料になるのが実行ログです。StatusIncomplete の行を取り出し、記録された ObjectGUID を対象に、止まった段階から手作業でやり直します。

$Server = 'dc01.example.local'

Import-Csv -Path '.\bulk-create-20260915-101500.csv' -Encoding UTF8 |
    Where-Object { $_.Status -eq 'Incomplete' } |
    Select-Object SamAccountName, Stage, ObjectGUID, Message

# 例: Stage が Created で止まった 1 件を、記録された ObjectGUID を指定してやり直す
$Guid     = '00000000-0000-0000-0000-000000000000'
$Password = Read-Host -Prompt '初期パスワード' -AsSecureString

Set-ADAccountPassword -Server $Server -Identity $Guid -Reset -NewPassword $Password -ErrorAction Stop
Set-ADUser            -Server $Server -Identity $Guid -ChangePasswordAtLogon $true -ErrorAction Stop
Enable-ADAccount      -Server $Server -Identity $Guid -ErrorAction Stop
$Password = $null

ここで whenCreated を判定の主軸にしないことが重要です。スキーマ定義では、この属性はオブジェクトの作成日時であり、値はシステムが設定するとされています。

参考: When-Created attribute(Microsoft Learn)
“The date when this object was created.”
(このオブジェクトが作成された日付)
https://learn.microsoft.com/en-us/windows/win32/adschema/a-whencreated

作成日時が作業時刻と近いことは、今回のスクリプトが作成したという証明にはなりません。同じ時間帯に別の管理者や同期ツールがユーザーを作成していれば、条件に当てはまります。whenCreated は、ログと照合したうえで時系列に矛盾がないかを見る補助情報として扱い、対象の特定は記録された ObjectGUID で行います。

記録がなく、今回の作成対象と特定できないユーザーは、自動処理の対象にしません。AD 上の状態と作成経緯を確認してから、個別に判断します。

避けたい対処

再実行を通すための対処として、次の 4 つは採らないことを推奨します。

  • 既存ユーザーを条件なしに上書きする。既存の属性やグループ所属を壊す可能性があります
  • ログオン名で見つかったという理由だけでパスワードを再設定する。別人のアカウントを変更する可能性があります
  • 登録先 OU のユーザーを一括削除してやり直す。今回の対象外のアカウントを巻き込みます
  • エラーを無条件に無視して先へ進む。どの行が未完了かが残らなくなります

確認と再開は、実行ログで状態が追える範囲にとどめ、自動で判断させる仕組みを作り込む方向へは広げないほうが、作業のたびに動作を追いやすくなります。

まとめ

AD DS のユーザー一括登録は、方法の選択と段階の分け方で作業のしやすさが変わります。パスワードを扱う新規登録では PowerShell を中心に組み立て、作成に成功したオブジェクトだけを後続の対象とし、実行ログと AD の現在の状態を突き合わせて確認する流れにすると、既存ユーザーへの影響を避けながら未完了の行を追えます。

  • csvde はパスワードを設定できず、書き出し用途が中心
  • ldifde は暗号化された接続と unicodePwd の書式が条件
  • 権限は委任で足りる場合があり、対象 OU に限定する
  • CSV には平文パスワードの列を持たせない
  • userAccountControl の 512 は有効なアカウントの既定値
  • 作成に失敗した行は後続の変更処理へ進ませない
  • 取得エラーを未作成と同じ扱いにしない
  • 再開の対象は記録した ObjectGUID で特定する

以上、最後までお読みいただきありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

関西を拠点に活動する、現役インフラエンジニア。経験20年超。

大手通信キャリアにて、中〜大規模インフラ(ネットワーク・サーバ・クラウド・セキュリティ)の設計・構築およびプロジェクトマネジメントに従事。現場で直面した技術課題への対処や、最新の脆弱性情報への実務対応を、一次情報として発信しています。

保有資格
CCIE Lifetime Emeritus(取得から20年以上)/ VCAP-DCA / Azure Solutions Architect Expert

▶ 運営者プロフィール(詳細)

目次