iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
IT Operation

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

Day 12|Server 清單不要寫死:用 CSV 管理不同角色與巡檢設定

  • 分享至 

  • xImage
  •  

Day 11 我們做了一件很重要的事情:把原本越寫越長的 PowerShell Script 拆成 Function。

現在主程式已經可以慢慢簡化成:

foreach ($Server in $Servers) {

$Result = Get-ServerHealth `
    -ComputerName $Server

$Results += $Result

}

比起把 CPU、Memory、Disk、Service 全部塞在主程式裡,已經清楚很多。

但目前還有一個地方不太理想:

$Servers = @(
"SERVER01"
"SERVER02"
"SERVER03"
)

Server 清單還是直接寫死在 .ps1 裡面。

如果今天新增:

SERVER04

就必須修改 Script。

如果今天移除 SERVER02,也要改 Script。

更麻煩的是:

不同 Server 的用途本來就不一樣,為什麼要用完全相同的巡檢規則?

所以 Day 12,我們要再往前走一步:

把「設定」跟「程式邏輯」分開。

也就是開始做:

Configuration-driven Automation
不同 Server,本來就應該檢查不同東西

例如公司有三台 Server。

DC01
WEB01
SQL01

用途完全不一樣。

DC01 是 Domain Controller,可能比較需要確認:

NTDS
DNS
Netlogon
KDC
DFSR

WEB01 是 IIS Server,可能比較在意:

W3SVC
WAS

SQL01 則可能需要:

MSSQLSERVER
SQLSERVERAGENT

如果我們直接在 Script 裡寫:

$CriticalServices = @(
"EventLog"
"W32Time"
"Winmgmt"
)

然後所有 Server 都檢查這三個 Service,

Script 雖然可以跑,但巡檢規則其實並沒有真正反映 Server 的用途。

Threshold 也不一定每台都相同

前面幾天我們暫時使用:

CPU

80% → Warning
90% → Critical

Memory:

80% → Warning
90% → Critical

拿來練習沒有問題。

但真正環境可能會遇到:

WEB01

CPU Warning = 80
CPU Critical = 90

SQL Server 則可能:

SQL01

Memory Warning = 90
Memory Critical = 95

因為 Database Server 的 Memory 使用方式跟一般 Application Server 本來就可能不同。

所以更合理的做法不是:

把所有數字寫在 Function 裡。

而是:

讓設定檔告訴程式,每台 Server 要套用什麼規則。

建立第一份 Servers.csv

今天先建立:

C:\ServerHealthCheck\Config\Servers.csv

內容可以這樣設計:

ComputerName,Role,Environment,CPUWarning,CPUCritical,MemoryWarning,MemoryCritical,CriticalServices
DC01,DomainController,Production,80,90,80,90,"NTDS;DNS;Netlogon;KDC;DFSR"
WEB01,WebServer,Production,80,90,80,90,"W3SVC;WAS"
SQL01,Database,Production,85,95,90,95,"MSSQLSERVER;SQLSERVERAGENT"
TEST01,General,Test,80,90,80,90,"EventLog;W32Time"

現在這份 CSV 已經不只是:

Server List

而是:

Server Configuration。

每個欄位代表什麼?

可以先整理成:

欄位 用途
ComputerName Server 名稱
Role Server 角色
Environment Production / Test 等環境
CPUWarning CPU Warning Threshold
CPUCritical CPU Critical Threshold
MemoryWarning Memory Warning Threshold
MemoryCritical Memory Critical Threshold
CriticalServices 必須監控的 Service

這樣以後新增 Server,就不用去動主要 Script。

只需要增加 CSV:

APP01,Application,Production,80,90,80,90,"AppService;W32Time"

程式本身不需要改。

為什麼 CriticalServices 使用分號?

這裡我們用了:

NTDS;DNS;Netlogon;KDC;DFSR

而不是:

NTDS,DNS,Netlogon,KDC,DFSR

原因很簡單。

CSV 本身已經使用:

,

分隔欄位。

所以同一格裡面如果又要放很多 Service,我會先使用:

;

當作第二層分隔符號。

之後 PowerShell 再:

-split ";"

把它拆回 Array。

這是一個很簡單的做法。

使用 Import-Csv 讀取設定

前面 Day 4 已經用過:

Import-Csv

現在直接:

$Servers = Import-Csv `
"C:\ServerHealthCheck\Config\Servers.csv"

執行:

$Servers

可能看到:

ComputerName Role Environment CPUWarning CPUCritical


DC01 DomainController Production 80 90
WEB01 WebServer Production 80 90
SQL01 Database Production 85 95
TEST01 General Test 80 90

這時候:

$Servers

已經不是單純的 String Array。

而是一組 Object。

這就跟前面學的 PowerShell Object 又接起來了。

取得其中一台 Server

例如:

$Servers[0]

可能得到:

ComputerName : DC01
Role : DomainController
Environment : Production
CPUWarning : 80
CPUCritical : 90
MemoryWarning : 80
MemoryCritical : 90
CriticalServices : NTDS;DNS;Netlogon;KDC;DFSR

所以:

$Servers[0].ComputerName

得到:

DC01

而:

$Servers[0].Role

就是:

DomainController
CSV 讀進來之後,大部分欄位都是文字

這裡有一個容易忽略的地方。

即使 CSV 裡面寫:

80
90

使用 Import-Csv 讀取後,通常還是:

String

也就是文字。

可以確認:

$Servers[0].CPUWarning.GetType()

你可能會看到:

System.String

但我們之後要拿:

80
90

做數值比較。

所以最好明確轉型。

例如:

[int]$CPUWarning =
$Server.CPUWarning

Critical:

[int]$CPUCritical =
$Server.CPUCritical

這樣:

if ($CPUUsage -ge $CPUCritical)

才比較明確。

CriticalServices 也需要轉成 Array

CSV 裡:

NTDS;DNS;Netlogon;KDC;DFSR

目前只是一個 String。

我們真正需要的是:

NTDS
DNS
Netlogon
KDC
DFSR

所以:

$CriticalServices =
$Server.CriticalServices -split ";"

如果:

$CriticalServices

就會得到:

NTDS
DNS
Netlogon
KDC
DFSR

現在就可以直接交給 Day 11 寫的:

Get-ServiceHealth

例如:

$ServiceResults =
Get-ServiceHealth `
-ServiceNames $CriticalServices
現在 foreach 不再只是 Server Name

以前我們寫:

foreach ($Server in $Servers) {

$Server

}

當時:

$Server

可能就只是:

SERVER01

但現在:

$Server

是一整筆設定。

所以主程式可以:

foreach ($Server in $Servers) {

$ComputerName =
    $Server.ComputerName

$Role =
    $Server.Role

$Environment =
    $Server.Environment

[int]$CPUWarning =
    $Server.CPUWarning

[int]$CPUCritical =
    $Server.CPUCritical

}

現在每跑一台 Server,我們同時知道:

它叫什麼?
它是什麼角色?
它在哪個環境?
CPU Threshold 是多少?
Memory Threshold 是多少?
需要監控哪些 Service?
把 Threshold 傳進 Function

Day 11 我們原本可能有:

function Get-CPUHealth {

if ($Usage -ge 90) {

    $Status = "Critical"

}
elseif ($Usage -ge 80) {

    $Status = "Warning"

}

}

這裡:

80
90

是寫死的。

今天可以改成:

function Get-CPUHealth {

param (

    [int]$WarningThreshold = 80,

    [int]$CriticalThreshold = 90

)


$Usage = [math]::Round(
    (
        Get-CimInstance Win32_Processor |
        Measure-Object `
            -Property LoadPercentage `
            -Average
    ).Average,
    2
)


if ($Usage -ge $CriticalThreshold) {

    $Status = "Critical"

}
elseif ($Usage -ge $WarningThreshold) {

    $Status = "Warning"

}
else {

    $Status = "Normal"

}


[PSCustomObject]@{

    Usage = $Usage

    Status = $Status

}

}

執行時:

$CPU = Get-CPUHealth -WarningThreshold 85
-CriticalThreshold 95

這台 Server 就會使用:

85 / 95

而不是 Function 裡固定的:

80 / 90
Memory 也是同樣做法
function Get-MemoryHealth {

param (

    [int]$WarningThreshold = 80,

    [int]$CriticalThreshold = 90

)


$OS =
    Get-CimInstance Win32_OperatingSystem


$Usage = [math]::Round(
    (
        (
            $OS.TotalVisibleMemorySize -
            $OS.FreePhysicalMemory
        ) /
        $OS.TotalVisibleMemorySize
    ) * 100,
    2
)


if ($Usage -ge $CriticalThreshold) {

    $Status = "Critical"

}
elseif ($Usage -ge $WarningThreshold) {

    $Status = "Warning"

}
else {

    $Status = "Normal"

}


[PSCustomObject]@{

    Usage = $Usage

    Status = $Status

}

}

SQL01:

Get-MemoryHealth -WarningThreshold 90
-CriticalThreshold 95

WEB01:

Get-MemoryHealth -WarningThreshold 80
-CriticalThreshold 90

同一個 Function 就可以服務不同 Server。

把 Server Configuration 傳進 Get-ServerHealth

接下來可以把 Day 11 的:

Get-ServerHealth

再改進一下。

原本:

Get-ServerHealth `
-ComputerName SERVER01

現在可以:

Get-ServerHealth -ComputerName $ComputerName
-CPUWarning $CPUWarning -CPUCritical $CPUCritical
-MemoryWarning $MemoryWarning -MemoryCritical $MemoryCritical
-CriticalServices $CriticalServices

Function Parameter:

function Get-ServerHealth {

param (

    [Parameter(Mandatory)]
    [string]$ComputerName,

    [int]$CPUWarning = 80,

    [int]$CPUCritical = 90,

    [int]$MemoryWarning = 80,

    [int]$MemoryCritical = 90,

    [string[]]$CriticalServices

)

# Health Check

}

這時 Function 就不需要知道:

DC01 的 CPU Threshold 是多少?

因為 Configuration 會告訴它。

Role 也要寫進最後報表

以前報表可能只有:

ComputerName
CPU
Memory
Status

今天開始可以加入:

Role
Environment

例如:

[PSCustomObject]@{

ComputerName = $ComputerName

Role = $Role

Environment = $Environment

CPUUsage = $CPU.Usage

CPUStatus = $CPU.Status

MemoryUsage = $Memory.Usage

MemoryStatus = $Memory.Status

}

最後:

Server Role Env CPU Memory Status
DC01 DomainController Production 22% 65% Healthy
WEB01 WebServer Production 84% 68% Warning
SQL01 Database Production 42% 92% Warning
TEST01 General Test 18% 44% Healthy

現在看到 Warning 時,還可以一起看到:

這台到底是什麼 Server。

甚至可以只巡 Production

因為現在有:

Environment

所以想只巡 Production,可以:

$Servers = Import-Csv `
"C:\ServerHealthCheck\Config\Servers.csv" |
Where-Object {
$_.Environment -eq "Production"
}

現在:

$Servers

不會包含:

TEST01

如果今天想只看 Web Server:

$Servers |
Where-Object {
$_.Role -eq "WebServer"
}

甚至:

$Servers |
Where-Object {
$.Environment -eq "Production" -and
$
.Role -eq "WebServer"
}

這就是把設定變成 Object 之後的好處。

先檢查 Configuration 有沒有問題

Configuration-driven Automation 有一個新的風險:

Script 沒問題,但 CSV 寫錯了。

例如:

WEB01,WebServer,Production,90,80,80,90,"W3SVC;WAS"

注意:

CPUWarning = 90
CPUCritical = 80

這就反了。

所以最好在執行前檢查:

if (
[int]$Server.CPUWarning -ge
[int]$Server.CPUCritical
) {

Write-Host `
    "$($Server.ComputerName) CPU threshold configuration invalid"

}

Memory 也一樣。

建立 Configuration Validation

可以寫成 Function:

function Test-ServerConfiguration {

param (

    [Parameter(Mandatory)]
    $Server

)


$Errors = @()


if (
    [string]::IsNullOrWhiteSpace(
        $Server.ComputerName
    )
) {

    $Errors += "ComputerName is empty"

}


if (
    [int]$Server.CPUWarning -ge
    [int]$Server.CPUCritical
) {

    $Errors +=
        "CPU Warning must be lower than CPU Critical"

}


if (
    [int]$Server.MemoryWarning -ge
    [int]$Server.MemoryCritical
) {

    $Errors +=
        "Memory Warning must be lower than Memory Critical"

}


[PSCustomObject]@{

    ComputerName = $Server.ComputerName

    Valid = ($Errors.Count -eq 0)

    Errors = $Errors -join "; "

}

}

使用:

foreach ($Server in $Servers) {

$Validation =
    Test-ServerConfiguration `
        -Server $Server

$Validation

}

可能得到:

ComputerName Valid Errors


DC01 True
WEB01 True
SQL01 True
TEST01 True

如果設定錯:

WEB01 False CPU Warning must be lower than CPU Critical

這樣至少不會帶著錯誤設定直接執行整批巡檢。

Configuration 錯誤最好不要直接巡檢

主程式可以寫:

foreach ($Server in $Servers) {

$Validation =
    Test-ServerConfiguration `
        -Server $Server


if (-not $Validation.Valid) {

    Write-Log `
        -Message "$($Server.ComputerName) configuration error: $($Validation.Errors)" `
        -Level "ERROR"

    continue

}


# Health Check

}

這裡:

continue

表示:

這一台不要繼續處理,直接進下一個 foreach。

所以:

DC01
→ Config OK
→ Check

WEB01
→ Config Error
→ Skip

SQL01
→ Config OK
→ Check

不會因為 WEB01 設定錯誤,把整批工作全部停掉。

今天的主程式可以變成什麼樣?

現在我們可以開始整理:

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

Configuration

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

$ConfigFile =
"C:\ServerHealthCheck\Config\Servers.csv"

$ReportFolder =
"C:\ServerHealthCheck\Reports"

$LogFolder =
"C:\ServerHealthCheck\Logs"

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

Import Server Configuration

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

$Servers = Import-Csv $ConfigFile

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

Results

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

$Results = @()

foreach ($Server in $Servers) {

$Validation =
    Test-ServerConfiguration `
        -Server $Server


if (-not $Validation.Valid) {

    Write-Log `
        -Message "$($Server.ComputerName) Config Error: $($Validation.Errors)" `
        -Level "ERROR"

    continue

}


$ComputerName =
    $Server.ComputerName

$Role =
    $Server.Role

$Environment =
    $Server.Environment


[int]$CPUWarning =
    $Server.CPUWarning

[int]$CPUCritical =
    $Server.CPUCritical


[int]$MemoryWarning =
    $Server.MemoryWarning

[int]$MemoryCritical =
    $Server.MemoryCritical


$CriticalServices =
    $Server.CriticalServices -split ";"


Write-Log `
    -Message "Checking $ComputerName"


$Health =
    Get-ServerHealth `
        -ComputerName $ComputerName `
        -CPUWarning $CPUWarning `
        -CPUCritical $CPUCritical `
        -MemoryWarning $MemoryWarning `
        -MemoryCritical $MemoryCritical `
        -CriticalServices $CriticalServices


$Result = [PSCustomObject]@{

    ComputerName =
        $ComputerName

    Role =
        $Role

    Environment =
        $Environment

    Connection =
        $Health.Connection

    CPUUsage =
        $Health.CPUUsage

    CPUStatus =
        $Health.CPUStatus

    MemoryUsage =
        $Health.MemoryUsage

    MemoryStatus =
        $Health.MemoryStatus

    OverallStatus =
        $Health.OverallStatus

    CheckTime =
        Get-Date

}


$Results += $Result

}

現在再看這段主程式,內容已經非常接近:

讀設定

驗證設定

取出參數

執行 Health Check

收集結果

這比:

幾百行全部寫死

容易維護很多。

最後輸出報表

跟前面一樣:

$Date =
Get-Date -Format "yyyyMMdd"

$ReportFile =
"$ReportFolder\Server_Health_$Date.csv"

$Results |
Export-Csv -Path $ReportFile
-NoTypeInformation `
-Encoding UTF8

最後的報表就可能是:

ComputerName Role Environment CPUUsage CPUStatus MemoryUsage MemoryStatus OverallStatus


DC01 DomainController Production 22 Normal 66.1 Normal Healthy
WEB01 WebServer Production 85 Warning 70.2 Normal Warning
SQL01 Database Production 41 Normal 92.1 Warning Warning
TEST01 General Test 15 Normal 43.8 Normal Healthy
Configuration 和程式分開之後,最大的差別是什麼?

以前:

增加 Server

修改 PowerShell Script

修改 Threshold

修改 PowerShell Script

增加 Service

修改 PowerShell Script

現在:

增加 Server

修改 Servers.csv

修改 Threshold

修改 Servers.csv

增加 Critical Service

修改 Servers.csv

PowerShell 的程式邏輯不需要一直跟著改。

這就是:

Configuration 與 Logic 分離。

不要把 Password 放進 Servers.csv

既然開始有設定檔,也要特別提醒一件事情。

不要把 CSV 做成:

ComputerName,Username,Password
SERVER01,Administrator,P@ssw0rd123

因為 CSV 本質上就是純文字。

任何可以打開檔案的人,都能看到帳密。

Server Configuration 可以保存:

Hostname
Role
Environment
Threshold
Service Name

但像:

Password
API Key
Secret
Token

應該用專門的 Secret / Credential 管理方式。

設定檔方便,不代表所有設定都適合放進去。

目前資料夾也開始有專案的樣子了

到 Day 12,我會把目錄整理成:

ServerHealthCheck/

├── Main.ps1

├── Config/
│ └── Servers.csv

├── Functions/
│ ├── Test-ServerConfiguration.ps1
│ ├── Get-ServerHealth.ps1
│ ├── Get-CPUHealth.ps1
│ ├── Get-MemoryHealth.ps1
│ ├── Get-DiskHealth.ps1
│ ├── Get-ServiceHealth.ps1
│ └── Write-Log.ps1

├── Reports/

└── Logs/

跟 Day 1 的:

幾條 PowerShell Command

相比,已經慢慢變成真正的小型維運專案。

不過 Servers.csv 還不是最終架構

目前這樣:

DC01,DomainController,...,"NTDS;DNS;Netlogon;KDC"
DC02,DomainController,...,"NTDS;DNS;Netlogon;KDC"
DC03,DomainController,...,"NTDS;DNS;Netlogon;KDC"

如果有 20 台 Domain Controller,

就代表:

NTDS;DNS;Netlogon;KDC

要重複寫 20 次。

所以更成熟的設計其實可以再拆成:

Servers.csv
+
Roles.csv

例如:

Servers.csv

只記:

ComputerName,Role,Environment
DC01,DomainController,Production
DC02,DomainController,Production
WEB01,WebServer,Production

然後:

Roles.csv

才保存:

DomainController
→ NTDS / DNS / Netlogon

WebServer
→ W3SVC / WAS

這樣會更接近真正的設定管理。

不過 Day 12 先做到一份 CSV 就好。

現在先把:

設定不要寫死在 Script。

這個觀念建立起來。

Day 12 小結

今天我們正式把:

$Servers = @(
"SERVER01"
"SERVER02"
)

改成:

$Servers =
Import-Csv "Servers.csv"

但更重要的是,我們不只是把 Server Name 搬進 CSV。

而是把:

ComputerName
Role
Environment
CPU Threshold
Memory Threshold
Critical Service

一起變成 Configuration。

整個架構也從:

PowerShell Script

├── Server Name
├── Threshold
├── Service
├── Logic
└── Report

慢慢變成:

Configuration


Servers.csv


PowerShell Logic


Functions


Health Check


Report

這代表我們開始從:

「寫一支 Script 解決一次問題」

往:

「建立一套可以調整設定、重複使用的 Automation Tool」

前進。

而且今天也多了一個很重要的觀念:

Configuration 本身也是系統的一部分,所以 Configuration 也需要被驗證。

不要假設:

CSV 能讀進來

設定一定正確

程式要能夠辨識:

Threshold 寫反
ComputerName 空白
設定缺欄位
Service List 不合理

並決定是停止、略過,還是記錄 Error。

這樣才比較像一套可以長期維護的維運工具。

Day 13 預告
Day 13|正式進入 Active Directory:用 PowerShell 查詢 User、Computer 與 OU

前 12 天,我們主要都在:

Windows Server

從 Day 13 開始,會正式進入這個系列另外一個核心:

Active Directory

我們會從:

Import-Module ActiveDirectory

以及:

Get-ADUser
Get-ADComputer
Get-ADOrganizationalUnit

開始。

先不要急著:

批次建立帳號
停用帳號
刪除帳號

Day 13 第一件事情會是:

先學會安全地「查資料」。

例如:

這個帳號 Enabled 嗎?
最後登入時間?
在哪個 OU?
有哪些 Computer Object?
一個 OU 有多少 User?

並把前面已經很熟的:

Object
Pipeline
Where-Object
Select-Object
Export-Csv

正式搬到 Active Directory。

從 Day 13 開始,這個系列會從 Windows Server Automation 正式進入 AD Automation。


上一篇
Day 11|把 PowerShell Script 拆成 Function:別讓巡檢工具變成 500 行的大檔案
下一篇
Day 13|正式進入 Active Directory:用 PowerShell 查詢 User、Computer 與 OU
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言