PowerShell 技能连载 - 配置即代码实践

适用于 PowerShell 7.0 及以上版本

在现代 DevOps 实践中,配置即代码(Configuration as Code, CaC)已经从一种理想化的口号变成了可落地的工程实践。无论是微服务应用的 appsettings.json,还是基础设施的 Terraform 状态文件,配置都以代码的形式被版本化管理、评审和自动化部署。PowerShell 作为 Windows 和跨平台自动化的利器,在配置即代码领域扮演着不可或缺的角色——它能解析、生成、验证和分发各种格式的配置文件。

企业环境中的配置管理面临着独特的挑战:开发、测试、预发布、生产四个环境各有一套参数,机密信息不能明文存储在仓库里,多台服务器的配置必须保持同步且可审计。手动维护这些配置极易出错,一次遗漏就可能导致生产事故。将配置纳入代码管理流程,通过 PowerShell 脚本实现配置的解析、合并、验证和漂移检测,可以有效降低人为失误的风险,让配置变更像代码变更一样可追溯、可回滚。

本文将从三个实战场景出发,演示如何用 PowerShell 构建一套完整的配置即代码工作流:第一,使用 YAML/JSON 作为配置源并通过 Schema 验证保证数据质量;第二,实现配置分层与深度合并,解决多环境配置管理的复杂性;第三,构建配置漂移检测与自动修正机制,确保系统始终处于期望状态。

YAML/JSON 配置模式与 Schema 验证

YAML 以其可读性优势成为配置文件的首选格式。PowerShell 7 原生支持 JSON,而对 YAML 的支持可以通过 powershell-yaml 模块实现。下面的代码演示了如何定义一套配置 Schema 并对配置文件进行自动化验证,确保开发人员提交的配置文件符合规范。

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# 定义配置 Schema(JSON Schema 格式)
$schema = @{
'$schema' = 'http://json-schema.org/draft-07/schema#'
title = 'Application Configuration Schema'
type = 'object'
required = @('application', 'server', 'database')
properties = @{
application = @{
type = 'object'
required = @('name', 'version')
properties = @{
name = @{ type = 'string'; minLength = 1 }
version = @{
type = 'string'
pattern = '^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$'
}
debug = @{ type = 'boolean'; default = $false }
}
}
server = @{
type = 'object'
required = @('port', 'host')
properties = @{
host = @{ type = 'string' }
port = @{ type = 'integer'; minimum = 1; maximum = 65535 }
enableSsl = @{ type = 'boolean'; default = $true }
maxConnection = @{ type = 'integer'; default = 100; minimum = 1 }
}
}
database = @{
type = 'object'
required = @('host', 'name')
properties = @{
host = @{ type = 'string' }
port = @{ type = 'integer'; default = 5432 }
name = @{ type = 'string' }
user = @{ type = 'string' }
password = @{ type = 'string' }
}
}
logging = @{
type = 'object'
properties = @{
level = @{ type = 'string'; enum = @('DEBUG', 'INFO', 'WARN', 'ERROR') }
path = @{ type = 'string' }
rotate = @{ type = 'boolean'; default = $true }
}
}
}
}

# 将 Schema 保存为 JSON 文件
$schemaPath = Join-Path $PSScriptRoot 'config-schema.json'
$schema | ConvertTo-Json -Depth 10 | Set-Content -Path $schemaPath -Encoding UTF8

# 创建示例 YAML 配置文件
$yamlContent = @"
application:
name: OrderService
version: 2.4.1
debug: false
server:
host: 0.0.0.0
port: 8080
enableSsl: true
maxConnection: 200
database:
host: db.internal.local
port: 5432
name: orders_db
user: app_user
password: `${VAULT:database/password}
logging:
level: INFO
path: /var/log/orderservice
rotate: true
"@

$configPath = Join-Path $PSScriptRoot 'config-base.yaml'
$yamlContent | Set-Content -Path $configPath -Encoding UTF8

# Schema 验证函数
function Test-ConfigSchema {
param(
[Parameter(Mandatory)]
[hashtable]$Config,

[Parameter(Mandatory)]
[hashtable]$Schema,

[string]$Path = 'root'
)

$errors = [System.Collections.Generic.List[string]]::new()

# 检查必填字段
if ($Schema.required) {
foreach ($field in $Schema.required) {
if (-not $Config.ContainsKey($field)) {
$errors.Add("[$Path] 缺少必填字段: $field")
}
}
}

# 检查属性类型和约束
if ($Schema.properties) {
foreach ($key in $Config.Keys) {
$propSchema = $Schema.properties[$key]
if (-not $propSchema) {
continue
}
$value = $Config[$key]
$currentPath = "$Path.$key"

if ($propSchema.type -eq 'object' -and $value -is [hashtable]) {
$childErrors = Test-ConfigSchema -Config $value -Schema $propSchema -Path $currentPath
$errors.AddRange($childErrors)
}
elseif ($propSchema.type -eq 'integer') {
if ($value -isnot [int] -and $value -isnot [long]) {
$errors.Add("[$currentPath] 类型错误: 期望 integer,实际为 $($value.GetType().Name)")
}
elseif ($propSchema.minimum -and $value -lt $propSchema.minimum) {
$errors.Add("[$currentPath] 值 $value 小于最小值 $($propSchema.minimum)")
}
elseif ($propSchema.maximum -and $value -gt $propSchema.maximum) {
$errors.Add("[$currentPath] 值 $value 大于最大值 $($propSchema.maximum)")
}
}
elseif ($propSchema.type -eq 'string') {
if ($propSchema.pattern -and $value -notmatch $propSchema.pattern) {
$errors.Add("[$currentPath] 值 '$value' 不匹配模式 '$($propSchema.pattern)'")
}
if ($propSchema.enum -and $value -notin $propSchema.enum) {
$errors.Add("[$currentPath] 值 '$value' 不在允许列表中: $($propSchema.enum -join ', ')")
}
}
elseif ($propSchema.type -eq 'boolean' -and $value -isnot [bool]) {
$errors.Add("[$currentPath] 类型错误: 期望 boolean,实际为 $($value.GetType().Name)")
}
}
}

return $errors
}

# 安装并导入 YAML 模块(如未安装)
if (-not (Get-Module -ListAvailable -Name 'powershell-yaml')) {
Install-Module -Name 'powershell-yaml' -Force -Scope CurrentUser
}
Import-Module 'powershell-yaml'

# 读取配置并验证
$configYaml = Get-Content -Path $configPath -Raw
$parsedConfig = ConvertFrom-Yaml -Yaml $configYaml
$validationErrors = Test-ConfigSchema -Config $parsedConfig -Schema $schema

if ($validationErrors.Count -eq 0) {
Write-Host "配置验证通过" -ForegroundColor Green
} else {
Write-Host "配置验证失败,发现 $($validationErrors.Count) 个错误:" -ForegroundColor Red
$validationErrors | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
}

执行结果示例:

1
配置验证通过

如果配置中存在错误(例如端口超出范围或版本号格式不对),输出如下:

1
2
3
配置验证失败,发现 2 个错误:
- [root.server.port] 值 99999 大于最大值 65535
- [root.application.version] 值 'v2.x' 不匹配模式 '^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$'

配置分层与深度合并

企业应用通常需要在基础配置之上叠加环境专属配置和机密注入。例如,开发环境使用本地数据库,生产环境使用集群数据库,而数据库密码则从 HashiCorp Vault 或 Azure Key Vault 动态获取。这就要求我们实现配置的分层加载和深度合并(Deep Merge)——当子层级的配置与基础配置有相同键时,子层级应覆盖基础配置的值,而非简单地替换整个节点。

下面的代码实现了一个完整的配置分层合并系统,支持基础配置、环境覆盖和机密注入三个层级。

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
145
146
147
148
149
150
151
# 深度合并函数:递归合并两个 hashtable
function Merge-ConfigDeep {
param(
[Parameter(Mandatory)]
[hashtable]$Base,

[Parameter(Mandatory)]
[hashtable]$Override
)

$result = @{}
# 复制基础配置的所有键
foreach ($key in $Base.Keys) {
$result[$key] = $Base[$key]
}

# 用覆盖配置进行深度合并
foreach ($key in $Override.Keys) {
if ($result.ContainsKey($key) -and
$result[$key] -is [hashtable] -and
$Override[$key] -is [hashtable]) {
# 两个值都是 hashtable,递归合并
$result[$key] = Merge-ConfigDeep -Base $result[$key] -Override $Override[$key]
}
else {
# 否则直接覆盖
$result[$key] = $Override[$key]
}
}

return $result
}

# 从环境变量或 Vault 注入机密
function Resolve-ConfigSecrets {
param(
[Parameter(Mandatory)]
[hashtable]$Config
)

$secretPattern = '^\$\{VAULT:(.+)\}$'

function Invoke-RecursiveResolve {
param($Node)

if ($Node -is [hashtable]) {
$resolved = @{}
foreach ($key in $Node.Keys) {
$resolved[$key] = Invoke-RecursiveResolve -Node $Node[$key]
}
return $resolved
}
elseif ($Node -is [string] -and $Node -match $secretPattern) {
$secretPath = $Matches[1]
# 优先从环境变量读取
$envKey = $secretPath -replace '[/\\:.]', '_'
$envValue = [System.Environment]::GetEnvironmentVariable($envKey)
if ($envValue) {
Write-Verbose "从环境变量解析机密: $secretPath"
return $envValue
}
# 回退到模拟 Vault(实际环境中对接真实 Vault API)
Write-Verbose "从 Vault 解析机密: $secretPath"
return "[VAULT-RESOLVED:$secretPath]"
}
elseif ($Node -is [array]) {
return @($Node | ForEach-Object { Invoke-RecursiveResolve -Node $_ })
}
return $Node
}

return Invoke-RecursiveResolve -Node $Config
}

# 基础配置(所有环境共享)
$baseConfig = @{
application = @{
name = 'OrderService'
version = '2.4.1'
debug = $false
}
server = @{
host = '0.0.0.0'
port = 8080
enableSsl = $true
maxConnection = 100
}
database = @{
host = 'db.internal.local'
port = 5432
name = 'orders_db'
user = 'app_user'
password = '${VAULT:database/password}'
}
logging = @{
level = 'INFO'
path = '/var/log/orderservice'
rotate = $true
}
}

# 生产环境覆盖配置
$productionOverride = @{
server = @{
host = '10.0.1.50'
port = 443
enableSsl = $true
maxConnection = 500
}
database = @{
host = 'prod-db-cluster.internal.local'
user = 'prod_app_user'
}
logging = @{
level = 'WARN'
path = '/var/log/orderservice/prod'
}
}

# 开发环境覆盖配置
$devOverride = @{
application = @{
debug = $true
}
server = @{
host = 'localhost'
port = 3000
enableSsl = $false
maxConnection = 10
}
database = @{
host = 'localhost'
name = 'orders_db_dev'
}
logging = @{
level = 'DEBUG'
path = './logs'
}
}

# 构建不同环境的最终配置
$environments = @{
production = Merge-ConfigDeep -Base $baseConfig -Override $productionOverride
development = Merge-ConfigDeep -Base $baseConfig -Override $devOverride
}

foreach ($env in $environments.Keys) {
Write-Host "`n=== $env 环境 ===" -ForegroundColor Cyan
$resolvedConfig = Resolve-ConfigSecrets -Config $environments[$env]
$resolvedConfig | ConvertTo-Json -Depth 5
}

执行结果示例:

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
=== production 环境 ===
{
"application": {
"name": "OrderService",
"version": "2.4.1",
"debug": false
},
"server": {
"host": "10.0.1.50",
"port": 443,
"enableSsl": true,
"maxConnection": 500
},
"database": {
"host": "prod-db-cluster.internal.local",
"port": 5432,
"name": "orders_db",
"user": "prod_app_user",
"password": "[VAULT-RESOLVED:database/password]"
},
"logging": {
"level": "WARN",
"path": "/var/log/orderservice/prod",
"rotate": true
}
}

=== development 环境 ===
{
"application": {
"name": "OrderService",
"version": "2.4.1",
"debug": true
},
"server": {
"host": "localhost",
"port": 3000,
"enableSsl": false,
"maxConnection": 10
},
"database": {
"host": "localhost",
"port": 5432,
"name": "orders_db_dev",
"user": "app_user",
"password": "[VAULT-RESOLVED:database/password]"
},
"logging": {
"level": "DEBUG",
"path": "./logs",
"rotate": true
}
}

配置漂移检测与自动修正

配置漂移(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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# 配置漂移检测与修正引擎
class ConfigDriftDetector {
[string]$ConfigName
[hashtable]$DesiredState
[System.Collections.Generic.List[hashtable]]$Drifts
[System.Collections.Generic.List[hashtable]]$AuditLog

ConfigDriftDetector([string]$Name, [hashtable]$Desired) {
$this.ConfigName = $Name
$this.DesiredState = $Desired
$this.Drifts = [System.Collections.Generic.List[hashtable]]::new()
$this.AuditLog = [System.Collections.Generic.List[hashtable]]::new()
}

# 递归对比期望状态与实际状态
[void] CompareState(
[hashtable]$Desired,
[hashtable]$Actual,
[string]$Path
) {
foreach ($key in $Desired.Keys) {
$currentPath = if ($Path) { "$Path.$key" } else { $key }

if (-not $Actual.ContainsKey($key)) {
$this.Drifts.Add(@{
Path = $currentPath
Type = 'Missing'
Desired = $Desired[$key]
Actual = $null
Severity = 'High'
})
continue
}

$desiredVal = $Desired[$key]
$actualVal = $Actual[$key]

if ($desiredVal -is [hashtable] -and $actualVal -is [hashtable]) {
$this.CompareState($desiredVal, $actualVal, $currentPath)
}
elseif ($desiredVal -is [array] -and $actualVal -is [array]) {
$diff = Compare-Object $desiredVal $actualVal
if ($diff) {
$this.Drifts.Add(@{
Path = $currentPath
Type = 'ArrayDiff'
Desired = ($desiredVal -join ', ')
Actual = ($actualVal -join ', ')
Severity = 'Medium'
})
}
}
elseif ("$desiredVal" -ne "$actualVal") {
$severity = if ($currentPath -match 'password|secret|key') {
'Critical'
} elseif ($currentPath -match 'debug|log') {
'Low'
} else {
'Medium'
}
$this.Drifts.Add(@{
Path = $currentPath
Type = 'ValueMismatch'
Desired = "$desiredVal"
Actual = "$actualVal"
Severity = $severity
})
}
}
}

# 生成漂移报告
[string] GenerateReport() {
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine("配置漂移报告 - $($this.ConfigName)")
[void]$sb.AppendLine("生成时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
[void]$sb.AppendLine("漂移项数: $($this.Drifts.Count)")
[void]$sb.AppendLine('---')

if ($this.Drifts.Count -eq 0) {
[void]$sb.AppendLine('未检测到配置漂移,系统状态正常。')
}
else {
$severityOrder = @{ Critical = 0; High = 1; Medium = 2; Low = 3 }
$sorted = $this.Drifts | Sort-Object { $severityOrder[$_.Severity] }

foreach ($drift in $sorted) {
[void]$sb.AppendLine(
"[$($drift.Severity)] $($drift.Path)"
)
[void]$sb.AppendLine(
" 期望: $($drift.Desired) | 实际: $($drift.Actual)"
)
[void]$sb.AppendLine(" 类型: $($drift.Type)")
[void]$sb.AppendLine()
}
}
return $sb.ToString()
}

# 自动修正漂移并记录审计日志
[void] Remediate([hashtable]$ActualRef) {
foreach ($drift in $this.Drifts) {
$pathParts = $drift.Path.Split('.')
$current = $ActualRef

# 导航到父级节点
for ($i = 0; $i -lt $pathParts.Count - 1; $i++) {
$current = $current[$pathParts[$i]]
}

$leafKey = $pathParts[-1]
$oldValue = $current[$leafKey]
$current[$leafKey] = $drift.Desired

$this.AuditLog.Add(@{
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Path = $drift.Path
OldValue = "$oldValue"
NewValue = "$($drift.Desired)"
Severity = $drift.Severity
Action = 'AutoRemediate'
})
}
}
}

# 定义期望配置
$desiredConfig = @{
application = @{
name = 'OrderService'
version = '2.4.1'
debug = $false
}
server = @{
host = '10.0.1.50'
port = 443
enableSsl = $true
maxConnection = 500
}
database = @{
host = 'prod-db-cluster.internal.local'
port = 5432
}
}

# 模拟当前实际配置(存在漂移)
$actualConfig = @{
application = @{
name = 'OrderService'
version = '2.3.0' # 版本落后
debug = $true # 调试模式未关闭
}
server = @{
host = '10.0.1.50'
port = 8080 # 端口错误
enableSsl = $false # SSL 被关闭
maxConnection = 500
}
database = @{
host = 'prod-db-cluster.internal.local'
port = 5432
}
}

# 执行漂移检测
$detector = [ConfigDriftDetector]::new('Production-OrderService', $desiredConfig)
$detector.CompareState($desiredConfig, $actualConfig, '')
Write-Host $detector.GenerateReport()

# 执行自动修正
Write-Host "`n正在执行自动修正..." -ForegroundColor Yellow
$detector.Remediate($actualConfig)

# 输出审计日志
Write-Host "`n审计日志:" -ForegroundColor Cyan
$detector.AuditLog | Format-Table -AutoSize

执行结果示例:

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
配置漂移报告 - Production-OrderService
生成时间: 2026-04-28 10:30:00
漂移项数: 4
---
[Critical] server.enableSsl
期望: True | 实际: False
类型: ValueMismatch

[High] application.version
期望: 2.4.1 | 实际: 2.3.0
类型: ValueMismatch

[Medium] server.port
期望: 443 | 实际: 8080
类型: ValueMismatch

[Low] application.debug
期望: False | 实际: True
类型: ValueMismatch


正在执行自动修正...

审计日志:

Timestamp Path OldValue NewValue Severity Action
--------- ---- -------- -------- -------- ------
2026-04-28 10:30:00 application.version 2.3.0 2.4.1 High AutoRemediate
2026-04-28 10:30:00 application.debug True False Low AutoRemediate
2026-04-28 10:30:00 server.port 8080 443 Medium AutoRemediate
2026-04-28 10:30:00 server.enableSsl False True Critical AutoRemediate

注意事项

  1. Schema 验证应在 CI 流水线中强制执行。将配置文件的 Schema 验证作为 Pull Request 的必经检查项,可以在配置进入主分支之前就拦截格式错误和遗漏字段,避免无效配置扩散到下游环境。

  2. 深度合并时注意数组合并策略。本文实现的深度合并对数组采用直接覆盖策略。如果业务要求数组进行并集合并(如白名单列表),需要在 Merge-ConfigDeep 函数中为数组类型添加专门的合并逻辑。

  3. 机密注入应与配置文件分离存储。示例中的 ${VAULT:path} 占位符机制确保机密不会出现在代码仓库中。生产环境中应对接 HashiCorp Vault、Azure Key Vault 或 AWS Secrets Manager 等专业机密管理工具,而非依赖环境变量。

  4. 漂移检测的频率需要根据业务场景调整。关键生产系统建议每小时检测一次,并配合告警通知(如 PagerDuty、企业微信)。非关键系统可以每天检测一次,配合每日巡检报告。

  5. 自动修正需设置白名单和审批机制。并非所有漂移都应该被自动修正——某些漂移可能是有意的应急措施。建议对修正操作设置严重级别白名单,High 和 Critical 级别自动修正,其他级别仅生成告警,由运维人员确认后再修正。

  6. 审计日志应持久化到不可篡改的存储。配置变更的审计日志是故障排查和安全合规的重要依据。建议将日志写入数据库(如 Elasticsearch)或对象存储(如 S3),并启用版本控制和防篡改保护。

PowerShell 技能连载 - Azure App Configuration 集中配置

适用于 PowerShell 7.0 及以上版本

在微服务架构中,每个服务都有自己的 appsettings.json 或环境变量文件来管理配置。随着服务数量增长,配置分散在各处的问题日益严重:修改一个数据库连接字符串需要逐个服务更新并重新部署,不同环境之间的配置差异难以追踪,紧急开关的生效时间更是无法保证。这种”配置碎片化”不仅增加了运维成本,也埋下了安全隐患。

Azure App Configuration 正是为解决这些问题而生的集中化配置管理服务。它提供统一的键值存储,支持标签(Label)实现环境隔离,内置 Feature Flag 功能支持灰度发布,并通过 Sentinel 机制实现配置的动态刷新——所有这些都不需要重启应用。结合 PowerShell 的自动化能力,我们可以将配置管理纳入 CI/CD 流水线,实现配置的版本控制、审计追踪和跨环境同步。

本文将围绕三个核心场景展开:配置存储的日常管理操作、Feature Flag 的创建与条件控制,以及配置的批量导入导出与环境间同步。掌握这些技能后,你就能用 PowerShell 构建一套完整的配置管理自动化方案。

配置存储管理

日常配置管理是最基础也是最频繁的操作。下面的脚本封装了连接 App Configuration 存储、读写键值对、使用标签隔离环境以及查询配置修订历史等常用功能,适合直接集成到运维工具链中。

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
function Invoke-AppConfigOperation {
<#
.SYNOPSIS
管理 Azure App Configuration 键值对
#>
param(
[Parameter(Mandatory)]
[string]$Endpoint,

[ValidateSet('Get', 'Set', 'Remove', 'List', 'History')]
[string]$Action = 'List',

[string]$Key,
[string]$Value,
[string]$Label,
[string]$Prefix
)

# 使用 Azure CLI 获取访问令牌
$token = (az account get-access-token --resource "https://azconfig.io" --query accessToken --output tsv)
$headers = @{
Authorization = "Bearer $token"
'Content-Type' = 'application/vnd.microsoft.appconfig.kv+json'
}

switch ($Action) {
'Get' {
$uri = "$Endpoint/kv/$($Key)?label=$Label"
$response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
return [PSCustomObject]@{
Key = $response.key
Value = $response.value
Label = $response.label
ETag = $response.etag
}
}

'Set' {
$uri = "$Endpoint/kv/$($Key)?label=$Label"
$body = @{
key = $Key
value = $Value
label = $Label
} | ConvertTo-Json

$response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Put -Body $body
Write-Host "已设置配置项: $Key = $Value (标签: $Label)" -ForegroundColor Green
return $response
}

'Remove' {
# 先获取当前 ETag
$existing = Invoke-AppConfigOperation -Endpoint $Endpoint -Action 'Get' -Key $Key -Label $Label
$uri = "$Endpoint/kv/$($Key)?label=$Label"
Invoke-RestMethod -Uri $uri -Headers $headers -Method Delete
Write-Host "已删除配置项: $Key (标签: $Label)" -ForegroundColor Yellow
}

'List' {
$filterParam = if ($Prefix) { "&key=$Prefix*" } else { '' }
$labelParam = if ($Label) { "&label=$Label" } else { '' }
$uri = "$Endpoint/kv?$filterParam$labelParam"
$response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
$items = $response.items
return $items | ForEach-Object {
[PSCustomObject]@{
Key = $_.key
Value = $_.value
Label = $_.label
}
}
}

'History' {
$uri = "$Endpoint/revisions?key=$Key&label=$Label"
$response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
return $response.items | ForEach-Object {
[PSCustomObject]@{
Key = $_.key
Value = $_.value
Label = $_.label
ETag = $_.etag
Timestamp = $_.last_modified
}
}
}
}
}

# 示例:按环境管理配置
$endpoint = "https://myappconfig.azconfig.io"

# 设置开发环境和生产环境的数据库连接串
Invoke-AppConfigOperation -Endpoint $endpoint -Action 'Set' `
-Key "Database:ConnectionString" -Value "Server=dev-sql;Database=MyApp" -Label "dev"

Invoke-AppConfigOperation -Endpoint $endpoint -Action 'Set' `
-Key "Database:ConnectionString" -Value "Server=prod-sql;Database=MyApp" -Label "prod"

# 列出开发环境下所有 Database 前缀的配置
Invoke-AppConfigOperation -Endpoint $endpoint -Action 'List' -Prefix "Database" -Label "dev"

# 查看某个配置项的修改历史
Invoke-AppConfigOperation -Endpoint $endpoint -Action 'History' `
-Key "Database:ConnectionString" -Label "prod"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
已设置配置项: Database:ConnectionString = Server=dev-sql;Database=MyApp (标签: dev)
已设置配置项: Database:ConnectionString = Server=prod-sql;Database=MyApp (标签: prod)

Key Value Label
--- ----- -----
Database:ConnectionString Server=dev-sql;Database=MyApp dev
Database:MaxPoolSize 50 dev
Database:CommandTimeout 30 dev

Key Value Label ETag Timestamp
--- ----- ----- ---- ---------
Database:ConnectionString Server=prod-sql;Database=MyApp prod "abc123" 2026-03-03T10:15:00Z
Database:ConnectionString Server=staging-sql;Database=MyApp prod "def456" 2026-03-01T08:30:00Z
Database:ConnectionString Server=prod-sql;Database=MyApp prod "ghi789" 2026-02-28T14:00:00Z

通过标签(Label)机制,同一个键可以对应不同环境的值。查询时指定标签即可获取对应环境的配置,无需维护多套命名规则。修订历史功能可以追溯每次配置变更,为审计和回滚提供依据。

Feature Flag 管理

Feature Flag 是实现灰度发布、A/B 测试和功能开关的核心机制。App Configuration 原生支持 Feature Flag 的存储和管理,下面演示如何用 PowerShell 创建、查询和配置带条件过滤器的 Feature Flag。

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
function Manage-FeatureFlag {
<#
.SYNOPSIS
管理 Azure App Configuration 中的 Feature Flag
#>
param(
[Parameter(Mandatory)]
[string]$Endpoint,

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

[ValidateSet('Create', 'Get', 'Enable', 'Disable', 'SetPercentage', 'SetTargeting')]
[string]$Action = 'Get',

[string]$Label,
[double]$Percentage = 50,
[string[]]$TargetGroups,
[string[]]$ExcludeUsers
)

$token = (az account get-access-token --resource "https://azconfig.io" --query accessToken --output tsv)
$headers = @{
Authorization = "Bearer $token"
'Content-Type' = 'application/vnd.microsoft.appconfig.kv+json'
}

$featureKey = ".appconfig.featureflag/$FlagName"

switch ($Action) {
'Create' {
$body = @{
key = $featureKey
value = @{
id = $FlagName
description = "Feature flag for $FlagName"
enabled = $false
conditions = @{
client_filters = @()
}
} | ConvertTo-Json -Depth 5
label = $Label
content_type = 'application/vnd.microsoft.appconfig.featureflag+json'
} | ConvertTo-Json -Depth 5

Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Put -Body $body
Write-Host "已创建 Feature Flag: $FlagName (状态: 关闭)" -ForegroundColor Green
}

'Enable' {
$existing = Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Get
$flagValue = $existing.value | ConvertFrom-Json
$flagValue.enabled = $true

$body = @{
key = $featureKey
value = ($flagValue | ConvertTo-Json -Depth 5)
label = $Label
content_type = 'application/vnd.microsoft.appconfig.featureflag+json'
} | ConvertTo-Json -Depth 5

Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Put -Body $body
Write-Host "已启用 Feature Flag: $FlagName" -ForegroundColor Green
}

'Disable' {
$existing = Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Get
$flagValue = $existing.value | ConvertFrom-Json
$flagValue.enabled = $false

$body = @{
key = $featureKey
value = ($flagValue | ConvertTo-Json -Depth 5)
label = $Label
content_type = 'application/vnd.microsoft.appconfig.featureflag+json'
} | ConvertTo-Json -Depth 5

Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Put -Body $body
Write-Host "已禁用 Feature Flag: $FlagName" -ForegroundColor Yellow
}

'SetPercentage' {
# 配置百分比灰度:按用户比例逐步放开功能
$existing = Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Get
$flagValue = $existing.value | ConvertFrom-Json
$flagValue.enabled = $true
$flagValue.conditions.client_filters = @(
@{
name = 'Microsoft.Targeting'
parameters = @{
Audiences = @(
@{
Id = "rollout-group"
Inclusion = $Percentage
Exclusion = 0
}
)
DefaultRolloutPercentage = $Percentage
}
}
)

$body = @{
key = $featureKey
value = ($flagValue | ConvertTo-Json -Depth 10)
label = $Label
content_type = 'application/vnd.microsoft.appconfig.featureflag+json'
} | ConvertTo-Json -Depth 10

Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Put -Body $body
Write-Host "已设置 $FlagName 灰度比例为 ${Percentage}%" -ForegroundColor Cyan
}

'SetTargeting' {
# 配置定向投放:指定用户组可见
$existing = Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Get
$flagValue = $existing.value | ConvertFrom-Json
$flagValue.enabled = $true
$flagValue.conditions.client_filters = @(
@{
name = 'Microsoft.Targeting'
parameters = @{
Users = @()
Groups = @(
@{
Name = "beta-testers"
RolloutPercentage = 100
}
)
DefaultRolloutPercentage = 0
}
}
)

$body = @{
key = $featureKey
value = ($flagValue | ConvertTo-Json -Depth 10)
label = $Label
content_type = 'application/vnd.microsoft.appconfig.featureflag+json'
} | ConvertTo-Json -Depth 10

Invoke-RestMethod -Uri "$Endpoint/kv/$featureKey?label=$Label" `
-Headers $headers -Method Put -Body $body
Write-Host "已设置 $FlagName 定向投放: beta-testers 组 100% 可见" -ForegroundColor Cyan
}
}
}

# 创建并配置新功能的 Feature Flag
$endpoint = "https://myappconfig.azconfig.io"

# 创建开关(初始关闭)
Manage-FeatureFlag -Endpoint $endpoint -FlagName "NewDashboard" `
-Action 'Create' -Label "prod"

# 开启灰度:先对 10% 用户开放
Manage-FeatureFlag -Endpoint $endpoint -FlagName "NewDashboard" `
-Action 'SetPercentage' -Percentage 10 -Label "prod"

# 逐步放量到 50%
Manage-FeatureFlag -Endpoint $endpoint -FlagName "NewDashboard" `
-Action 'SetPercentage' -Percentage 50 -Label "prod"

# 设置定向投放:beta-testers 组全部可见
Manage-FeatureFlag -Endpoint $endpoint -FlagName "NewDashboard" `
-Action 'SetTargeting' -Label "prod"

执行结果示例:

1
2
3
4
已创建 Feature Flag: NewDashboard (状态: 关闭)
已设置 NewDashboard 灰度比例为 10%
已设置 NewDashboard 灰度比例为 50%
已设置 NewDashboard 定向投放: beta-testers 组 100% 可见

Feature Flag 的核心价值在于将功能发布与代码部署解耦。通过百分比灰度,可以按照 10% → 50% → 100% 的节奏逐步放量;通过定向投放,可以让内部测试团队或特定用户组优先体验新功能。所有调整都是实时生效的,不需要重新部署应用。

配置导入导出与同步

在多环境管理中,将配置从开发环境同步到测试、预发布和生产环境是常见的运维任务。下面的脚本支持批量导入配置、跨环境同步,并能生成配置差异报告供审核。

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
145
146
147
148
149
150
151
152
153
154
155
156
function Sync-AppConfigEnvironment {
<#
.SYNOPSIS
跨环境同步 Azure App Configuration 配置
#>
param(
[Parameter(Mandatory)]
[string]$SourceEndpoint,

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

[string]$SourceLabel = 'dev',

[string]$TargetLabel = 'staging',

[string]$KeyPrefix,

[switch]$DryRun,

[switch]$Force
)

$token = (az account get-access-token --resource "https://azconfig.io" --query accessToken --output tsv)
$headers = @{
Authorization = "Bearer $token"
'Content-Type' = 'application/vnd.microsoft.appconfig.kv+json'
}

# 从源环境读取全部配置
Write-Host "正在从源环境读取配置 (标签: $SourceLabel)..." -ForegroundColor Cyan
$filter = if ($KeyPrefix) { "&key=$KeyPrefix*" } else { '' }
$sourceResponse = Invoke-RestMethod -Uri "$SourceEndpoint/kv?label=$SourceLabel$filter" `
-Headers $headers -Method Get
$sourceItems = @{}
foreach ($item in $sourceResponse.items) {
$sourceItems[$item.key] = $item.value
}
Write-Host " 读取到 $($sourceItems.Count) 个配置项"

# 从目标环境读取对应配置
Write-Host "正在从目标环境读取配置 (标签: $TargetLabel)..." -ForegroundColor Cyan
$targetResponse = Invoke-RestMethod -Uri "$TargetEndpoint/kv?label=$TargetLabel$filter" `
-Headers $headers -Method Get
$targetItems = @{}
foreach ($item in $targetResponse.items) {
$targetItems[$item.key] = $item.value
}
Write-Host " 读取到 $($targetItems.Count) 个配置项"

# 计算差异
$toAdd = $sourceItems.Keys | Where-Object { $_ -notin $targetItems.Keys }
$toUpdate = $sourceItems.Keys | Where-Object {
$_ -in $targetItems.Keys -and $sourceItems[$_] -ne $targetItems[$_]
}
$toRemove = $targetItems.Keys | Where-Object { $_ -notin $sourceItems.Keys }

# 输出差异报告
Write-Host "`n===== 配置同步差异报告 =====" -ForegroundColor White
Write-Host "源: $SourceLabel → 目标: $TargetLabel"
Write-Host "新增: $($toAdd.Count) 项 | 更新: $($toUpdate.Count) 项 | 删除: $($toRemove.Count) 项"
Write-Host "=============================`n"

if ($toAdd.Count -gt 0) {
Write-Host "[新增] 以下配置将添加到目标环境:" -ForegroundColor Green
$toAdd | ForEach-Object { Write-Host " + $_ = $($sourceItems[$_].Substring(0, [Math]::Min(60, $sourceItems[$_].Length)))" }
}

if ($toUpdate.Count -gt 0) {
Write-Host "[更新] 以下配置将在目标环境更新:" -ForegroundColor Yellow
$toUpdate | ForEach-Object {
Write-Host " ~ $_"
Write-Host " 旧值: $($targetItems[$_].Substring(0, [Math]::Min(50, $targetItems[$_].Length)))"
Write-Host " 新值: $($sourceItems[$_].Substring(0, [Math]::Min(50, $sourceItems[$_].Length)))"
}
}

if ($toRemove.Count -gt 0) {
Write-Host "[删除] 以下配置将从目标环境移除:" -ForegroundColor Red
$toRemove | ForEach-Object { Write-Host " - $_" }
}

if ($DryRun) {
Write-Host "`n[试运行模式] 未执行任何变更。移除 -DryRun 参数以实际执行同步。" -ForegroundColor Magenta
return
}

if (-not $Force) {
$confirm = Read-Host "`n确认执行同步?(输入 YES 确认)"
if ($confirm -ne 'YES') {
Write-Host "已取消同步操作。" -ForegroundColor Yellow
return
}
}

# 执行新增和更新
$changeCount = 0
foreach ($key in ($toAdd + $toUpdate)) {
$body = @{
key = $key
value = $sourceItems[$key]
label = $TargetLabel
} | ConvertTo-Json

Invoke-RestMethod -Uri "$TargetEndpoint/kv/$key?label=$TargetLabel" `
-Headers $headers -Method Put -Body $body
$changeCount++
}

Write-Host "`n同步完成!共处理 $changeCount 个配置项。" -ForegroundColor Green
}

# 导出配置为本地 JSON 文件(用于备份或版本控制)
function Export-AppConfigToJson {
param(
[string]$Endpoint,
[string]$Label,
[string]$OutputPath = "./appconfig-backup.json"
)

$token = (az account get-access-token --resource "https://azconfig.io" --query accessToken --output tsv)
$headers = @{ Authorization = "Bearer $token" }

$labelParam = if ($Label) { "?label=$Label" } else { '' }
$response = Invoke-RestMethod -Uri "$Endpoint/kv$labelParam" -Headers $headers -Method Get

$export = $response.items | ForEach-Object {
[PSCustomObject]@{
key = $_.key
value = $_.value
label = $_.label
etag = $_.etag
}
}

$export | ConvertTo-Json -Depth 5 | Out-File -FilePath $OutputPath -Encoding utf8
Write-Host "已导出 $($export.Count) 个配置项到 $OutputPath" -ForegroundColor Green
}

# 使用示例
$devEndpoint = "https://myappconfig-dev.azconfig.io"
$stagingEndpoint = "https://myappconfig-staging.azconfig.io"

# 先试运行查看差异
Sync-AppConfigEnvironment -SourceEndpoint $devEndpoint `
-TargetEndpoint $stagingEndpoint `
-SourceLabel "dev" -TargetLabel "staging" -DryRun

# 确认无误后执行同步
Sync-AppConfigEnvironment -SourceEndpoint $devEndpoint `
-TargetEndpoint $stagingEndpoint `
-SourceLabel "dev" -TargetLabel "staging" -Force

# 导出生产配置为 JSON 备份
Export-AppConfigToJson -Endpoint "https://myappconfig.azconfig.io" `
-Label "prod" -OutputPath "./config-backup-prod-20260303.json"

执行结果示例:

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
正在从源环境读取配置 (标签: dev)...
读取到 24 个配置项
正在从目标环境读取配置 (标签: staging)...
读取到 22 个配置项

===== 配置同步差异报告 =====
源: dev → 目标: staging
新增: 3 项 | 更新: 2 项 | 删除: 1 项
============================

[新增] 以下配置将添加到目标环境:
+ Cache:RedisUrl = redis-staging.cache.windows.net:6380
+ Feature:NewSearch = true
+ Logging:StructuredJson = true
[更新] 以下配置将在目标环境更新:
~ Database:CommandTimeout
旧值: 30
新值: 60
~ Api:RateLimitPerMinute
旧值: 100
新值: 200
[删除] 以下配置将从目标环境移除:
- Legacy:OldApiEndpoint

同步完成!共处理 5 个配置项。
已导出 22 个配置项到 ./config-backup-prod-20260303.json

同步脚本采用”先报告后执行”的模式:先通过 DryRun 参数生成差异报告,确认无误后再实际执行。这种方式可以有效防止误操作,尤其是在生产环境同步时更为重要。导出的 JSON 文件可以纳入 Git 版本控制,实现配置的代码化管理。

注意事项

  1. 访问权限控制:App Configuration 支持 Azure RBAC 和访问密钥两种认证方式。生产环境建议使用托管标识(Managed Identity)配合 RBAC,避免在脚本中硬编码连接字符串或访问密钥。

  2. 配置值的加密:虽然 App Configuration 在传输和存储时都会加密,但敏感值(如数据库密码、API 密钥)应配合 Azure Key Vault 使用。App Configuration 支持通过 Key Vault 引用将敏感值存储在 Key Vault 中,配置中只保存引用指针。

  3. 标签命名规范:建议统一使用环境名称作为标签(如 devstagingprod),避免混用不同维度的标签。如果需要多维度分类(如环境 + 区域),建议在键名中体现区域信息,保持标签维度的单一性。

  4. Feature Flag 的清理:长期未使用的 Feature Flag 应及时清理。可以编写定时脚本扫描所有 Feature Flag,标记超过 90 天未变更且处于启用状态的 Flag,通知相关团队评估是否可以移除。

  5. 同步操作的原子性:跨环境同步不具备事务特性,如果中途失败可能导致部分配置已更新而部分未更新的不一致状态。建议在同步前先导出目标环境的配置作为备份,失败时可以从备份恢复。

  6. 网络延迟与重试策略:App Configuration 的 REST API 调用可能因网络波动失败。在生产脚本中应加入重试逻辑(如指数退避),并设置合理的超时时间。可以使用 PowerShell 的 $MaximumRetryCount$RetryIntervalSec 参数简化重试实现。

PowerShell 技能连载 - 配置管理模式

适用于 PowerShell 5.1 及以上版本

几乎所有脚本都涉及配置管理——数据库连接字符串、API 端点、超时设置、日志级别。如何存储、加载、验证、合并配置,直接影响脚本的可维护性和灵活性。硬编码的配置难以适应不同环境,缺少验证的配置导致运行时错误,没有合并机制的配置无法覆盖默认值。

本文将讲解 PowerShell 中的配置管理模式,从简单的配置文件到多层配置合并。

配置文件格式

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
# 方式 1:JSON 配置文件
$jsonConfig = @"
{
"appName": "MyApp",
"version": "2.5.0",
"database": {
"server": "prod-db01",
"port": 5432,
"name": "myapp_production",
"timeout": 30
},
"logging": {
"level": "Warning",
"path": "C:\\Logs\\MyApp",
"maxSizeMB": 100
},
"features": {
"enableCache": true,
"enableMetrics": true,
"maxRetries": 3
}
}
"@

$jsonConfig | Set-Content "C:\MyApp\config.json" -Encoding UTF8

# 加载 JSON 配置
$config = Get-Content "C:\MyApp\config.json" -Raw | ConvertFrom-Json
Write-Host "应用名:$($config.appName)"
Write-Host "数据库:$($config.database.server):$($config.database.port)"
Write-Host "日志级别:$($config.logging.level)"

# 方式 2:PSD1 配置文件(PowerShell 数据文件)
$psd1Config = @"
@{
AppName = 'MyApp'
Version = '2.5.0'
Database = @{
Server = 'prod-db01'
Port = 5432
Name = 'myapp_production'
Timeout = 30
}
Logging = @{
Level = 'Warning'
Path = 'C:\Logs\MyApp'
MaxSizeMB = 100
}
}
"@

$psd1Config | Set-Content "C:\MyApp\config.psd1" -Encoding UTF8

# 加载 PSD1 配置
$psd1Data = Invoke-Expression (Get-Content "C:\MyApp\config.psd1" -Raw)
Write-Host "`nPSD1 数据库配置:$($psd1Data.Database.Server)"

# 方式 3:环境变量配置
$env:MYAPP_DB_SERVER = "prod-db01"
$env:MYAPP_DB_PORT = "5432"
$env:MYAPP_LOG_LEVEL = "Debug"
Write-Host "环境变量配置:$env:MYAPP_DB_SERVER`:$env:MYAPP_DB_PORT"

执行结果示例:

1
2
3
4
5
6
应用名:MyApp
数据库:prod-db01:5432
日志级别:Warning

PSD1 数据库配置:prod-db01
环境变量配置:prod-db01:5432

多层配置合并

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
# 多层配置:默认值 < 配置文件 < 环境变量 < 命令行参数
function Get-ApplicationConfig {
param(
[string]$ConfigPath = "C:\MyApp\config.json",
[string]$Environment = "production"
)

# 第一层:默认配置
$defaults = @{
appName = "MyApp"
version = "1.0.0"
database = @{
server = "localhost"
port = 5432
name = "myapp"
timeout = 30
}
logging = @{
level = "Info"
path = "C:\Logs\MyApp"
maxSizeMB = 50
}
features = @{
enableCache = $true
enableMetrics = $false
maxRetries = 3
}
}

# 第二层:配置文件
if (Test-Path $ConfigPath) {
$fileConfig = Get-Content $ConfigPath -Raw | ConvertFrom-Json
$defaults = Merge-Config $defaults (ConvertTo-Hashtable $fileConfig)
Write-Host "已加载配置文件:$ConfigPath" -ForegroundColor Green
}

# 第三层:环境特定配置
$envConfigPath = $ConfigPath -replace '\.json$', ".$Environment.json"
if (Test-Path $envConfigPath) {
$envConfig = Get-Content $envConfigPath -Raw | ConvertFrom-Json
$defaults = Merge-Config $defaults (ConvertTo-Hashtable $envConfig)
Write-Host "已加载环境配置:$envConfigPath" -ForegroundColor Green
}

# 第四层:环境变量覆盖
$envPrefix = "MYAPP_"
Get-ChildItem Env: | Where-Object { $_.Name -like "$envPrefix*" } | ForEach-Object {
$key = $_.Name.Substring($envPrefix.Length).Replace("_", ".")
Set-NestedValue -Config $defaults -Path $key -Value $_.Value
}

return $defaults
}

# 辅助函数:深度合并
function Merge-Config {
param($Base, $Override)

$result = @{}
foreach ($key in $Base.Keys) { $result[$key] = $Base[$key] }

foreach ($key in $Override.Keys) {
if ($result[$key] -is [hashtable] -and $Override[$key] -is [hashtable]) {
$result[$key] = Merge-Config $result[$key] $Override[$key]
} else {
$result[$key] = $Override[$key]
}
}
return $result
}

function ConvertTo-Hashtable {
param([Parameter(Mandatory)][object]$InputObject)
if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
$hash = @{}
$InputObject.PSObject.Properties | ForEach-Object {
$hash[$_.Name] = ConvertTo-Hashtable $_.Value
}
return $hash
}
return $InputObject
}

function Set-NestedValue {
param($Config, [string]$Path, $Value)

$keys = $Path -split '\.'
$current = $Config
for ($i = 0; $i -lt $keys.Count - 1; $i++) {
if (-not $current[$keys[$i]]) {
$current[$keys[$i]] = @{}
}
$current = $current[$keys[$i]]
}
# 尝试类型转换
$lastKey = $keys[-1]
if ($Value -match '^\d+$') { $Value = [int]$Value }
elseif ($Value -match '^(true|false)$') { $Value = $Value -eq 'true' }
$current[$lastKey] = $Value
}

# 使用多层配置
$config = Get-ApplicationConfig -ConfigPath "C:\MyApp\config.json" -Environment "production"
Write-Host "`n最终配置:" -ForegroundColor Cyan
Write-Host " 应用:$($config.appName) v$($config.version)"
Write-Host " 数据库:$($config.database.server):$($config.database.port)/$($config.database.name)"
Write-Host " 日志级别:$($config.logging.level)"

执行结果示例:

1
2
3
4
5
已加载配置文件:C:\MyApp\config.json
最终配置:
应用:MyApp v2.5.0
数据库:prod-db01:5432/myapp_production
日志级别:Warning

配置验证

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
# 配置验证器
function Test-ConfigSchema {
param(
[Parameter(Mandatory)]
[hashtable]$Config,

[hashtable]$Schema
)

$errors = @()

foreach ($key in $Schema.Keys) {
$rule = $Schema[$key]

# 检查必填项
if ($rule.Required -and -not $Config.ContainsKey($key)) {
$errors += "缺少必填配置:$key"
continue
}

if (-not $Config.ContainsKey($key)) { continue }

$value = $Config[$key]

# 类型检查
if ($rule.Type -and $value -isnot $rule.Type) {
$errors += "$key 类型错误:期望 $($rule.Type.Name),实际 $($value.GetType().Name)"
}

# 范围检查
if ($rule.Min -and $value -lt $rule.Min) {
$errors += "$key$value 小于最小值 $($rule.Min)"
}
if ($rule.Max -and $value -gt $rule.Max) {
$errors += "$key$value 大于最大值 $($rule.Max)"
}

# 枚举检查
if ($rule.ValidValues -and $value -notin $rule.ValidValues) {
$errors += "$key 值 '$value' 不在允许列表中:$($rule.ValidValues -join ', ')"
}

# 正则检查
if ($rule.Pattern -and $value -notmatch $rule.Pattern) {
$errors += "$key 值 '$value' 不匹配模式:$($rule.Pattern)"
}
}

if ($errors) {
Write-Host "配置验证失败:" -ForegroundColor Red
$errors | ForEach-Object { Write-Host " ! $_" -ForegroundColor Red }
return $false
}

Write-Host "配置验证通过" -ForegroundColor Green
return $true
}

# 定义配置模式
$schema = @{
appName = @{ Required = $true; Type = [string] }
version = @{ Required = $true; Pattern = '^\d+\.\d+\.\d+$' }
database = @{
Required = $true
}
logging = @{
Required = $false
}
}

$validConfig = @{
appName = "MyApp"
version = "2.5.0"
database = @{ server = "localhost" }
}

Test-ConfigSchema -Config $validConfig -Schema $schema

$invalidConfig = @{
appName = 123
version = "invalid"
}

Test-ConfigSchema -Config $invalidConfig -Schema $schema

执行结果示例:

1
2
3
4
5
配置验证通过
配置验证失败:
! appName 类型错误:期望 String,实际 Int32
! version 值 'invalid' 不匹配模式:^\d+\.\d+\.\d+$
! 缺少必填配置:database

注意事项

  1. 敏感配置:密码、Token 等敏感配置不应存储在配置文件中,使用环境变量或密钥管理服务
  2. 配置优先级:明确文档化配置层的优先级,让用户知道哪个配置源优先生效
  3. 默认值:所有配置都应有合理的默认值,确保脚本在缺少配置文件时仍能运行
  4. 配置热加载:长时间运行的服务应支持配置热加载,无需重启
  5. PSD1 安全Invoke-Expression 加载 PSD1 存在安全风险,生产环境使用 Import-PowerShellDataFile
  6. 环境隔离:不同环境(开发、测试、生产)的配置应完全独立,避免误操作

PowerShell 技能连载 - YAML 与 TOML 配置处理

适用于 PowerShell 7.0 及以上版本

YAML 和 TOML 是现代配置文件的两大主流格式。YAML 以其可读性和丰富的数据类型支持,被 Kubernetes、Docker Compose、GitHub Actions 等广泛采用;TOML 则以简洁明了著称,是 Rust、Python 项目的首选配置格式。PowerShell 原生支持 JSON 和 XML,但处理 YAML 和 TOML 需要借助第三方模块。本文将讲解如何在 PowerShell 中高效处理这两种配置格式。

YAML 处理

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
# 安装 YamlDotNet 模块
Install-Module -Name powershell-yaml -Scope CurrentUser -Force

# 解析 YAML 字符串
$yaml = @"
server:
host: 0.0.0.0
port: 8080
ssl:
enabled: true
cert: /etc/ssl/cert.pem
key: /etc/ssl/key.pem

database:
host: db.internal
port: 5432
name: myapp
pool:
min: 5
max: 20
timeout: 30

logging:
level: info
file: /var/log/myapp.log
rotation:
max_size: 100MB
max_files: 10
"@

$config = $yaml | ConvertFrom-Yaml

# 访问配置值
Write-Host "服务器端口:$($config.server.port)"
Write-Host "SSL 启用:$($config.server.ssl.enabled)"
Write-Host "数据库连接池最大值:$($config.database.pool.max)"

# 读取 YAML 文件
$k8sDeployment = Get-Content "k8s/deployment.yaml" -Raw | ConvertFrom-Yaml
$k8sDeployment.metadata.name
$k8sDeployment.spec.replicas

# 修改 YAML 配置
$config.server.port = 9090
$config.logging.level = "debug"
$config.database.pool.max = 50

# 导出为 YAML
$updatedYaml = $config | ConvertTo-Yaml
Write-Host "更新后的配置:" -ForegroundColor Cyan
Write-Host $updatedYaml

执行结果示例:

1
2
3
4
5
6
7
8
9
10
服务器端口:8080
SSL 启用:True
数据库连接池最大值:20
更新后的配置:
server:
host: 0.0.0.0
port: 9090
ssl:
enabled: true
cert: /etc/ssl/cert.pem

YAML 配置文件管理

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
function Get-YamlConfig {
<#
.SYNOPSIS
读取 YAML 配置文件并支持环境变量替换
#>
param(
[Parameter(Mandatory)]
[string]$Path,

[string]$Environment
)

$content = Get-Content $Path -Raw
$config = $content | ConvertFrom-Yaml

# 环境变量替换 ${VAR_NAME} 格式
$config | ConvertTo-Yaml | ForEach-Object {
$pattern = '\$\{(\w+)\}'
while ($_ -match $pattern) {
$varName = $Matches[1]
$varValue = [System.Environment]::GetEnvironmentVariable($varName) ?? ''
$_ = $_ -replace "\`\{$varName\}", $varValue
}
}

return $config
}

# 合并多层配置
function Merge-YamlConfig {
param(
[string[]]$ConfigPaths
)

$merged = @{}

foreach ($path in $ConfigPaths) {
if (Test-Path $path) {
$config = Get-Content $path -Raw | ConvertFrom-Yaml
foreach ($key in $config.Keys) {
if ($merged[$key] -is [hashtable] -and $config[$key] -is [hashtable]) {
$merged[$key] = Merge-Hashtables $merged[$key] $config[$key]
} else {
$merged[$key] = $config[$key]
}
}
}
}

return $merged
}

# 按优先级合并配置:默认 < 环境 < 本地覆盖
$config = Merge-YamlConfig -ConfigPaths @(
"config/default.yaml"
"config/production.yaml"
"config/local.yaml"
)

执行结果示例:

1
# 配置合并按路径优先级覆盖

TOML 处理

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
# 安装 TOML 模块
Install-Module -Name PSToml -Scope CurrentUser -Force

# 解析 TOML 字符串
$toml = @"
[server]
host = "0.0.0.0"
port = 8080

[server.ssl]
enabled = true
cert = "/etc/ssl/cert.pem"

[database]
host = "db.internal"
port = 5432
name = "myapp"

[database.pool]
min = 5
max = 20

[logging]
level = "info"
file = "/var/log/myapp.log"

[[logging.rotation]]
max_size = "100MB"
max_files = 10
"@

$config = $toml | ConvertFrom-Toml

# 访问配置值
Write-Host "服务器端口:$($config.server.port)"
Write-Host "SSL 启用:$($config.server.ssl.enabled)"

# 读取 TOML 文件(如 Cargo.toml, pyproject.toml)
$cargoConfig = Get-Content "Cargo.toml" -Raw | ConvertFrom-Toml
Write-Host "项目名称:$($cargoConfig.package.name)"
Write-Host "版本:$($cargoConfig.package.version)"

# 修改并保存
$config.server.port = 9090
$updatedToml = $config | ConvertTo-Toml
Set-Content -Path "config_updated.toml" -Value $updatedToml

执行结果示例:

1
2
3
4
服务器端口:8080
SSL 启用:True
项目名称:my-rust-project
版本:0.1.0

格式转换工具

在配置管理中经常需要在不同格式间转换:

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
function Convert-ConfigFormat {
<#
.SYNOPSIS
在 JSON、YAML、TOML 之间转换配置格式
#>
param(
[Parameter(Mandatory)]
[string]$InputPath,

[ValidateSet('json', 'yaml', 'toml')]
[string]$From,

[ValidateSet('json', 'yaml', 'toml')]
[string]$To,

[string]$OutputPath
)

# 读取源文件
$content = Get-Content $InputPath -Raw
$data = switch ($From) {
'json' { $content | ConvertFrom-Json -AsHashtable }
'yaml' { $content | ConvertFrom-Yaml }
'toml' { $content | ConvertFrom-Toml }
}

# 转换为目标格式
$output = switch ($To) {
'json' { $data | ConvertTo-Json -Depth 10 }
'yaml' { $data | ConvertTo-Yaml }
'toml' { $data | ConvertTo-Toml }
}

# 输出
if ($OutputPath) {
Set-Content -Path $OutputPath -Value $output -Encoding UTF8
Write-Host "已转换:$InputPath ($From) => $OutputPath ($To)" -ForegroundColor Green
} else {
Write-Output $output
}
}

# YAML 转 JSON
Convert-ConfigFormat -InputPath "docker-compose.yaml" -From yaml -To json -OutputPath "docker-compose.json"

# TOML 转 YAML
Convert-ConfigFormat -InputPath "Cargo.toml" -From toml -To yaml -OutputPath "Cargo.yaml"

执行结果示例:

1
2
已转换:docker-compose.yaml (yaml) => docker-compose.json (json)
已转换:Cargo.toml (toml) => Cargo.yaml (yaml)

注意事项

  1. YAML 缩进:YAML 对缩进非常敏感,使用空格而非 Tab。生成 YAML 时注意保持一致的缩进风格
  2. TOML 类型限制:TOML 不支持 null 值,所有键必须有值
  3. 模块兼容性powershell-yamlPSToml 是社区模块,注意版本兼容性
  4. 大文件性能:YAML 解析比 JSON 慢,大型配置文件考虑使用 JSON 格式
  5. 安全风险:YAML 的锚点(anchor)和合并(merge)特性可能被利用,解析不受信任的 YAML 时需注意
  6. 配置验证:使用 JSON Schema 或自定义脚本验证配置文件的完整性和合法性

PowerShell 技能连载 - XML 处理与配置管理

适用于 PowerShell 5.1 及以上版本

XML 是 Windows 生态中最常见的配置格式——从 web.config、app.config 到 NuGet 的 packages.config,从 WIX 安装配置到 MSBuild 项目文件,XML 无处不在。PowerShell 对 XML 有一流的内置支持,[xml] 类型加速器可以将 XML 文档直接转换为可导航的对象图,比传统的正则表达式解析简单得多。

本文将讲解 XML 的读取、查询、修改和创建,以及常见的配置文件管理场景。

XML 文档读取

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
# 使用 [xml] 类型加速器解析 XML
$xml = [xml]@"
<configuration>
<appSettings>
<add key="AppName" value="MyApp" />
<add key="Version" value="2.5.0" />
<add key="Debug" value="true" />
</appSettings>
<connectionStrings>
<add name="Default" connectionString="Server=localhost;Database=mydb;" />
<add name="Audit" connectionString="Server=audit-db;Database=audit;" />
</connectionStrings>
<system.web>
<compilation debug="true" targetFramework="4.8" />
<httpRuntime targetFramework="4.8" maxRequestLength="4096" />
</system.web>
</configuration>
"@

# 通过点号导航 XML 节点
$xml.configuration.appSettings.add | ForEach-Object {
[PSCustomObject]@{ Key = $_.key; Value = $_.value }
} | Format-Table -AutoSize

# 读取连接字符串
$xml.configuration.connectionStrings.add |
Select-Object name, connectionString |
Format-Table -AutoSize

# 从文件加载 XML
$configPath = "C:\Projects\MyApp\web.config"
$config = [xml](Get-Content $configPath)
Write-Host "已加载配置文件:$configPath"

# 读取特定配置值
$appName = $config.configuration.appSettings.add |
Where-Object { $_.key -eq 'AppName' } |
Select-Object -ExpandProperty value
Write-Host "应用名称:$appName"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
Key     Value
--- -----
AppName MyApp
Version 2.5.0
Debug true

name connectionString
---- ----------------
Default Server=localhost;Database=mydb;
Audit Server=audit-db;Database=audit;

应用名称:MyApp

XPath 查询

XPath 是 XML 的查询语言,适合在复杂 XML 结构中精确查找节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用 Select-XmlNode 执行 XPath 查询
$nav = $xml.CreateNavigator()

# 查找所有 add 节点
$nodes = $xml.SelectNodes('//add')
$nodes | ForEach-Object { "$($_.key) = $($_.value)" }

# 精确查找特定 key
$debugNode = $xml.SelectSingleNode("//add[@key='Debug']")
Write-Host "Debug 设置:$($debugNode.value)"

# 查找所有带 connectionString 的节点
$connections = $xml.SelectNodes("//connectionStrings/add")
$connections | ForEach-Object {
Write-Host " $($_.name): $($_.connectionString)"
}

# 使用 PowerShell 的 XPath 更复杂的查询
$allAttributes = $xml.SelectNodes('//add/@*') |
ForEach-Object { "$($_.ParentNode.key).$($_.Name) = $($_.Value)" }

$allAttributes | ForEach-Object { Write-Host $_ }

执行结果示例:

1
2
3
4
5
6
AppName = MyApp
Version = 2.5.0
Debug = true
Debug 设置:true
Default: Server=localhost;Database=mydb;
Audit: Server=audit-db;Database=audit;

修改 XML 配置

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
function Set-AppConfig {
<#
.SYNOPSIS
修改 .NET 应用配置文件
#>
param(
[Parameter(Mandatory)]
[string]$Path,

[Parameter(Mandatory)]
[hashtable]$Settings,

[switch]$Backup
)

# 备份原文件
if ($Backup) {
Copy-Item $Path "$Path.bak" -Force
Write-Host "已备份:$Path.bak" -ForegroundColor Green
}

# 加载 XML
$xml = [xml](Get-Content $Path)

# 修改配置值
foreach ($key in $Settings.Keys) {
$node = $xml.SelectSingleNode("//appSettings/add[@key='$key']")
if ($node) {
$oldValue = $node.value
$node.value = $Settings[$key]
Write-Host "$key : $oldValue => $($Settings[$key])" -ForegroundColor Cyan
} else {
# 如果 key 不存在,创建新节点
$newNode = $xml.CreateElement('add')
$newNode.SetAttribute('key', $key)
$newNode.SetAttribute('value', $Settings[$key])
$xml.configuration.appSettings.AppendChild($newNode) | Out-Null
Write-Host "$key : (新增) => $($Settings[$key])" -ForegroundColor Yellow
}
}

# 保存
$xml.Save($Path)
Write-Host "配置已保存:$Path" -ForegroundColor Green
}

# 修改应用配置
Set-AppConfig -Path "C:\Projects\MyApp\web.config" -Backup -Settings @{
Debug = 'false'
LogLevel = 'Warning'
CacheTime = '3600'
}

执行结果示例:

1
2
3
4
5
已备份:C:\Projects\MyApp\web.config.bak
Debug : true => false
LogLevel : (新增) => Warning
CacheTime : (新增) => 3600
配置已保存

创建 XML 文档

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
# 方法一:使用 XmlDocument 创建
$doc = [System.Xml.XmlDocument]::new()
$doc.AppendChild($doc.CreateXmlDeclaration('1.0', 'UTF-8', $null)) | Out-Null

$root = $doc.CreateElement('servers')
$doc.AppendChild($root) | Out-Null

$serverData = @(
@{ Name = 'WEB-01'; IP = '192.168.1.101'; Role = 'Web'; Enabled = 'true' }
@{ Name = 'WEB-02'; IP = '192.168.1.102'; Role = 'Web'; Enabled = 'true' }
@{ Name = 'DB-01'; IP = '192.168.1.201'; Role = 'Database'; Enabled = 'true' }
)

foreach ($srv in $serverData) {
$serverNode = $doc.CreateElement('server')
$serverNode.SetAttribute('name', $srv.Name)
$serverNode.SetAttribute('ip', $srv.IP)
$serverNode.SetAttribute('role', $srv.Role)
$serverNode.SetAttribute('enabled', $srv.Enabled)
$root.AppendChild($serverNode) | Out-Null
}

$doc.Save('C:\Config\servers.xml')
Write-Host "XML 配置文件已创建" -ForegroundColor Green

# 方法二:使用 here-string 快速生成简单 XML
$manifest = @"
<?xml version="1.0" encoding="UTF-8"?>
<application>
<metadata>
<name>MyApp</name>
<version>2.5.0</version>
<author>DevOps Team</author>
</metadata>
<dependencies>
<dependency name="SqlServer" version="2019+" />
<dependency name="IIS" version="10.0" />
<dependency name=".NET" version="8.0" />
</dependencies>
</application>
"@

Set-Content -Path "C:\Config\manifest.xml" -Value $manifest -Encoding UTF8

执行结果示例:

1
XML 配置文件已创建

注意事项

  1. XML 声明编码:保存 XML 时使用 $xml.Save() 方法会正确处理编码,避免使用 Out-File 导致编码问题
  2. 命名空间处理:带命名空间的 XML(如 WSDL、SVG)需要使用 XmlNamespaceManager 进行 XPath 查询
  3. 大型 XML 文件:处理大 XML 时使用 XmlReader(流式读取)代替 [xml](全部加载到内存)
  4. 空节点处理:XML 中的空节点和自闭合节点 <node /><node></node> 是等价的
  5. 特殊字符转义:XML 中 <, >, &, ', " 需要转义,使用 $xml.CreateTextNode() 自动处理
  6. 配置文件热更新:修改 IIS 或应用配置后,可能需要重启应用池使配置生效

PowerShell 技能连载 - JSON 与 YAML 配置管理

适用于 PowerShell 7.0 及以上版本

在 DevOps 和基础设施即代码的实践中,配置文件管理是核心能力。无论是应用部署、容器编排还是 CI/CD 流水线,JSON 和 YAML 格式的配置文件无处不在。PowerShell 原生支持 JSON 的读写与转换,配合 powershell-yaml 模块也能轻松处理 YAML,包括 Kubernetes 风格的多文档格式。

本文将从实际场景出发,逐步介绍如何用 PowerShell 完成 JSON 配置读取与修改、YAML 解析、Schema 验证、模板渲染以及环境配置切换。

读取 JSON 配置

日常运维中,我们经常需要从 JSON 文件中提取特定的配置项。PowerShell 的 ConvertFrom-Json 可以将 JSON 文本转换为对象,然后用属性访问语法直接取值。下面封装了一个通用函数,支持按层级路径读取指定区段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 读取 JSON 配置文件,支持按层级路径提取指定区段
function Get-JsonConfig {
param(
[Parameter(Mandatory)]
[string]$Path,

[string[]]$Sections
)

# 读取原始内容并转换为 PowerShell 对象
$json = Get-Content $Path -Raw | ConvertFrom-Json

# 如果指定了区段路径,逐层深入取值
if ($Sections) {
$result = $json
foreach ($section in $Sections) {
$result = $result.$section
}
return $result
}

return $json
}

假设我们有一个应用配置文件 appsettings.json,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"server": {
"host": "0.0.0.0",
"port": 8080
},
"database": {
"host": "db.internal",
"port": 5432,
"name": "app_prod"
},
"logging": {
"level": "INFO",
"path": "/var/log/app.log"
}
}

读取整个配置或某个区段:

1
2
3
4
5
6
7
# 读取完整配置
$config = Get-JsonConfig -Path ".\appsettings.json"
$config.database.host

# 只读取数据库区段
$dbConfig = Get-JsonConfig -Path ".\appsettings.json" -Sections "database"
$dbConfig

执行结果示例:

1
2
3
4
5
db.internal

host port name
---- ---- ----
db.internal 5432 app_prod

修改 JSON 配置

读取只是第一步,更多时候我们需要修改配置项并写回文件。下面的函数支持用点号分隔的路径(如 database.host)来定位嵌套属性,并可选地在修改前备份原文件。

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
# 修改 JSON 配置,支持点号路径定位嵌套属性
function Set-JsonConfig {
param(
[Parameter(Mandatory)]
[string]$Path,

[Parameter(Mandatory)]
[hashtable]$Properties,

[switch]$Backup
)

# 可选:修改前备份原文件
if ($Backup) {
Copy-Item $Path "$Path.bak.$(Get-Date -Format 'yyyyMMddHHmmss')"
}

$json = Get-Content $Path -Raw | ConvertFrom-Json

# 遍历每个要修改的属性
foreach ($key in $Properties.Keys) {
$parts = $key -split '\.'
$obj = $json
# 逐层导航到父对象
for ($i = 0; $i -lt $parts.Count - 1; $i++) {
$obj = $obj.$($parts[$i])
}
# 设置最终属性值
$obj.$($parts[-1]) = $Properties[$key]
}

# 写回文件,深度设为 10 层以覆盖大多数嵌套结构
$json | ConvertTo-Json -Depth 10 | Set-Content $Path -Encoding UTF8
}

将数据库主机和日志级别修改为新值:

1
2
3
4
Set-JsonConfig -Path ".\appsettings.json" -Backup -Properties @{
"database.host" = "db-new.internal"
"logging.level" = "DEBUG"
}

执行结果示例:

1
2
3
4
5
6
7
# 原文件已备份为 appsettings.json.bak.20250402100000
# appsettings.json 中的值已更新
Get-Content .\appsettings.json | ConvertFrom-Json | Select-Object -ExpandProperty database

host port name
---- ---- ----
db-new.internal 5432 app_prod

读取 YAML 配置

YAML 在 Kubernetes、CI/CD 等场景中广泛使用。PowerShell 本身不支持 YAML,但 powershell-yaml 模块弥补了这个缺口。下面的函数封装了 YAML 读取逻辑,并在模块缺失时给出友好提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 读取 YAML 配置文件(需先安装 powershell-yaml 模块)
function Get-YamlConfig {
param(
[Parameter(Mandatory)]
[string]$Path
)

# 检查模块是否已安装
if (-not (Get-Module -ListAvailable -Name powershell-yaml)) {
Write-Warning "需要安装 powershell-yaml 模块: Install-Module powershell-yaml -Scope CurrentUser"
return $null
}

Import-Module powershell-yaml
$content = Get-Content $Path -Raw
return $content | ConvertFrom-Yaml
}

使用方式很简单:

1
2
3
4
5
6
# 首次使用前安装模块(只需执行一次)
# Install-Module -Name powershell-yaml -Scope CurrentUser

# 读取 YAML 配置
$config = Get-YamlConfig -Path ".\config.yaml"
$config.server.host

执行结果示例:

1
0.0.0.0

解析 Kubernetes 多文档 YAML

Kubernetes 的资源清单文件通常包含多个文档,用 --- 分隔。标准的 YAML 解析器只处理第一个文档,因此需要先分割再逐一解析。下面的函数会将每个文档提取出 kindnamenamespace 等关键信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 读取 Kubernetes 风格的多文档 YAML,提取每个资源的元数据
function Get-YamlMultiDocument {
param([Parameter(Mandatory)][string]$Path)

Import-Module powershell-yaml
$content = Get-Content $Path -Raw

# 按 --- 分割多文档,过滤空行
$documents = $content -split '(?m)^---\s*$' | Where-Object { $_.Trim() }

foreach ($doc in $documents) {
$yaml = $doc | ConvertFrom-Yaml
[PSCustomObject]@{
Kind = $yaml.kind
Name = $yaml.metadata.name
Namespace = $yaml.metadata.namespace
Content = $yaml
}
}
}

假设有一个 deploy.yaml 包含 Namespace 和 Deployment 两个资源:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Namespace
metadata:
name: my-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
namespace: my-app
spec:
replicas: 3

解析结果如下:

1
Get-YamlMultiDocument -Path ".\deploy.yaml" | Format-Table Kind, Name, Namespace

执行结果示例:

1
2
3
4
Kind       Name        Namespace
---- ---- ---------
Namespace my-app
Deployment web-server my-app

Schema 验证

配置项的错误往往在运行时才暴露,提前做 Schema 验证可以有效减少故障。下面的函数支持必填检查、类型校验、长度限制、枚举值和正则匹配等规则。

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
# 对配置对象执行 Schema 验证
function Test-ConfigSchema {
param(
[Parameter(Mandatory)]
[PSCustomObject]$Config,

[Parameter(Mandatory)]
[hashtable]$Schema
)

$errors = @()

foreach ($rule in $Schema.GetEnumerator()) {
$key = $rule.Key
$constraints = $rule.Value
$value = $Config.$key

# 必填检查
if ($constraints.Required -and -not $value) {
$errors += "缺少必填字段: $key"
continue
}

if ($null -ne $value) {
# 类型检查
if ($constraints.Type -and $value.GetType().Name -ne $constraints.Type) {
$errors += "$key 类型错误: 期望 $($constraints.Type), 实际 $($value.GetType().Name)"
}
# 最小长度检查
if ($constraints.MinLength -and $value.Length -lt $constraints.MinLength) {
$errors += "$key 长度不足: 最小 $($constraints.MinLength)"
}
# 枚举值检查
if ($constraints.AllowedValues -and $value -notin $constraints.AllowedValues) {
$errors += "$key 值非法: $value, 允许值: $($constraints.AllowedValues -join ', ')"
}
# 正则匹配检查
if ($constraints.Pattern -and $value -notmatch $constraints.Pattern) {
$errors += "$key 格式不匹配: $($constraints.Pattern)"
}
}
}

if ($errors) {
Write-Host "配置验证失败:" -ForegroundColor Red
$errors | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
return $false
}

Write-Host "配置验证通过" -ForegroundColor Green
return $true
}

定义验证规则并执行检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义 Schema 规则
$schema = @{
serverName = @{ Required = $true; Type = "String"; MinLength = 3 }
port = @{ Required = $true; Type = "Int32" }
environment = @{ Required = $true; AllowedValues = @("dev", "staging", "prod") }
database = @{ Required = $true; Pattern = "^[a-z][a-z0-9_]+$" }
}

# 构造配置对象(这里故意写错 environment 值)
$config = [PSCustomObject]@{
serverName = "prod-sql-01"
port = 1433
environment = "production"
database = "app_db"
}

Test-ConfigSchema -Config $config -Schema $schema

执行结果示例:

1
2
3
配置验证失败:
- environment 值非法: production, 允许值: dev, staging, prod
False

模板渲染

在管理 Nginx、HAProxy 等配置时,硬编码不利于多环境复用。模板渲染可以将占位符替换为实际值,还支持条件块来控制内容是否输出。

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
# 渲染配置模板,支持变量替换和条件块
function Resolve-ConfigTemplate {
param(
[Parameter(Mandatory)]
[string]$TemplatePath,

[Parameter(Mandatory)]
[hashtable]$Variables,

[string]$OutputPath
)

$template = Get-Content $TemplatePath -Raw

# 替换 {{变量名}} 占位符
foreach ($var in $Variables.GetEnumerator()) {
$template = $template -replace "\{\{$($var.Key)\}\}", $var.Value
}

# 处理条件块 {{#if VAR}}...{{/if}}
$template = [regex]::Replace(
$template,
'\{\{#if\s+(\w+)\}\}(.*?)\{\{/if\}\}',
{
param($m)
if ($Variables[$m.Groups[1].Value]) { $m.Groups[2].Value } else { "" }
},
[System.Text.RegularExpressions.RegexOptions]::Singleline
)

# 输出到文件或返回渲染结果
if ($OutputPath) {
$template | Set-Content $OutputPath -Encoding UTF8
Write-Host "配置已渲染: $OutputPath"
}

return $template
}

准备一个 Nginx 配置模板:

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
# 创建 Nginx 配置模板
$template = @"
server {
listen {{PORT}};
server_name {{HOST}};
{{#if SSL}}listen 443 ssl;
ssl_certificate {{SSL_CERT}};{{/if}}
location / {
proxy_pass http://{{BACKEND}};
}
}
"@

$template | Set-Content "$env:TEMP\nginx.tpl"

# 渲染模板:启用 SSL 的生产环境
Resolve-ConfigTemplate -TemplatePath "$env:TEMP\nginx.tpl" `
-Variables @{
PORT = "8080"
HOST = "app.example.com"
SSL = $true
SSL_CERT = "/etc/ssl/cert.pem"
BACKEND = "127.0.0.1:3000"
} `
-OutputPath ".\nginx.conf"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
配置已渲染: .\nginx.conf

# 生成的 nginx.conf 内容:
server {
listen 8080;
server_name app.example.com;
listen 443 ssl;
ssl_certificate /etc/ssl/cert.pem;
location / {
proxy_pass http://127.0.0.1:3000;
}
}

环境配置切换

在不同环境(开发、预发布、生产)之间切换时,需要加载对应的配置并注入到环境变量中。下面的函数根据环境名称查找对应配置文件,将所有配置项导出为以 APP_ 为前缀的环境变量。

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
# 切换应用环境,加载对应配置并导出为环境变量
function Switch-AppEnvironment {
param(
[Parameter(Mandatory)]
[ValidateSet("dev", "staging", "prod")]
[string]$Environment,

[string]$ConfigDir = ".\config"
)

# 查找对应环境的配置文件
$configFile = Join-Path $ConfigDir "$Environment.json"

if (-not (Test-Path $configFile)) {
throw "配置文件不存在: $configFile"
}

$config = Get-JsonConfig -Path $configFile

# 将每个配置项导出为环境变量
foreach ($prop in $config.PSObject.Properties) {
[Environment]::SetEnvironmentVariable(
"APP_$($prop.Name.ToUpper())", $prop.Value, "Process"
)
}

# 写入当前环境标记
[Environment]::SetEnvironmentVariable("APP_ENV", $Environment, "Process")

Write-Host "已切换到 $Environment 环境" -ForegroundColor Green
return $config
}

在开发环境与生产环境之间切换:

1
2
3
4
5
6
# 切换到开发环境
Switch-AppEnvironment -Environment dev -ConfigDir ".\config"

# 查看加载的环境变量
$env:APP_ENV
$env:APP_SERVER_HOST

执行结果示例:

1
2
3
4
已切换到 dev 环境

dev
127.0.0.1

管理配置文件时,建议将敏感信息(密码、密钥)从配置文件中分离,使用环境变量或密钥管理服务替代。模板渲染前务必做 Schema 验证,避免无效配置上线。通过本文介绍的工具函数,你可以构建一套完整的配置管理流程:读取配置、Schema 校验、模板渲染、环境切换,覆盖从开发到生产的全链路。