Day 17 我們第一次從:
Query / Audit / Report
走進:
Change Automation
也碰到了:
New-ADUser
Set-ADUser
Disable-ADAccount
Move-ADObject
Add-ADGroupMember
但昨天只處理單一 User。
真正進到企業環境,新人建立帳號往往不是:
今天建立 1 個 User
而可能是:
新人報到日
↓
20 個 User
組織擴編
↓
50 個 User
大量工讀 / 約聘
↓
100 個 User
如果每個人都:
打開 ADUC
↓
New User
↓
填姓名
↓
填帳號
↓
填 Department
↓
設定 OU
↓
加入 Group
↓
下一個
不只花時間,也很容易發生:
帳號拼錯
OU 放錯
漏掉 Group
Department 填錯
UPN 重複
同一個人建立兩次
所以 Day 18 要正式做第一套:
CSV-driven AD Onboarding Automation
架構會變成:
NewUsers.csv
↓
Import-Csv
↓
Validation
↓
帳號重複?
UPN 重複?
OU 存在?
Group 存在?
↓
Preview
↓
Create User
↓
Add Groups
↓
Verification
↓
Result Report
但今天仍然維持 Day 17 的原則:
先 Preview,再 Change。
先從 CSV 開始
假設 HR 或 IT 給我們新人資料。
建立:
C:\ADAutomation\Input\NewUsers.csv
內容:
GivenName,Surname,DisplayName,SamAccountName,UserPrincipalName,Department,Title,OU,Groups
Ming,Wang,Ming Wang,ming.wang,ming.wang@contoso.com,IT,System Engineer,"OU=Users,OU=Taipei,DC=contoso,DC=com","IT-Team;VPN-Users"
Amy,Chen,Amy Chen,amy.chen,amy.chen@contoso.com,Finance,Accountant,"OU=Users,OU=Taipei,DC=contoso,DC=com","Finance-Team;VPN-Users"
David,Lin,David Lin,david.lin,david.lin@contoso.com,Sales,Sales Specialist,"OU=Users,OU=Taipei,DC=contoso,DC=com","Sales-Team"
這份 CSV 開始扮演:
Onboarding Input
為什麼 OU 要加雙引號?
因為 Distinguished Name 本身包含:
,
例如:
OU=Users,OU=Taipei,DC=contoso,DC=com
但 CSV 本身也是用:
,
分欄位。
所以一定要:
"OU=Users,OU=Taipei,DC=contoso,DC=com"
否則 CSV 會把 OU 拆成很多欄。
Groups 為什麼使用分號?
跟 Day 12 一樣。
同一個 User 可能需要:
IT-Team
VPN-Users
FileServer-IT-RW
所以 CSV 裡使用:
"IT-Team;VPN-Users;FileServer-IT-RW"
PowerShell 再:
$Groups = $Row.Groups -split ";"
就可以變回:
IT-Team
VPN-Users
FileServer-IT-RW
密碼不要放在 CSV
不要做成:
SamAccountName,Password
ming.wang,Company123!
amy.chen,Company123!
因為:
CSV = 純文字
只要可以開啟檔案,就可以看到所有密碼。
所以今天的 Automation 我故意會先:
建立 Disabled Account。
也就是:
建立 AD User
↓
設定屬性
↓
加入 Group
↓
驗證
↓
帳號仍 Disabled
之後再透過獨立的密碼與啟用流程:
設定 Temporary Password
↓
ChangePasswordAtLogon
↓
Enable Account
這樣安全性會比把新人密碼直接塞在 CSV 好很多。
第一步:Import-Csv
$InputFile =
"C:\ADAutomation\Input\NewUsers.csv"
$Users =
Import-Csv $InputFile
查看:
$Users
可能看到:
GivenName SamAccountName Department Title
Ming ming.wang IT System Engineer
Amy amy.chen Finance Accountant
David david.lin Sales Sales Specialist
現在:
$Users
就是一組 PowerShell Object。
先不要急著 foreach + New-ADUser
很容易寫成:
foreach ($User in $Users) {
New-ADUser ...
}
但這少了最重要的一層:
Validation
在真正建立帳號以前,我至少希望確認:
必要欄位都有嗎?
SamAccountName 重複嗎?
UPN 重複嗎?
AD 裡已經存在嗎?
OU 存在嗎?
Group 存在嗎?
如果其中一項有問題,就先:
Invalid
不要建立。
先檢查 CSV 是否真的有資料
if (-not (Test-Path $InputFile)) {
Write-Error "Input file not found."
return
}
接著:
$Users = @(Import-Csv $InputFile)
if ($Users.Count -eq 0) {
Write-Error "CSV contains no users."
return
}
這是很基本、但非常值得做的防呆。
檢查必要欄位
例如我們要求:
GivenName
Surname
DisplayName
SamAccountName
UserPrincipalName
Department
Title
OU
可以:
$RequiredColumns = @(
"GivenName"
"Surname"
"DisplayName"
"SamAccountName"
"UserPrincipalName"
"Department"
"Title"
"OU"
)
取得 CSV Header:
$Columns =
$Users[0].PSObject.Properties.Name
找缺少的:
$MissingColumns =
$RequiredColumns |
Where-Object {
$_ -notin $Columns
}
如果:
$MissingColumns.Count -gt 0
就停止:
if ($MissingColumns.Count -gt 0) {
Write-Error `
"Missing columns: $($MissingColumns -join ', ')"
return
}
這比跑到第 37 個 User 才發現:
欄位名稱打錯
好很多。
CSV 裡自己也可能有重複帳號
例如:
Ming,Wang,Ming Wang,ming.wang,...
Ming,Wang,Ming Wang,ming.wang,...
可以先:
$DuplicateAccounts =
$Users |
Group-Object SamAccountName |
Where-Object {
$_.Count -gt 1
}
查看:
$DuplicateAccounts |
Select-Object Name, Count
例如:
Name Count
ming.wang 2
這種就應該:
先修 CSV,再執行。
UPN 也要檢查重複
同樣:
$DuplicateUPNs =
$Users |
Group-Object UserPrincipalName |
Where-Object {
$_.Count -gt 1
}
因為不能只保證:
SamAccountName 不重複
卻忽略:
UserPrincipalName
開始逐筆 Validation
建立:
$ValidationResults = @()
然後:
foreach ($Row in $Users) {
$Errors = @()
$SamAccountName =
$Row.SamAccountName.Trim()
$UPN =
$Row.UserPrincipalName.Trim()
$TargetOU =
$Row.OU.Trim()
# Validation...
}
這裡 $Errors 很重要。
因為同一筆資料可能同時有:
OU 不存在
Group 不存在
UPN 重複
我希望一次全部列出來,而不是每次只告訴一個 Error。
檢查必要資料是不是空白
例如:
if (
[string]::IsNullOrWhiteSpace(
$Row.SamAccountName
)
) {
$Errors +=
"SamAccountName is empty"
}
UPN:
if (
[string]::IsNullOrWhiteSpace(
$Row.UserPrincipalName
)
) {
$Errors +=
"UserPrincipalName is empty"
}
OU:
if (
[string]::IsNullOrWhiteSpace(
$Row.OU
)
) {
$Errors +=
"OU is empty"
}
檢查 SamAccountName 是否已經存在
例如:
$ExistingAccount =
Get-ADUser -Filter "SamAccountName -eq '$SamAccountName'"
-ErrorAction SilentlyContinue
如果:
$null -ne $ExistingAccount
代表:
AD 裡已經有這個 Account。
加入:
$Errors +=
"SamAccountName already exists"
這樣可以避免:
新人建立兩次
或:
CSV 重複執行
造成問題。
UPN 也要查
$ExistingUPN =
Get-ADUser -Filter "UserPrincipalName -eq '$UPN'"
-ErrorAction SilentlyContinue
如果存在:
if ($null -ne $ExistingUPN) {
$Errors +=
"UserPrincipalName already exists"
}
現在兩種 Identity 都檢查。
OU 是否存在?
例如:
try {
Get-ADOrganizationalUnit `
-Identity $TargetOU `
-ErrorAction Stop |
Out-Null
}
catch {
$Errors +=
"OU does not exist"
}
這可以防止:
OU 拼錯
OU 已改名
CSV 填錯 DistinguishedName
Groups 先拆成 Array
$Groups = @()
if (
-not [string]::IsNullOrWhiteSpace(
$Row.Groups
)
) {
$Groups =
$Row.Groups `
-split ";" |
ForEach-Object {
$_.Trim()
} |
Where-Object {
$_ -ne ""
}
}
例如:
IT-Team;VPN-Users
變成:
IT-Team
VPN-Users
每個 Group 都先確認存在
foreach ($GroupName in $Groups) {
try {
Get-ADGroup `
-Identity $GroupName `
-ErrorAction Stop |
Out-Null
}
catch {
$Errors +=
"Group not found: $GroupName"
}
}
例如 CSV:
IT-Team
VPN-User
但真正 AD 裡叫:
VPN-Users
Validation 就會抓到:
Group not found: VPN-User
不會等到建立 User 之後才發現少權限。
建立 Preview Result
Validation 完成後:
$ValidationResults +=
[PSCustomObject]@{
DisplayName =
$Row.DisplayName
SamAccountName =
$SamAccountName
UserPrincipalName =
$UPN
Department =
$Row.Department
OU =
$TargetOU
Groups =
$Groups -join ";"
Ready =
($Errors.Count -eq 0)
ValidationErrors =
$Errors -join " | "
}
結果可能:
User Account Ready Error
Ming Wang ming.wang True
Amy Chen amy.chen False VPN-Finance not found
David Lin david.lin False UPN already exists
這才是我想要的第一份:
Onboarding Preview Report
Preview 跟 Execution 分開
我不希望:
Validation 做完
↓
同一秒直接 New-ADUser
比較安全的方式是:
Step 1
NewUsers.csv
↓
Validation
↓
Preview CSV
Step 2
人工確認 / Approval
Step 3
Approved Input
↓
Execution
也就是:
Request 跟 Execution 分離。
這是 Day 17 的架構繼續延伸。
Preview 也可以跑 WhatIf
假設這筆 Validation 已通過。
可以:
New-ADUser -Name $Row.DisplayName
-GivenName $Row.GivenName -Surname $Row.Surname
-DisplayName $Row.DisplayName -SamAccountName $Row.SamAccountName
-UserPrincipalName $Row.UserPrincipalName -Department $Row.Department
-Title $Row.Title -Path $Row.OU
-Enabled $false `
-WhatIf
注意:
-Enabled $false
今天我們預設:
先建立 Disabled Account。
為什麼先 Disabled?
假設流程:
Create User
→ Success
Add IT-Team
→ Success
Add VPN
→ Failed
Add File Permission Group
→ Failed
如果 User 一建立就是 Enabled:
帳號可以登入
但權限只建了一半
這可能讓狀態更難處理。
如果:
User 先 Disabled
↓
所有設定完成
↓
Verification
↓
最後才 Activation
安全很多。
這其實是在模擬:
Staging → Activation
正式建立時使用 -PassThru
例如:
$NewUser =
New-ADUser -Name $Row.DisplayName
-GivenName $Row.GivenName -Surname $Row.Surname
-DisplayName $Row.DisplayName -SamAccountName $Row.SamAccountName
-UserPrincipalName $Row.UserPrincipalName -Department $Row.Department
-Title $Row.Title -Path $Row.OU
-Enabled $false -PassThru
-ErrorAction Stop
這裡:
-PassThru
很有用。
因為建立成功後,$NewUser 會拿到新建立的 AD User Object。
後面可以直接:
$NewUser.DistinguishedName
而不用重新猜。
接著加入 Group
foreach ($GroupName in $Groups) {
Add-ADGroupMember `
-Identity $GroupName `
-Members $NewUser `
-ErrorAction Stop
}
例如:
ming.wang
↓
IT-Team
VPN-Users
但是 Group 可能只成功一半
這就是 Day 18 很重要的問題。
假設:
100 個新人
其中第 37 個:
User 建立成功
Group A 成功
Group B 失敗
我們不能:
整支 Script 直接停止
也不能假裝:
Success
所以每個 User 都應該有自己的:
try
catch
以及自己的結果。
不需要第 37 個失敗就停掉全部
比較合理:
User01 → Success
User02 → Success
...
User37 → Partial / Failed
User38 → 繼續
User39 → 繼續
...
User100 → 繼續
最後再輸出:
誰成功?
誰失敗?
失敗在哪裡?
這比:
第 37 個爆掉
↓
後面 63 個全部沒跑
實用很多。
定義幾種 Result Status
我會先用:
Success
Partial
Failed
Skipped
例如:
Success
→ User 建立 + Groups 全部完成
Partial
→ User 建立成功,但部分 Group 失敗
Failed
→ User 本身建立失敗
Skipped
→ Validation 不通過 / User 已存在
這樣報表會比:
OK / NG
清楚很多。
Group 加入失敗,不要立刻 Delete User
可能有人會想到:
Create User 成功
↓
Group 加入失敗
↓
Remove-ADUser
讓它 Rollback。
我反而不建議第一版這樣做。
因為:
Remove-ADUser
本身又是一個破壞性操作。
更安全的做法是:
User 保持 Disabled
↓
Status = Partial
↓
記錄哪個 Group 失敗
↓
人工修正
因為帳號目前 Disabled,不會直接投入使用。
我們保留更多修復空間。
Verification:建立完一定重新 Query
例如:
$VerifyUser =
Get-ADUser -Identity $Row.SamAccountName
-Properties Department, Title, Enabled, MemberOf
-ErrorAction Stop
可以確認:
帳號真的存在?
Department 正確?
Title 正確?
仍然 Disabled?
不要只相信:
New-ADUser 沒有跳 Error
Group Membership 也確認
例如:
$CurrentGroups =
Get-ADPrincipalGroupMembership -Identity $Row.SamAccountName | Select-Object
-ExpandProperty Name
然後:
foreach ($GroupName in $Groups) {
if ($CurrentGroups -notcontains $GroupName) {
# Verification Failed
}
}
這就是:
Action
↓
Query Again
↓
Verify
今天完整 Preview Script
先做最重要的:
完全不修改 AD 的 Validation / Preview。
Import-Module ActiveDirectory
$InputFile =
"C:\ADAutomation\Input\NewUsers.csv"
$ReportFolder =
"C:\ADAutomation\Reports"
$Date =
Get-Date -Format "yyyyMMdd"
if (-not (Test-Path $InputFile)) {
Write-Error "Input file not found."
return
}
if (-not (Test-Path $ReportFolder)) {
New-Item `
-Path $ReportFolder `
-ItemType Directory |
Out-Null
}
$Users =
@(Import-Csv $InputFile)
if ($Users.Count -eq 0) {
Write-Error "No users found in CSV."
return
}
$RequiredColumns = @(
"GivenName"
"Surname"
"DisplayName"
"SamAccountName"
"UserPrincipalName"
"Department"
"Title"
"OU"
"Groups"
)
$Columns =
$Users[0].PSObject.Properties.Name
$MissingColumns =
$RequiredColumns |
Where-Object {
$_ -notin $Columns
}
if (@($MissingColumns).Count -gt 0) {
Write-Error `
"Missing columns: $($MissingColumns -join ', ')"
return
}
$DuplicateAccounts =
$Users |
Group-Object SamAccountName |
Where-Object {
$_.Count -gt 1
}
$DuplicateUPNs =
$Users |
Group-Object UserPrincipalName |
Where-Object {
$_.Count -gt 1
}
$PreviewResults = @()
foreach ($Row in $Users) {
$Errors = @()
$SamAccountName =
[string]$Row.SamAccountName
$UPN =
[string]$Row.UserPrincipalName
$TargetOU =
[string]$Row.OU
$SamAccountName =
$SamAccountName.Trim()
$UPN =
$UPN.Trim()
$TargetOU =
$TargetOU.Trim()
# --------------------------------------
# Required Values
# --------------------------------------
if (
[string]::IsNullOrWhiteSpace(
$SamAccountName
)
) {
$Errors +=
"SamAccountName is empty"
}
if (
[string]::IsNullOrWhiteSpace(
$UPN
)
) {
$Errors +=
"UserPrincipalName is empty"
}
if (
[string]::IsNullOrWhiteSpace(
$TargetOU
)
) {
$Errors +=
"OU is empty"
}
# --------------------------------------
# Duplicate in CSV
# --------------------------------------
if (
$DuplicateAccounts.Name `
-contains $SamAccountName
) {
$Errors +=
"Duplicate SamAccountName in CSV"
}
if (
$DuplicateUPNs.Name `
-contains $UPN
) {
$Errors +=
"Duplicate UPN in CSV"
}
# --------------------------------------
# Existing AD Account
# --------------------------------------
if (
-not [string]::IsNullOrWhiteSpace(
$SamAccountName
)
) {
$ExistingAccount =
Get-ADUser `
-Filter "SamAccountName -eq '$SamAccountName'" `
-ErrorAction SilentlyContinue
if ($null -ne $ExistingAccount) {
$Errors +=
"SamAccountName already exists"
}
}
# --------------------------------------
# Existing UPN
# --------------------------------------
if (
-not [string]::IsNullOrWhiteSpace(
$UPN
)
) {
$ExistingUPN =
Get-ADUser `
-Filter "UserPrincipalName -eq '$UPN'" `
-ErrorAction SilentlyContinue
if ($null -ne $ExistingUPN) {
$Errors +=
"UserPrincipalName already exists"
}
}
# --------------------------------------
# OU Validation
# --------------------------------------
if (
-not [string]::IsNullOrWhiteSpace(
$TargetOU
)
) {
try {
Get-ADOrganizationalUnit `
-Identity $TargetOU `
-ErrorAction Stop |
Out-Null
}
catch {
$Errors +=
"OU not found"
}
}
# --------------------------------------
# Group Validation
# --------------------------------------
$Groups = @()
if (
-not [string]::IsNullOrWhiteSpace(
$Row.Groups
)
) {
$Groups =
$Row.Groups `
-split ";" |
ForEach-Object {
$_.Trim()
} |
Where-Object {
$_ -ne ""
}
}
foreach ($GroupName in $Groups) {
try {
Get-ADGroup `
-Identity $GroupName `
-ErrorAction Stop |
Out-Null
}
catch {
$Errors +=
"Group not found: $GroupName"
}
}
# --------------------------------------
# Preview Result
# --------------------------------------
$PreviewResults +=
[PSCustomObject]@{
DisplayName =
$Row.DisplayName
SamAccountName =
$SamAccountName
UserPrincipalName =
$UPN
Department =
$Row.Department
Title =
$Row.Title
OU =
$TargetOU
Groups =
$Groups -join ";"
Ready =
($Errors.Count -eq 0)
ValidationErrors =
$Errors -join " | "
}
}
$PreviewFile =
"$ReportFolder\Onboarding_Preview_$Date.csv"
$PreviewResults |
Export-Csv -Path $PreviewFile
-NoTypeInformation `
-Encoding UTF8
Write-Host ""
Write-Host "===== Onboarding Preview ====="
Write-Host ""
$PreviewResults |
Format-Table DisplayName, SamAccountName, Ready, ValidationErrors
-AutoSize
Write-Host ""
Write-Host "Preview Report:"
Write-Host $PreviewFile
Write-Host ""
Write-Host "No Active Directory changes were made."
Preview 執行結果
可能:
DisplayName SamAccountName Ready ValidationErrors
Ming Wang ming.wang True
Amy Chen amy.chen False Group not found: Finance-VPN
David Lin david.lin False SamAccountName already exists
這時候:
Ming Wang
→ Ready
Amy Chen
→ 修正 Group
David Lin
→ 確認是不是重複 Request
還沒有任何 AD 變更。
這就是我們要的。
正式 Execution 的核心邏輯
經過 Review / Approval 後,真正建立 User 的核心可以長這樣:
$ExecutionResults = @()
foreach ($Row in $ApprovedUsers) {
$Groups =
$Row.Groups -split ";" |
ForEach-Object {
$_.Trim()
} |
Where-Object {
$_
}
try {
# ----------------------------------
# Create Disabled User
# ----------------------------------
$NewUser =
New-ADUser `
-Name $Row.DisplayName `
-GivenName $Row.GivenName `
-Surname $Row.Surname `
-DisplayName $Row.DisplayName `
-SamAccountName $Row.SamAccountName `
-UserPrincipalName $Row.UserPrincipalName `
-Department $Row.Department `
-Title $Row.Title `
-Path $Row.OU `
-Enabled $false `
-PassThru `
-ErrorAction Stop
$FailedGroups = @()
# ----------------------------------
# Add Groups
# ----------------------------------
foreach ($GroupName in $Groups) {
try {
Add-ADGroupMember `
-Identity $GroupName `
-Members $NewUser `
-ErrorAction Stop
}
catch {
$FailedGroups +=
"$GroupName : $($_.Exception.Message)"
}
}
# ----------------------------------
# Determine Result
# ----------------------------------
if ($FailedGroups.Count -gt 0) {
$Status = "Partial"
}
else {
$Status = "Success"
}
# ----------------------------------
# Verification
# ----------------------------------
$VerifyUser =
Get-ADUser `
-Identity $Row.SamAccountName `
-Properties `
Department,
Title,
Enabled `
-ErrorAction Stop
$ExecutionResults +=
[PSCustomObject]@{
SamAccountName =
$Row.SamAccountName
DisplayName =
$VerifyUser.Name
Created =
$true
Enabled =
$VerifyUser.Enabled
Status =
$Status
FailedGroups =
$FailedGroups -join " | "
ErrorMessage =
""
CheckTime =
Get-Date
}
}
catch {
$ExecutionResults +=
[PSCustomObject]@{
SamAccountName =
$Row.SamAccountName
DisplayName =
$Row.DisplayName
Created =
$false
Enabled =
$false
Status =
"Failed"
FailedGroups =
""
ErrorMessage =
$_.Exception.Message
CheckTime =
Get-Date
}
}
}
如果第 37 個失敗會怎樣?
假設:
User01~User36 → Success
User37
→ New-ADUser Failed
User38~User100
→ 繼續
因為每個 User 都有自己的:
try
catch
所以不會因為:
User37
而讓整批工作停止。
最後可能得到:
Success = 96
Partial = 2
Failed = 2
然後只需要針對:
Partial
Failed
進一步處理。
Partial 比 Failed 更值得分開
例如:
ming.wang
User Created = Yes
IT-Team = Success
VPN-Users = Failed
如果報表只寫:
Failed
你不知道帳號到底有沒有建立。
所以我們寫:
Status = Partial
代表:
Object 已經建立,但 Provisioning 沒全部完成。
這在真正 Automation 裡非常重要。
Partial Account 保持 Disabled
這也是為什麼今天一開始:
-Enabled $false
如果:
Group 設定只成功一半
帳號還不能直接投入使用。
我們可以:
Status = Partial
Enabled = False
等 IT 修好後才:
設定 Password
↓
Verify Groups
↓
Enable Account
這比建完就立即 Enabled 安全很多。
最後 Activation 可以獨立做
例如所有設定確認完成後:
$Password =
Read-Host "Enter temporary password"
-AsSecureString
設定密碼:
Set-ADAccountPassword -Identity "ming.wang"
-Reset `
-NewPassword $Password
要求首次登入修改:
Set-ADUser -Identity "ming.wang"
-ChangePasswordAtLogon $true
最後:
Enable-ADAccount `
-Identity "ming.wang"
再確認:
Get-ADUser -Identity "ming.wang" | Select-Object
SamAccountName,
Enabled
預期:
SamAccountName Enabled
ming.wang True
這個 Activation 流程最好有自己的核准與密碼交付規則;正式環境也不應把臨時密碼寫進 CSV 或一般 Log。
最終流程已經很像真正 Onboarding
現在整個架構變成:
HR / IT
│
▼
NewUsers.csv
│
▼
Schema Validation
│
├── Required Field
├── Duplicate Account
├── Duplicate UPN
├── OU Validation
└── Group Validation
│
▼
Preview Report
│
▼
Review / Approval
│
▼
Create Disabled User
│
▼
Set Attributes
│
▼
Add Groups
│
▼
Verification
│
├── Success
├── Partial
└── Failed
│
▼
Password / Activation
│
▼
Final Report
這已經不只是:
New-ADUser
而是:
完整的 Provisioning Workflow。
甚至可以做到 Idempotent
這裡開始接觸一個 Automation 很重要的概念:
Idempotency
簡單說:
同一份 Automation 重跑,不應該一直重複建立相同的東西。
假設:
NewUsers.csv
昨天跑過一次。
今天不小心又跑。
如果 Script 完全沒檢查:
就可能一直報錯
但我們現在會先:
Get-ADUser
確認:
SamAccountName 已存在
然後:
Skipped
而不是重新建立。
也就是:
想建立 User
↓
先看目前 State
↓
已經存在?
↓
不要重複做
這是從 Script 走向 Automation 很重要的觀念。
不要讓 Script 自己猜帳號名稱
例如:
王小明
自動猜成:
ming.wang
看起來很方便。
但可能馬上遇到:
同名
英文名稱不同
姓氏格式不同
特殊字元
既有帳號規則
例如:
ming.wang
ming.wang2
ming.wang01
所以第一版我比較喜歡:
Input CSV 明確提供 SamAccountName 與 UPN。
之後真的有完整命名規範,再把:
Account Naming
獨立成 Function。
不要把 HR 資料直接全部相信
即使 CSV 是別的部門提供的,也仍然要 Validation。
例如:
Department = ITT
可能只是:
IT
打錯。
或者:
OU=Users,OU=ShangHai,...
根本不存在。
Automation 的作用不是:
「把 Input 無條件照做。」
而應該是:
先檢查 Input 是否符合系統規則,再執行。
今天專案結構可以再往前一步
到 Day 18,我會開始整理成:
ADAutomation/
│
├── Input/
│ └── NewUsers.csv
│
├── Config/
│ └── OnboardingConfig.csv
│
├── Scripts/
│ ├── Preview-Onboarding.ps1
│ ├── Invoke-Onboarding.ps1
│ └── Enable-NewUser.ps1
│
├── Reports/
│ ├── Onboarding_Preview.csv
│ └── Onboarding_Result.csv
│
└── Logs/
這裡我特別把:
Preview-Onboarding.ps1
跟:
Invoke-Onboarding.ps1
拆開。
因為:
Preview 跟 Production Change 最好不要只差一個手滑的參數。
Day 18 小結
今天我們正式把:
New-ADUser
從:
建立一個帳號
升級成:
CSV-driven Bulk Onboarding Automation
最重要的流程是:
CSV
↓
Validate Schema
↓
Validate User Data
↓
Check Existing Account
↓
Check UPN
↓
Check OU
↓
Check Groups
↓
Preview
↓
Approval
↓
Create Disabled User
↓
Assign Groups
↓
Verify
↓
Success / Partial / Failed
↓
Activation
今天尤其要記住三件事情。
第一個:
Password 不應該放在 CSV。
第二個:
大量建立 User 時,第 37 筆失敗,不代表後面 63 筆都要跟著失敗。
每一筆都應該獨立:
處理
記錄
繼續
第三個:
先建立 Disabled Account,再完成 Provisioning,最後才 Enable。
這會比:
Create
↓
立刻 Enabled
↓
後面再慢慢設定
更容易控制未完成狀態。
到這裡,我們已經不是單純學:
New-ADUser
而是在設計一個真正比較像企業會使用的:
Identity Provisioning Workflow。
Day 19 預告
Day 19|AD 批次停用離職帳號:從 Offboarding CSV 到安全的 Disable Workflow
Day 18 解決:
新人進來
↓
Create Account
下一篇就處理另一端:
員工離職
↓
Offboarding
我們會建立:
SamAccountName,Ticket,OffboardingDate
user01,INC001001,2026-09-30
user02,INC001002,2026-09-30
然後做:
Offboarding.csv
↓
User Validation
↓
Before-State Snapshot
↓
Group Membership Backup
↓
Preview
↓
Disable Account
↓
移除指定 Sensitive Groups
↓
Move Disabled OU
↓
Update Description
↓
Verification
↓
Result Report
而且 Day 19 會特別處理:
如果帳號 Disable 成功,但 Move OU 失敗,這筆工作到底應該算 Success、Failed,還是 Partial?
也就是正式開始面對 Automation 裡很重要的:
Partial Failure 與 Change State 管理。