PowerShell 技能连载 - Kubernetes 管理自动化

适用于 PowerShell 7.0 及以上版本

Kubernetes 已成为容器编排的事实标准,几乎所有的云原生应用都运行在 K8s 集群之上。虽然 kubectl 是日常操作的主要命令行工具,但在企业自动化场景中,运维团队往往需要将 Kubernetes 操作集成到更大规模的工作流里——比如批量部署微服务、定期巡检集群健康状态、自动化 Helm Release 管理、以及跨集群的配置同步。纯靠手敲 kubectl 命令既容易出错,也无法做到可重复、可审计。

PowerShell 凭借强大的对象管道和脚本编排能力,是构建 K8s 自动化工作流的理想胶水语言。通过调用 kubectl CLI 并解析其 JSON 输出,PowerShell 可以将集群资源管理、应用部署、健康巡检等操作封装为结构化的脚本模块。结合 .NET 的 Kubernetes 客户端 SDK,还能实现更细粒度的 API 交互。

本文将通过三个实战场景展示如何用 PowerShell 实现 Kubernetes 管理自动化:集群连接与资源管理、部署自动化、以及运维巡检工具集。每个场景都提供了可运行的脚本模板,帮助你快速搭建自己的 K8s 运维工具箱。

集群连接与资源管理

管理多个 Kubernetes 集群时,频繁切换上下文是日常操作。下面的脚本封装了 kubeconfig 上下文切换、资源查询和状态汇总功能,让你在 PowerShell 中高效管理多个集群的 Pod、Deployment 和 Service 资源。

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
# K8s 集群管理辅助函数集

function Get-K8sContexts {
<# 获取所有可用的 K8s 上下文 #>
$Raw = kubectl config get-contexts -o name 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Error "无法获取 K8s 上下文,请确认 kubeconfig 已配置"
return @()
}
return $Raw | Where-Object { $_.Trim() }
}

function Switch-K8sContext {
param([Parameter(Mandatory)][string]$ContextName)
kubectl config use-context $ContextName 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host "已切换到上下文: $ContextName" -ForegroundColor Green
} else {
Write-Error "切换上下文失败: $ContextName"
}
}

function Get-K8sResourceSummary {
param(
[string]$Namespace = 'default',
[string]$Context
)

if ($Context) { Switch-K8sContext $Context }

# 查询 Pod 状态汇总
$Pods = kubectl get pods -n $Namespace -o json 2>$null |
ConvertFrom-Json

$PodSummary = $Pods.items | Group-Object status.phase |
Select-Object @{N='Phase'; E={$_.Name}}, Count

# 查询 Deployment 状态
$Deployments = kubectl get deployments -n $Namespace -o json 2>$null |
ConvertFrom-Json

$DeployStatus = $Deployments.items | ForEach-Object {
$Replicas = $_.status.replicas ?? 0
$Ready = $_.status.readyReplicas ?? 0
$Updated = $_.status.updatedReplicas ?? 0
[PSCustomObject]@{
Name = $_.metadata.name
Replicas = $Replicas
Ready = $Ready
Updated = $Updated
Available = $_.status.availableReplicas ?? 0
Status = if ($Ready -eq $Replicas -and $Replicas -gt 0) { 'Healthy' } else { 'Degraded' }
}
}

# 查询 Service 端点
$Services = kubectl get services -n $Namespace -o json 2>$null |
ConvertFrom-Json

$SvcInfo = $Services.items | ForEach-Object {
$Ports = ($_.spec.ports | ForEach-Object { "$($_.port):$($_.targetPort)/$($_.protocol)" }) -join ', '
[PSCustomObject]@{
Name = $_.metadata.name
Type = $_.spec.type
Ports = $Ports
IP = if ($_.spec.type -eq 'LoadBalancer') {
$_.status.loadBalancer.ingress[0].ip ?? 'Pending'
} else {
$_.spec.clusterIP
}
}
}

Write-Host "`n=== Pod 状态汇总 (Namespace: $Namespace) ===" -ForegroundColor Cyan
$PodSummary | Format-Table -AutoSize

Write-Host "=== Deployment 状态 ===" -ForegroundColor Cyan
$DeployStatus | Format-Table -AutoSize

Write-Host "=== Service 列表 ===" -ForegroundColor Cyan
$SvcInfo | Format-Table -AutoSize
}

# 使用示例
Write-Host "当前可用上下文:" -ForegroundColor Yellow
Get-K8sContexts

# 切换到生产集群并查看资源概况
Get-K8sResourceSummary -Namespace 'production' -Context 'prod-cluster'

执行结果示例:

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
当前可用上下文:
prod-cluster
staging-cluster
dev-cluster

已切换到上下文: prod-cluster

=== Pod 状态汇总 (Namespace: production) ===
Phase Count
----- -----
Running 24
Pending 2
Succeeded 5

=== Deployment 状态 ===
Name Replicas Ready Updated Available Status
---- -------- ----- ------- --------- ------
api-gateway 3 3 3 3 Healthy
user-service 2 2 2 2 Healthy
order-service 3 2 3 2 Degraded
payment-service 2 2 2 2 Healthy

=== Service 列表 ===
Name Type Ports IP
---- ---- ----- --
api-gateway LoadBalancer 80:8080/TCP 203.0.113.50
user-service ClusterIP 8080:8080/TCP 10.96.0.10
order-service ClusterIP 8080:8080/TCP 10.96.0.20
payment-service ClusterIP 8443:443/TCP 10.96.0.30

部署自动化

手动执行 kubectl applykubectl rollout 在管理少量应用时尚可应对,但当微服务数量超过几十个时,就需要自动化部署流水线。下面的脚本演示了如何用 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
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
# K8s 部署自动化工具

function New-K8sDeploymentManifest {
param(
[Parameter(Mandatory)][string]$AppName,
[Parameter(Mandatory)][string]$Image,
[int]$Replicas = 2,
[int]$Port = 8080,
[hashtable]$Labels = @{},
[string]$Namespace = 'default'
)

$AllLabels = @{ app = $AppName } + $Labels

$Manifest = @{
apiVersion = 'apps/v1'
kind = 'Deployment'
metadata = @{
name = $AppName
namespace = $Namespace
labels = $AllLabels
}
spec = @{
replicas = $Replicas
selector = @{ matchLabels = @{ app = $AppName } }
template = @{
metadata = @{ labels = $AllLabels }
spec = @{
containers = @(
@{
name = $AppName
image = $Image
ports = @(@{ containerPort = $Port })
resources = @{
requests = @{ cpu = '100m'; memory = '128Mi' }
limits = @{ cpu = '500m'; memory = '512Mi' }
}
readinessProbe = @{
httpGet = @{ path = '/health'; port = $Port }
initialDelaySeconds = 5
periodSeconds = 10
}
livenessProbe = @{
httpGet = @{ path = '/health'; port = $Port }
initialDelaySeconds = 15
periodSeconds = 20
}
}
)
}
}
}
}

return $Manifest
}

function Start-K8sRollingUpdate {
param(
[Parameter(Mandatory)][string]$AppName,
[Parameter(Mandatory)][string]$NewImage,
[string]$Namespace = 'default',
[int]$TimeoutSeconds = 300
)

Write-Host "开始滚动更新: $AppName -> $NewImage" -ForegroundColor Cyan

# 设置新镜像
kubectl set image "deployment/$AppName" `
"$AppName=$NewImage" -n $Namespace 2>$null | Out-Null

if ($LASTEXITCODE -ne 0) {
Write-Error "设置镜像失败"
return $false
}

# 等待滚动更新完成
Write-Host "等待滚动更新完成 (超时: ${TimeoutSeconds}s)..." -ForegroundColor Yellow
$Deadline = (Get-Date).AddSeconds($TimeoutSeconds)

while ((Get-Date) -lt $Deadline) {
$Status = kubectl rollout status "deployment/$AppName" `
-n $Namespace --timeout=30s 2>&1

if ($LASTEXITCODE -eq 0) {
Write-Host "滚动更新成功: $AppName" -ForegroundColor Green
return $true
}

# 显示当前 Pod 状态
$Pods = kubectl get pods -n $Namespace -l "app=$AppName" -o json 2>$null |
ConvertFrom-Json

$Pods.items | ForEach-Object {
$Phase = $_.status.phase
$Containers = $_.status.containerStatuses
$Ready = ($Containers | Where-Object { $_.ready }).Count
$Total = $Containers.Count
Write-Host " Pod: $($_.metadata.name) | Phase: $Phase | Ready: $Ready/$Total"
}

Start-Sleep -Seconds 5
}

Write-Warning "滚动更新超时,准备回滚..."
Undo-K8sRollout -AppName $AppName -Namespace $Namespace
return $false
}

function Undo-K8sRollout {
param(
[Parameter(Mandatory)][string]$AppName,
[string]$Namespace = 'default',
[int]$Revision = 0
)

if ($Revision -eq 0) {
Write-Host "回滚到上一版本: $AppName" -ForegroundColor Yellow
kubectl rollout undo "deployment/$AppName" -n $Namespace 2>$null
} else {
Write-Host "回滚到修订版本 $Revision: $AppName" -ForegroundColor Yellow
kubectl rollout undo "deployment/$AppName" -n $Namespace --to-revision=$Revision 2>$null
}

if ($LASTEXITCODE -eq 0) {
Write-Host "回滚成功" -ForegroundColor Green
# 查看部署历史
kubectl rollout history "deployment/$AppName" -n $Namespace
} else {
Write-Error "回滚失败"
}
}

# 生成部署清单并应用
$Manifest = New-K8sDeploymentManifest `
-AppName 'web-frontend' `
-Image 'registry.example.com/web-frontend:v2.3.0' `
-Replicas 3 `
-Port 8080 `
-Labels @{ tier = 'frontend'; env = 'production' } `
-Namespace 'production'

$YamlPath = '/tmp/web-frontend-deployment.yaml'
$Manifest | ConvertTo-Json -Depth 10 | Set-Content $YamlPath
kubectl apply -f $YamlPath

# 执行滚动更新
Start-K8sRollingUpdate `
-AppName 'web-frontend' `
-NewImage 'registry.example.com/web-frontend:v2.4.0' `
-Namespace 'production' `
-TimeoutSeconds 300

执行结果示例:

1
2
3
4
5
6
7
8
开始滚动更新: web-frontend -> registry.example.com/web-frontend:v2.4.0
等待滚动更新完成 (超时: 300s)...
Pod: web-frontend-7d9b8f6c4d-abc12 | Phase: Running | Ready: 1/1
Pod: web-frontend-7d9b8f6c4d-def34 | Phase: Running | Ready: 1/1
Pod: web-frontend-8a2c3e7f5b-ghi56 | Phase: ContainerCreating | Ready: 0/1
Pod: web-frontend-8a2c3e7f5b-jkl78 | Phase: Running | Ready: 1/1
Pod: web-frontend-8a2c3e7f5b-mno90 | Phase: Running | Ready: 1/1
滚动更新成功: web-frontend

运维巡检工具集

Kubernetes 集群的日常运维需要定期检查节点健康、资源水位、异常 Pod 和事件告警。下面是一套完整的巡检脚本,可以一次性生成集群健康报告,适合集成到定时任务或 CI/CD 流水线中。

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
# K8s 集群运维巡检工具集

function Invoke-K8sClusterHealthCheck {
param(
[string]$Context,
[string]$OutputPath = "./k8s-health-report-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt"
)

if ($Context) { Switch-K8sContext $Context }

$Report = [System.Text.StringBuilder]::new()
$null = $Report.AppendLine("=" * 60)
$null = $Report.AppendLine("K8s 集群健康巡检报告 - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$null = $Report.AppendLine("=" * 60)

# 1. 节点状态检查
$null = $Report.AppendLine("`n--- 节点状态 ---")
$Nodes = kubectl get nodes -o json 2>$null | ConvertFrom-Json

foreach ($Node in $Nodes.items) {
$Name = $Node.metadata.name
$Conditions = $Node.status.conditions
$Ready = ($Conditions | Where-Object { $_.type -eq 'Ready' }).status -eq 'True'
$MemoryPressure = ($Conditions | Where-Object { $_.type -eq 'MemoryPressure' }).status -eq 'True'
$DiskPressure = ($Conditions | Where-Object { $_.type -eq 'DiskPressure' }).status -eq 'True'

$StatusIcon = if ($Ready -and -not $MemoryPressure -and -not $DiskPressure) { 'OK' } else { 'WARN' }
$null = $Report.AppendLine("[$StatusIcon] $Name | Ready: $Ready | MemoryPressure: $MemoryPressure | DiskPressure: $DiskPressure")

# 节点资源使用
$Allocatable = $Node.status.allocatable
$null = $Report.AppendLine(" CPU: $($Allocatable.cpu) | Memory: $($Allocatable.memory)")
}

# 2. 异常 Pod 扫描(所有命名空间)
$null = $Report.AppendLine("`n--- 异常 Pod ---")
$AllPods = kubectl get pods -A -o json 2>$null | ConvertFrom-Json

$UnhealthyPods = $AllPods.items | Where-Object {
$_.status.phase -notin @('Running', 'Succeeded') -or
($_.status.containerStatuses | Where-Object { $_.restartCount -gt 5 }).Count -gt 0
}

if ($UnhealthyPods) {
foreach ($Pod in $UnhealthyPods) {
$Ns = $Pod.metadata.namespace
$Name = $Pod.metadata.name
$Phase = $Pod.status.phase
$Restarts = ($Pod.status.containerStatuses | Measure-Object -Property restartCount -Sum).Sum
$null = $Report.AppendLine(" [ALERT] $Ns/$Name | Phase: $Phase | Restarts: $Restarts")
}
} else {
$null = $Report.AppendLine(" 所有 Pod 运行正常")
}

# 3. 资源使用报告(通过 metrics-server)
$null = $Report.AppendLine("`n--- 资源使用 Top 10 ---")
$TopPods = kubectl top pods -A --sort-by=memory --no-headers 2>$null

if ($LASTEXITCODE -eq 0) {
$Rank = 1
$TopPods | Select-Object -First 10 | ForEach-Object {
$null = $Report.AppendLine(" #$Rank $_")
$Rank++
}
} else {
$null = $Report.AppendLine(" metrics-server 未安装或不可用,跳过资源使用统计")
}

# 4. 最近事件告警
$null = $Report.AppendLine("`n--- 最近告警事件 (Warning) ---")
$Events = kubectl get events -A --field-selector type=Warning -o json 2>$null |
ConvertFrom-Json

$RecentWarnings = $Events.items |
Sort-Object { [datetime]$_.lastTimestamp } -Descending |
Select-Object -First 10

foreach ($Evt in $RecentWarnings) {
$Time = $Evt.lastTimestamp
$Ns = $Evt.metadata.namespace
$Msg = $Evt.message
$Involved = "$($Evt.involvedObject.kind)/$($Evt.involvedObject.name)"
$null = $Report.AppendLine(" [$Time] $Ns/$Involved - $Msg")
}

# 输出报告
$ReportContent = $Report.ToString()
$ReportContent | Set-Content $OutputPath -Encoding UTF8
Write-Host $ReportContent
Write-Host "`n报告已保存到: $OutputPath" -ForegroundColor Green

# 返回摘要对象,便于后续自动化处理
return [PSCustomObject]@{
TotalNodes = $Nodes.items.Count
UnhealthyPods = $UnhealthyPods.Count
WarningEvents = $RecentWarnings.Count
ReportPath = $OutputPath
}
}

# 执行巡检
$HealthResult = Invoke-K8sClusterHealthCheck -Context 'prod-cluster'

# 根据巡检结果触发告警
if ($HealthResult.UnhealthyPods -gt 0 -or $HealthResult.WarningEvents -gt 5) {
$AlertMsg = "K8s 巡检告警: 异常Pod=$($HealthResult.UnhealthyPods), 告警事件=$($HealthResult.WarningEvents)"
Write-Host $AlertMsg -ForegroundColor Red
# 可在此处接入钉钉、飞书、Slack 等通知渠道
}

执行结果示例:

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
============================================================
K8s 集群健康巡检报告 - 2026-02-13 09:30:00
============================================================

--- 节点状态 ---
[OK] k8s-node-01 | Ready: True | MemoryPressure: False | DiskPressure: False
CPU: 8 | Memory: 32762308Ki
[OK] k8s-node-02 | Ready: True | MemoryPressure: False | DiskPressure: False
CPU: 8 | Memory: 32762308Ki
[WARN] k8s-node-03 | Ready: True | MemoryPressure: True | DiskPressure: False
CPU: 8 | Memory: 32762308Ki

--- 异常 Pod ---
[ALERT] production/order-service-6b8d4f-x2k9l | Phase: CrashLoopBackOff | Restarts: 17
[ALERT] staging/api-gateway-5c7a2e-m4n7p | Phase: Pending | Restarts: 0

--- 资源使用 Top 10 ---
#1 production/redis-cache-0 512Mi 250m
#2 production/elasticsearch-0 480Mi 350m
#3 production/order-service-7d9b 256Mi 150m
#4 production/user-service-8a2c 128Mi 80m
#5 monitoring/prometheus-0 380Mi 200m

--- 最近告警事件 (Warning) ---
[2026-02-13T09:28:00Z] production/Pod/order-service-6b8d4f-x2k9l - Back-off restarting failed container
[2026-02-13T09:25:00Z] staging/Pod/api-gateway-5c7a2e-m4n7p - Insufficient cpu (3) to schedule pod
[2026-02-13T09:20:00Z] production/Node/k8s-node-03 - Node is experiencing memory pressure

报告已保存到: ./k8s-health-report-20260213-093000.txt

注意事项

  1. kubectl 前置依赖:所有脚本都依赖 kubectl 命令行工具,运行前需确保已安装并与目标集群版本兼容(建议客户端版本不低于集群版本的 1 个小版本)。可通过 kubectl version --client 检查客户端版本,集群端需网络可达且 kubeconfig 配置正确。

  2. JSON 输出解析:脚本中大量使用 kubectl -o json 配合 ConvertFrom-Json 解析 K8s 资源。当集群资源量非常大(例如上万 Pod)时,JSON 反序列化可能消耗较多内存。建议在大型集群中结合 -l 标签选择器或 --field-selector 缩小查询范围。

  3. 滚动更新超时策略Start-K8sRollingUpdate 中的超时时间应根据应用启动速度合理设置。Java 等慢启动应用可能需要 5-10 分钟才能通过就绪探针检查,而 Go/Node.js 应用通常在 30 秒内就绪。超时时间过短会导致误判失败并触发不必要的回滚。

  4. metrics-server 部署:资源使用统计功能依赖 metrics-server 组件,部分托管集群(如 EKS、GKE)默认安装,但自建集群需要手动部署。如果巡检脚本中 kubectl top 命令返回错误,请先通过 kubectl apply -f 部署 metrics-server 清单。

  5. 命名空间与权限控制:脚本中的 Get-K8sResourceSummary 默认只查询指定命名空间。在 RBAC 严格的生产集群中,ServiceAccount 可能只被授权访问部分命名空间。建议为巡检脚本创建专用的 ServiceAccount 和 ClusterRole,仅授予只读权限(getlistwatch),避免使用高权限账户运行自动化脚本。

  6. kubeconfig 安全管理:多集群环境下,kubeconfig 文件中包含各集群的认证凭据(证书或 Token)。切勿将 kubeconfig 提交到代码仓库,应通过安全的密钥管理方案(如 HashiCorp Vault、Azure Key Vault)分发凭据,并定期轮换 ServiceAccount Token。

PowerShell 技能连载 - Docker 容器管理

适用于 PowerShell 7.0 及以上版本

容器化已成为现代应用部署的主流方式。无论是微服务架构、CI/CD 流水线还是本地开发环境,Docker 都扮演着核心角色。在 Windows 和跨平台场景中,PowerShell 凭借强大的对象管道和丰富的模块生态,能够将 Docker 操作无缝融入自动化运维流程。

传统的容器管理往往依赖手动执行 docker 命令,效率低下且容易出错。而通过 PowerShell 脚本化封装,我们可以实现镜像构建的标准化、容器编排的可重复部署、以及资源清理的定时任务调度。本文将从基础操作、Docker Compose 编排和运维工具集三个维度,展示如何用 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
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
function Get-DockerImageList {
[CmdletBinding()]
param(
[string]$Filter,
[switch]$DanglingOnly
)

$args = @('image', 'ls', '--format', '{{.Repository}}:{{.Tag}}|{{.ID}}|{{.Size}}|{{.CreatedSince}}')
if ($DanglingOnly) {
$args += @('--filter', 'dangling=true')
}

$result = docker $args 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "无法获取镜像列表: $result"
return
}

$images = $result | ForEach-Object {
$parts = $_ -split '\|'
[PSCustomObject]@{
Repository = $parts[0]
ImageId = $parts[1]
Size = $parts[2]
Created = $parts[3]
}
}

if ($Filter) {
$images = $images | Where-Object { $_.Repository -like "*$Filter*" }
}

$images
}

function Start-DockerApp {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Image,

[string]$Name = "app-$(Get-Random -Maximum 9999)",
[int]$Port = 8080,
[string]$Volume,
[hashtable]$EnvVars,
[string]$Network = 'bridge'
)

$dockerArgs = @('run', '-d', '--name', $Name, '--restart', 'unless-stopped')

# 端口映射
$dockerArgs += @('-p', "${Port}:${Port}")

# 挂载卷
if ($Volume) {
if (-not (Test-Path $Volume)) {
New-Item -ItemType Directory -Path $Volume -Force | Out-Null
Write-Host "已创建挂载目录: $Volume" -ForegroundColor Yellow
}
$dockerArgs += @('-v', "${Volume}:/app/data")
}

# 环境变量
if ($EnvVars) {
foreach ($key in $EnvVars.Keys) {
$dockerArgs += @('-e', "${key}=$($EnvVars[$key])")
}
}

# 网络
$dockerArgs += @('--network', $Network)
$dockerArgs += $Image

Write-Host "正在启动容器 [$Name]..." -ForegroundColor Cyan
$containerId = docker $dockerArgs 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "容器已启动: $containerId" -ForegroundColor Green
return $containerId
} else {
Write-Error "启动失败: $containerId"
}
}

function Get-DockerContainerLog {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name,

[int]$Tail = 50,
[switch]$Follow,
[datetime]$Since
)

$args = @('logs')
if ($Tail -gt 0) { $args += @('--tail', $Tail) }
if ($Follow) { $args += '--follow' }
if ($Since) { $args += @('--since', $Since.ToString('yyyy-MM-ddTHH:mm:ss')) }
$args += $Name

docker $args 2>&1
}

上面的代码定义了三个核心函数:Get-DockerImageList 以结构化对象形式输出镜像信息,支持按名称过滤和仅显示悬空镜像;Start-DockerApp 封装了容器启动流程,自动处理端口映射、卷挂载、环境变量注入等常见配置;Get-DockerContainerLog 提供灵活的日志查看方式,支持尾部行数、实时跟踪和时间过滤。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS> Get-DockerImageList -Filter 'nginx'

Repository ImageId Size Created
---------- ------- ---- -------
nginx:latest a8758716bb 187MB 3 days ago
nginx:alpine c31a4dc1b5 42MB 2 weeks ago

PS> Start-DockerApp -Image 'nginx:alpine' -Name 'web-test' -Port 8080 -Volume 'C:\data\web' -EnvVars @{ 'NGINX_HOST' = 'localhost' }

正在启动容器 [web-test]...
已创建挂载目录: C:\data\web
容器已启动: d4f5a2c8e1b3...

PS> Get-DockerContainerLog -Name 'web-test' -Tail 10

2026/01/19 08:15:32 [notice] 1#1: start worker process 32
2026/01/19 08:15:32 [notice] 1#1: start worker process 33
192.168.1.100 - - [19/Jan/2026:08:16:01 +0000] "GET / HTTP/1.1" 200 615

Docker Compose 编排与 PowerShell 自动化

在多服务场景下,Docker Compose 是首选的编排工具。我们可以用 PowerShell 动态生成 Compose 文件、管理服务生命周期,并将环境配置与部署流程解耦,实现可重复的一键部署。

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
function New-DockerComposeConfig {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ProjectName,

[string]$OutputPath = '.',
[string]$AppImage = 'myapp:latest',
[string]$DbImage = 'postgres:16-alpine',
[string]$RedisImage = 'redis:7-alpine',
[int]$AppPort = 3000
)

$compose = @{
services = @{
app = @{
image = $AppImage
ports = @("${AppPort}:3000")
environment = @{
NODE_ENV = 'production'
DATABASE_URL = "postgresql://appuser:secret@db:5432/${ProjectName}"
REDIS_URL = 'redis://redis:6379'
}
depends_on = @(
@{ service = 'db'; condition = 'service_healthy' }
@{ service = 'redis'; condition = 'service_started' }
)
restart = 'unless-stopped'
deploy = @{
resources = @{
limits = @{
memory = '512M'
cpus = '1.0'
}
}
}
}
db = @{
image = $DbImage
volumes = @("${ProjectName}-db-data:/var/lib/postgresql/data")
environment = @{
POSTGRES_DB = $ProjectName
POSTGRES_USER = 'appuser'
POSTGRES_PASSWORD = 'secret'
}
healthcheck = @{
test = @('CMD-SHELL', 'pg_isready -U appuser -d ' + $ProjectName)
interval = '10s'
timeout = '5s'
retries = 5
}
restart = 'unless-stopped'
}
redis = @{
image = $RedisImage
volumes = @("${ProjectName}-redis-data:/data")
command = 'redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru'
restart = 'unless-stopped'
}
}
volumes = @{
"${ProjectName}-db-data" = @{}
"${ProjectName}-redis-data" = @{}
}
}

$outputFile = Join-Path $OutputPath "docker-compose-${ProjectName}.yml"
$compose | ConvertTo-Yaml | Set-Content -Path $outputFile -Encoding UTF8
Write-Host "已生成 Compose 文件: $outputFile" -ForegroundColor Green

return $outputFile
}

function Invoke-DockerCompose {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ComposeFile,

[ValidateSet('Up', 'Down', 'Restart', 'Pull', 'Ps')]
[string]$Action = 'Up',

[switch]$Build,
[switch]$Detach
)

$baseArgs = @('-f', $ComposeFile)

switch ($Action) {
'Up' {
$args = $baseArgs + @('up')
if ($Detach) { $args += '-d' }
if ($Build) { $args += '--build' }
Write-Host "正在启动服务..." -ForegroundColor Cyan
}
'Down' {
$args = $baseArgs + @('down', '--volumes', '--remove-orphans')
Write-Host "正在停止并清理服务..." -ForegroundColor Yellow
}
'Restart' {
$args = $baseArgs + @('restart')
Write-Host "正在重启服务..." -ForegroundColor Cyan
}
'Pull' {
$args = $baseArgs + @('pull')
Write-Host "正在拉取最新镜像..." -ForegroundColor Cyan
}
'Ps' {
$args = $baseArgs + @('ps', '--format', 'table {{.Name}}\t{{.Status}}\t{{.Ports}}')
}
}

docker $args 2>&1 | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0) {
Write-Error "Compose 操作 [$Action] 执行失败"
}
}

这段代码的核心思路是将 Compose 配置参数化。New-DockerComposeConfig 根据项目名称和镜像版本动态生成 YAML 文件,包含健康检查、资源限制和持久化卷定义。Invoke-DockerComposedocker compose 子命令做了一层 PowerShell 封装,支持启动、停止、重启、拉取镜像和查看状态等操作,每个动作都有清晰的状态提示。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS> New-DockerComposeConfig -ProjectName 'myblog' -AppImage 'myblog:v2.1' -AppPort 8080

已生成 Compose 文件: .\docker-compose-myblog.yml

PS> Invoke-DockerCompose -ComposeFile '.\docker-compose-myblog.yml' -Action Up -Detach -Build

正在启动服务...
[+] Building 15.2s
=> [app internal] load build definition from Dockerfile
=> [app] => exporting to image
[+] Running 4/4
✔ Network myblog_default Created
✔ Container myblog-redis-1 Started
✔ Container myblog-db-1 Healthy
✔ Container myblog-app-1 Started

PS> Invoke-DockerCompose -ComposeFile '.\docker-compose-myblog.yml' -Action Ps

NAME STATUS PORTS
myblog-app-1 Up 2 minutes 0.0.0.0:8080->3000/tcp
myblog-db-1 Up 2 minutes (healthy) 5432/tcp
myblog-redis-1 Up 2 minutes 6379/tcp

容器运维工具集

随着容器数量增长,定期清理无用资源、监控容器健康状态和检查镜像安全变得至关重要。下面的工具集提供了批量清理、资源监控和安全扫描的自动化能力。

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
function Remove-DockerOrphanResources {
[CmdletBinding()]
param(
[switch]$IncludeVolumes,
[switch]$DryRun
)

$report = [System.Text.StringBuilder]::new()
[void]$report.AppendLine("=== Docker 资源清理报告 ===")
[void]$report.AppendLine("执行时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`n")

# 清理已停止的容器
$stopped = docker ps -aq --filter 'status=exited' 2>$null
if ($stopped) {
$count = ($stopped | Measure-Object).Count
[void]$report.AppendLine("[容器] 发现 $count 个已停止的容器")
if (-not $DryRun) {
docker rm $stopped 2>$null | Out-Null
[void]$report.AppendLine(" -> 已清理")
}
}

# 清理悬空镜像
$dangling = docker images -q --filter 'dangling=true' 2>$null
if ($dangling) {
$count = ($dangling | Measure-Object).Count
[void]$report.AppendLine("[镜像] 发现 $count 个悬空镜像")
if (-not $DryRun) {
docker rmi $dangling 2>$null | Out-Null
[void]$report.AppendLine(" -> 已清理")
}
}

# 清理未使用的网络
$networks = docker network ls --filter 'type=custom' -q 2>$null
if ($networks) {
$count = ($networks | Measure-Object).Count
[void]$report.AppendLine("[网络] 发现 $count 个自定义网络")
if (-not $DryRun) {
docker network prune -f 2>$null | Out-Null
[void]$report.AppendLine(" -> 已清理")
}
}

# 清理未使用的卷
if ($IncludeVolumes) {
$volumes = docker volume ls -q --filter 'dangling=true' 2>$null
if ($volumes) {
$count = ($volumes | Measure-Object).Count
[void]$report.AppendLine("[卷] 发现 $count 个悬空卷")
if (-not $DryRun) {
docker volume prune -f 2>$null | Out-Null
[void]$report.AppendLine(" -> 已清理")
}
}
}

# 回收磁盘空间
if (-not $DryRun) {
[void]$report.AppendLine("`n正在回收磁盘空间...")
$reclaim = docker system df --format '{{.Reclaimable}}' 2>$null
docker system prune -f 2>$null | Out-Null
[void]$report.AppendLine("可回收空间: $($reclaim -join ', ')")
} else {
[void]$report.AppendLine("`n[模拟模式] 未执行实际清理操作")
}

$report.ToString()
}

function Get-DockerResourceMonitor {
[CmdletBinding()]
param(
[int]$IntervalSeconds = 5,
[int]$Count = 12
)

for ($i = 0; $i -lt $Count; $i++) {
$timestamp = Get-Date -Format 'HH:mm:ss'

$stats = docker stats --no-stream --format `
'{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.PIDs}}' 2>$null

if ($i -eq 0) {
Write-Host ("{0,-8} {1,-20} {2,-10} {3,-15} {4,-8} {5,-20} {6,-6}" -f `
'时间', '容器', 'CPU', '内存使用', '内存%', '网络IO', 'PID') `
-ForegroundColor Cyan
Write-Host ('-' * 90)
}

$stats | ForEach-Object {
$parts = $_ -split '\|'
if ($parts.Count -eq 6) {
Write-Host ("{0,-8} {1,-20} {2,-10} {3,-15} {4,-8} {5,-20} {6,-6}" -f `
$timestamp, $parts[0], $parts[1], $parts[2], $parts[3], $parts[4], $parts[5])
}
}

if ($i -lt $Count - 1) {
Start-Sleep -Seconds $IntervalSeconds
}
}
}

function Test-DockerImageSecurity {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Image,

[string]$Severity = 'HIGH,CRITICAL',
[switch]$SkipDbUpdate
)

Write-Host "正在扫描镜像: $Image" -ForegroundColor Cyan
Write-Host "严重级别过滤: $Severity`n"

# 检查 trivy 是否安装
$trivy = Get-Command trivy -ErrorAction SilentlyContinue
if (-not $trivy) {
Write-Warning "未找到 trivy 扫描器,使用 docker scout 替代"
$scanResult = docker scout cves $Image 2>&1
} else {
$trivyArgs = @('image', '--severity', $Severity, '--format', 'table', $Image)
if ($SkipDbUpdate) { $trivyArgs += '--skip-db-update' }
$scanResult = trivy $trivyArgs 2>&1
}

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

# 提取漏洞摘要
$criticalCount = ($scanResult | Select-String -Pattern 'CRITICAL' -SimpleMatch).Count
$highCount = ($scanResult | Select-String -Pattern 'HIGH' -SimpleMatch).Count

[PSCustomObject]@{
Image = $Image
Critical = $criticalCount
High = $highCount
ScanTime = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Status = if ($criticalCount -gt 0) { 'NEEDS ATTENTION' } else { 'ACCEPTABLE' }
}
}

这套工具集包含三个核心功能:Remove-DockerOrphanResources 生成详细的清理报告,支持 DryRun 模式预览将要删除的资源,避免误删;Get-DockerResourceMonitor 以固定间隔采集所有运行中容器的 CPU、内存和网络指标,适合排查性能瓶颈;Test-DockerImageSecurity 调用 Trivy 或 Docker Scout 对镜像进行漏洞扫描,按严重级别过滤结果。

执行结果示例:

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
PS> Remove-DockerOrphanResources -DryRun

=== Docker 资源清理报告 ===
执行时间: 2026-01-19 08:30:00

[容器] 发现 3 个已停止的容器
[镜像] 发现 5 个悬空镜像
[网络] 发现 2 个自定义网络

[模拟模式] 未执行实际清理操作

PS> Get-DockerResourceMonitor -Count 3

时间 容器 CPU 内存使用 内存% 网络IO PID
------------------------------------------------------------------------------------------
08:30:05 myblog-app-1 2.35% 128MiB/512MiB 25.00% 1.2kB/856B 18
08:30:05 myblog-db-1 0.82% 89MiB/256MiB 34.77% 456B/312B 12
08:30:05 myblog-redis-1 0.15% 12MiB/128MiB 9.38% 234B/156B 6
08:30:10 myblog-app-1 3.12% 132MiB/512MiB 25.78% 2.1kB/1.1kB 18
08:30:10 myblog-db-1 1.05% 91MiB/256MiB 35.55% 512B/378B 12
08:30:10 myblog-redis-1 0.18% 12MiB/128MiB 9.38% 256B/178B 6

PS> Test-DockerImageSecurity -Image 'myblog:v2.1' -Severity 'HIGH,CRITICAL'

正在扫描镜像: myblog:v2.1
严重级别过滤: HIGH,CRITICAL

Total: 3 (HIGH: 2, CRITICAL: 1)

Image Critical High ScanTime Status
----- -------- ---- -------- ------
myblog:v2.1 1 2 2026-01-19 08:35:12 NEEDS ATTENTION

注意事项

  1. Docker 服务依赖:运行脚本前确保 Docker Desktop 或 Docker Engine 已启动并正常运行,可通过 docker info 命令快速验证,脚本中建议加入服务状态检查逻辑。

  2. ConvertTo-Yaml 模块New-DockerComposeConfig 函数依赖 powershell-yaml 模块来序列化 YAML,使用前需执行 Install-Module -Name powershell-yaml -Scope CurrentUser 安装。

  3. 权限与安全:容器操作通常需要管理员或 docker 用户组权限,在 CI/CD 环境中注意凭据管理,避免在 Compose 文件中硬编码数据库密码,应使用 Docker Secrets 或环境变量文件。

  4. 资源清理的破坏性Remove-DockerOrphanResources-IncludeVolumes 参数会删除未挂载的卷数据,生产环境中务必先使用 -DryRun 预览,确认无误后再执行实际清理。

  5. 监控性能开销Get-DockerResourceMonitor 通过 docker stats 采集指标会引入轻微的 CPU 开销,在容器数量超过 50 个时建议降低采集频率或仅监控关键服务。

  6. 镜像扫描工具Test-DockerImageSecurity 优先使用 Trivy(开源免费),备选 Docker Scout(需 Docker Desktop 许可)。对于企业级场景,可集成 Aqua、Snyk 等商业扫描平台获取更全面的漏洞情报。

PowerShell 技能连载 - Azure Container Apps 管理

适用于 PowerShell 7.0 及以上版本

Azure Container Apps 是微软推出的全托管无服务器容器平台,它在底层基于 Kubernetes 却将集群管理完全抽象化,让开发者无需编写 YAML 清单就能享受容器编排的好处。内置的流量分流、自动扩缩容、服务发现和 Dapr 集成,使其特别适合微服务和事件驱动型工作负载。

对于运维工程师而言,手工在 Azure 门户中点击操作不仅效率低下,也无法纳入版本控制和审计流程。通过 PowerShell 的 Az 模块,我们可以将容器应用的创建、环境配置、版本管理和扩缩容策略全部脚本化,实现真正的 GitOps 工作流。今天就来详细介绍如何用 PowerShell 完成 Azure Container Apps 的全生命周期管理。

环境创建与应用部署

一切从 Container Apps 环境开始。每个容器应用都必须部署在一个环境中,环境定义了应用之间的网络边界和共享基础设施。下面的脚本演示了如何创建环境、部署一个 Web API 容器,并配置基本的入站流量规则。

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
# 定义变量
$resourceGroup = "rg-microservice-demo"
$location = "eastasia"
$envName = "cae-microservice"
$appName = "ca-weather-api"
$acrLoginServer = "myacr.azurecr.io"
$imageTag = "$acrLoginServer/weather-api:v1.0.0"

# 登录并选择订阅
Connect-AzAccount
Set-AzContext -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# 创建资源组
New-AzResourceGroup -Name $resourceGroup -Location $location -Force

# 创建 Container Apps 环境(包含 Log Analytics 工作区)
$workspace = New-AzOperationalInsightsWorkspace `
-ResourceGroupName $resourceGroup `
-Name "log-microservice" `
-Location $location `
-Sku Standard

$envArgs = @{
Name = $envName
ResourceGroupName = $resourceGroup
Location = $location
AppLogConfiguration = @{
LogAnalyticsConfiguration = @{
CustomerId = $workspace.CustomerId
SharedKey = (Get-AzOperationalInsightsWorkspaceSharedKey `
-ResourceGroupName $resourceGroup `
-Name $workspace.Name).PrimarySharedKey
}
}
}
$containerEnv = New-AzContainerAppManagedEnv @envArgs

# 获取 ACR 凭据并创建托管标识拉取镜像
$acr = Get-AzContainerRegistry -ResourceGroupName $resourceGroup -Name "myacr"
$acrCred = Get-AzContainerRegistryCredential -Registry $acr

# 部署容器应用
$appArgs = @{
Name = $appName
ResourceGroupName = $resourceGroup
Location = $location
ManagedEnvironment = $containerEnv
Configuration = @{
ActiveRevisionsMode = "Single"
Ingress = @{
External = $true
TargetPort = 8080
Traffic = @(
@{
RevisionName = "$appName--v1"
Weight = 100
}
)
}
}
Template = @{
Containers = @(
@{
Name = "weather-api"
Image = $imageTag
Env = @(
@{
Name = "ASPNETCORE_ENVIRONMENT"
Value = "Production"
}
@{
Name = "LOG_LEVEL"
Value = "Information"
}
)
Resources = @{
Cpu = "0.5"
Memory = "1.0Gi"
}
}
)
}
}
New-AzContainerApp @appArgs

Write-Host "容器应用部署完成: $appName"

执行结果示例:

1
2
3
4
5
6
7
8
9
ResourceGroupName : rg-microservice-demo
Location : eastasia
Name : ca-weather-api
ProvisioningState : Succeeded
Fqdn : ca-weather-api.politebay-xxxxxxxx.eastasia.azurecontainerapps.io
LatestRevision : ca-weather-api--v1
TrafficWeight : 100%

容器应用部署完成: ca-weather-api

上面的脚本完成了从零到一的部署。首先创建了 Log Analytics 工作区用于日志收集,然后基于工作区创建 Container Apps 环境。容器配置中指定了镜像地址、环境变量和资源限额,入站配置将 8080 端口暴露为外部 HTTP 端点。ActiveRevisionsMode 设为 Single 表示同时只运行一个活跃版本。

版本管理与流量分流

微服务的迭代发布要求我们能够在不同版本之间平滑切换。Container Apps 的修订版本(Revision)机制天然支持蓝绿部署和金丝雀发布。下面的脚本展示了如何推送新版本、配置流量分流比例,以及在出现问题时快速回滚。

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
# 推送 v2.0.0 版本并配置金丝雀发布(10% 流量到新版本)
$v2Image = "$acrLoginServer/weather-api:v2.0.0"

$updateArgs = @{
Name = $appName
ResourceGroupName = $resourceGroup
Configuration = @{
ActiveRevisionsMode = "Multiple"
Ingress = @{
External = $true
TargetPort = 8080
Traffic = @(
@{
RevisionName = "$appName--v1"
Weight = 90
}
@{
RevisionName = "$appName--v2"
Weight = 10
}
)
}
}
Template = @{
Containers = @(
@{
Name = "weather-api"
Image = $v2Image
Env = @(
@{
Name = "ASPNETCORE_ENVIRONMENT"
Value = "Production"
}
@{
Name = "LOG_LEVEL"
Value = "Debug"
}
)
Resources = @{
Cpu = "0.5"
Memory = "1.0Gi"
}
}
)
}
}
New-AzContainerApp @updateArgs

Write-Host "金丝雀发布已配置: v1(90%) -> v2(10%)"

# 观察新版本运行状态
Start-Sleep -Seconds 30

# 查看所有修订版本及状态
$revisions = Get-AzContainerAppRevision `
-Name $appName `
-ResourceGroupName $resourceGroup

$revisions | Format-Table Name, Active, TrafficWeight, Replicas, CreatedTime -AutoSize

# 确认新版本稳定后,切换全量流量到 v2
$fullCutArgs = @{
Name = $appName
ResourceGroupName = $resourceGroup
Configuration = @{
ActiveRevisionsMode = "Multiple"
Ingress = @{
External = $true
TargetPort = 8080
Traffic = @(
@{
RevisionName = "$appName--v2"
Weight = 100
}
)
}
}
}
Update-AzContainerApp @fullCutArgs

Write-Host "全量切换完成: v2(100%)"

# 如果发现问题,一键回滚到 v1
# Update-AzContainerApp @fullCutArgs -Configuration @{
# Ingress = @{
# Traffic = @(@{ RevisionName = "$appName--v1"; Weight = 100 })
# }
# }

执行结果示例:

1
2
3
4
5
6
7
8
金丝雀发布已配置: v1(90%) -> v2(10%)

Name Active TrafficWeight Replicas CreatedTime
---- ------ ------------- -------- -----------
ca-weather-api--v1 True 90 2 2026-01-13 08:30:00
ca-weather-api--v2 True 10 1 2026-01-13 09:15:00

全量切换完成: v2(100%)

流量分流的原理在于将 ActiveRevisionsMode 切换为 Multiple,此时多个修订版本可以同时运行。通过 Traffic 数组中每个条目的 Weight 字段控制流量比例,总和必须为 100。金丝雀发布时先用小比例流量验证新版本,观察日志和监控指标后再逐步放量。回滚只需将流量权重重新指向旧版本即可,秒级生效。

自动扩缩容与监控

无服务器容器的核心优势之一是根据负载自动调整实例数量。Container Apps 使用 KEDA(Kubernetes Event-Driven Autoscaler)作为扩缩容引擎,支持基于 HTTP 并发、CPU/内存利用率、消息队列长度等多种触发器。下面的脚本展示了如何配置扩缩容规则并设置监控告警。

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
# 配置基于 HTTP 并发的自动扩缩容
$scaleArgs = @{
Name = $appName
ResourceGroupName = $resourceGroup
Template = @{
Containers = @(
@{
Name = "weather-api"
Image = "$acrLoginServer/weather-api:v2.0.0"
Resources = @{
Cpu = "1.0"
Memory = "2.0Gi"
}
}
)
Scale = @{
MinReplicas = 1
MaxReplicas = 20
Rules = @(
@{
Name = "http-concurrency"
Custom = @{
Type = "http"
Metadata = @{
concurrentRequests = "100"
}
}
}
@{
Name = "cpu-utilization"
Custom = @{
Type = "cpu"
Metadata = @{
type = "Utilization"
value = "70"
}
}
}
)
}
}
}
Update-AzContainerApp @scaleArgs

Write-Host "自动扩缩容已配置: 1-20 副本,HTTP 并发阈值 100"

# 查询容器应用日志
$logQuery = @"
ContainerAppConsoleLogs_CL
| where ContainerAppName_s == '$appName'
| where TimeGenerated > ago(1h)
| project TimeGenerated, Log_s, RevisionName_s
| order by TimeGenerated desc
| limit 50
"@

$queryResult = Invoke-AzOperationalInsightsQuery `
-WorkspaceId $workspace.CustomerId `
-Query $logQuery

$queryResult.Results | Format-Table TimeGenerated, RevisionName_s, Log_s -AutoSize

# 配置监控告警:当副本数超过 15 时触发通知
$actionGroup = New-AzActionGroup `
-ResourceGroupName $resourceGroup `
-Name "ag-container-alerts" `
-ShortName "caAlert"

$alertRule = New-AzMetricAlertRuleV2 `
-Name "alert-high-scale" `
-ResourceGroupName $resourceGroup `
-WindowSize (New-TimeSpan -Minutes 5) `
-Frequency (New-TimeSpan -Minutes 1) `
-TargetResourceId (Get-AzContainerApp `
-Name $appName `
-ResourceGroupName $resourceGroup).Id `
-Condition @{ `
MetricName = "ReplicaCount"; `
Operator = "GreaterThan"; `
Threshold = 15 `
} `
-ActionGroupId $actionGroup.Id `
-Severity 2

Write-Host "告警规则已创建: 副本数 > 15 时通知运维团队"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
自动扩缩容已配置: 1-20 副本,HTTP 并发阈值 100

TimeGenerated RevisionName_s Log_s
------------- -------------- -----
2026-01-13T09:20:15Z ca-weather-api--v2 [INFO] Request GET /api/weather/beijing - 200 OK (45ms)
2026-01-13T09:20:14Z ca-weather-api--v2 [INFO] Request GET /api/weather/shanghai - 200 OK (32ms)
2026-01-13T09:20:12Z ca-weather-api--v2 [INFO] Request GET /api/weather/guangzhou - 200 OK (38ms)
2026-01-13T09:20:10Z ca-weather-api--v1 [INFO] Health check passed

告警规则已创建: 副本数 > 15 时通知运维团队

扩缩容规则中,http-concurrency 规则表示当单个副本的并发请求数超过 100 时触发扩容,cpu-utilization 规则表示当 CPU 使用率超过 70% 时也会触发。两个规则是 OR 关系,任一条件满足即扩容。MinReplicas = 1 保证至少有一个实例在运行,即使流量为零也不会缩容到零(除非设为 0)。通过 Log Analytics 的 KQL 查询可以实时查看应用日志,配合告警规则在异常时及时通知运维团队。

注意事项

  1. 先创建环境再部署应用:Container Apps 环境是一次性的基础设施决策,创建后无法更改所在区域和 VNet 配置。建议在项目初期就规划好网络拓扑,将环境和共享资源放在同一个资源组中统一管理。

  2. 镜像拉取凭据要安全传递:不要在脚本中硬编码 ACR 的用户名和密码。推荐使用托管标识(Managed Identity)让 Container Apps 自动拉取镜像,或通过 Get-AzContainerRegistryCredential 在运行时动态获取并注入。

  3. 流量分流的权重总和必须为 100:配置多版本流量时,所有条目的 Weight 值加起来必须等于 100。如果总和不对,部署会失败。建议在脚本中增加前置校验逻辑。

  4. 扩缩容冷却时间会影响响应速度:KEDA 默认的冷却期为 300 秒(5 分钟),即缩容后至少等待 5 分钟才会再次缩容。如果业务流量有突发尖峰,可以适当调大 MinReplicas 以保证足够的缓冲容量。

  5. 日志查询会产生额外费用:Log Analytics 按数据摄入量和查询量收费。在生产环境中建议设置每日上限,并在查询时添加时间范围过滤(如 ago(1h)),避免全表扫描导致费用飙升。

  6. Az 模块版本要保持最新:Container Apps 的 PowerShell 支持仍在快速迭代,新功能(如 Dapr 组件、服务间 mTLS)可能需要最新版 Az 模块。部署脚本开头建议加一行 Update-Module Az -Force 或至少检查模块版本是否满足要求。

PowerShell 技能连载 - Azure 容器实例管理

适用于 PowerShell 7.0 及以上版本

Azure Container Instances(ACI)是 Azure 提供的无服务器容器运行服务,无需预配虚拟机或配置 Kubernetes 集群,就能在云端快速启动容器。对于运维人员来说,这意味着可以用最低的基础设施开销来运行批处理任务、CI/CD 作业或临时服务。

在日常运维场景中,我们经常需要快速部署一个容器来验证功能、处理数据或提供临时 API。传统方式需要登录 Azure 门户、手动点击多项配置,效率低下且容易出错。通过 PowerShell 的 Az 模块,可以将这些操作全部自动化,从创建资源组到部署容器、再到监控和清理,一条流水线搞定。

本文将介绍如何使用 PowerShell 完成 ACI 的基础操作、安全部署和监控清理,帮助你建立一套可复用的容器管理自动化方案。

ACI 基础操作:创建资源组与容器组

第一步是准备 Azure 环境并创建容器实例。以下脚本演示了登录 Azure、创建资源组、部署容器组并查询运行状态的完整流程。

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
# 登录 Azure 账户
Connect-AzAccount

# 定义变量
$ResourceGroupName = 'aci-demo-rg'
$Location = 'eastus'
$ContainerGroupName = 'demo-container-group'
$Image = 'mcr.microsoft.com/aci/helloworld'

# 创建资源组
$rg = New-AzResourceGroup -Name $ResourceGroupName -Location $Location
Write-Host "资源组 '$($rg.ResourceGroupName)' 创建完成,位置:$($rg.Location)"

# 创建容器组
$containerGroup = New-AzContainerGroup `
-ResourceGroupName $ResourceGroupName `
-Name $ContainerGroupName `
-Image $Image `
-OsType 'Linux' `
-Cpu 1 `
-MemoryInGB 1 `
-Port 80 `
-IpAddressType 'Public'

Write-Host "容器组 '$ContainerGroupName' 已创建"

# 查询容器组状态
$status = Get-AzContainerGroup `
-ResourceGroupName $ResourceGroupName `
-Name $ContainerGroupName

Write-Host "容器状态:$($status.ProvisioningState)"
Write-Host "公共 IP:$($status.IpAddress)"
Write-Host "FQDN:$($status.Fqdn)"

执行后,你将看到类似如下的输出:

1
2
3
4
5
资源组 'aci-demo-rg' 创建完成,位置:eastus
容器组 'demo-container-group' 已创建
容器状态:Succeeded
公共 IP:20.51.100.42
FQDN:demo-container-group.eastus.azurecontainer.io

创建成功后,可以通过返回的公共 IP 或 FQDN 直接访问容器内运行的服务。Get-AzContainerGroup 命令可以随时查询容器的最新状态,方便集成到监控脚本中。

容器部署:环境变量与密钥管理

实际生产中,容器通常需要配置环境变量和敏感信息。ACI 支持通过安全挂载 Azure Key Vault 中的密钥,避免在代码或命令行中暴露凭据。

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
# 定义安全引用变量
$ResourceGroupName = 'aci-demo-rg'
$ContainerGroupName = 'secure-app-group'

# 创建 Key Vault(如果不存在)
$VaultName = 'aci-demo-kv'
$kv = Get-AzKeyVault -VaultName $VaultName -ErrorAction SilentlyContinue
if (-not $kv) {
$kv = New-AzKeyVault `
-VaultName $VaultName `
-ResourceGroupName $ResourceGroupName `
-Location 'eastus'
Write-Host "Key Vault '$VaultName' 已创建"
}

# 向 Key Vault 添加数据库连接字符串
$secretValue = ConvertTo-SecureString `
-String 'Server=db.example.com;Database=appdb;User=appuser;Pwd=Str0ngP@ss!' `
-AsPlainText -Force

Set-AzKeyVaultSecret `
-VaultName $VaultName `
-Name 'DatabaseConnectionString' `
-SecretValue $secretValue

Write-Host "密钥已写入 Key Vault"

# 使用环境变量部署容器
$envVars = @(
@{ Name = 'APP_ENV'; Value = 'production' }
@{ Name = 'LOG_LEVEL'; Value = 'info' }
@{ Name = 'WORKERS'; Value = '4' }
)

$containerGroup = New-AzContainerGroup `
-ResourceGroupName $ResourceGroupName `
-Name $ContainerGroupName `
-Image 'nginx:latest' `
-OsType 'Linux' `
-Cpu 1 `
-MemoryInGB 1.5 `
-Port 80 `
-IpAddressType 'Public' `
-EnvironmentVariable $envVars

Write-Host "容器 '$ContainerGroupName' 已部署,含环境变量配置"

执行结果示例:

1
2
3
Key Vault 'aci-demo-kv' 已创建
密钥已写入 Key Vault
容器 'secure-app-group' 已部署,含环境变量配置

通过 Key Vault 管理敏感信息是 Azure 的最佳实践。环境变量适合传入非敏感的配置项(如运行模式、日志级别),而数据库密码、API 密钥等则应存储在 Key Vault 中,通过托管标识或安全引用注入容器,确保凭据不会出现在脚本或日志中。

容器监控、日志收集与自动清理

容器的生命周期管理同样重要。以下脚本演示了如何获取容器日志、检查资源使用情况,以及在任务完成后自动清理资源。

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
# 监控和清理变量
$ResourceGroupName = 'aci-demo-rg'
$ContainerGroupName = 'demo-container-group'

# 获取容器日志(最近 50 行)
$logs = Get-AzContainerInstanceLog `
-ResourceGroupName $ResourceGroupName `
-ContainerGroupName $ContainerGroupName `
-ContainerName $ContainerGroupName `
-Tail 50

Write-Host "=== 容器日志(最近 50 行)==="
Write-Host $logs

# 列出资源组中所有容器组的状态
$allContainers = Get-AzContainerGroup -ResourceGroupName $ResourceGroupName

foreach ($cg in $allContainers) {
$startTime = $cg.Containers[0].InstanceView.CurrentState.StartTime
Write-Host ("容器组:{0,-30} 状态:{1,-12} IP:{2}" -f `
$cg.Name, `
$cg.ProvisioningState, `
$cg.IpAddress)
}

# 自动清理:删除运行超过 24 小时的容器组
$cutoff = (Get-Date).AddHours(-24)

foreach ($cg in $allContainers) {
$startTime = $cg.Containers[0].InstanceView.CurrentState.StartTime
if ($startTime -and $startTime -lt $cutoff) {
Write-Host "清理过期容器组:$($cg.Name)(启动于 $startTime)"
Remove-AzContainerGroup `
-ResourceGroupName $ResourceGroupName `
-Name $cg.Name `
-Confirm:$false
}
}

Write-Host "`n清理完成。剩余容器组:"
$remaining = Get-AzContainerGroup -ResourceGroupName $ResourceGroupName
$remaining | ForEach-Object { Write-Host " - $($_.Name)" }

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
=== 容器日志(最近 50 行)===
Starting web server on port 80...
Serving content from /usr/local/apache2/htdocs/
GET / 200 15ms
GET /favicon.ico 404 2ms

容器组:demo-container-group 状态:Succeeded IP:20.51.100.42
容器组:secure-app-group 状态:Succeeded IP:20.51.100.88
清理过期容器组:demo-container-group(启动于 2025-11-30 06:30:00)

清理完成。剩余容器组:
- secure-app-group

自动清理机制可以有效控制成本。ACI 按容器运行时间计费,如果不及时清理临时容器,费用会持续累积。建议将清理脚本配置为定时任务(如 Azure Automation Runbook),每天自动扫描并回收超期的容器实例。

注意事项

  1. 权限要求:执行 ACI 操作需要 Azure 订阅的 Contributor 角色权限,建议创建专用服务主体(Service Principal)并授予最小权限,避免在日常脚本中使用管理员账户。

  2. 区域可用性:ACI 支持的 Azure 区域和 SKU 类型有限,部署前请确认目标区域是否支持你需要的特性(如 GPU 容器、虚拟网络集成等)。

  3. 资源配额限制:每个订阅对 ACI 有默认配额限制(如 CPU 核心数、容器组数量),大规模部署前可通过 Get-AzVMUsage 检查当前使用量,必要时提交配额提升申请。

  4. 密钥轮换:Key Vault 中的密钥应建立定期轮换机制,可结合 Azure Key Vault 的自动轮换策略,避免长期使用同一套凭据。

  5. 日志持久化:ACI 容器重启后本地日志会丢失,生产环境应将日志输出到 Azure Log Analytics 或外部存储,通过容器环境变量配置日志转发地址。

  6. 成本控制:ACI 按秒计费,适合短期任务和突发负载。如果容器需要长期运行(超过 1 周),建议迁移到 Azure Kubernetes Service(AKS),综合成本更低。