iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
IT Operation

系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server系列 第 20 篇

Day 20|讓 PowerShell 自己每天跑:Task Scheduler 自動排程 Server 與 AD 巡檢

  • 分享至 

  • xImage
  •  

前面 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

當然正常。
因為現在:
Current Directory

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 開頭整理成:

==========================================

Environment

==========================================

$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 外框
把今天核心概念整理:

==========================================

Scheduled Server Health Check

Day 20

==========================================

==========================================

Paths

==========================================

$ConfigFile =
Join-Path $PSScriptRoot
"Config\Servers.csv"

$ReportFolder =
Join-Path $PSScriptRoot
"Reports"

$LogFolder =
Join-Path $PSScriptRoot
"Logs"

==========================================

Create Folders

==========================================

foreach ($Folder in @(
$ReportFolder,
$LogFolder
)) {

if (-not (Test-Path $Folder)) {

    New-Item `
        -Path $Folder `
        -ItemType Directory `
        -Force |
        Out-Null
}

}

==========================================

Log

==========================================

$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

}

==========================================

Start

==========================================

$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"

}

==========================================

Exit

==========================================

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 從「有資料」提升成「人可以快速看懂結果」。


上一篇
Day 19|AD 批次停用離職帳號:從 Offboarding CSV 到安全的 Disable Workflow
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言