PowerShell 技能连载 - 微服务健康检查

适用于 PowerShell 7.0 及以上版本

在微服务架构中,服务之间的依赖关系错综复杂。一个服务异常可能引发连锁故障,导致整个系统崩溃。为了及时发现问题并触发自动恢复机制,我们需要对每个服务进行持续的健康检查。健康检查通常通过 HTTP 端点暴露服务的运行状态,包括数据库连接、缓存可用性、外部 API 响应等关键指标。

Kubernetes、Docker Swarm、Consul 等容器编排和服务发现工具都依赖健康检查端点来决定流量路由和容器重启策略。对于 PowerShell 运维人员来说,能够用脚本快速探测所有微服务的健康状态,是日常巡检和故障排查的基本功。

本文将从零开始,用 PowerShell 构建一套轻量级的微服务健康检查工具:先封装单服务探测函数,再扩展为批量并行检查,最后输出结构化的健康报告。

单服务健康检查函数

我们首先封装一个通用的健康检查函数,支持 HTTP 和 HTTPS 端点探测,返回结构化的检查结果。该函数会记录响应时间、状态码以及响应体中的关键信息。

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 Invoke-HealthCheck {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Name,

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

[int]$TimeoutSeconds = 5,

[string]$ExpectedStatus = '200'
)

$result = [ordered]@{
Name = $Name
Url = $Url
Status = 'Unknown'
StatusCode = $null
LatencyMs = $null
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Error = $null
}

try {
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$response = Invoke-WebRequest -Uri $Url -TimeoutSec $TimeoutSeconds `
-UseBasicParsing -ErrorAction Stop
$stopwatch.Stop()

$result.StatusCode = [int]$response.StatusCode
$result.LatencyMs = $stopwatch.ElapsedMilliseconds
$result.Status = if ($response.StatusCode -eq $ExpectedStatus) {
'Healthy'
} else {
'Degraded'
}
}
catch {
$stopwatch.Stop()
$result.Status = 'Unhealthy'
$result.LatencyMs = $stopwatch.ElapsedMilliseconds
$result.Error = $_.Exception.Message
}

[PSCustomObject]$result
}
1
2
3
4
5
6
7
8
9
PS> Invoke-HealthCheck -Name '用户服务' -Url 'https://api.example.com/users/health'

Name : 用户服务
Url : https://api.example.com/users/health
Status : Healthy
StatusCode : 200
LatencyMs : 47
Timestamp : 2025-09-23 08:15:32
Error :

批量并行健康检查

在生产环境中,微服务数量通常有几十甚至上百个。逐个串行检查效率太低,我们利用 PowerShell 的 ForEach-Object -Parallel(PowerShell 7+)实现并行探测,并设置合理的并发度和超时时间。

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
$services = @(
@{ Name = 'API 网关'; Url = 'https://api.example.com/gateway/health' }
@{ Name = '用户服务'; Url = 'https://api.example.com/users/health' }
@{ Name = '订单服务'; Url = 'https://api.example.com/orders/health' }
@{ Name = '支付服务'; Url = 'https://api.example.com/payment/health' }
@{ Name = '库存服务'; Url = 'https://api.example.com/inventory/health' }
@{ Name = '通知服务'; Url = 'https://api.example.com/notification/health' }
)

# 批量并行检查,最大并发 8 个
$healthResults = $services | ForEach-Object -ThrottleLimit 8 -Parallel {
# 在并行运行空间中重新定义函数
function Invoke-HealthCheck {
param($Name, $Url, $TimeoutSeconds = 5)
$result = [ordered]@{
Name = $Name
Url = $Url
Status = 'Unknown'
StatusCode = $null
LatencyMs = $null
Timestamp = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Error = $null
}
try {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$r = Invoke-WebRequest -Uri $Url -TimeoutSec $TimeoutSeconds `
-UseBasicParsing -ErrorAction Stop
$sw.Stop()
$result.StatusCode = [int]$r.StatusCode
$result.LatencyMs = $sw.ElapsedMilliseconds
$result.Status = if ($r.StatusCode -eq 200) { 'Healthy' } else { 'Degraded' }
}
catch {
$sw.Stop()
$result.Status = 'Unhealthy'
$result.LatencyMs = $sw.ElapsedMilliseconds
$result.Error = $_.Exception.Message
}
[PSCustomObject]$result
}

Invoke-HealthCheck -Name $_.Name -Url $_.Url
}

# 按状态排序输出:不健康 > 降级 > 健康
$statusOrder = @{ Healthy = 2; Degraded = 1; Unhealthy = 0 }
$healthResults | Sort-Object { $statusOrder[$_.Status] } | Format-Table -AutoSize
1
2
3
4
5
6
7
8
Name       Url                                                    Status     StatusCode LatencyMs Timestamp           Error
---- --- ------ ---------- --------- --------- -----
支付服务 https://api.example.com/payment/health Unhealthy 0 5012 2025-09-23 08:16:01 Unable to connect...
库存服务 https://api.example.com/inventory/health Degraded 503 234 2025-09-23 08:16:01
API 网关 https://api.example.com/gateway/health Healthy 200 38 2025-09-23 08:16:01
用户服务 https://api.example.com/users/health Healthy 200 47 2025-09-23 08:16:01
订单服务 https://api.example.com/orders/health Healthy 200 62 2025-09-23 08:16:01
通知服务 https://api.example.com/notification/health Healthy 200 85 2025-09-23 08:16:01

生成结构化健康报告

检查结果可以导出为 JSON 或 HTML 报告,方便集成到监控告警系统。下面的脚本将结果输出为 JSON 文件,并生成一份简洁的 HTML 报告。

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 Export-HealthReport {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[PSCustomObject[]]$Results,

[string]$OutputPath = './health-report'
)

$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'

# 导出 JSON 报告
$jsonPath = Join-Path $OutputPath "health-$timestamp.json"
$Results | ConvertTo-Json -Depth 5 | Out-File -FilePath $jsonPath -Encoding utf8

# 统计摘要
$healthyCount = ($Results | Where-Object Status -eq 'Healthy').Count
$degradedCount = ($Results | Where-Object Status -eq 'Degraded').Count
$unhealthyCount = ($Results | Where-Object Status -eq 'Unhealthy').Count
$totalCount = $Results.Count

$summary = @{
Total = $totalCount
Healthy = $healthyCount
Degraded = $degradedCount
Unhealthy = $unhealthyCount
CheckTime = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Overall = if ($unhealthyCount -gt 0) { 'CRITICAL' }
elseif ($degradedCount -gt 0) { 'WARNING' }
else { 'OK' }
}

Write-Host "`n========== 健康检查报告 ==========" -ForegroundColor Cyan
Write-Host "检查时间: $($summary.CheckTime)"
Write-Host "服务总数: $totalCount"
Write-Host "健康: $healthyCount" -ForegroundColor Green
Write-Host "降级: $degradedCount" -ForegroundColor Yellow
Write-Host "不健康: $unhealthyCount" -ForegroundColor Red
Write-Host "总体状态: $($summary.Overall)" -ForegroundColor $(
if ($summary.Overall -eq 'OK') { 'Green' }
elseif ($summary.Overall -eq 'WARNING') { 'Yellow' }
else { 'Red' }
)
Write-Host "===================================`n"

# 导出摘要 JSON
$summaryPath = Join-Path $OutputPath "summary-$timestamp.json"
$summary | ConvertTo-Json | Out-File -FilePath $summaryPath -Encoding utf8

Write-Host "JSON 报告已保存: $jsonPath"
Write-Host "摘要已保存: $summaryPath"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
PS> Export-HealthReport -Results $healthResults -OutputPath './reports'

========== 健康检查报告 ==========
检查时间: 2025-09-23 08:17:15
服务总数: 6
健康: 4
降级: 1
不健康: 1
总体状态: CRITICAL
===================================

JSON 报告已保存: /reports/health-20250923-081715.json
摘要已保存: /reports/summary-20250923-081715.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
24
25
26
27
28
29
30
31
32
33
# 配置文件 services.json 示例
$configContent = @{
services = @(
@{
name = 'API 网关'
url = 'https://api.example.com/gateway/health'
timeoutSeconds = 3
latencyWarnMs = 200
latencyCritMs = 1000
}
@{
name = '用户服务'
url = 'https://api.example.com/users/health'
timeoutSeconds = 5
latencyWarnMs = 500
latencyCritMs = 2000
}
@{
name = '订单服务'
url = 'https://api.example.com/orders/health'
timeoutSeconds = 10
latencyWarnMs = 1000
latencyCritMs = 5000
}
)
notify = @{
webhookUrl = 'https://hooks.slack.com/services/XXX/YYY/ZZZ'
enabled = $true
}
}

$configPath = './services.json'
$configContent | ConvertTo-Json -Depth 5 | Out-File -FilePath $configPath -Encoding utf8
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
PS> Get-Content ./services.json | ConvertFrom-Json | ConvertTo-Json -Depth 3

{
"services": [
{
"name": "API 网关",
"url": "https://api.example.com/gateway/health",
"timeoutSeconds": 3,
"latencyWarnMs": 200,
"latencyCritMs": 1000
},
{
"name": "用户服务",
"url": "https://api.example.com/users/health",
"timeoutSeconds": 5,
"latencyWarnMs": 500,
"latencyCritMs": 2000
},
{
"name": "订单服务",
"url": "https://api.example.com/orders/health",
"timeoutSeconds": 10,
"latencyWarnMs": 1000,
"latencyCritMs": 5000
}
],
"notify": {
"webhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ",
"enabled": true
}
}

接下来从配置文件加载并执行检查,根据延迟阈值自动判定告警级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 加载配置并执行检查
$config = Get-Content ./services.json -Raw | ConvertFrom-Json

$checkResults = foreach ($svc in $config.services) {
$check = Invoke-HealthCheck -Name $svc.name -Url $svc.url `
-TimeoutSeconds $svc.timeoutSeconds

# 根据延迟阈值判定告警级别
$alertLevel = 'OK'
if ($check.Status -eq 'Unhealthy') {
$alertLevel = 'CRITICAL'
}
elseif ($check.LatencyMs -gt $svc.latencyCritMs) {
$alertLevel = 'CRITICAL'
}
elseif ($check.LatencyMs -gt $svc.latencyWarnMs) {
$alertLevel = 'WARNING'
}

$check | Add-Member -NotePropertyName 'AlertLevel' `
-NotePropertyValue $alertLevel -PassThru
}

$checkResults | Format-Table Name, Status, AlertLevel, LatencyMs -AutoSize
1
2
3
4
5
Name       Status   AlertLevel LatencyMs
---- ------ ---------- ---------
API 网关 Healthy OK 35
用户服务 Healthy WARNING 612
订单服务 Healthy OK 78

注意事项

  1. 超时设置要合理:健康检查的超时时间不宜过长,通常 3-5 秒即可。过长的超时会导致批量检查时总耗时增加,也会拖慢容器编排系统的故障检测速度。

  2. 并行运行的函数作用域隔离ForEach-Object -Parallel 在独立的运行空间中执行,外部定义的函数和变量不会自动继承。需要在并行块内重新定义函数,或使用 $using: 传递变量。

  3. HTTPS 证书验证:内部服务的健康检查端点可能使用自签名证书。在测试环境中可以通过 -SkipCertificateCheck 参数跳过验证,但生产环境应配置受信任的 CA 证书。

  4. 配置文件管理:服务列表和阈值应维护在 JSON 或 YAML 配置文件中,纳入版本控制。避免在脚本中硬编码服务地址,便于环境迁移和 CI/CD 集成。

  5. 告警去重与静默:当某个服务持续不健康时,应避免重复发送告警。建议在告警逻辑中加入静默窗口(如 5 分钟内同一服务不重复告警),减少告警疲劳。

  6. 日志持久化:每次健康检查的结果应持久化存储,用于趋势分析和容量规划。可以将 JSON 报告写入时序数据库(如 InfluxDB)或对象存储(如 S3),配合 Grafana 等可视化工具查看历史趋势。

PowerShell 技能连载 - REST API 设计与实现

适用于 PowerShell 7.0 及以上版本

PowerShell 不仅能调用 API,还能创建 API。PolarisPode 等模块让 PowerShell 可以快速搭建 Web 服务器,处理 HTTP 请求。虽然不建议用 PowerShell 替代专业的 Web 框架,但在内网工具、运维自动化接口、轻量级中间服务等场景中,PowerShell API 服务器非常实用——快速实现一个健康检查接口、暴露系统信息给监控平台、提供配置查询 API。

本文将讲解如何使用 PowerShell 构建轻量级 REST API 服务。

使用 Pode 搭建 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
# 安装 Pode 模块
Install-Module -Name Pode -Force -Scope CurrentUser

# 创建 API 服务器脚本
$startApi = {
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http

# 日志中间件
Add-PodeMiddleware -Name 'Logger' -ScriptBlock {
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$method = $WebEvent.Method
$path = $WebEvent.Path
Write-PodeHost "[$timestamp] $method $path"
}

# GET /api/health —— 健康检查
Add-PodeRoute -Method Get -Path '/api/health' -ScriptBlock {
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$uptime = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)

Write-PodeJsonResponse -Value @{
status = 'healthy'
timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
computer = $env:COMPUTERNAME
uptime = "$uptime 天"
cpuUsage = "$($cpu.LoadPercentage)%"
memoryUsage = "$([math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize * 100, 1))%"
}
}

# GET /api/processes —— 进程列表
Add-PodeRoute -Method Get -Path '/api/processes' -ScriptBlock {
$top = if ($WebEvent.Query['top']) { [int]$WebEvent.Query['top'] } else { 10 }
$sortBy = if ($WebEvent.Query['sort']) { $WebEvent.Query['sort'] } else { 'Memory' }

$processes = Get-Process | Sort-Object WorkingSet64 -Descending |
Select-Object -First $top |
ForEach-Object {
@{
name = $_.Name
pid = $_.Id
memoryMB = [math]::Round($_.WorkingSet64 / 1MB, 1)
cpu = [math]::Round($_.CPU, 2)
}
}

Write-PodeJsonResponse -Value @{
count = $processes.Count
data = $processes
}
}

# GET /api/disks —— 磁盘信息
Add-PodeRoute -Method Get -Path '/api/disks' -ScriptBlock {
$disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
$totalGB = [math]::Round($_.Size / 1GB, 2)
$freeGB = [math]::Round($_.FreeSpace / 1GB, 2)
@{
drive = $_.DeviceID
totalGB = $totalGB
freeGB = $freeGB
usagePct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
}
}

Write-PodeJsonResponse -Value @{ data = $disks }
}
}
}

Write-Host "API 服务器启动中..." -ForegroundColor Cyan
Write-Host "端点:" -ForegroundColor Yellow
Write-Host " GET http://localhost:8080/api/health" -ForegroundColor Green
Write-Host " GET http://localhost:8080/api/processes?top=5" -ForegroundColor Green
Write-Host " GET http://localhost:8080/api/disks" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
API 服务器启动中...
端点:
GET http://localhost:8080/api/health
GET http://localhost:8080/api/processes?top=5
GET http://localhost:8080/api/disks
[2025-07-23 08:30:15] GET /api/health

POST 接口与请求处理

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
# 在同一 Pode 服务器中添加 POST 接口
$apiWithPost = {
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http

# POST /api/execute —— 执行命令(需要认证)
Add-PodeRoute -Method Post -Path '/api/execute' -ScriptBlock {
$body = $WebEvent.Data

if (-not $body.command) {
Set-PodeResponseStatus -Code 400
Write-PodeJsonResponse -Value @{ error = '缺少 command 参数' }
return
}

# 安全限制:只允许特定命令
$allowed = @('Get-Process', 'Get-Service', 'Get-EventLog', 'Get-CimInstance')
$cmdName = ($body.command -split '\s+')[0]
if ($cmdName -notin $allowed) {
Set-PodeResponseStatus -Code 403
Write-PodeJsonResponse -Value @{ error = "命令不允许:$cmdName" }
return
}

try {
$result = Invoke-Expression $body.command -ErrorAction Stop
$jsonResult = $result | ConvertTo-Json -Depth 3 -Compress

Write-PodeJsonResponse -Value @{
success = $true
command = $body.command
result = $result
}
} catch {
Set-PodeResponseStatus -Code 500
Write-PodeJsonResponse -Value @{
success = $false
error = $_.Exception.Message
}
}
}

# POST /api/alert —— 接收告警
Add-PodeRoute -Method Post -Path '/api/alert' -ScriptBlock {
$body = $WebEvent.Data

$alertInfo = @{
timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
source = $body.source
level = $body.level
message = $body.message
hostname = $body.hostname
receivedAt = Get-Date -Format 'HH:mm:ss'
}

# 记录到文件
$alertInfo | ConvertTo-Json | Add-Content "C:\Logs\alerts.json" -Encoding UTF8

Write-PodeJsonResponse -Value @{
received = $true
id = [guid]::NewGuid().ToString()
}
}

# GET /api/alerts —— 查询告警
Add-PodeRoute -Method Get -Path '/api/alerts' -ScriptBlock {
$count = if ($WebEvent.Query['count']) { [int]$WebEvent.Query['count'] } else { 10 }
$level = $WebEvent.Query['level']

if (Test-Path "C:\Logs\alerts.json") {
$alerts = Get-Content "C:\Logs\alerts.json" -Tail $count |
ConvertFrom-Json |
Where-Object { if ($level) { $_.level -eq $level } else { $true } }

Write-PodeJsonResponse -Value @{
count = $alerts.Count
data = $alerts
}
} else {
Write-PodeJsonResponse -Value @{ count = 0; data = @() }
}
}
}
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
# 调用 POST 接口
$body = @{ command = "Get-Process | Select-Object -First 3 Name,Id" } | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8081/api/execute" -Method Post -Body $body -ContentType "application/json"

# 发送告警
$alert = @{
source = "monitor"
level = "ERROR"
message = "磁盘空间不足"
hostname = "SRV01"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8081/api/alert" -Method Post -Body $alert -ContentType "application/json"

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
# 封装为可复用的 API 客户端函数
function Get-ServerHealth {
param([string]$ServerUrl = "http://localhost:8080")

try {
$response = Invoke-RestMethod -Uri "$ServerUrl/api/health" -TimeoutSec 5
return $response
} catch {
Write-Host "健康检查失败:$($_.Exception.Message)" -ForegroundColor Red
return $null
}
}

function Get-ServerProcesses {
param(
[string]$ServerUrl = "http://localhost:8080",
[int]$Top = 10
)

return Invoke-RestMethod -Uri "$ServerUrl/api/processes?top=$Top" -TimeoutSec 10
}

# 批量检查多台服务器
$servers = @(
"http://web-01:8080",
"http://web-02:8080",
"http://db-01:8080"
)

Write-Host "集群健康检查:" -ForegroundColor Cyan
foreach ($server in $servers) {
$health = Get-ServerHealth -ServerUrl $server
if ($health) {
$status = $health.status
$color = if ($status -eq "healthy") { "Green" } else { "Red" }
Write-Host " $server : $status (CPU: $($health.cpuUsage), MEM: $($health.memoryUsage))" -ForegroundColor $color
} else {
Write-Host " $server : 不可达" -ForegroundColor Red
}
}

执行结果示例:

1
2
3
4
集群健康检查:
http://web-01:8080 : healthy (CPU: 35%, MEM: 72.4%)
http://web-02:8080 : healthy (CPU: 28%, MEM: 65.1%)
http://db-01:8080 : healthy (CPU: 42%, MEM: 85.3%)

注意事项

  1. 安全风险:暴露命令执行接口极度危险,生产环境必须加认证、限白名单、审计日志
  2. 性能限制:PowerShell HTTP 服务器性能有限,不适合高并发场景(建议使用反向代理)
  3. 认证:生产 API 必须添加认证(API Key、JWT、Basic Auth),Pode 内置认证中间件
  4. HTTPS:生产环境使用 HTTPS,Pode 支持自签名证书和 Let’s Encrypt
  5. 服务化:长期运行的 API 应注册为 Windows 服务,使用 nssmRegister-PodeService
  6. 错误处理:API 接口必须有完善的错误处理和统一的错误响应格式

PowerShell 技能连载 - Microsoft Graph API 集成

适用于 PowerShell 5.1 及以上版本,需安装 Microsoft.Graph 模块

Microsoft Graph 是 Microsoft 365 平台的统一 API 网关——它整合了 Azure AD(现称 Entra ID)、Exchange Online、SharePoint、Teams、OneDrive 等所有 Microsoft 365 服务的数据和操作。通过 PowerShell 的 Microsoft.Graph 模块,运维人员可以用脚本化管理用户、组、许可证、设备策略等,替代传统的多个独立模块。

本文将讲解 Microsoft Graph PowerShell 的连接、用户管理、组操作和常用自动化场景。

连接与认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 安装 Microsoft Graph 模块
Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force

# 选择性安装(更快)
# Install-Module -Name Microsoft.Graph.Users, Microsoft.Graph.Groups -Scope CurrentUser

# 连接到 Microsoft Graph(交互式登录)
Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All", "Directory.Read.All"

# 查看当前连接信息
Get-MgContext | Select-Object Account, Tenant, Scopes |
Format-List

# 使用应用权限连接(自动化场景)
$clientId = "your-app-client-id"
$tenantId = "your-tenant-id"
$certThumbprint = "ABC123DEF456"

Connect-MgGraph -ClientId $clientId -TenantId $tenantId `
-CertificateThumbprint $certThumbprint

# 断开连接
Disconnect-MgGraph

执行结果示例:

1
2
3
4
5
Account : admin@contoso.com
Tenant : contoso.onmicrosoft.com
Scopes : {User.Read.All, Group.Read.All, Directory.Read.All}

Welcome To Microsoft Graph!

用户管理

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
# 列出所有用户
Get-MgUser -All -ConsistencyLevel eventual |
Select-Object DisplayName, UserPrincipalName, Mail, Department, JobTitle |
Format-Table -AutoSize

# 搜索特定用户
$user = Get-MgUser -Filter "displayName eq 'John Doe'" -ConsistencyLevel eventual
$user | Select-Object Id, DisplayName, UserPrincipalName, Department

# 创建新用户
$newUser = @{
AccountEnabled = $true
DisplayName = "张伟"
MailNickname = "wei.zhang"
UserPrincipalName = "wei.zhang@contoso.com"
Department = "IT"
JobTitle = "DevOps 工程师"
PasswordProfile = @{
ForceChangePasswordNextSignIn = $true
Password = "TempP@ssw0rd123!"
}
}

New-MgUser -BodyParameter $newUser
Write-Host "用户已创建:wei.zhang@contoso.com" -ForegroundColor Green

# 更新用户信息
Update-MgUser -UserId "wei.zhang@contoso.com" -Department "DevOps"

# 禁用用户账户
Update-MgUser -UserId "wei.zhang@contoso.com" -AccountEnabled:$false

# 获取用户的组成员身份
Get-MgUserMemberOf -UserId "john.doe@contoso.com" |
ForEach-Object {
$group = Get-MgGroup -GroupId $_.Id
[PSCustomObject]@{
GroupName = $group.DisplayName
GroupType = $group.GroupTypes -join ','
}
} | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DisplayName  UserPrincipalName       Department  JobTitle
----------- ----------------- ---------- --------
John Doe john.doe@contoso.com Engineering Senior Dev
Jane Smith jane.smith@contoso.com HR HR Manager

Id : 12345678-abcd-...
DisplayName : John Doe
UserPrincipalName : john.doe@contoso.com

用户已创建:wei.zhang@contoso.com

GroupName GroupType
--------- ---------
All Users {}
IT-Admins {}
Developers {DynamicMembership}

组管理

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
# 列出所有组
Get-MgGroup -All |
Select-Object DisplayName, Description,
@{N='类型'; E={if ($_.GroupTypes -contains 'Unified') { 'Microsoft 365' } else { 'Security' }}},
@{N='成员数'; E={$_.Members.Count}} |
Sort-Object DisplayName |
Format-Table -AutoSize

# 创建安全组
New-MgGroup -DisplayName "Cloud-Admins" `
-Description "云平台管理员组" `
-MailEnabled:$false `
-SecurityEnabled:$true

# 添加成员到组
$userId = (Get-MgUser -Filter "displayName eq 'John Doe'").Id
$groupId = (Get-MgGroup -Filter "displayName eq 'Cloud-Admins'").Id

New-MgGroupMember -GroupId $groupId -DirectoryObjectId $userId
Write-Host "已将 John Doe 添加到 Cloud-Admins 组" -ForegroundColor Green

# 查看组成员
Get-MgGroupMember -GroupId $groupId |
ForEach-Object {
Get-MgUser -UserId $_.Id |
Select-Object DisplayName, UserPrincipalName
} | Format-Table -AutoSize

# 创建动态组(基于规则的自动成员管理)
$dynamicGroup = @{
DisplayName = "All-Engineering"
Description = "工程部门所有成员"
GroupTypes = @("DynamicMembership")
MailEnabled = $false
SecurityEnabled = $true
MembershipRule = 'user.department -eq "Engineering"'
MembershipRuleProcessingState = "On"
}

New-MgGroup -BodyParameter $dynamicGroup

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
DisplayName    类型           成员数
----------- ---- ------
All Users Security 245
IT-Admins Security 12
Developers Microsoft 365 35
Cloud-Admins Security 3

已将 John Doe 添加到 Cloud-Admins

DisplayName UserPrincipalName
----------- -----------------
John Doe john.doe@contoso.com
Jane Smith jane.smith@contoso.com

许可证管理

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
# 查看租户可用的许可证
Get-MgSubscribedSku |
Select-Object SkuPartNumber,
@{N='已用'; E={$_.PrepaidUnits.Enabled - $_.Consumed}},
@{N='总量'; E={$_.PrepaidUnits.Enabled}},
@{N='已消耗'; E={$_.Consumed}} |
Format-Table -AutoSize

# 为用户分配许可证
$license = Get-MgSubscribedSku | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK'

Set-MgUserLicense -UserId "wei.zhang@contoso.com" `
-AddLicenses @{ SkuId = $license.SkuId } `
-RemoveLicenses @()

Write-Host "已分配 Office 365 E3 许可证" -ForegroundColor Green

# 批量分配许可证
$users = Get-MgUser -Filter "department eq 'Engineering'" -ConsistencyLevel eventual -All
$skuId = (Get-MgSubscribedSku | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK').SkuId

foreach ($user in $users) {
Set-MgUserLicense -UserId $user.Id `
-AddLicenses @{ SkuId = $skuId } `
-RemoveLicenses @()
Write-Host "已分配:$($user.DisplayName)" -ForegroundColor Green
}

执行结果示例:

1
2
3
4
5
6
7
8
SkuPartNumber       已用 总量 已消耗
------------ ---- ---- ------
ENTERPRISEPACK 120 250 130
EMS 45 100 55

已分配 Office 365 E3 许可证
已分配:John Doe
已分配:Alice Smith

注意事项

  1. 权限范围:使用 -Scopes 参数指定最小必要权限,避免过度授权
  2. ConsistencyLevel:部分高级查询需要添加 -ConsistencyLevel eventual$Count 参数
  3. 分页处理:大量结果时使用 -All 参数自动处理分页,否则只返回第一页
  4. 应用权限:自动化脚本应使用应用权限(Client Credentials 流),而非用户委派权限
  5. Graph API 版本:MgGraph 模块默认使用 v1.0 端点,使用 Invoke-MgGraphRequest 可以调用 beta 端点
  6. 速率限制:Microsoft Graph 有 API 调用频率限制,大批量操作时添加适当延迟