iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
IT Operation

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

Day 21|PowerShell HTML 報表:把冷冰冰的 CSV 變成一眼看懂的巡檢 Dashboard

  • 分享至 

  • xImage
  •  

Day 20 我們終於把 PowerShell Script 放進:
Task Scheduler

現在已經可以做到:
06:00
↓
自動執行
↓
Server Health Check
↓
AD Audit
↓
產生 CSV
↓
寫入 Log
↓
回傳 Exit Code

這已經算是真正的:
Unattended Automation

但接下來又會遇到新的問題。
假設每天早上產生:
ServerHealth_20260929.csv

Inactive_AD_Users_20260929.csv

Inactive_AD_Computers_20260929.csv

AD_Group_Summary_20260929.csv

工程師還是要:
打開 CSV
↓
找 CPU
↓
找 Memory
↓
找 Critical
↓
換下一個 CSV
↓
再找異常

如果今天只有 5 台 Server 沒問題。
但如果:
50 台
100 台
200 台

CSV 很快就變成:
有資料,但是不好閱讀。

所以 Day 21 我們要開始處理另一個問題:
如何把 Automation 的結果變成人可以快速理解的資訊?
今天主要會使用:
ConvertTo-Html

把我們前面收集的資料做成:
Daily Infrastructure Health Report

最後產生:
Daily_Health_Report_20260929.html

打開之後可以直接看到:
Daily Infrastructure Report

Summary
Total Servers : 20
Healthy : 16
Warning : 3
Critical : 1

Server Health
SERVER01 Healthy
SERVER02 Warning
SERVER03 Critical

AD Audit
Inactive Users : 15
Inactive Computers : 28
Group Issues : 3

而不是每天先打開四五份 CSV。
HTML Report 有什麼好處?
CSV 非常適合:
資料交換
Excel 分析
後續 PowerShell 處理
保存原始結果

所以 Day 21 並不是:
有 HTML 之後就不要 CSV。

而是變成:
Automation
│
├── CSV
│ ↓
│ Raw Data
│
└── HTML
↓
Human View

CSV 留給:
工程師
稽核
資料處理
後續 Script

HTML 則讓:
工程師
主管
值班人員

可以快速看懂今天的狀態。
第一個 ConvertTo-Html
先做最簡單的。
假設:
$Servers = @(
[PSCustomObject]@{
ComputerName = "SERVER01"
CPUUsage = 25
MemoryUsage = 65
OverallStatus = "Healthy"
}

[PSCustomObject]@{
    ComputerName = "SERVER02"
    CPUUsage = 85
    MemoryUsage = 73
    OverallStatus = "Warning"
}

[PSCustomObject]@{
    ComputerName = "SERVER03"
    CPUUsage = 93
    MemoryUsage = 88
    OverallStatus = "Critical"
}

)

直接:
$Servers |
ConvertTo-Html

PowerShell 會產生類似:

...

也就是:
PowerShell Object → HTML Table

輸出成 HTML 檔案
例如:
$ReportFolder =
"C:\Automation\Reports"

$Date =
Get-Date -Format "yyyyMMdd"

$HtmlFile =
"$ReportFolder\Daily_Health_Report_$Date.html"

然後:
$Servers |
ConvertTo-Html -Title "Daily Infrastructure Report" | Out-File
-FilePath $HtmlFile `
-Encoding UTF8

最後:
C:\Automation\Reports
└── Daily_Health_Report_20260929.html

直接用 Browser 打開即可。
不要把所有 Property 都塞進 HTML
跟 Day 3、Day 4 的概念一樣。
假設 $Servers 裡有:
ComputerName
Connection
CPUUsage
CPUStatus
MemoryUsage
MemoryStatus
DiskStatus
ServiceStatus
UptimeDays
EventStatus
ErrorMessage
CheckTime
PSComputerName
RunspaceId
...

直接:
$Servers |
ConvertTo-Html

報表可能超級寬。
所以先:
$ServerView =
$Servers |
Select-Object `
ComputerName,
Connection,
CPUUsage,
MemoryUsage,
DiskStatus,
ServiceStatus,
UptimeDays,
OverallStatus

再:
$ServerView |
ConvertTo-Html

也就是:
HTML Report 不是把所有資料倒出去,而是選擇人真正需要看的資訊。

使用 PreContent 加標題
例如:
$ServerView |
ConvertTo-Html -Title "Daily Infrastructure Report"
-PreContent "Daily Infrastructure Report"

打開就會看到:
Daily Infrastructure Report
然後下面才是 Server Table。
使用 PostContent
也可以加入:
$GeneratedTime =
Get-Date `
-Format "yyyy-MM-dd HH:mm:ss"

然後:
ConvertTo-Html -PostContent
"Generated: $GeneratedTime"

讓報表知道:
這份資料是什麼時候產生的?

這對每日巡檢很重要。
但原始 HTML 很醜
ConvertTo-Html 可以產生表格。
但預設外觀通常比較像:
1990 年代內部系統

功能有了,但不好閱讀。
所以接下來要加入:
CSS
這裡不需要學前端框架。
我們只需要少量 CSS,就可以讓報表好看非常多。
建立 CSS
例如:
$Css = @"

"@

這一段:
@"

"@

在 PowerShell 裡叫:
Here-String

很適合放多行:
HTML
CSS
SQL
長文字

把 CSS 放進 ConvertTo-Html
例如:
$ServerView |
ConvertTo-Html -Title "Daily Infrastructure Report"
-Head $Css -PreContent "<h1>Daily Infrastructure Report</h1>" | Out-File
$HtmlFile `
-Encoding UTF8

現在 Browser 打開,整份報表會整齊很多。
先做 Summary
真正巡檢報表第一眼不應該看到:
SERVER001
SERVER002
SERVER003
...
SERVER100

應該先看到:
今天整體狀況怎麼樣?

例如:
$TotalServers =
@($Servers).Count

Healthy:
$HealthyServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Healthy"
}
).Count

Warning:
$WarningServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Warning"
}
).Count

Critical:
$CriticalServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Critical"
}
).Count

Unknown:
$UnknownServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Unknown"
}
).Count

建立 Summary Object
$Summary = [PSCustomObject]@{

Total =
    $TotalServers

Healthy =
    $HealthyServers

Warning =
    $WarningServers

Critical =
    $CriticalServers

Unknown =
    $UnknownServers

}

顯示:
Total Healthy Warning Critical Unknown


20 16 2 1 1

這就是:
Management / Operator View

Summary 也轉成 HTML
$SummaryHtml =
$Summary |
ConvertTo-Html -Fragment
-PreContent "Summary"

注意這次使用:
-Fragment

-Fragment 是什麼?
如果不加:
-Fragment

PowerShell 會產生:

完整 HTML。
但是今天我們需要:
Summary Table
+
Server Table
+
AD Table

如果每一段都自己產生:

最後會很難組合。
所以:
-Fragment

意思就是:
只產生這一小段 HTML。

例如:

最後我們再自己組成一個完整頁面。
Server Health 也做 Fragment
$ServerHtml =
$Servers |
Select-Object ComputerName, Connection, CPUUsage, CPUStatus, MemoryUsage, MemoryStatus, DiskStatus, ServiceStatus, UptimeDays, OverallStatus | ConvertTo-Html
-Fragment `
-PreContent "Server Health"

現在:
$SummaryHtml

是一段 HTML。
$ServerHtml

也是一段 HTML。
最後可以拼在一起。
建立完整 HTML
例如:
$GeneratedTime =
Get-Date `
-Format "yyyy-MM-dd HH:mm:ss"

然後:
$Html = @"

$Css

$SummaryHtml

$ServerHtml

輸出:
$Html |
Out-File -FilePath $HtmlFile
-Encoding UTF8

這就是第一份完整的:
Daily Health Dashboard

接下來最重要的是 Status 顏色
目前:
Healthy
Warning
Critical
Unknown

全部都是一般文字。
工程師還是要自己找。
我們希望:
Healthy → 容易辨識
Warning → 醒目
Critical → 最醒目
Unknown → 需要確認

這裡 CSS 就可以幫忙。
加入 Status Class
在 CSS 加:
.status-healthy {
font-weight: bold;
}

.status-warning {
font-weight: bold;
}

.status-critical {
font-weight: bold;
}

.status-unknown {
font-weight: bold;
}

如果是實際文章 / Lab,你也可以依自己公司的 Dashboard 規範設定背景或文字色。
這裡重點不是顏色本身。
而是:
不同 Severity 應該有視覺區分。

ConvertTo-Html 不會自動幫我們判斷 Status
這是一個很重要的地方。
PowerShell 不會看到:
Critical

就自動產生:
class="status-critical"

最簡單的做法之一,是先產生 HTML,再做字串替換。
例如:
$ServerHtml =
$ServerHtml -replace
'Healthy',
'Healthy'

Warning:
$ServerHtml =
$ServerHtml -replace
'Warning',
'Warning'

Critical:
$ServerHtml =
$ServerHtml -replace
'Critical',
'Critical'

Unknown:
$ServerHtml =
$ServerHtml -replace
'Unknown',
'Unknown'

這樣 HTML 裡就會變成:

CSS 才能針對它做不同顯示。
這種 -replace 寫法很適合目前這個 Lab;如果報表未來變成複雜的正式 Web UI,就應該考慮更完整的 HTML Template,而不是一直用字串替換。
Summary 也可以做成 Cards
Table 可以用,但 Dashboard 第一眼如果能看到:
┌─────────────┐
│ Total 20 │
└─────────────┘

┌─────────────┐
│ Healthy 16 │
└─────────────┘

┌─────────────┐
│ Warning 2 │
└─────────────┘

┌─────────────┐
│ Critical 1 │
└─────────────┘

會更容易看。
我們可以直接建立 HTML:
$SummaryCards = @"

<div class="summary-card">
    <div class="summary-title">
        Total Servers
    </div>

    <div class="summary-value">
        $TotalServers
    </div>
</div>


<div class="summary-card">
    <div class="summary-title">
        Healthy
    </div>

    <div class="summary-value">
        $HealthyServers
    </div>
</div>


<div class="summary-card">
    <div class="summary-title">
        Warning
    </div>

    <div class="summary-value">
        $WarningServers
    </div>
</div>


<div class="summary-card">
    <div class="summary-title">
        Critical
    </div>

    <div class="summary-value">
        $CriticalServers
    </div>
</div>

"@

CSS:
.summary-container {
display: flex;
gap: 15px;
margin: 20px 0;
flex-wrap: wrap;
}

.summary-card {
background: white;
padding: 20px;
min-width: 140px;
border-radius: 6px;
}

.summary-title {
font-size: 14px;
}

.summary-value {
font-size: 28px;
font-weight: bold;
margin-top: 5px;
}

這時就開始真的比較像:
Dashboard

而不是單純把 CSV 搬進 Browser。
把 AD Audit 也放進同一份報表
目前前幾天已經有:
Inactive AD Users
Inactive AD Computers
AD Group Audit

如果今天 Script 直接取得:
$InactiveUsers
$InactiveComputers
$GroupIssues

可以計算:
$InactiveUserCount =
@($InactiveUsers).Count

$InactiveComputerCount =
@($InactiveComputers).Count

$GroupIssueCount =
@($GroupIssues).Count

建立:
$ADSummary = [PSCustomObject]@{

InactiveUsers =
    $InactiveUserCount

InactiveComputers =
    $InactiveComputerCount

GroupIssues =
    $GroupIssueCount

}

再:
$ADSummaryHtml =
$ADSummary |
ConvertTo-Html -Fragment
-PreContent "Active Directory Audit"

最後插入主 HTML:
Server Summary
↓
Server Health
↓
AD Audit

如果不同 Script 已經各自輸出 CSV 呢?
Day 20 我們可能已經讓不同排程各自產生:
ServerHealth_20260929.csv

Inactive_AD_Users_20260929.csv

Inactive_AD_Computers_20260929.csv

那 Day 21 不一定需要把所有 Query 重跑一次。
可以直接:
$ServerData =
Import-Csv `
".\Reports\ServerHealth_20260929.csv"

Inactive User:
$InactiveUsers =
Import-Csv `
".\Reports\Inactive_AD_Users_20260929.csv"

Inactive Computer:
$InactiveComputers =
Import-Csv `
".\Reports\Inactive_AD_Computers_20260929.csv"

然後:
Existing CSV
↓
Report Generator
↓
HTML Dashboard

這種設計其實更漂亮。
把「資料收集」跟「報表產生」分開
以前:
HealthCheck.ps1

什麼都做。
現在可以拆成:
Collect-ServerHealth.ps1
↓
ServerHealth.csv

Collect-ADAudit.ps1
↓
ADAudit.csv

New-DailyHtmlReport.ps1
↓
讀 CSV
↓
產生 HTML

這就是:
Separation of Concerns
每個 Script 有自己的主要責任。
Collector
→ 收資料

Report Generator
→ 整理資料

HTML
→ 顯示

這比:
一支 1500 行 PowerShell
什麼都做

容易維護很多。
HTML Report 不代表原始 CSV 可以刪
這點我會保留兩種格式:
Raw
│
└── CSV

Presentation
│
└── HTML

因為假設主管問:
為什麼 SERVER02 被判定 Warning?

HTML 可能只顯示:
SERVER02
CPU 87%
Warning

但 CSV 可以保留更多細節。
所以:
HTML 是 View,不是原始資料的替代品。

先把異常 Server 放前面
如果有:
100 Servers

Healthy 95
Warning 4
Critical 1

使用者不應該先看:
95 台 Healthy

然後滑到最下面才找到 Critical。
可以排序:
$ServerView =
$Servers |
Sort-Object `
@{Expression={
switch ($_.OverallStatus) {

            "Critical" { 1 }

            "Warning"  { 2 }

            "Unknown"  { 3 }

            "Healthy"  { 4 }

            default    { 5 }
        }
    }},
    ComputerName

這樣:
Critical
Warning
Unknown
Healthy

會依序排。
這對 Dashboard 很實用。
甚至只顯示異常
可以另外建立:
$ProblemServers =
$Servers |
Where-Object {
$_.OverallStatus -ne "Healthy"
}

再:
$ProblemHtml =
$ProblemServers |
Select-Object ComputerName, CPUStatus, MemoryStatus, DiskStatus, ServiceStatus, OverallStatus | ConvertTo-Html
-Fragment `
-PreContent "Servers Requiring Attention"

這樣首頁先看到:
需要注意

下面再放:
All Servers

這就很符合實際巡檢:
先看異常,再看全部。

如果沒有異常呢?
假設:
$ProblemServers.Count

是:
0

不應該產生一個空 Table。
可以:
if (
@($ProblemServers).Count -eq 0
) {

$ProblemHtml =
    "<p>No server issues detected.</p>"

}
else {

$ProblemHtml =
    $ProblemServers |
    ConvertTo-Html `
        -Fragment

}

這樣使用者看到:
No server issues detected.

比空白表格直覺很多。
Empty Report 也不能直接代表 Healthy
但是這裡要小心。
假設:
ServerHealth.csv

根本沒有產生。
結果:
$Servers.Count

變成 0。
不能直接顯示:
No server issues detected.

因為真正情況可能是:
Collector 根本沒跑成功。

所以 Dashboard 至少要分:
No Issues

跟:
No Data

這是 Monitoring / Reporting 很重要的差別。
加入 Data Status
例如:
if (
-not (Test-Path $ServerCsv)
) {

$ServerDataStatus =
    "NoData"

}
else {

$ServerDataStatus =
    "Available"

}

HTML 可以顯示:
Server Data Status : Available

或者:
Server Data Status : No Data

不能把沒有資料錯當:
Everything Healthy

這跟前面:
Unknown

的觀念完全一樣。
Report 本身也需要 Timestamp
例如:
Report Generated
2026-09-29 06:30

Server Data
2026-09-29 06:00

AD Data
2026-09-29 06:10

因為:
報表現在是 06:30 產生,不代表所有資料都是 06:30 收集。

成熟一點的 Dashboard 應該知道:
Data Freshness

也就是:
我現在看到的到底是不是最新資料?

使用 CheckTime
前面很多 Report 已經有:
CheckTime

所以可以:
$LatestServerCheck =
$Servers |
Measure-Object -Property CheckTime
-Maximum

不過要注意:
Import-Csv 之後:
CheckTime

是 String。
如果要真的做日期計算,最好轉成:
[datetime]

例如:
$ServerCheckTimes =
$Servers |
ForEach-Object {
[datetime]$_.CheckTime
}

再找最大值:
$LatestServerCheck =
$ServerCheckTimes |
Measure-Object -Maximum |
Select-Object `
-ExpandProperty Maximum

今天完整 Dashboard 範例
下面整合:
Summary
Problem Servers
All Servers
AD Audit
Generated Time

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

Daily Infrastructure HTML Report

Day 21

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

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

Paths

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

$ReportFolder =
Join-Path $PSScriptRoot
"Reports"

$Date =
Get-Date `
-Format "yyyyMMdd"

$HtmlFile =
Join-Path $ReportFolder
"Daily_Health_Report_$Date.html"

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

Example Data

正式環境可改成 Import-Csv

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

$Servers = @(
[PSCustomObject]@{
ComputerName = "SERVER01"
Connection = "Success"
CPUUsage = 25
CPUStatus = "Normal"
MemoryUsage = 65
MemoryStatus = "Normal"
DiskStatus = "Normal"
ServiceStatus = "Normal"
UptimeDays = 15
OverallStatus = "Healthy"
}

[PSCustomObject]@{
    ComputerName = "SERVER02"
    Connection = "Success"
    CPUUsage = 85
    CPUStatus = "Warning"
    MemoryUsage = 72
    MemoryStatus = "Normal"
    DiskStatus = "Normal"
    ServiceStatus = "Normal"
    UptimeDays = 32
    OverallStatus = "Warning"
}

[PSCustomObject]@{
    ComputerName = "SERVER03"
    Connection = "Success"
    CPUUsage = 93
    CPUStatus = "Critical"
    MemoryUsage = 88
    MemoryStatus = "Warning"
    DiskStatus = "Critical"
    ServiceStatus = "Normal"
    UptimeDays = 102
    OverallStatus = "Critical"
}

)

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

Summary

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

$TotalServers =
@($Servers).Count

$HealthyServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Healthy"
}
).Count

$WarningServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Warning"
}
).Count

$CriticalServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Critical"
}
).Count

$UnknownServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -eq "Unknown"
}
).Count

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

Summary Cards

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

$SummaryCards = @"

"@

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

Problem Servers

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

$ProblemServers =
@(
$Servers |
Where-Object {
$_.OverallStatus -ne "Healthy"
}
)

if ($ProblemServers.Count -eq 0) {

$ProblemHtml =
    @"

"@

}
else {

$ProblemHtml =
    $ProblemServers |
    Select-Object `
        ComputerName,
        CPUUsage,
        CPUStatus,
        MemoryUsage,
        MemoryStatus,
        DiskStatus,
        ServiceStatus,
        OverallStatus |
    ConvertTo-Html `
        -Fragment `
        -PreContent `
        "<h2>Servers Requiring Attention</h2>"

}

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

All Servers

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

$SortedServers =
$Servers |
Sort-Object `
@{
Expression = {

            switch (
                $_.OverallStatus
            ) {

                "Critical" {
                    1
                }

                "Warning" {
                    2
                }

                "Unknown" {
                    3
                }

                "Healthy" {
                    4
                }

                default {
                    5
                }
            }
        }
    },
    ComputerName

$ServerHtml =
$SortedServers |
Select-Object ComputerName, Connection, CPUUsage, CPUStatus, MemoryUsage, MemoryStatus, DiskStatus, ServiceStatus, UptimeDays, OverallStatus | ConvertTo-Html
-Fragment -PreContent
"All Servers"

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

Status Classes

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

foreach ($VariableName in @(
"ProblemHtml",
"ServerHtml"
)) {

$Value =
    Get-Variable `
        -Name $VariableName `
        -ValueOnly


$Value =
    $Value `
    -replace `
    '<td>Healthy</td>',
    '<td class="status-healthy">Healthy</td>'


$Value =
    $Value `
    -replace `
    '<td>Warning</td>',
    '<td class="status-warning">Warning</td>'


$Value =
    $Value `
    -replace `
    '<td>Critical</td>',
    '<td class="status-critical">Critical</td>'


$Value =
    $Value `
    -replace `
    '<td>Unknown</td>',
    '<td class="status-unknown">Unknown</td>'


Set-Variable `
    -Name $VariableName `
    -Value $Value

}

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

AD Summary

Example Data

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

$InactiveUserCount = 15

$InactiveComputerCount = 28

$GroupIssueCount = 3

$ADSummary =
[PSCustomObject]@{

    InactiveUsers =
        $InactiveUserCount

    InactiveComputers =
        $InactiveComputerCount

    GroupIssues =
        $GroupIssueCount
}

$ADHtml =
$ADSummary |
ConvertTo-Html -Fragment
-PreContent `
"Active Directory Audit"

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

CSS

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

$Css = @"

"@

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

Generate Final HTML

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

$GeneratedTime =
Get-Date `
-Format "yyyy-MM-dd HH:mm:ss"

$Html = @"

$Css

Generated:
$GeneratedTime

$SummaryCards

$ProblemHtml

$ServerHtml

$ADHtml

Generated automatically by
PowerShell Infrastructure Automation.

"@

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

Export

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

$Html |
Out-File -FilePath $HtmlFile
-Encoding UTF8

Write-Host ""

Write-Host `
"HTML Report Generated:"

Write-Host `
$HtmlFile

最後打開會看到什麼?
整體架構會像:
Daily Infrastructure Report
Generated: 2026-09-29 06:30

┌────────┐
│ Total │
│ 20 │
└────────┘

┌────────┐
│Healthy │
│ 16 │
└────────┘

┌────────┐
│Warning │
│ 3 │
└────────┘

┌────────┐
│Critical│
│ 1 │
└────────┘

Servers Requiring Attention

SERVER03 CPU Critical Disk Critical
SERVER05 Memory Warning
SERVER12 Connection Unknown

All Servers

SERVER03 ...
SERVER05 ...
SERVER12 ...
SERVER01 ...
SERVER02 ...

Active Directory Audit

InactiveUsers 15
InactiveComputers 28
GroupIssues 3

一打開就知道:
今天有沒有事情要處理?

Healthy 不需要跟 Critical 一樣搶眼
這也是 Dashboard 設計的一個重要概念。
假設:
100 Servers

Healthy 98
Warning 1
Critical 1

工程師真正需要注意的是:
1 Warning
1 Critical

所以報表不是:
每一個欄位都做得很華麗。

而是:
讓異常比正常更容易被看到。

這也是 Monitoring Dashboard 很重要的一個設計方向。
Report 產生失敗也要寫 Log
Day 20 已經有:
Write-Log

所以 Day 21 Report Generator 也可以:
try {

# Generate HTML

Write-Log `
    "HTML report generated: $HtmlFile"

}
catch {

Write-Log `
    -Message "HTML report failed: $($_.Exception.Message)" `
    -Level "ERROR"

exit 1

}

不要因為:
Health Check 成功

就假設:
HTML Report 一定成功

Collector 與 Reporter 是兩個不同的工作。
Task Scheduler 也可以串起來
現在可以有:
Task 1
06:00
Collect Server Health

Task 2
06:10
Collect AD Audit

Task 3
06:20
Generate HTML Report

但這裡會出現另一個問題。
如果:
Task 1
06:00 開始

結果今天跑到 06:25

可是:
Task 3
06:20

已經開始產 Report。
那 Report 可能讀到:
舊資料或不完整資料。

這就是後面會開始面對的:
Job Dependency

問題。
一個簡單的方式:Master Script
也可以做:
DailyAutomation.ps1

裡面:
Collect Server Health
↓
成功 / Partial
↓
Collect AD Audit
↓
成功 / Partial
↓
Generate HTML
↓
Exit Code

也就是:
& "$PSScriptRoot\Collect-ServerHealth.ps1"

& "$PSScriptRoot\Collect-ADAudit.ps1"

& "$PSScriptRoot\New-DailyHtmlReport.ps1"

不過這裡還要處理:
子 Script Exit Code
一個 Job 失敗要不要繼續?
Report 可以接受 Partial Data 嗎?

這就開始有一點:
Orchestration

的味道了。
PowerShell HTML Report 不是 Monitoring System
這裡也要把邊界分清楚。
我們今天做:
每天 06:00
↓
產一份 HTML

適合:
Daily Check
Management Report
Audit
Morning Review

但如果需求是:
CPU 超過 95%
10 秒內通知

就不適合依賴:
每天一份 HTML

那是:
Real-time Monitoring

應該交給:
Nagios
Zabbix
PRTG
SCOM
Prometheus

這些監控系統。
所以我們現在做的是:
Daily Operational Reporting

不是要重新發明完整 Monitoring Platform。
Day 21 小結
今天我們第一次把前面收集的:
Server Health
AD User Audit
AD Computer Audit
AD Group Audit

從:
CSV

轉成:
HTML Dashboard

主要使用:
ConvertTo-Html

-Fragment

-PreContent

-Head

Out-File

並加入:
CSS
Summary
Status
Problem Servers
AD Summary
Generated Time

讓整套流程從:
有資料

開始往:
資料容易被理解

前進。
目前我們的 Automation 已經變成:
Windows Server / AD
↓
PowerShell Collector
↓
CSV Raw Data
↓
PowerShell Report Generator
↓
HTML Dashboard
↓
Engineer Review

今天最重要的幾個觀念是:
CSV 是 Raw Data,HTML 是 Presentation。

兩個都可以保留。
另外:
No Data 不等於 Healthy。

如果 Collector 沒跑成功,Dashboard 應該顯示:
Unknown
No Data
Collection Failed

而不是:
Everything OK

最後則是:
Dashboard 的目的不是把所有資料塞進去,而是讓真正需要處理的異常更容易被看到。

Day 22 預告
Day 22|PowerShell 自動通知:巡檢出現 Warning / Critical 時主動發送 Email
現在我們已經可以做到:
06:00
↓
自動巡檢

06:20
↓
產生 HTML Dashboard

但還是有一個問題:
如果工程師今天根本忘記打開 HTML 呢?

所以 Day 22 會開始做:
Health Check
↓
判斷 Status
↓
全部 Healthy
│
└── 只產 Report

有 Warning / Critical
│
└── 主動通知

會開始碰到:
Send-MailMessage

但也會特別說明為什麼它在現代環境裡不一定是最理想的長期方案,以及實務上可以怎麼接:
SMTP Relay
Microsoft 365
Teams Webhook / Workflow
其他 Notification Channel

最後我們希望做到:
SERVER03
CPU = Critical

SERVER05
Disk = Warning

↓
PowerShell 自動整理摘要
↓
附上 / 連結 HTML Report
↓
通知維運人員

Day 22 會讓整套工具從「自己會執行、自己會做報表」,再往前一步變成「發現異常後會主動叫人來處理」。


上一篇
Day 20|讓 PowerShell 自己每天跑:Task Scheduler 自動排程 Server 與 AD 巡檢
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言