PowerShell 技能连载 - DSC v3 配置即代码

适用于 PowerShell 7.0 及以上版本

在基础设施即代码(IaC)的实践中,配置管理一直是最核心也最容易出问题的环节。传统的 DSC v2 依赖本地配置管理器(LCM)和 MOF 文档,虽然功能强大但架构笨重,调试困难,且难以与现代 GitOps 流水线无缝对接。微软推出的 DSC v3 彻底重新设计了架构,将配置引擎与资源提供者解耦,采用 JSON 作为配置文档格式,并原生支持跨平台运行。

DSC v3 的设计哲学是”配置即代码”(Configuration as Code)。配置文档以 JSON 格式存储,天然适合纳入 Git 版本控制;资源提供者可以基于任何语言开发(PowerShell、Python、Go 均可),通过标准化的 JSON Schema 接口与 DSC 引擎通信。这种松耦合架构让 DSC v3 能够轻松融入 CI/CD 流水线,与 Azure Machine Configuration、Ansible、Terraform 等工具协同工作。

本文将从实战角度出发,演示如何用 DSC v3 编写 JSON 配置文档、开发自定义 PowerShell 资源,以及构建配置漂移检测机制,帮助你建立可靠的配置即代码工作流。

DSC v3 配置文档编写

DSC v3 使用 JSON 格式的配置文档来声明系统的期望状态。配置文档包含资源实例的列表,每个实例通过 type 指定资源类型,通过 properties 定义期望的配置值。下面的代码展示如何编写一份完整的 DSC v3 配置文档,并利用资源发现功能验证配置的合法性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 定义 DSC v3 配置文档(JSON 格式)
$configDocument = @{
'$schema' = 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json'
contentVersion = '1.0.0'
resources = @(
@{
type = 'Microsoft.Windows/Registry'
name = 'EnableLongPaths'
properties = @{
keyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem'
valueName = 'LongPathsEnabled'
valueData = @{
dword = 1
}
ensure = 'Present'
}
}
@{
type = 'Microsoft.Windows/Feature'
name = 'InstallSSH'
properties = @{
name = 'OpenSSH.Server'
ensure = 'Present'
includeAllSubFeature = $false
}
}
@{
type = 'Microsoft/Process'
name = 'EnsureSSHService'
properties = @{
path = '/usr/sbin/sshd'
running = $true
}
}
)
}

# 将配置保存为 JSON 文件
$configPath = Join-Path $env:TEMP 'dsc-v3-server-config.json'
$configDocument | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath -Encoding UTF8

Write-Host "配置文档已保存至: $configPath"

# 列出当前可用的 DSC 资源类型
Write-Host "`n--- 已注册的 DSC 资源 ---"
dsc resource list 2>$null | ForEach-Object {
$r = $_ | ConvertFrom-Json
Write-Host (" {0,-40} {1}" -f $r.type, $r.version)
}

# 验证配置文档的合法性
Write-Host "`n--- 验证配置文档 ---"
$validation = dsc config validate -p $configPath 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "配置文档验证通过" -ForegroundColor Green
$validation | ConvertFrom-Json | ForEach-Object {
Write-Host (" 资源: {0} -> 状态: {1}" -f $_.name, '有效')
}
} else {
Write-Host "配置文档验证失败:" -ForegroundColor Red
Write-Host $validation
}

# 导出配置文档的完整 JSON Schema(用于 IDE 智能提示)
Write-Host "`n--- 配置文档概览 ---"
$schemaInfo = @{
配置路径 = $configPath
资源数量 = $configDocument.resources.Count
资源清单 = $configDocument.resources | ForEach-Object { "$($_.type)[$($_.name)]" }
版本 = $configDocument.contentVersion
}
$schemaInfo | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
配置文档已保存至: /tmp/dsc-v3-server-config.json

--- 已注册的 DSC 资源 ---
Microsoft.Windows/Registry 0.1.0
Microsoft.Windows/Feature 0.2.0
Microsoft/Process 0.1.0
Microsoft/OSInfo 0.1.0

--- 验证配置文档 ---
配置文档验证通过
资源: EnableLongPaths -> 状态: 有效
资源: InstallSSH -> 状态: 有效
资源: EnsureSSHService -> 状态: 有效

--- 配置文档概览 ---

配置路径 资源数量 资源清单 版本
-------- -------- -------- ----
/tmp/dsc-v3-server-config.json 3 {Microsoft.Windows/Registry[EnableLongPaths]...} 1.0.0

自定义 DSC 资源开发

DSC v3 的资源提供者采用适配器模式,每个资源需要实现 getsettestdelete 四个操作。对于 PowerShell 用户来说,最自然的方式是使用 Class-based 资源。下面的代码演示如何创建一个管理本地用户配置文件的自定义 DSC 资源,包括资源定义、导出和注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# 定义自定义 DSC 资源的清单文件(manifest)
$resourceManifest = @{
'$schema' = 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/resource/manifest.json'
type = 'Contoso.UserProfile'
version = '1.0.0'
description = '管理用户环境配置文件'
get = @{
executable = 'pwsh'
args = @(
'-NoLogo', '-NonInteractive', '-NoProfile', '-Command'
"Import-Module 'Contoso.DscResources'; Get-TargetResource"
)
}
set = @{
executable = 'pwsh'
args = @(
'-NoLogo', '-NonInteractive', '-NoProfile', '-Command'
"Import-Module 'Contoso.DscResources'; Set-TargetResource"
)
preTest = $true
}
test = @{
executable = 'pwsh'
args = @(
'-NoLogo', '-NonInteractive', '-NoProfile', '-Command'
"Import-Module 'Contoso.DscResources'; Test-TargetResource"
)
}
schema = @{
embedded = @{
type = 'object'
properties = @{
UserName = @{ type = 'string'; description = '用户名' }
HomeDirectory = @{ type = 'string'; description = '主目录路径' }
Shell = @{ type = 'string'; description = '默认 Shell' }
Ensure = @{
type = 'string'
enum = @('Present', 'Absent')
description = '确保状态'
}
}
required = @('UserName')
}
}
}

# 保存资源清单
$manifestPath = Join-Path $env:TEMP 'Contoso.UserProfile.dsc.resource.json'
$resourceManifest | ConvertTo-Json -Depth 10 | Set-Content -Path $manifestPath -Encoding UTF8

# 编写 PowerShell 脚本资源实现
$scriptResource = @'
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$UserName,

[string]$HomeDirectory,
[string]$Shell = '/bin/zsh',
[ValidateSet('Present', 'Absent')]
[string]$Ensure = 'Present'
)

begin {
function Write-DscOutput {
param([hashtable]$Data)
$Data | ConvertTo-Json -Depth 5 -Compress
}
}

process {
# 获取当前状态的实际值
$actualUser = Get-LocalUser -Name $UserName -ErrorAction SilentlyContinue
$actualHome = $actualUser ? (Get-ADUserResultantHomePath $UserName) : $null

# 构建输出对象(符合 DSC v3 JSON Schema)
$result = @{
actualState = @{
UserName = $UserName
HomeDirectory = if ($actualHome) { $actualHome } else { '未设置' }
Shell = $Shell
Ensure = if ($actualUser) { 'Present' } else { 'Absent' }
InDesiredState = ($null -ne $actualUser -and $Ensure -eq 'Present')
}
}

Write-DscOutput -Data $result
}
'@

$resourceScript = Join-Path $env:TEMP 'Contoso.DscResources.psm1'
$scriptResource | Set-Content -Path $resourceScript -Encoding UTF8

# 注册自定义资源到 DSC v3
Write-Host "自定义资源清单已保存至: $manifestPath"
Write-Host "资源实现脚本已保存至: $resourceScript"

# 在配置文档中引用自定义资源
$customConfig = @{
'$schema' = 'https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json'
contentVersion = '1.0.0'
resources = @(
@{
type = 'Contoso.UserProfile'
name = 'DevOperator'
properties = @{
UserName = 'devops'
HomeDirectory = '/home/devops'
Shell = '/bin/zsh'
Ensure = 'Present'
}
}
)
}

$customConfigPath = Join-Path $env:TEMP 'dsc-v3-custom-config.json'
$customConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $customConfigPath -Encoding UTF8

Write-Host "`n自定义资源配置文档已保存至: $customConfigPath"
Write-Host "资源类型: Contoso.UserProfile v1.0.0"
Write-Host "包含属性: UserName, HomeDirectory, Shell, Ensure"

执行结果示例:

1
2
3
4
5
6
自定义资源清单已保存至: /tmp/Contoso.UserProfile.dsc.resource.json
资源实现脚本已保存至: /tmp/Contoso.DscResources.psm1

自定义资源配置文档已保存至: /tmp/dsc-v3-custom-config.json
资源类型: Contoso.UserProfile v1.0.0
包含属性: UserName, HomeDirectory, Shell, Ensure

配置测试与偏差修正

DSC v3 的核心工作模式是 Get-Test-Set 循环。get 操作获取当前实际状态,test 操作比较实际状态与期望状态,set 操作将系统收敛到期望状态。下面的代码演示如何构建一个完整的配置漂移检测和修正流程,并生成可读的漂移报告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# 定义漂移检测与修正函数
function Invoke-DscDriftDetection {
param(
[Parameter(Mandatory)]
[string]$ConfigPath,

[switch]$AutoRemediate,

[switch]$DetailedReport
)

$report = [System.Collections.Generic.List[PSObject]]::new()
$timestamp = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK'

# 步骤 1: 获取当前状态(dsc config get)
Write-Host "[$timestamp] 正在获取当前配置状态..." -ForegroundColor Cyan
$getResult = dsc config get -p $ConfigPath 2>&1
$currentState = $getResult | ConvertFrom-Json

# 步骤 2: 测试配置漂移(dsc config test)
Write-Host "[$timestamp] 正在检测配置漂移..." -ForegroundColor Cyan
$testResult = dsc config test -p $ConfigPath 2>&1
$testState = $testResult | ConvertFrom-Json

$driftCount = 0
$inSpecCount = 0

foreach ($resource in $testState.results) {
$entry = [ordered]@{
Timestamp = $timestamp
ResourceType = $resource.type
ResourceName = $resource.name
InDesiredState = $resource.inDesiredState
DriftDetails = $null
}

if (-not $resource.inDesiredState) {
$driftCount++
# 提取漂移的具体属性差异
$diffs = @()
if ($resource.differences) {
foreach ($diff in $diff) {
$diffs += "{0}: 期望='{1}', 实际='{2}'" -f @(
$diff.property
$diff.expected
$diff.actual
)
}
} else {
$diffs = @('状态不匹配')
}
$entry.DriftDetails = $diffs -join '; '
Write-Host (" [漂移] {0}[{1}]: {2}" -f @(
$resource.type, $resource.name, ($diffs -join ', ')
)) -ForegroundColor Yellow
} else {
$inSpecCount++
if ($DetailedReport) {
Write-Host (" [合规] {0}[{1}]" -f @(
$resource.type, $resource.name
)) -ForegroundColor Green
}
}

$report.Add([PSCustomObject]$entry)
}

# 步骤 3: 输出汇总报告
Write-Host "`n========== 配置漂移报告 ==========" -ForegroundColor White
Write-Host ("检测时间: {0}" -f $timestamp)
Write-Host ("配置文件: {0}" -f $ConfigPath)
Write-Host ("资源总数: {0}" -f ($driftCount + $inSpecCount))
Write-Host ("合规数量: {0}" -f $inSpecCount) -ForegroundColor Green
Write-Host ("漂移数量: {0}" -f $driftCount) -ForegroundColor $(if ($driftCount -gt 0) { 'Red' } else { 'Green' })

# 步骤 4: 自动修正(如果启用)
if ($driftCount -gt 0 -and $AutoRemediate) {
Write-Host "`n[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] 正在执行自动修正..." -ForegroundColor Cyan
$setResult = dsc config set -p $ConfigPath 2>&1
$setState = $setResult | ConvertFrom-Json

$remediated = 0
foreach ($resource in $setState.results) {
if ($resource.rebootRequired) {
Write-Host (" [注意] {0}[{1}] 需要重启" -f @(
$resource.type, $resource.name
)) -ForegroundColor Magenta
}
$remediated++
}

Write-Host ("修正完成: {0} 个资源已收敛" -f $remediated) -ForegroundColor Green
} elseif ($driftCount -gt 0) {
Write-Host "`n提示: 使用 -AutoRemediate 参数可自动修正漂移" -ForegroundColor DarkGray
}

Write-Host "=================================="

# 返回结构化报告
return $report
}

# 执行漂移检测(仅检测,不修正)
$serverConfig = Join-Path $env:TEMP 'dsc-v3-server-config.json'
$driftReport = Invoke-DscDriftDetection -ConfigPath $serverConfig -DetailedReport

# 将漂移报告导出为 JSON(供 CI/CD 流水线消费)
$reportPath = Join-Path $env:TEMP "drift-report-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
$driftReport | ConvertTo-Json -Depth 5 | Set-Content -Path $reportPath -Encoding UTF8
Write-Host "`n漂移报告已导出至: $reportPath"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[2026-01-06T10:30:15+08:00] 正在获取当前配置状态...
[2026-01-06T10:30:17+08:00] 正在检测配置漂移...
[漂移] Microsoft.Windows/Registry[EnableLongPaths]: 状态不匹配
[合规] Microsoft.Windows/Feature[InstallSSH]
[合规] Microsoft/Process[EnsureSSHService]

========== 配置漂移报告 ==========
检测时间: 2026-01-06T10:30:17+08:00
配置文件: /tmp/dsc-v3-server-config.json
资源总数: 3
合规数量: 2
漂移数量: 1
提示: 使用 -AutoRemediate 参数可自动修正漂移
==================================

漂移报告已导出至: /tmp/drift-report-20260106-103017.json

注意事项

  1. DSC v3 需要独立安装:DSC v3 是独立的原生可执行文件(dsc),不随 PowerShell 自带。你需要从 PowerShell DSC 的 GitHub Releases 页面单独下载安装,或通过 winget install Microsoft.DSC 获取。

  2. JSON Schema 验证很重要:编写配置文档时务必引用官方 JSON Schema。大多数现代编辑器(VS Code、JetBrains)能根据 $schema 字段提供智能提示和实时校验,大幅减少语法错误。在应用到生产环境前,始终先运行 dsc config validate

  3. 自定义资源的幂等性:自定义资源必须确保 set 操作的幂等性——即多次执行结果一致。在 test 操作中精确比较期望状态与实际状态,避免产生不必要的修正操作。对于复杂属性,建议逐字段对比而非整体序列化比较。

  4. 配置漂移报告纳入 CI/CD:将漂移检测集成到 CI/CD 流水线中,每次配置变更都自动触发漂移检测。-AutoRemediate 参数在生产环境使用时应格外谨慎,建议先以只读模式运行检测,人工确认漂移报告后再执行修正。

  5. 与 Azure Machine Configuration 配合:DSC v3 是 Azure Machine Configuration( formerly Azure Policy Guest Configuration)的原生引擎。如果你的环境在 Azure 中,可以通过 Azure Policy 将 DSC v3 配置分配给虚拟机,实现大规模的配置合规性审计和自动修正。

  6. 版本管理配置文档:配置文档应纳入 Git 版本控制,通过 Pull Request 审核配置变更。建议在仓库中设置 dsc config validate 作为 pre-commit hook,确保每次提交的配置文档都是合法的。同时保留历史配置版本,便于回滚和变更追溯。

PowerShell 技能连载 - 配置即代码

适用于 PowerShell 7.0 及以上版本

在 DevOps 和基础设施自动化的浪潮中,”配置即代码”(Configuration as Code,CaC)已经成为一种核心实践。传统的服务器配置往往依赖运维人员手动登录、逐台修改,这种方式不仅效率低下,而且容易出现人为错误,更无法保证环境之间的一致性。当服务器规模从几台增长到几十台、上百台时,手动配置的方式就完全不可行了。

配置即代码的核心理念是将系统的期望状态用声明式的代码描述出来,然后通过工具自动将系统收敛到这个状态。PowerShell 作为 Windows 生态的首选自动化工具,天然具备实现配置即代码的能力。从 PowerShell Desired State Configuration(DSC)到自定义的配置管理框架,我们可以灵活选择适合团队规模的方案。

本文将演示如何用 PowerShell 构建一套轻量级的配置即代码框架,包括定义配置文件、编写配置测试、实现幂等的配置应用,以及生成配置漂移报告。

定义 JSON 配置清单

首先,我们需要一种结构化的方式来描述系统的期望状态。JSON 是一种通用且易于阅读的格式,非常适合充当配置清单的角色。下面的代码定义了一个配置清单结构,并创建一份示例配置文件,涵盖 Windows 功能、注册表项、文件路径和服务状态等常见配置项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 定义配置清单的目录和文件路径
$configRoot = "$env:USERPROFILE\Documents\ConfigAsCode"
$manifestPath = Join-Path $configRoot "server-manifest.json"

# 确保配置目录存在
if (-not (Test-Path $configRoot)) {
New-Item -Path $configRoot -ItemType Directory -Force | Out-Null
Write-Host "已创建配置目录:$configRoot" -ForegroundColor Green
}

# 定义期望状态的配置清单
$manifest = [PSCustomObject]@{
Metadata = [PSCustomObject]@{
Name = "WebServer-Production"
Version = "1.2.0"
Author = "DevOps Team"
LastUpdated = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
Description = "生产环境 Web 服务器标准配置"
}
WindowsFeatures = @(
[PSCustomObject]@{ Name = "Web-Server"; Ensure = "Present" }
[PSCustomObject]@{ Name = "Web-Mgmt-Tools"; Ensure = "Present" }
[PSCustomObject]@{ Name = "Telnet-Client"; Ensure = "Absent" }
)
RegistrySettings = @(
[PSCustomObject]@{
Key = "HKLM:\SOFTWARE\MyApp"
Name = "LogLevel"
Value = 2
Type = "DWord"
}
[PSCustomObject]@{
Key = "HKLM:\SOFTWARE\MyApp"
Name = "MaxConnections"
Value = 100
Type = "DWord"
}
)
Files = @(
[PSCustomObject]@{
Path = "C:\MyApp\config\appsettings.json"
Content = '{"Logging":{"Level":"Information"}}'
Ensure = "Present"
}
)
Services = @(
[PSCustomObject]@{ Name = "W3SVC"; State = "Running"; StartType = "Automatic" }
[PSCustomObject]@{ Name = "Spooler"; State = "Stopped"; StartType = "Disabled" }
)
}

# 将配置清单写入 JSON 文件
$manifest | ConvertTo-Json -Depth 10 | Set-Content -Path $manifestPath -Encoding UTF8
Write-Host "配置清单已保存至:$manifestPath" -ForegroundColor Cyan

# 验证文件内容
$config = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json
Write-Host "`n配置名称:$($config.Metadata.Name)"
Write-Host "配置版本:$($config.Metadata.Version)"
Write-Host "Windows 功能项:$($config.WindowsFeatures.Count) 个"
Write-Host "注册表设置:$($config.RegistrySettings.Count) 项"
Write-Host "文件配置:$($config.Files.Count) 个"
Write-Host "服务配置:$($config.Services.Count) 个"
1
2
3
4
5
6
7
8
9
已创建配置目录:C:\Users\admin\Documents\ConfigAsCode
配置清单已保存至:C:\Users\admin\Documents\ConfigAsCode\server-manifest.json

配置名称:WebServer-Production
配置版本:1.2.0
Windows 功能项:3 个
注册表设置:2 项
文件配置:1 个
服务配置:2 个

编写配置测试与漂移检测

配置即代码的核心价值在于”可验证”。我们需要一套测试机制,定期对比系统的实际状态与期望状态,发现配置漂移(Configuration Drift)。下面的代码实现了一个漂移检测函数,它会逐项检查配置清单中的每一类资源,输出合规状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# 配置漂移检测函数
function Test-ConfigurationDrift {
param(
[Parameter(Mandatory)]
[string]$ManifestPath,

[string[]]$Categories = @("All")
)

$manifest = Get-Content -Path $ManifestPath -Raw | ConvertFrom-Json
$drifts = [System.Collections.Generic.List[PSCustomObject]]::new()

Write-Host "`n========== 配置漂移检测 ==========" -ForegroundColor Cyan
Write-Host "目标配置:$($manifest.Metadata.Name) v$($manifest.Metadata.Version)"
Write-Host "检测时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "==================================`n"

# 检查注册表设置
if ($Categories -contains "All" -or $Categories -contains "Registry") {
Write-Host "--- 注册表设置 ---" -ForegroundColor Yellow
foreach ($reg in $manifest.RegistrySettings) {
$actualValue = $null
$compliant = $false

if (Test-Path $reg.Key) {
$item = Get-ItemProperty -Path $reg.Key -Name $reg.Name -ErrorAction SilentlyContinue
if ($null -ne $item) {
$actualValue = $item.($reg.Name)
$compliant = ($actualValue -eq $reg.Value)
}
}

$drifts.Add([PSCustomObject]@{
Category = "Registry"
Resource = "$($reg.Key)\$($reg.Name)"
Expected = $reg.Value
Actual = $actualValue
Compliant = $compliant
})

$status = if ($compliant) { "[OK]" } else { "[DRIFT]" }
$color = if ($compliant) { "Green" } else { "Red" }
Write-Host " $status $($reg.Name):期望=$($reg.Value),实际=$actualValue" -ForegroundColor $color
}
}

# 检查文件配置
if ($Categories -contains "All" -or $Categories -contains "Files") {
Write-Host "`n--- 文件配置 ---" -ForegroundColor Yellow
foreach ($file in $manifest.Files) {
$exists = Test-Path $file.Path
$compliant = $false
$actualContent = $null

if ($exists -and $file.Ensure -eq "Present") {
$actualContent = Get-Content -Path $file.Path -Raw
$compliant = ($actualContent.Trim() -eq $file.Content.Trim())
}
elseif (-not $exists -and $file.Ensure -eq "Absent") {
$compliant = $true
}

$drifts.Add([PSCustomObject]@{
Category = "Files"
Resource = $file.Path
Expected = $file.Ensure
Actual = if ($exists) { "Present" } else { "Absent" }
Compliant = $compliant
})

$status = if ($compliant) { "[OK]" } else { "[DRIFT]" }
$color = if ($compliant) { "Green" } else { "Red" }
Write-Host " $status $($file.Path):$($file.Ensure)" -ForegroundColor $color
}
}

# 检查服务状态
if ($Categories -contains "All" -or $Categories -contains "Services") {
Write-Host "`n--- 服务配置 ---" -ForegroundColor Yellow
foreach ($svc in $manifest.Services) {
$service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue
$compliant = $false

if ($null -ne $service) {
$stateOk = ($service.Status.ToString() -eq $svc.State)
$startTypeOk = ($service.StartType.ToString() -eq $svc.StartType)
$compliant = ($stateOk -and $startTypeOk)
}

$drifts.Add([PSCustomObject]@{
Category = "Services"
Resource = $svc.Name
Expected = "$($svc.State) / $($svc.StartType)"
Actual = if ($service) { "$($service.Status) / $($service.StartType)" } else { "NotFound" }
Compliant = $compliant
})

$status = if ($compliant) { "[OK]" } else { "[DRIFT]" }
$color = if ($compliant) { "Green" } else { "Red" }
Write-Host " $status $($svc.Name):期望=$($svc.State)/$($svc.StartType)" -ForegroundColor $color
}
}

# 汇总报告
$total = $drifts.Count
$compliantCount = ($drifts | Where-Object { $_.Compliant }).Count
$driftCount = $total - $compliantCount
$complianceRate = if ($total -gt 0) { [math]::Round($compliantCount / $total * 100, 1) } else { 0 }

Write-Host "`n========== 漂移汇总 ==========" -ForegroundColor Cyan
Write-Host " 总检测项:$total"
Write-Host " 合规项: $compliantCount" -ForegroundColor Green
Write-Host " 漂移项: $driftCount" -ForegroundColor Red
Write-Host " 合规率: $complianceRate%"
Write-Host "================================`n"

return $drifts
}

# 执行漂移检测
$driftResults = Test-ConfigurationDrift -ManifestPath $manifestPath
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
========== 配置漂移检测 ==========
目标配置:WebServer-Production v1.2.0
检测时间:2025-09-30 14:30:00
==================================

--- 注册表设置 ---
[DRIFT] LogLevel:期望=2,实际=
[DRIFT] MaxConnections:期望=100,实际=

--- 文件配置 ---
[DRIFT] C:\MyApp\config\appsettings.json:Present

--- 服务配置 ---
[OK] W3SVC:期望=Running/Automatic
[DRIFT] Spooler:期望=Stopped/Disabled

========== 漂移汇总 ==========
总检测项:5
合规项: 1
漂移项: 4
合规率: 20.0%
================================

实现幂等的配置应用

检测到漂移后,下一步是自动修复。配置应用函数必须是幂等的(Idempotent),即多次执行的结果与一次执行相同。下面的代码会根据漂移检测结果,逐项将系统收敛到期望状态,并记录每一步的操作日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 幂等配置应用函数
function Invoke-ConfigurationApply {
param(
[Parameter(Mandatory)]
[string]$ManifestPath,

[switch]$WhatIf
)

$manifest = Get-Content -Path $ManifestPath -Raw | ConvertFrom-Json
$logEntries = [System.Collections.Generic.List[PSCustomObject]]::new()

Write-Host "`n========== 配置应用 ==========" -ForegroundColor Cyan
Write-Host "目标配置:$($manifest.Metadata.Name) v$($manifest.Metadata.Version)"
if ($WhatIf) {
Write-Host "模式:WhatIf(仅预览,不执行变更)" -ForegroundColor Yellow
}
Write-Host "开始时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "==============================`n"

# 应用注册表设置
foreach ($reg in $manifest.RegistrySettings) {
$action = "Set"
if (-not (Test-Path $reg.Key)) {
if (-not $WhatIf) {
New-Item -Path $reg.Key -Force | Out-Null
}
$action = "Create+Set"
}

if (-not $WhatIf) {
Set-ItemProperty -Path $reg.Key -Name $reg.Name -Value $reg.Value -Type $reg.Type -Force
}

$logEntries.Add([PSCustomObject]@{
Time = Get-Date -Format "HH:mm:ss"
Action = $action
Target = "$($reg.Key)\$($reg.Name)"
Value = $reg.Value
Status = "Applied"
})

Write-Host " [$action] $($reg.Key)\$($reg.Name) = $($reg.Value)" -ForegroundColor Green
}

# 应用文件配置
foreach ($file in $manifest.Files) {
$parentDir = Split-Path $file.Path -Parent

if ($file.Ensure -eq "Present") {
if (-not (Test-Path $parentDir)) {
if (-not $WhatIf) {
New-Item -Path $parentDir -ItemType Directory -Force | Out-Null
}
}

if (-not $WhatIf) {
Set-Content -Path $file.Path -Value $file.Content -NoNewline -Force
}

$logEntries.Add([PSCustomObject]@{
Time = Get-Date -Format "HH:mm:ss"
Action = "Write"
Target = $file.Path
Value = "(content)"
Status = "Applied"
})

Write-Host " [Write] $($file.Path)" -ForegroundColor Green
}
elseif ($file.Ensure -eq "Absent" -and (Test-Path $file.Path)) {
if (-not $WhatIf) {
Remove-Item -Path $file.Path -Force
}

$logEntries.Add([PSCustomObject]@{
Time = Get-Date -Format "HH:mm:ss"
Action = "Remove"
Target = $file.Path
Value = ""
Status = "Applied"
})

Write-Host " [Remove] $($file.Path)" -ForegroundColor Green
}
}

# 应用服务配置
foreach ($svc in $manifest.Services) {
$service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue

if ($null -ne $service) {
# 设置启动类型
if ($service.StartType.ToString() -ne $svc.StartType) {
if (-not $WhatIf) {
Set-Service -Name $svc.Name -StartupType $svc.StartType
}
Write-Host " [SetStartup] $($svc.Name) -> $($svc.StartType)" -ForegroundColor Green
}

# 设置服务状态
if ($service.Status.ToString() -ne $svc.State) {
if (-not $WhatIf) {
if ($svc.State -eq "Running") {
Start-Service -Name $svc.Name
}
elseif ($svc.State -eq "Stopped") {
Stop-Service -Name $svc.Name -Force
}
}
Write-Host " [SetState] $($svc.Name) -> $($svc.State)" -ForegroundColor Green
}

$logEntries.Add([PSCustomObject]@{
Time = Get-Date -Format "HH:mm:ss"
Action = "Configure"
Target = $svc.Name
Value = "$($svc.State) / $($svc.StartType)"
Status = "Applied"
})
}
}

Write-Host "`n==============================" -ForegroundColor Cyan
Write-Host "变更总数:$($logEntries.Count)"
Write-Host "完成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "==============================`n"

return $logEntries
}

# 先用 WhatIf 预览变更
Write-Host ">>> 预览模式 <<<" -ForegroundColor Magenta
$previewLog = Invoke-ConfigurationApply -ManifestPath $manifestPath -WhatIf

# 确认后执行实际变更
Write-Host "`n>>> 执行模式 <<<" -ForegroundColor Magenta
$applyLog = Invoke-ConfigurationApply -ManifestPath $manifestPath

# 再次检测漂移,验证修复效果
Write-Host "`n>>> 修复验证 <<<" -ForegroundColor Magenta
$verifyResults = Test-ConfigurationDrift -ManifestPath $manifestPath
$verifyCompliant = ($verifyResults | Where-Object { $_.Compliant }).Count
Write-Host "修复后合规项:$verifyCompliant / $($verifyResults.Count)" -ForegroundColor Green
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
>>> 预览模式 <<<

========== 配置应用 ==========
目标配置:WebServer-Production v1.2.0
模式:WhatIf(仅预览,不执行变更)
开始时间:2025-09-30 14:35:00
==============================

[Create+Set] HKLM:\SOFTWARE\MyApp\LogLevel = 2
[Create+Set] HKLM:\SOFTWARE\MyApp\MaxConnections = 100
[Write] C:\MyApp\config\appsettings.json
[Configure] W3SVC
[Configure] Spooler

==============================
变更总数:5
完成时间:2025-09-30 14:35:00
==============================

>>> 执行模式 <<<

========== 配置应用 ==========
目标配置:WebServer-Production v1.2.0
开始时间:2025-09-30 14:35:12
==============================

[Create+Set] HKLM:\SOFTWARE\MyApp\LogLevel = 2
[Create+Set] HKLM:\SOFTWARE\MyApp\MaxConnections = 100
[Write] C:\MyApp\config\appsettings.json
[SetStartup] Spooler -> Disabled
[SetState] Spooler -> Stopped

==============================
变更总数:5
完成时间:2025-09-30 14:35:14
==============================

>>> 修复验证 <<<

========== 配置漂移检测 ==========
目标配置:WebServer-Production v1.2.0
检测时间:2025-09-30 14:35:15
==================================

--- 注册表设置 ---
[OK] LogLevel:期望=2,实际=2
[OK] MaxConnections:期望=100,实际=100

--- 文件配置 ---
[OK] C:\MyApp\config\appsettings.json:Present

--- 服务配置 ---
[OK] W3SVC:期望=Running/Automatic
[OK] Spooler:期望=Stopped/Disabled

========== 漂移汇总 ==========
总检测项:5
合规项: 5
漂移项: 0
合规率: 100.0%
================================

修复后合规项:5 / 5

生成配置变更历史报告

配置管理的最后一环是审计。我们需要记录每次配置变更的详情,包括变更时间、操作者、变更内容和结果,以便在出现问题时快速回溯。下面的代码实现了一个简单的变更历史追踪机制,将每次应用配置的日志追加到 CSV 文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# 配置变更历史追踪
function Write-ConfigurationAuditLog {
param(
[Parameter(Mandatory)]
[PSCustomObject[]]$LogEntries,

[string]$AuditLogPath = "$configRoot\audit-log.csv"
)

# 为每条日志添加审计字段
$auditRecords = foreach ($entry in $LogEntries) {
[PSCustomObject]@{
Timestamp = "$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ss')"
Operator = $env:USERNAME
Machine = $env:COMPUTERNAME
Action = $entry.Action
Target = $entry.Target
Value = $entry.Value
Status = $entry.Status
ConfigRef = "WebServer-Production v1.2.0"
}
}

# 追加到 CSV 审计日志(如文件不存在则创建表头)
$auditRecords | Export-Csv -Path $AuditLogPath -NoTypeInformation -Append -Force -Encoding UTF8
Write-Host "审计日志已追加至:$AuditLogPath" -ForegroundColor Green

# 显示最近的审计记录
Write-Host "`n最近 5 条审计记录:" -ForegroundColor Cyan
$recentLogs = Import-Csv -Path $AuditLogPath -Encoding UTF8 |
Select-Object -Last 5

$recentLogs | Format-Table Timestamp, Operator, Action, Target, Status -AutoSize
}

# 计算合规趋势(适用于定期巡检场景)
function Get-ComplianceTrend {
param(
[string]$TrendLogPath = "$configRoot\compliance-trend.csv"
)

# 记录本次合规率
$drifts = Test-ConfigurationDrift -ManifestPath $manifestPath
$total = $drifts.Count
$compliant = ($drifts | Where-Object { $_.Compliant }).Count
$rate = if ($total -gt 0) { [math]::Round($compliant / $total * 100, 1) } else { 100 }

$trendRecord = [PSCustomObject]@{
Date = Get-Date -Format "yyyy-MM-dd"
Time = Get-Date -Format "HH:mm:ss"
TotalItems = $total
Compliant = $compliant
Drifted = $total - $compliant
RatePercent = $rate
}

$trendRecord | Export-Csv -Path $TrendLogPath -NoTypeInformation -Append -Force -Encoding UTF8

Write-Host "`n合规趋势已记录:" -ForegroundColor Cyan
Write-Host " 日期:$($trendRecord.Date)"
Write-Host " 合规率:$($trendRecord.RatePercent)%"
Write-Host " 漂移项:$($trendRecord.Drifted)"

# 显示近 7 次巡检趋势
if (Test-Path $TrendLogPath) {
$history = Import-Csv -Path $TrendLogPath -Encoding UTF8 |
Select-Object -Last 7

Write-Host "`n近 7 次巡检合规率趋势:" -ForegroundColor Yellow
foreach ($record in $history) {
$bar = "=" * ([math]::Floor([int]$record.RatePercent / 5))
Write-Host " $($record.Date) [$($record.RatePercent)%] $bar"
}
}
}

# 写入审计日志
Write-ConfigurationAuditLog -LogEntries $applyLog

# 记录合规趋势
Get-ComplianceTrend
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
审计日志已追加至:C:\Users\admin\Documents\ConfigAsCode\audit-log.csv

最近 5 条审计记录:
Timestamp Operator Action Target Status
--------- -------- ------ ------ ------
2025-09-30T14:35:12 admin Create+Set HKLM:\SOFTWARE\MyApp\LogLevel Applied
2025-09-30T14:35:12 admin Create+Set HKLM:\SOFTWARE\MyApp\MaxConnections Applied
2025-09-30T14:35:13 admin Write C:\MyApp\config\appsettings.json Applied
2025-09-30T14:35:13 admin Configure W3SVC Applied
2025-09-30T14:35:14 admin Configure Spooler Applied

========== 配置漂移检测 ==========
目标配置:WebServer-Production v1.2.0
检测时间:2025-09-30 14:36:00
==================================

--- 注册表设置 ---
[OK] LogLevel:期望=2,实际=2
[OK] MaxConnections:期望=100,实际=100

--- 文件配置 ---
[OK] C:\MyApp\config\appsettings.json:Present

--- 服务配置 ---
[OK] W3SVC:期望=Running/Automatic
[OK] Spooler:期望=Stopped/Disabled

========== 漂移汇总 ==========
总检测项:5
合规项: 5
漂移项: 0
合规率: 100.0%
================================

合规趋势已记录:
日期:2025-09-30
合规率:100.0%
漂移项:0

近 7 次巡检合规率趋势:
2025-09-24 [60.0%] ============
2025-09-25 [80.0%] ================
2025-09-26 [80.0%] ================
2025-09-27 [40.0%] ========
2025-09-28 [60.0%] ============
2025-09-29 [80.0%] ================
2025-09-30 [100.0%] ====================

注意事项

  1. 幂等性是底线:配置应用函数必须保证多次执行结果一致,避免重复创建、重复写入等副作用,每次操作前先检查当前状态
  2. JSON Schema 验证:在生产环境中,应在加载配置清单前用 JSON Schema 验证其结构完整性,防止因配置文件格式错误导致不可预期的变更
  3. WhatIf 先行:所有配置变更操作都应先以 -WhatIf 模式预览,确认变更范围后再执行,结合 CI/CD 流水线可实现审批门控
  4. 变更回滚机制:每次应用配置前应备份当前状态,或维护一个”上一个已知良好配置”,出现问题时能快速回退
  5. 敏感信息保护:配置清单中可能包含密码、API 密钥等敏感数据,应使用 Azure Key Vault、Windows Credential Manager 或 PowerShell SecretManagement 模块管理,不要明文存储在 JSON 中
  6. 跨平台兼容性:本文示例以 Windows 注册表和服务为主,如果需要管理 Linux 节点,可将配置目标替换为文件权限、systemd 服务和包管理器,核心框架逻辑不变