前面 19 天,我們已經慢慢做出不少可以實際拿來使用的 PowerShell Automation。
目前包含:
Server Health Check
│
├── CPU
├── Memory
├── Disk
├── Service
├── Uptime
└── Event Log
Active Directory
│
├── User Inventory
├── Inactive User Audit
├── Inactive Computer Audit
├── Group Membership Audit
├── Onboarding
└── Offboarding
但目前還有一個問題:
每次都要有人打開 PowerShell,再手動執行 Script。
例如每天早上:
C:\Automation\ServerHealthCheck.ps1
如果還是要工程師記得:
06:00 登入 Server
↓
開 PowerShell
↓
執行 Script
↓
等結果
↓
看 CSV
這其實只是:
半自動化。
真正希望做到的是:
每天 06:00
↓
Windows Task Scheduler
↓
PowerShell 自動啟動
↓
執行 Health Check
↓
產生 CSV
↓
寫入 Log
↓
回傳 Exit Code
就算工程師還在睡覺,巡檢也會自己跑。
所以 Day 20 正式進入:
Windows Task Scheduler
為什麼先學 Task Scheduler?
現在很多 Automation 會想到:
Jenkins
GitHub Actions
Azure Automation
Ansible
Rundeck
CI/CD Pipeline
這些當然都可以。
但如果今天環境主要是:
Windows Server
Active Directory
PowerShell
內部網路
其實 Windows 本身就已經有很好用的:
Task Scheduler
而且很多企業內部的小型 Automation:
每天產報表
每天檢查 AD
每天備份
每小時巡檢
每天清 Log
Task Scheduler 就已經足夠。
今天我們先排程什麼?
假設 Day 10~12 已經有:
C:\Automation\ServerHealthCheck.ps1
這支 Script 執行之後:
Server List
↓
PowerShell Remoting
↓
CPU
Memory
Disk
Service
Uptime
↓
CSV Report
現在希望:
每天 06:00
自動執行。
第一步:Script 本身要能「無人操作」
這是一個非常重要的前提。
排程 Script 裡不要有:
Read-Host
例如:
$Password =
Read-Host "Enter Password"
因為 Task Scheduler 在背景執行時:
沒有人可以輸入。
同樣也不要依賴:
請按 Enter
請選 Y / N
跳出視窗
人工確認
排程 Automation 應該:
Input 已經準備好
↓
執行
↓
自己處理結果
↓
自己記 Log
↓
結束
Change Script 不一定適合直接排程
這裡也要先做區分。
像:
Server Health Check
Inactive User Report
Inactive Computer Report
Group Audit
這種:
Read Only
的工作非常適合直接排程。
但像:
Disable User
Offboarding
Remove Group Membership
這些會修改 AD 的 Script,
我不建議只是:
每天 06:00 自動掃 CSV
↓
看到資料就直接 Disable
除非企業已經有完整:
Approval
Change Window
Input Validation
Audit Log
Recovery Process
今天 Task Scheduler 的主要實作會以:
Read-Only Health Check / Audit
為主。
第二步:不要依賴目前的 Working Directory
這是 Task Scheduler 最常踩到的坑之一。
假設 Script 裡:
Import-Csv ".\Config\Servers.csv"
你在 PowerShell 裡:
cd C:\Automation
.\ServerHealthCheck.ps1
C:\Automation
但 Task Scheduler 啟動時,目前路徑不一定是:
C:\Automation
有時候你會發現 Script 突然說:
Servers.csv not found
使用 $PSScriptRoot
PowerShell 有一個很好用的變數:
$PSScriptRoot
它代表:
目前這支 .ps1 所在的資料夾。
假設:
C:\Automation\ServerHealthCheck.ps1
裡面:
$PSScriptRoot
就是:
C:\Automation
所以可以:
$ConfigFile =
Join-Path $PSScriptRoot
"Config\Servers.csv"
Report:
$ReportFolder =
Join-Path $PSScriptRoot
"Reports"
Log:
$LogFolder =
Join-Path $PSScriptRoot
"Logs"
這樣不管 Task Scheduler 從哪裡啟動:
Script 都知道自己的 Config 在哪裡。
我會把目錄整理成這樣
C:\Automation
│
├── ServerHealthCheck.ps1
│
├── Config
│ └── Servers.csv
│
├── Reports
│
└── Logs\
然後 Script:
$ConfigFile =
Join-Path $PSScriptRoot "Config\Servers.csv"
$ReportFolder =
Join-Path $PSScriptRoot "Reports"
$LogFolder =
Join-Path $PSScriptRoot "Logs"
這比在 Script 到處寫:
C:\Automation...
更容易移動與維護。
第三步:一定要有 Log
如果人工執行:
.\ServerHealthCheck.ps1
出錯時,我們會看到:
紅色 Error
但 Task Scheduler 凌晨 06:00 執行時:
沒有人坐在螢幕前看。
所以:
Write-Host "Error"
對排程 Automation 的幫助非常有限。
更重要的是:
寫 Log 檔
建立 Write-Log
沿用 Day 6:
$LogFolder =
Join-Path $PSScriptRoot "Logs"
if (-not (Test-Path $LogFolder)) {
New-Item `
-Path $LogFolder `
-ItemType Directory |
Out-Null
}
$Date =
Get-Date -Format "yyyyMMdd"
$LogFile =
Join-Path $LogFolder
"HealthCheck_$Date.log"
Function:
function Write-Log {
param (
[string]$Message,
[ValidateSet(
"INFO",
"WARNING",
"ERROR"
)]
[string]$Level = "INFO"
)
$Time =
Get-Date `
-Format "yyyy-MM-dd HH:mm:ss"
$LogMessage =
"$Time [$Level] $Message"
Add-Content `
-Path $LogFile `
-Value $LogMessage `
-Encoding UTF8
}
開始:
Write-Log "Health Check started."
成功:
Write-Log "Health Check completed."
錯誤:
Write-Log -Message $_.Exception.Message
-Level "ERROR"
Log 最後可能長這樣
2026-09-28 06:00:00 [INFO] Health Check started.
2026-09-28 06:00:01 [INFO] Checking SERVER01.
2026-09-28 06:00:03 [INFO] SERVER01 completed.
2026-09-28 06:00:03 [INFO] Checking SERVER02.
2026-09-28 06:00:05 [ERROR] SERVER02 WinRM connection failed.
2026-09-28 06:00:05 [INFO] Checking SERVER03.
2026-09-28 06:00:08 [INFO] SERVER03 completed.
2026-09-28 06:00:09 [INFO] Health Check completed.
隔天工程師可以直接知道:
凌晨到底發生什麼事。
第四步:讓 Script 有 Exit Code
這個觀念到了排程非常重要。
一般程式執行完會回傳一個:
Exit Code
最基本可以設計:
0
→ Success
1
→ Failed
甚至:
0 = Success
1 = Fatal Error
2 = Partial Success
PowerShell 可以直接 exit
例如:
exit 0
代表:
成功
失敗:
exit 1
Partial:
exit 2
為什麼 Exit Code 很重要?
因為 Task Scheduler 需要知道:
Script 到底成功還是失敗?
如果 Script 裡:
Write-Host "ERROR"
然後正常跑到最後,
Task Scheduler 可能仍然只看到:
Process completed
甚至某些 PowerShell Error 並不會自動讓整個 process 回傳你預期的非零 Exit Code。
所以我們最好自己明確定義。
建立 Script Result
例如:
$HasFatalError = $false
$HasPartialError = $false
執行過程:
try {
# Critical Operation
}
catch {
$HasFatalError = $true
Write-Log `
-Message $_.Exception.Message `
-Level "ERROR"
}
如果只是某台 Server 失敗:
$HasPartialError = $true
最後:
if ($HasFatalError) {
Write-Log `
"Health Check failed." `
"ERROR"
exit 1
}
elseif ($HasPartialError) {
Write-Log `
"Health Check completed with errors." `
"WARNING"
exit 2
}
else {
Write-Log `
"Health Check completed successfully."
exit 0
}
現在 Script 有三種結果
0
Success
所有主要工作完成
1
Failed
整個 Script 無法正常完成
2
Partial
Script 有完成,但部分 Server / Query 失敗
這跟 Day 19 的:
Success
Partial
Failed
概念其實完全一樣。
只是現在變成:
Process-level Status。
不要因為一個 Server 掛掉就 exit 1
假設:
SERVER01 → Success
SERVER02 → WinRM Failed
SERVER03 → Success
如果 SERVER02 一失敗就:
exit 1
那 SERVER03 根本不會跑。
比較合理:
SERVER02
→ Result = Failed
繼續 SERVER03
最後
→ Exit Code 2
代表:
整體工作有完成,但存在需要 Review 的項目。
這正是前幾天一直建立 Partial Failure 概念的原因。
Script 最外層也要有 try/catch
例如:
try {
Write-Log "Script started."
# ==========================
# Import Config
# ==========================
$Servers =
Import-Csv `
$ConfigFile `
-ErrorAction Stop
# ==========================
# Health Check
# ==========================
foreach ($Server in $Servers) {
# Check...
}
# ==========================
# Export
# ==========================
$Results |
Export-Csv `
-Path $ReportFile `
-NoTypeInformation `
-Encoding UTF8 `
-ErrorAction Stop
}
catch {
Write-Log `
-Message "Fatal error: $($_.Exception.Message)" `
-Level "ERROR"
exit 1
}
像:
Config File 不存在
CSV 完全讀不到
Report Folder 無法寫入
這種比較接近:
Fatal Error。
現在開始設定 Task Scheduler
可以從 Windows:
Start
↓
Task Scheduler
開啟:
Task Scheduler
我通常不太喜歡:
Create Basic Task
正式設定比較習慣:
Create Task
因為可以控制的選項比較完整。
General 設定
例如 Task Name:
Daily Server Health Check
Description:
Daily PowerShell Windows Server health check
接著需要考慮:
Run only when user is logged on
還是:
Run whether user is logged on or not
真正背景 Automation 通常希望:
沒有人登入也會執行。
所以會使用:
Run whether user is logged on or not
但這就會牽涉:
執行帳號
密碼
權限
後面會談。
Run with highest privileges 要不要勾?
很多教學會直接說:
Run with highest privileges
✓
但我的想法還是一樣:
需要才開。
如果 Script 只是在:
讀 AD
讀 Event Log
輸出 CSV
不一定每個情境都需要最高權限。
如果操作確實需要 Administrative Privilege,再設定。
原則還是:
Least Privilege
不要因為方便就全部跑 Administrator。
Trigger
例如:
Daily
06:00
概念就是:
每天
↓
06:00
↓
啟動 PowerShell Script
如果是健康巡檢:
每天 06:00
很合理。
如果是:
每 5 分鐘
就要重新考慮:
Task Scheduler 是不是最適合這種高頻率 Monitoring?
因為真正即時監控通常更適合:
Monitoring System
例如:
Zabbix
PRTG
Nagios
SCOM
Prometheus
Task Scheduler 更適合:
定時 Job。
Action 最重要
選:
Start a program
Program/script:
powershell.exe
Arguments 可以:
-NoProfile -NonInteractive -File "C:\Automation\ServerHealthCheck.ps1"
完整概念:
powershell.exe
↓
-NoProfile
↓
-NonInteractive
↓
-File
↓
ServerHealthCheck.ps1
-NoProfile
表示:
不載入使用者 PowerShell Profile。
為什麼?
因為如果你的 Script 依賴:
我自己的 $PROFILE
例如 Profile 裡剛好有:
Import-Module Something
換一個執行帳號,排程就可能失敗。
Automation Script 最好自己明確:
Import-Module ActiveDirectory
不要依賴某個工程師自己的 PowerShell Profile。
-NonInteractive
表示:
這次 PowerShell 不預期與使用者互動。
這也可以提醒我們:
排程 Script 不應該等待使用者輸入。
Execution Policy 不要直接無腦 Bypass
網路上很多 Task Scheduler 範例會:
-ExecutionPolicy Bypass
然後說:
不然 Script 跑不起來。
我不建議一開始就用這種方式繞過設定。
比較好的方式是先確認:
Get-ExecutionPolicy -List
了解公司目前:
Group Policy
Local Policy
CurrentUser
LocalMachine
到底怎麼設定。
企業環境如果要求:
RemoteSigned
AllSigned
就應該依公司的 PowerShell 執行政策處理:
簽署 Script
適當放置檔案
設定允許的 Policy
而不是排程全部:
Bypass
不要用 PowerShell ISE 跑排程
Program 應該:
powershell.exe
如果後面環境使用 PowerShell 7:
pwsh.exe
例如:
C:\Program Files\PowerShell\7\pwsh.exe
但這裡要注意:
Windows PowerShell 5.1 與 PowerShell 7 的 Module Compatibility 並不是完全一樣。
尤其:
ActiveDirectory Module
Windows-specific Module
Legacy Script
在轉 PowerShell 7 前應該先測試。
目前這個系列的 Windows Server / AD 實作,使用 Windows PowerShell 5.1 就可以。
Task Scheduler 的「Start in」很有用
GUI 裡通常還可以設定:
Start in (optional)
可以填:
C:\Automation
但即使設定了,我仍然建議 Script 裡使用:
$PSScriptRoot
因為:
Script 自己知道路徑,比依賴外部 Scheduler 設定更穩定。
執行帳號是 Task Scheduler 最重要的問題之一
你手動執行:
.\HealthCheck.ps1
是使用:
你的帳號
但 Task Scheduler 可能用:
SYSTEM
Service Account
另一個 Domain Account
這代表:
權限
網路存取
AD 存取
檔案存取
Remoting
可能完全不同。
所以最常見的情況就是:
我手動跑明明成功,為什麼 Task Scheduler 失敗?
答案常常不是 Script。
而是:
Execution Context 不同。
先確認排程到底用誰執行
在 Script 裡可以加入:
$CurrentIdentity =
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Write-Log `
"Running as: $CurrentIdentity"
Log 就會看到:
2026-09-28 06:00:00 [INFO] Running as: CONTOSO\svc-automation
這非常適合 Troubleshooting。
Service Account
企業環境裡可能建立專門:
CONTOSO\svc-automation
來跑 Automation。
好處是:
不依賴某個員工帳號
權限可以單獨控制
Audit 比較清楚
不要用:
Ted 的個人 AD Account
長期跑公司的自動化。
因為哪天:
密碼改了
帳號停用
員工離職
權限調整
所有 Task 都可能一起掛掉。
成熟的環境還可以評估:
gMSA
Group Managed Service Account
用來降低傳統 Service Account 密碼管理的負擔。
不過這部分後面再獨立展開。
SYSTEM 帳號不是萬能
很多人會想:
那全部用 SYSTEM 就好了。
SYSTEM 在本機權限很高。
但問題是:
Local SYSTEM
跟:
Domain User
完全不是同一個 Identity。
如果 Script 要:
存取 File Server
PowerShell Remoting
SQL
其他 Server
網路 Share
權限行為可能不同。
所以:
本機權限高,不代表網路資源權限一定適合。
不要依賴 Network Drive Letter
手動登入後你可能有:
Z:
指向:
\FILESERVER01\Reports
所以 Script:
Export-Csv Z:\Health.csv
手動跑成功。
Task Scheduler 卻說:
Drive Z: not found
這很正常。
因為:
Network Drive Mapping 通常跟使用者 Session 有關。
排程帳號可能根本沒有:
Z:
使用 UNC Path
比起:
Z:\Reports
建議:
\FILESERVER01\Reports
例如:
$ReportFolder =
"\FILESERVER01\ITReports\HealthCheck"
但是使用 UNC 之後,
排程使用的帳號必須真的有:
Share Permission
+
NTFS Permission
這才是完整的權限。
先不要把 Credential 寫進 Script
不要:
$Password = "Password123"
然後存在:
C:\Automation\HealthCheck.ps1
也不要把 Service Account Password 放在:
Arguments
CSV
TXT
BAT
裡。
Task Scheduler 本身可以在建立排程時依 Windows 的機制處理執行帳號憑證;更成熟的網域環境則可以評估 gMSA 或其他受管身分方案。
Conditions 要注意
Task Scheduler 有:
Conditions
例如:
Start the task only if the computer is on AC power
如果是在:
Laptop
這可能讓排程因為沒接電源而不跑。
Server 上通常比較少碰到。
另外有:
Start only if network connection is available
如果你的 Script 需要:
AD
Remote Server
File Share
網路確實很重要。
但實際要不要設定,還是看環境。
Settings 裡我會特別看幾個東西
例如:
Allow task to be run on demand
非常適合測試。
以及:
If the task fails, restart every...
可以依需求設定重試。
另外:
If the task is already running
這個也很重要。
如果上一個 Task 還沒跑完怎麼辦?
假設:
Health Check
每天 06:00 執行
正常:
06:00
↓
06:10 結束
但某天卡住:
06:00
↓
一直跑
↓
隔天 06:00
第二個 Instance 要不要再啟動?
Task Scheduler 可以設定:
Do not start a new instance
對大部分巡檢 Script,我比較傾向這種策略。
因為我不希望:
HealthCheck 1
HealthCheck 2
HealthCheck 3
同時查同一批 Server。
也要限制最大執行時間
例如正常 Health Check:
10 分鐘完成
結果有一天:
跑 8 小時
代表很可能卡住了。
Task Scheduler 可以設定:
Stop the task if it runs longer than
例如:
1 hour
具體時間依環境調整。
先手動 Run Task
Task 建好後不要等明天 06:00。
直接:
Task Scheduler
↓
選 Task
↓
Run
然後確認:
Task 有沒有啟動?
Script 有沒有 Log?
CSV 有沒有出現?
執行帳號正確嗎?
Last Run Result
Task Scheduler 會有:
Last Run Result
正常可能看到:
0x0
通常代表工作正常完成。
但這裡要注意:
Task Scheduler 顯示的 Last Run Result,最終跟啟動的 Process 怎麼結束有關。
這也是為什麼我們前面自己設計:
exit 0
exit 1
exit 2
這樣排程才有比較清楚的結果。
不要只相信 Last Run Result
即使看到:
0x0
我仍然會確認:
Log
Report
Output
因為可能出現:
PowerShell Process 有正常結束
↓
但實際某些 Server Query Failed
所以:
Task Result
與:
Business Result
最好分開。
例如:
Task Scheduler
Process completed
Health Check Report
2 Servers Failed
這兩件事情不衝突。
用 PowerShell 建立 Scheduled Task
除了 GUI,也可以:
New-ScheduledTaskAction
New-ScheduledTaskTrigger
Register-ScheduledTask
先建立 Action:
$Action =
New-ScheduledTaskAction -Execute "powershell.exe"
-Argument '-NoProfile -NonInteractive -File "C:\Automation\ServerHealthCheck.ps1"' `
-WorkingDirectory "C:\Automation"
Trigger:
$Trigger =
New-ScheduledTaskTrigger -Daily
-At "06:00"
Settings:
$Settings =
New-ScheduledTaskSettingsSet `
-StartWhenAvailable
然後可以建立 Task 定義:
$Task =
New-ScheduledTask -Action $Action
-Trigger $Trigger -Settings $Settings
-Description "Daily Windows Server Health Check"
正式註冊時,還需要決定:
用哪個 Identity 執行?
使用哪種 Logon Type?
需要什麼權限?
這部分不要把密碼硬寫進 .ps1。
如果企業已有 Automation Service Account,應依既有身分與密碼管理流程建立。
查看 Task
Get-ScheduledTask `
-TaskName "Daily Server Health Check"
Task 執行資訊:
Get-ScheduledTaskInfo `
-TaskName "Daily Server Health Check"
可能看到:
LastRunTime
LastTaskResult
NextRunTime
NumberOfMissedRuns
這些欄位之後甚至可以拿來:
監控 Automation 自己有沒有正常工作。
手動啟動 Task
PowerShell:
Start-ScheduledTask `
-TaskName "Daily Server Health Check"
接著:
Get-ScheduledTaskInfo `
-TaskName "Daily Server Health Check"
不用每次開 GUI。
「手動成功、排程失敗」排查思路
這是今天最重要的 Troubleshooting 情境。
假設:
手動:
.\ServerHealthCheck.ps1
→ Success
Task Scheduler:
→ Failed
我會從這個方向查:
執行帳號
↓
工作目錄
↓
Script Path
↓
PowerShell Version
↓
Module
↓
File Permission
↓
Network Permission
↓
AD Permission
↓
WinRM Permission
↓
Execution Policy
↓
Log
↓
Exit Code
這時候不要第一個想法就是:
Script 寫錯了。
因為同一支 Script 在不同 Execution Context,結果真的可能不同。
檢查 PowerShell 版本
Log 可以記:
Write-Log `
"PowerShell Version: $($PSVersionTable.PSVersion)"
例如:
PowerShell Version: 5.1.20348...
這可以避免你以為 Task 跑:
PowerShell 7
實際卻是:
Windows PowerShell 5.1
或反過來。
記錄 Hostname
Write-Log `
"Running on: $env:COMPUTERNAME"
Identity:
$Identity =
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Write-Log `
"Running as: $Identity"
Script Path:
Write-Log `
"Script Root: $PSScriptRoot"
這三個資訊對排程 Troubleshooting 非常有用。
建立一個適合 Scheduled Task 的 Script Header
我會把 Script 開頭整理成:
$ScriptRoot =
$PSScriptRoot
$LogFolder =
Join-Path $ScriptRoot
"Logs"
$ReportFolder =
Join-Path $ScriptRoot
"Reports"
if (-not (Test-Path $LogFolder)) {
New-Item `
-Path $LogFolder `
-ItemType Directory |
Out-Null
}
if (-not (Test-Path $ReportFolder)) {
New-Item `
-Path $ReportFolder `
-ItemType Directory |
Out-Null
}
$Date =
Get-Date `
-Format "yyyyMMdd"
$LogFile =
Join-Path $LogFolder
"Automation_$Date.log"
接著:
function Write-Log {
param (
[string]$Message,
[string]$Level = "INFO"
)
$Time =
Get-Date `
-Format "yyyy-MM-dd HH:mm:ss"
Add-Content `
-Path $LogFile `
-Value "$Time [$Level] $Message" `
-Encoding UTF8
}
一開始先把執行環境寫進 Log
Write-Log "================================"
Write-Log "Automation started."
Write-Log `
"Computer: $env:COMPUTERNAME"
Write-Log `
"PowerShell: $($PSVersionTable.PSVersion)"
Write-Log `
"ScriptRoot: $PSScriptRoot"
$Identity =
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Write-Log `
"Identity: $Identity"
如果 Task 掛掉:
Automation.log
第一段就能知道:
在哪台機器?
誰執行?
哪個 PowerShell?
Script 從哪裡執行?
今天完整 Scheduled Health Check 外框
把今天核心概念整理:
$ConfigFile =
Join-Path $PSScriptRoot
"Config\Servers.csv"
$ReportFolder =
Join-Path $PSScriptRoot
"Reports"
$LogFolder =
Join-Path $PSScriptRoot
"Logs"
foreach ($Folder in @(
$ReportFolder,
$LogFolder
)) {
if (-not (Test-Path $Folder)) {
New-Item `
-Path $Folder `
-ItemType Directory `
-Force |
Out-Null
}
}
$Date =
Get-Date `
-Format "yyyyMMdd"
$LogFile =
Join-Path $LogFolder
"HealthCheck_$Date.log"
function Write-Log {
param (
[string]$Message,
[ValidateSet(
"INFO",
"WARNING",
"ERROR"
)]
[string]$Level = "INFO"
)
$Time =
Get-Date `
-Format "yyyy-MM-dd HH:mm:ss"
Add-Content `
-Path $LogFile `
-Value "$Time [$Level] $Message" `
-Encoding UTF8
}
$HasFatalError =
$false
$HasPartialError =
$false
try {
Write-Log "Health Check started."
Write-Log `
"Computer: $env:COMPUTERNAME"
$Identity =
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Write-Log `
"Identity: $Identity"
Write-Log `
"PowerShell: $($PSVersionTable.PSVersion)"
# ======================================
# Import Configuration
# ======================================
if (-not (Test-Path $ConfigFile)) {
throw "Config file not found: $ConfigFile"
}
$Servers =
@(Import-Csv `
$ConfigFile `
-ErrorAction Stop)
Write-Log `
"Loaded $($Servers.Count) servers."
# ======================================
# Health Check
# ======================================
$Results = @()
foreach ($Server in $Servers) {
Write-Log `
"Checking $($Server.ComputerName)"
try {
$Result =
Get-ServerHealth `
-ComputerName `
$Server.ComputerName
$Results +=
$Result
if (
$Result.Connection -eq
"Failed"
) {
$HasPartialError =
$true
Write-Log `
-Message "$($Server.ComputerName) check failed." `
-Level "WARNING"
}
else {
Write-Log `
"$($Server.ComputerName) completed."
}
}
catch {
$HasPartialError =
$true
Write-Log `
-Message "$($Server.ComputerName): $($_.Exception.Message)" `
-Level "ERROR"
}
}
# ======================================
# Export Report
# ======================================
$ReportFile =
Join-Path `
$ReportFolder `
"ServerHealth_$Date.csv"
$Results |
Export-Csv `
-Path $ReportFile `
-NoTypeInformation `
-Encoding UTF8 `
-ErrorAction Stop
Write-Log `
"Report created: $ReportFile"
}
catch {
$HasFatalError =
$true
Write-Log `
-Message "Fatal Error: $($_.Exception.Message)" `
-Level "ERROR"
}
if ($HasFatalError) {
Write-Log `
-Message "Automation failed." `
-Level "ERROR"
exit 1
}
if ($HasPartialError) {
Write-Log `
-Message "Automation completed with warnings." `
-Level "WARNING"
exit 2
}
Write-Log `
"Automation completed successfully."
exit 0
Get-ServerHealth 可以使用 Day 11 已經拆好的 Function;如果 Function 存在另一個 .ps1 或 Module,排程腳本也要明確載入它,不要依賴個人 PowerShell Profile。
執行流程現在變成
06:00
│
▼
Task Scheduler
│
▼
powershell.exe
│
▼
ServerHealthCheck.ps1
│
├── Environment Check
│
├── Load Config
│
├── Health Check
│
├── Report
│
└── Log
│
▼
Exit Code
│
├── 0 Success
├── 1 Failed
└── 2 Partial
這時才真的開始有:
Unattended Automation
的感覺。
Day 20 小結
今天我們沒有新增:
CPU
Memory
Disk
AD User
這些新的檢查項目。
而是把前面做好的 Script 真正放進:
Production-like Automation Flow
最大的變化是:
以前:
工程師
↓
開 PowerShell
↓
執行 Script
現在:
Task Scheduler
↓
PowerShell
↓
自動執行
↓
Log
↓
Report
↓
Exit Code
今天最重要的觀念可以整理成:
Scheduled Script
不能依賴人工輸入
路徑
不要依賴 Current Directory
使用
$PSScriptRoot
背景執行
一定要留下 Log
Script
最好明確回傳 Exit Code
手動執行帳號
不一定等於 Task Scheduler 帳號
Mapped Drive
不要假設背景 Session 看得到
Read-Only Audit
很適合排程
High-Risk Change
不要沒有 Approval 就直接排程
另外今天第一次把:
Success
Partial
Failed
從:
AD Workflow
一路延伸到:
整支 PowerShell Process
甚至可以對應:
exit 0
exit 2
exit 1
這樣 Task Scheduler 不只是:
「有啟動 Script。」
而開始知道:
這次 Automation 最後到底是什麼狀態。
到了 Day 20,我們的工具已經從:
PowerShell Command
走過:
Script
↓
Function
↓
Configuration
↓
Remoting
↓
AD Automation
↓
Workflow
↓
Scheduled Job
開始真正接近一套日常可以使用的維運 Automation。
Day 21 預告
Day 21|PowerShell HTML 報表:把冷冰冰的 CSV 變成一眼看懂的巡檢 Dashboard
目前每天 06:00 已經可以自動產生:
ServerHealth_20260928.csv
Inactive_AD_Users_20260928.csv
AD_Group_Summary_20260928.csv
但是 CSV 最大的問題是:
工程師還是要打開檔案,一列一列找異常。
所以 Day 21 我們會使用:
ConvertTo-Html
把資料做成:
Daily Infrastructure Report
│
├── Summary
│ ├── Total Servers
│ ├── Healthy
│ ├── Warning
│ └── Critical
│
├── Server Health
│ ├── CPU
│ ├── Memory
│ ├── Disk
│ └── Uptime
│
└── AD Audit
├── Inactive Users
├── Inactive Computers
└── Group Issues
最後直接輸出:
Daily_Health_Report_20260929.html
並開始讓:
Critical
Warning
Healthy
在 HTML 裡有更明確的視覺區分。
Day 21 會開始把我們的 Automation 從「有資料」提升成「人可以快速看懂結果」。