PowerShell 技能连载 - Azure Private Endpoint 网络隔离

适用于 PowerShell 7.0 及以上版本

在混合云架构中,数据安全的核心诉求之一是确保敏感流量不经过公共互联网。Azure PaaS 服务(如 Storage Account、SQL Database、Key Vault)默认通过公网端点提供服务,虽然传输层使用 TLS 加密,但出于合规要求(如金融行业的 PCI-DSS、医疗行业的 HIPAA),很多企业需要将所有数据路径锁定在私有网络内。Azure Private Endpoint 正是为满足这一需求而设计的网络隔离方案。

Private Endpoint 在虚拟网络中分配一个私有 IP 地址,将 PaaS 服务的入口映射到你的 VNet 内部。通过配合 Private DNS Zone,客户端可以使用原有的服务 FQDN(如 mystorage.blob.core.windows.net)直接解析到私有 IP,无需修改应用代码。整个数据平面流量都在 Microsoft 骨干网内传输,完全不暴露在公网上。

本文将从三个层面展开实战:首先是 Private Endpoint 的创建与 DNS 配置自动化;然后是 Private Link Service 的构建,将内部服务安全地暴露给合作伙伴网络;最后是网络隔离合规验证,通过脚本自动检测配置漂移并生成合规报告。

Private Endpoint 创建与 DNS 配置

创建 Private Endpoint 需要同时处理网络接口和 DNS 解析两个层面。下面的脚本封装了完整的端点创建流程,包括自动关联 Private DNS Zone、配置 A 记录以及验证 DNS 解析结果。

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
function New-AzPrivateEndpointWithDns {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ResourceGroupName,

[Parameter(Mandatory = $true)]
[string]$VirtualNetworkName,

[Parameter(Mandatory = $true)]
[string]$SubnetName,

[Parameter(Mandatory = $true)]
[string]$PrivateEndpointName,

[Parameter(Mandatory = $true)]
[string]$TargetResourceId,

[Parameter(Mandatory = $true)]
[string]$TargetSubresource,

[Parameter(Mandatory = $false)]
[string]$DnsZoneResourceGroup
)

# 获取子网信息,确认子网存在且可用
$vnet = Get-AzVirtualNetwork -Name $VirtualNetworkName -ResourceGroupName $ResourceGroupName
$subnet = Get-AzVirtualNetworkSubnetConfig -Name $SubnetName -VirtualNetwork $vnet

if (-not $subnet) {
Write-Error "子网 $SubnetName 不存在于虚拟网络 $VirtualNetworkName 中"
return
}

Write-Host "目标子网: $($subnet.Name) ($($subnet.AddressPrefix))" -ForegroundColor Cyan

# 构建 Private Link Service 连接配置
$privateLinkConnection = New-AzPrivateLinkServiceConnection `
-Name "$PrivateEndpointName-connection" `
-PrivateLinkServiceId $TargetResourceId `
-GroupId $TargetSubresource

# 创建 Private Endpoint
Write-Host "正在创建 Private Endpoint: $PrivateEndpointName ..." -ForegroundColor Yellow
$pe = New-AzPrivateEndpoint `
-Name $PrivateEndpointName `
-ResourceGroupName $ResourceGroupName `
-Location $vnet.Location `
-Subnet $subnet `
-PrivateLinkServiceConnection $privateLinkConnection `
-ManualRequest:$false

Write-Host "Private Endpoint 创建完成" -ForegroundColor Green

# 自动确定 DNS Zone 名称
$targetResource = Get-AzResource -ResourceId $TargetResourceId
$dnsZoneMap = @{
"Microsoft.Storage/storageAccounts" = "privatelink.blob.core.windows.net"
"Microsoft.Storage/storageAccounts/file" = "privatelink.file.core.windows.net"
"Microsoft.Sql/servers" = "privatelink.database.windows.net"
"Microsoft.KeyVault/vaults" = "privatelink.vaultcore.azure.net"
"Microsoft.ContainerRegistry/registries" = "privatelink.azurecr.io"
"Microsoft.Web/sites" = "privatelink.azurewebsites.net"
}

$dnsZoneName = $dnsZoneMap[$targetResource.ResourceType]
if (-not $dnsZoneName) {
Write-Warning "未找到资源类型 $($targetResource.ResourceType) 的默认 DNS Zone,请手动指定"
return $pe
}

# 查找或创建 Private DNS Zone
$dnsRg = if ($DnsZoneResourceGroup) { $DnsZoneResourceGroup } else { $ResourceGroupName }
$dnsZone = Get-AzPrivateDnsZone -ResourceGroupName $dnsRg -Name $dnsZoneName -ErrorAction SilentlyContinue

if (-not $dnsZone) {
Write-Host "创建 Private DNS Zone: $dnsZoneName" -ForegroundColor Yellow
$dnsZone = New-AzPrivateDnsZone -ResourceGroupName $dnsRg -Name $dnsZoneName
}

# 创建 Virtual Network Link,将 DNS Zone 关联到 VNet
$vnetLinkName = "link-$($VirtualNetworkName)-$($dnsZoneName -replace '\.','')"
$existingLink = Get-AzPrivateDnsVirtualNetworkLink `
-ResourceGroupName $dnsRg -ZoneName $dnsZoneName -Name $vnetLinkName -ErrorAction SilentlyContinue

if (-not $existingLink) {
Write-Host "关联 DNS Zone 到虚拟网络: $VirtualNetworkName" -ForegroundColor Yellow
$null = New-AzPrivateDnsVirtualNetworkLink `
-ResourceGroupName $dnsRg `
-ZoneName $dnsZoneName `
-Name $vnetLinkName `
-VirtualNetworkId $vnet.Id
}

# 获取 Private Endpoint 的网络接口 IP 地址
$networkInterface = Get-AzNetworkInterface -ResourceId $pe.NetworkInterfaces[0].Id
$privateIpAddress = $networkInterface.IpConfigurations[0].PrivateIpAddress

Write-Host "Private Endpoint IP: $privateIpAddress" -ForegroundColor Green

# 创建 DNS A 记录
$dnsRecordName = $targetResource.Name
$existingRecord = Get-AzPrivateDnsRecordSet `
-ResourceGroupName $dnsRg -ZoneName $dnsZoneName -Name $dnsRecordName -RecordType A -ErrorAction SilentlyContinue

if (-not $existingRecord) {
$null = New-AzPrivateDnsRecordSet `
-ResourceGroupName $dnsRg `
-ZoneName $dnsZoneName `
-Name $dnsRecordName `
-RecordType A `
-Ttl 300 `
-PrivateDnsRecord (New-AzPrivateDnsRecordConfig -IPv4Address $privateIpAddress)
Write-Host "DNS A 记录已创建: $dnsRecordName -> $privateIpAddress" -ForegroundColor Green
}

return @{
PrivateEndpoint = $pe
PrivateIpAddress = $privateIpAddress
DnsZoneName = $dnsZoneName
DnsRecordName = $dnsRecordName
}
}

# 示例:为 Storage Account 创建 Private Endpoint
$result = New-AzPrivateEndpointWithDns `
-ResourceGroupName "rg-production" `
-VirtualNetworkName "vnet-prod-eastus" `
-SubnetName "snet-endpoints" `
-PrivateEndpointName "pe-prod-storage" `
-TargetResourceId "/subscriptions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/resourceGroups/rg-production/providers/Microsoft.Storage/storageAccounts/stproddata001" `
-TargetSubresource "blob"

# 验证 DNS 解析
$storageFqdn = "stproddata001.blob.core.windows.net"
$resolved = Resolve-DnsName -Name $storageFqdn -DnsOnly -ErrorAction SilentlyContinue
Write-Host "`nDNS 解析验证: $storageFqdn -> $($resolved.IPAddress -join ', ')" -ForegroundColor Cyan

执行结果示例:

1
2
3
4
5
6
7
8
9
目标子网: snet-endpoints (10.0.2.0/24)
正在创建 Private Endpoint: pe-prod-storage ...
Private Endpoint 创建完成
创建 Private DNS Zone: privatelink.blob.core.windows.net
关联 DNS Zone 到虚拟网络: vnet-prod-eastus
Private Endpoint IP: 10.0.2.4
DNS A 记录已创建: stproddata001 -> 10.0.2.4

DNS 解析验证: stproddata001.blob.core.windows.net -> 10.0.2.4

从输出可以看到,Storage Account 的 FQDN 已经解析到 VNet 内的私有 IP 10.0.2.4,而不是公网地址。这意味着同一 VNet 内的所有虚拟机访问该 Storage Account 时,流量不会离开 Microsoft 骨干网。DNS Zone 的自动关联确保了无需修改应用配置,原有的连接字符串即可直接工作。

Private Link Service 是 Private Endpoint 的镜像能力——它允许你将 VNet 内部运行的服务(如 API 服务、中间件)通过私有连接暴露给其他 Azure 租户或订阅。下面的脚本展示了如何创建 Private Link Service,配置 NAT 规则和访问控制策略。

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
function New-AzPrivateLinkServiceAutomation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ResourceGroupName,

[Parameter(Mandatory = $true)]
[string]$ServiceName,

[Parameter(Mandatory = $true)]
[string]$LoadBalancerName,

[Parameter(Mandatory = $true)]
[string]$FrontendIpConfigName,

[Parameter(Mandatory = $false)]
[string[]]$AllowedSubscriptions = @(),

[Parameter(Mandatory = $false)]
[bool]$EnableProxyProtocol = $false,

[Parameter(Mandatory = $false)]
[string]$Visibility = "AutoApproval"
)

# 获取内部负载均衡器和前端 IP 配置
$lb = Get-AzLoadBalancer -Name $LoadBalancerName -ResourceGroupName $ResourceGroupName
$frontendIp = $lb.FrontendIpConfigurations | Where-Object { $_.Name -eq $FrontendIpConfigName }

if (-not $frontendIp) {
Write-Error "未找到前端 IP 配置: $FrontendIpConfigName"
return
}

Write-Host "负载均衡器: $($lb.Name)" -ForegroundColor Cyan
Write-Host "前端 IP: $($frontendIp.PrivateIpAddress)" -ForegroundColor Cyan

# 创建 Private Link Service 的 IP 配置
$ipConfig = New-AzPrivateLinkServiceIpConfig `
-Name "$ServiceName-ipconfig" `
-PrivateIpAddress "10.0.3.10" `
-Subnet (Get-AzVirtualNetwork -ResourceGroupName $ResourceGroupName |
Get-AzVirtualNetworkSubnetConfig -Name "snet-privatelink")

# 构建访问控制参数
$autoApproval = $null
$visibilityList = @()

if ($Visibility -eq "AutoApproval" -and $AllowedSubscriptions.Count -gt 0) {
# 指定的订阅自动批准连接请求
$autoApproval = $AllowedSubscriptions
$visibilityList = $AllowedSubscriptions
Write-Host "访问策略: 自动批准 (订阅白名单: $($AllowedSubscriptions.Count) 个)" -ForegroundColor Yellow
}
elseif ($Visibility -eq "Private") {
# 仅对特定订阅可见
$visibilityList = $AllowedSubscriptions
Write-Host "访问策略: 私有可见 (订阅白名单: $($AllowedSubscriptions.Count) 个)" -ForegroundColor Yellow
}
else {
# 对所有订阅可见
Write-Host "访问策略: 公开可见 (需手动批准连接)" -ForegroundColor Yellow
}

# 创建 Private Link Service
Write-Host "正在创建 Private Link Service: $ServiceName ..." -ForegroundColor Yellow

$plsParams = @{
Name = $ServiceName
ResourceGroupName = $ResourceGroupName
Location = $lb.Location
LoadBalancerFrontendIpConfiguration = $frontendIp
IpConfiguration = $ipConfig
EnableProxyProtocol = $EnableProxyProtocol
}

if ($visibilityList.Count -gt 0) {
$plsParams["Visibility"] = $visibilityList
}

if ($autoApproval) {
$plsParams["AutoApproval"] = $autoApproval
}

$pls = New-AzPrivateLinkService @plsParams

Write-Host "Private Link Service 创建完成" -ForegroundColor Green
Write-Host "服务别名 (Alias): $($pls.Alias)" -ForegroundColor Green
Write-Host "服务资源 ID: $($pls.Id)" -ForegroundColor DarkGray

# 查看待处理的连接请求
$connections = Get-AzPrivateEndpointConnection -PrivateLinkServiceId $pls.Id -ErrorAction SilentlyContinue
if ($connections) {
Write-Host "`n当前连接请求:" -ForegroundColor Cyan
$connections | Format-Table Name, PrivateEndpointId, ConnectionState, Description -AutoSize
}

return $pls
}

# 示例:创建 Private Link Service,暴露内部 API 给合作伙伴
$partnerSubscriptions = @(
"b2c3d4e5-f6a7-8901-bcde-f12345678901"
"c3d4e5f6-a7b8-9012-cdef-123456789012"
)

$pls = New-AzPrivateLinkServiceAutomation `
-ResourceGroupName "rg-production" `
-ServiceName "pls-internal-api" `
-LoadBalancerName "lb-internal-api" `
-FrontendIpConfigName "feip-api-v1" `
-AllowedSubscriptions $partnerSubscriptions `
-EnableProxyProtocol $false `
-Visibility "AutoApproval"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
负载均衡器: lb-internal-api
前端 IP: 10.0.3.5
访问策略: 自动批准 (订阅白名单: 2 个)
正在创建 Private Link Service: pls-internal-api ...
Private Link Service 创建完成
服务别名 (Alias): pls-internal-api.7b8c9d0e-eastus.azure.privatelinkservice
服务资源 ID: /subscriptions/a1b2c3d4-.../Microsoft.Network/privateLinkServices/pls-internal-api

当前连接请求:
Name PrivateEndpointId ConnectionState Description
---- ----------------- --------------- -----------
pe-partner-a-eastus /subscriptions/b2c3d4e5-.../pe-partner Approved
pe-partner-b-eastus /subscriptions/c3d4e5f6-.../pe-partner Pending

Private Link Service 的核心价值在于安全可控的跨租户连接。通过 AutoApproval 机制,指定的合作伙伴订阅(如 b2c3d4e5-...)创建的 Private Endpoint 连接会被自动批准,无需人工干预;而 Pending 状态的连接则需要运维团队手动审核。Alias 是消费方创建 Private Endpoint 时使用的唯一标识,不需要暴露你的资源 ID。EnableProxyProtocol 参数在需要获取客户端真实 IP 的场景中开启,但会增加少量网络开销。

网络隔离验证与合规检查

Private Endpoint 部署完成后,持续验证网络隔离状态是合规审计的基本要求。下面的脚本实现了自动化合规检查,覆盖 DNS 解析、公网端点状态、网络路由和访问策略等多个维度,并生成结构化的合规报告。

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
function Test-AzPrivateEndpointCompliance {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ResourceGroupName,

[Parameter(Mandatory = $false)]
[string]$OutputPath = "$HOME/PrivateEndpointCompliance_$(Get-Date -Format 'yyyyMMdd_HHmmss').json"
)

$results = @()
$now = Get-Date
$compliantCount = 0
$nonCompliantCount = 0

Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host "Private Endpoint 网络隔离合规检查" -ForegroundColor Cyan
Write-Host "检查时间: $($now.ToString('yyyy-MM-dd HH:mm:ss'))"
Write-Host "资源组: $ResourceGroupName"
Write-Host ("=" * 70) -ForegroundColor Cyan

# 获取资源组内所有 Private Endpoint
$privateEndpoints = Get-AzPrivateEndpoint -ResourceGroupName $ResourceGroupName
Write-Host "`n发现 $($privateEndpoints.Count) 个 Private Endpoint" -ForegroundColor Yellow

foreach ($pe in $privateEndpoints) {
$checkResult = [ordered]@{
Name = $pe.Name
ResourceId = $pe.Id
Location = $pe.Location
CheckTime = $now.ToString("yyyy-MM-dd HH:mm:ss")
Status = "Compliant"
Issues = @()
}

Write-Host "`n--- 检查: $($pe.Name) ---" -ForegroundColor White

# 检查 1: 连接状态是否为 Approved
foreach ($conn in $pe.PrivateLinkServiceConnections) {
$connStatus = $conn.PrivateLinkServiceConnectionState.Status
if ($connStatus -ne "Approved") {
$checkResult.Status = "NonCompliant"
$checkResult.Issues += "连接状态非 Approved: $connStatus (服务: $($conn.PrivateLinkServiceId))"
Write-Host " [FAIL] 连接状态: $connStatus" -ForegroundColor Red
}
else {
Write-Host " [PASS] 连接状态: Approved" -ForegroundColor Green
}
}

# 检查 2: 关联的网络接口是否具有私有 IP
$nic = Get-AzNetworkInterface -ResourceId $pe.NetworkInterfaces[0].Id
$privateIp = $nic.IpConfigurations[0].PrivateIpAddress
if ($privateIp -match "^10\.|^172\.(1[6-9]|2[0-9]|3[01])\.|^192\.168\.") {
Write-Host " [PASS] 私有 IP 地址: $privateIp (RFC1918)" -ForegroundColor Green
}
else {
$checkResult.Status = "NonCompliant"
$checkResult.Issues += "IP 地址不在 RFC1918 私有范围: $privateIp"
Write-Host " [FAIL] IP 地址不在 RFC1918 范围: $privateIp" -ForegroundColor Red
}

# 检查 3: 目标资源的公网端点是否已禁用
$targetResourceId = $pe.PrivateLinkServiceConnections[0].PrivateLinkServiceId
$targetResource = Get-AzResource -ResourceId $targetResourceId
$resourceType = $targetResource.ResourceType

$publicAccessEnabled = $false
switch ($resourceType) {
"Microsoft.Storage/storageAccounts" {
$storage = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -Name $targetResource.Name
$publicAccessEnabled = $storage.NetworkRuleSet.DefaultAction -eq "Allow"
}
"Microsoft.KeyVault/vaults" {
$kv = Get-AzKeyVault -ResourceGroupName $ResourceGroupName -VaultName $targetResource.Name
$publicAccessEnabled = $kv.NetworkAcls.DefaultAction -eq "Allow"
}
"Microsoft.Sql/servers" {
$sql = Get-AzSqlServer -ResourceGroupName $ResourceGroupName -ServerName $targetResource.Name
$publicAccessEnabled = $sql.PublicNetworkAccess -eq "Enabled"
}
}

if ($publicAccessEnabled) {
$checkResult.Status = "NonCompliant"
$checkResult.Issues += "目标资源的公网访问未禁用 (类型: $resourceType)"
Write-Host " [FAIL] 公网端点未禁用 ($resourceType)" -ForegroundColor Red
}
else {
Write-Host " [PASS] 公网端点已禁用 ($resourceType)" -ForegroundColor Green
}

# 检查 4: DNS 解析是否指向私有 IP
$dnsZoneMap = @{
"Microsoft.Storage/storageAccounts" = "{0}.blob.core.windows.net"
"Microsoft.KeyVault/vaults" = "{0}.vault.azure.net"
"Microsoft.Sql/servers" = "{0}.database.windows.net"
}

$fqdnTemplate = $dnsZoneMap[$resourceType]
if ($fqdnTemplate) {
$fqdn = $fqdnTemplate -f $targetResource.Name
$resolved = Resolve-DnsName -Name $fqdn -DnsOnly -ErrorAction SilentlyContinue
$resolvedIp = if ($resolved) { $resolved[0].IPAddress } else { "未解析" }

if ($resolvedIp -eq $privateIp) {
Write-Host " [PASS] DNS 解析: $fqdn -> $resolvedIp" -ForegroundColor Green
}
else {
$checkResult.Status = "NonCompliant"
$checkResult.Issues += "DNS 解析不匹配: $fqdn 解析为 $resolvedIp,应为 $privateIp"
Write-Host " [FAIL] DNS 解析不匹配: $resolvedIp (应为 $privateIp)" -ForegroundColor Red
}
}

# 汇总
if ($checkResult.Status -eq "Compliant") {
$compliantCount++
}
else {
$nonCompliantCount++
}

$results += [PSCustomObject]$checkResult
}

# 生成合规报告
$report = [ordered]@{
ReportTime = $now.ToString("yyyy-MM-dd HH:mm:ss")
ResourceGroup = $ResourceGroupName
TotalEndpoints = $privateEndpoints.Count
Compliant = $compliantCount
NonCompliant = $nonCompliantCount
ComplianceRate = if ($privateEndpoints.Count -gt 0) {
[math]::Round(($compliantCount / $privateEndpoints.Count) * 100, 1)
} else { 100 }
Details = $results
}

$reportJson = $report | ConvertTo-Json -Depth 5
$reportJson | Out-File -FilePath $OutputPath -Encoding utf8

Write-Host "`n" ("=" * 70) -ForegroundColor Cyan
Write-Host "合规检查完成" -ForegroundColor Cyan
Write-Host " 通过: $compliantCount / $($privateEndpoints.Count)" -ForegroundColor Green
Write-Host " 不通过: $nonCompliantCount / $($privateEndpoints.Count)" -ForegroundColor Red
Write-Host " 合规率: $($report.ComplianceRate)%" -ForegroundColor Cyan
Write-Host " 报告已保存: $OutputPath" -ForegroundColor White
Write-Host ("=" * 70) -ForegroundColor Cyan

return $report
}

# 执行合规检查
$compliance = Test-AzPrivateEndpointCompliance `
-ResourceGroupName "rg-production" `
-OutputPath "$HOME/PrivateEndpointCompliance_20260326.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
34
35
36
37
38
39
======================================================================
Private Endpoint 网络隔离合规检查
检查时间: 2026-03-26 08:30:00
资源组: rg-production
======================================================================

发现 4 个 Private Endpoint

--- 检查: pe-prod-storage ---
[PASS] 连接状态: Approved
[PASS] 私有 IP 地址: 10.0.2.4 (RFC1918)
[PASS] 公网端点已禁用 (Microsoft.Storage/storageAccounts)
[PASS] DNS 解析: stproddata001.blob.core.windows.net -> 10.0.2.4

--- 检查: pe-prod-keyvault ---
[PASS] 连接状态: Approved
[PASS] 私有 IP 地址: 10.0.2.5 (RFC1918)
[PASS] 公网端点已禁用 (Microsoft.KeyVault/vaults)
[PASS] DNS 解析: kv-prod-eastus.vault.azure.net -> 10.0.2.5

--- 检查: pe-prod-sql ---
[PASS] 连接状态: Approved
[PASS] 私有 IP 地址: 10.0.2.6 (RFC1918)
[FAIL] 公网端点未禁用 (Microsoft.Sql/servers)
[PASS] DNS 解析: sql-prod-eastus.database.windows.net -> 10.0.2.6

--- 检查: pe-staging-storage ---
[PASS] 连接状态: Approved
[PASS] 私有 IP 地址: 10.0.2.10 (RFC1918)
[PASS] 公网端点已禁用 (Microsoft.Storage/storageAccounts)
[FAIL] DNS 解析不匹配: 20.150.12.34 (应为 10.0.2.10)

======================================================================
合规检查完成
通过: 2 / 4
不通过: 2 / 4
合规率: 50.0%
报告已保存: /home/admin/PrivateEndpointCompliance_20260326.json
======================================================================

合规报告清晰地暴露了两个风险点:pe-prod-sql 的 SQL Server 公网端点未禁用,意味着虽然 Private Endpoint 已配置,但数据仍可能通过公网路径被访问,网络隔离形同虚设;pe-staging-storage 的 DNS 解析指向公网 IP 20.150.12.34,说明 Private DNS Zone 配置不完整或 VNet Link 缺失。合规率 50% 表明环境存在明显的配置漂移,建议将此检查集成到 Azure DevOps Pipeline 中,在每次基础设施变更后自动运行。

注意事项

  1. 子网委派要求:Private Endpoint 所在的子网必须禁用 privateEndpointNetworkPolicies 属性。创建子网时需要显式执行 Set-AzVirtualNetworkSubnetConfig -PrivateEndpointNetworkPoliciesFlag Disabled,否则 Private Endpoint 创建会失败。建议在 Terraform 或 Bicep 模板中将此设置标准化。

  2. DNS 解析范围:Private DNS Zone 的 A 记录仅在关联的 VNet 内生效。如果本地数据中心通过 ExpressRoute 或 VPN 连接到 Azure VNet,需要额外配置 DNS 条件转发器或自定义 DNS 服务器,将 Azure 服务域名转发到 Azure 提供的 DNS 代理(168.63.129.16)进行解析。

  3. 连接审批流程:当 Private Endpoint 的连接请求需要手动审批时,服务提供方会收到 Azure Activity Log 事件。可以使用 Approve-AzPrivateEndpointConnectionDeny-AzPrivateEndpointConnection cmdlet 在脚本中自动处理审批流程,结合 Azure Automation Runbook 实现审批自动化。

  4. 配额与限制:每个 Private Endpoint 占用子网中的一个 IP 地址,单个子网最多支持一定数量的 Private Endpoint(具体取决于子网大小)。此外,每个订阅的 Private Endpoint 数量有默认配额限制,大规模部署前请通过 Azure Portal 提交配额提升请求。

  5. 公网端点必须显式禁用:仅创建 Private Endpoint 并不能阻止公网访问。对于 Storage Account,需要将网络规则集的 DefaultAction 设为 Deny;对于 SQL Server,需要将 PublicNetworkAccess 设为 Disabled。合规检查脚本中已包含此验证,建议在 CI/CD 流程中强制执行。

  6. 成本考量:Private Endpoint 按小时计费(约 $0.01/小时),加上数据处理的出入站费用。在大规模部署场景下(数十个 PaaS 服务 + 多个区域),Private Endpoint 成本可能显著增加。建议按环境区分策略——生产环境全面使用 Private Endpoint,开发和测试环境可选择性使用或使用 Service Endpoint 作为替代。

PowerShell 技能连载 - Azure Front Door 与 CDN 管理

适用于 PowerShell 7.0 及以上版本

Azure Front Door 是微软 Azure 平台上的云原生应用交付控制器(ADC),集成了全球 CDN、智能七层路由、Web 应用防火墙(WAF)和 SSL 卸载等核心能力。对于面向全球用户的 Web 应用来说,Front Door 能通过遍布全球的边缘节点,将用户请求自动路由到最近的后端实例,显著降低访问延迟并提升可用性。

在实际运维中,Front Door 的配置往往涉及多个层面:后端池定义、健康探测、路由规则、WAF 策略、缓存行为和自定义域名绑定。当环境数量增多(开发、预发布、生产)或者需要频繁调整规则时,纯手工在 Azure 门户中操作不仅效率低下,还容易因为人为疏漏导致配置漂移。

通过 PowerShell 的 Az.FrontDoor 模块,我们可以将 Front Door 的完整配置脚本化,实现基础设施即代码(IaC)的管理方式。本文将围绕三个核心场景展开:Front Door 配置管理、WAF 安全策略配置,以及缓存与性能优化,帮助你用 PowerShell 系统化地管理全球流量入口。

Front Door 配置管理

下面的脚本演示了如何创建 Front Door 配置文件,包括后端池定义、健康探测设置和路由规则的完整流程。每个步骤都通过 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
# 连接 Azure 并选择订阅
Connect-AzAccount -Subscription 'MySubscription'

# 定义变量
$resourceGroup = 'rg-frontdoor-demo'
$location = 'global'
$frontDoorName = 'fd-demo-001'

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

# 定义后端地址池
$backend1 = New-AzFrontDoorBackendObject `
-Address 'app-eastasia.azurewebsites.net' `
-HttpPort 80 `
-HttpsPort 443 `
-Priority 1 `
-Weight 50

$backend2 = New-AzFrontDoorBackendObject `
-Address 'app-eastus.azurewebsites.net' `
-HttpPort 80 `
-HttpsPort 443 `
-Priority 2 `
-Weight 50

$backendPool = New-AzFrontDoorBackendPoolObject `
-Name 'primaryPool' `
-FrontDoorName $frontDoorName `
-ResourceGroupName $resourceGroup `
-Backend $backend1, $backend2

# 定义健康探测
$healthProbe = New-AzFrontDoorHealthProbeSettingObject `
-Name 'appHealthProbe' `
-Path '/health' `
-Protocol Https `
-IntervalInSeconds 30

# 定义负载均衡设置
$loadBalancing = New-AzFrontDoorLoadBalancingSettingObject `
-Name 'appLoadBalancing' `
-SampleSize 4 `
-SuccessfulSamplesRequired 2

# 定义前端端点
$frontendEndpoint = New-AzFrontDoorFrontendEndpointObject `
-Name 'frontendEndpoint1' `
-HostName "$frontDoorName.azurefd.net"

# 定义路由规则
$routingRule = New-AzFrontDoorRoutingRuleObject `
-Name 'defaultRouting' `
-FrontDoorName $frontDoorName `
-ResourceGroupName $resourceGroup `
-FrontendEndpointName 'frontendEndpoint1' `
-BackendPoolName 'primaryPool' `
-AcceptedProtocol Https `
-PatternsToMatch '/*' `
-EnabledState Enabled

# 创建 Front Door 实例
$frontDoor = New-AzFrontDoor `
-ResourceGroupName $resourceGroup `
-Name $frontDoorName `
-BackendPool $backendPool `
-HealthProbeSetting $healthProbe `
-LoadBalancingSetting $loadBalancing `
-FrontendEndpoint $frontendEndpoint `
-RoutingRule $routingRule

Write-Host "Front Door 创建完成:$($frontDoor.Name)"
Write-Host "前端访问地址:https://$frontDoorName.azurefd.net"

执行结果示例:

1
2
3
4
5
6
7
8
9
Front Door 创建完成:fd-demo-001
前端访问地址:https://fd-demo-001.azurefd.net

ResourceGroupName : rg-frontdoor-demo
Name : fd-demo-001
Type : Microsoft.Network/frontdoors
Location : global
ProvisioningState : Succeeded
Cname : fd-demo-001.azurefd.net

可以看到,Front Door 的创建过程是声明式的:我们先定义后端池、健康探测和负载均衡策略等子资源,然后将它们组合成一个完整的 Front Door 实例。global 区域意味着 Front Door 会在全球所有边缘节点自动部署。

WAF 安全策略配置

Web 应用防火墙(WAF)是 Front Door 安全体系的核心组件。下面的脚本展示了如何创建 WAF 策略,配置自定义规则、托管规则集和速率限制,为应用提供多层安全防护。

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
# 定义 WAF 策略变量
$wafPolicyName = 'waf-demo-policy'
$resourceGroup = 'rg-frontdoor-demo'

# 创建自定义规则:阻断特定恶意 User-Agent
$customRule1 = New-AzFrontDoorWafCustomRuleObject `
-Name 'blockBadBots' `
-RuleType MatchRule `
-MatchCondition @{
MatchVariable = 'RequestHeader'
Selector = 'User-Agent'
OperatorProperty = 'Contains'
MatchValue = @('BadBot', 'Scraper', 'MaliciousCrawler')
NegateCondition = $false
} `
-Action Block `
-Priority 100

# 创建自定义规则:仅允许特定国家/地区访问
$customRule2 = New-AzFrontDoorWafCustomRuleObject `
-Name 'geoFilterRule' `
-RuleType MatchRule `
-MatchCondition @{
MatchVariable = 'SocketAddr'
OperatorProperty = 'GeoMatch'
MatchValue = @('CN', 'HK', 'TW', 'SG', 'JP')
NegateCondition = $true
} `
-Action Block `
-Priority 200

# 创建速率限制规则:防止暴力破解
$rateLimitRule = New-AzFrontDoorWafCustomRuleObject `
-Name 'rateLimitLogin' `
-RuleType RateLimitRule `
-MatchCondition @{
MatchVariable = 'RequestUri'
OperatorProperty = 'Contains'
MatchValue = @('/api/login', '/api/auth')
} `
-RateLimitDurationInMinutes 1 `
-RateLimitThreshold 20 `
-Action Block `
-Priority 300

# 创建托管规则集(OWASP Top 10 防护)
$managedRuleSet = New-AzFrontDoorWafManagedRuleObject `
-Type 'Microsoft_DefaultRuleSet' `
-Version '2.1'

$botRuleSet = New-AzFrontDoorWafManagedRuleObject `
-Type 'Microsoft_BotManagerRuleSet' `
-Version '1.0'

# 组合创建 WAF 策略
$wafPolicy = New-AzFrontDoorWafPolicy `
-ResourceGroupName $resourceGroup `
-Name $wafPolicyName `
-CustomRule $customRule1, $customRule2, $rateLimitRule `
-ManagedRule $managedRuleSet, $botRuleSet `
-EnabledState Enabled `
-Mode Prevention

Write-Host "WAF 策略创建完成:$($wafPolicy.Name)"
Write-Host "运行模式:$($wafPolicy.Mode)"
Write-Host "自定义规则数量:$($wafPolicy.CustomRules.Count)"
Write-Host "托管规则集数量:$($wafPolicy.ManagedRules.Count)"

# 将 WAF 策略关联到 Front Door 前端端点
$frontDoorName = 'fd-demo-001'
$frontendEndpoint = Get-AzFrontDoorFrontendEndpoint `
-ResourceGroupName $resourceGroup `
-FrontDoorName $frontDoorName `
-Name 'frontendEndpoint1'

# 更新前端端点以关联 WAF 策略
$wafPolicyId = $wafPolicy.Id
Update-AzFrontDoorFrontendEndpoint `
-ResourceGroupName $resourceGroup `
-FrontDoorName $frontDoorName `
-Name 'frontendEndpoint1' `
-WebApplicationFirewallPolicyLink $wafPolicyId

Write-Host "WAF 策略已关联到前端端点"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
WAF 策略创建完成:waf-demo-policy
运行模式:Prevention
自定义规则数量:3
托管规则集数量:2
WAF 策略已关联到前端端点

PolicyId : /subscriptions/xxxx/resourceGroups/rg-frontdoor-demo/providers/Microsoft.Network/frontdoorwebappfirewallpolicies/waf-demo-policy
EnabledState : Enabled
Mode : Prevention
CustomRules : {blockBadBots, geoFilterRule, rateLimitLogin}
ManagedRules : {Microsoft_DefaultRuleSet-2.1, Microsoft_BotManagerRuleSet-1.0}

WAF 策略的三层防护各司其职:自定义规则处理业务特定逻辑(如地域访问控制),速率限制防御暴力攻击,托管规则集则覆盖 OWASP Top 10 等常见攻击模式。Prevention 模式会直接阻断恶意请求,调试阶段可以切换为 Detection 仅记录日志。

缓存与性能优化

合理的缓存策略能大幅减少后端负载,同时提升用户访问速度。下面的脚本展示了如何配置缓存规则、启用压缩,以及生成流量分析报告。

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
# 定义变量
$resourceGroup = 'rg-frontdoor-demo'
$frontDoorName = 'fd-demo-001'

# 获取当前 Front Door 配置
$frontDoor = Get-AzFrontDoor `
-ResourceGroupName $resourceGroup `
-Name $frontDoorName

# 创建带缓存的路由规则(静态资源)
$cachedBackendPool = $frontDoor.BackendPools[0]
$cachedFrontend = $frontDoor.FrontendEndpoints[0]

# 更新路由规则,添加缓存配置
$routingRuleWithCache = New-AzFrontDoorRoutingRuleObject `
-Name 'staticAssetsRouting' `
-FrontDoorName $frontDoorName `
-ResourceGroupName $resourceGroup `
-FrontendEndpointName $cachedFrontend.Name `
-BackendPoolName $cachedBackendPool.Name `
-AcceptedProtocol Http, Https `
-PatternsToMatch '/static/*', '/images/*', '/css/*', '/js/*' `
-EnabledState Enabled `
-DefaultCacheBehavior `
-CacheDuration 'P30D' `
-ForwardingProtocol HttpsOnly `
-CacheKeyQueryParameterStripAll

# 创建 API 路径的动态缓存规则
$apiCacheRule = New-AzFrontDoorRoutingRuleObject `
-Name 'apiCacheRouting' `
-FrontDoorName $frontDoorName `
-ResourceGroupName $resourceGroup `
-FrontendEndpointName $cachedFrontend.Name `
-BackendPoolName $cachedBackendPool.Name `
-AcceptedProtocol Https `
-PatternsToMatch '/api/products/*', '/api/catalog/*' `
-EnabledState Enabled `
-DefaultCacheBehavior `
-CacheDuration 'PT5M' `
-ForwardingProtocol HttpsOnly

# 查看当前 Front Door 的流量统计
$endTime = Get-Date
$startTime = $endTime.AddDays(-7)

# 获取 Front Door 的指标数据
$metrics = Get-AzMetric `
-ResourceId $frontDoor.Id `
-MetricName 'RequestCount', 'BackendRequestLatency', 'CacheHitRatio' `
-StartTime $startTime `
-EndTime $endTime `
-TimeGrain 'PT1H' `
-AggregationType Total, Average

# 生成流量分析报告
$report = [PSCustomObject]@{
时间范围 = "$startTime ~ $endTime"
总请求数 = ($metrics | Where-Object { $_.Name -eq 'RequestCount' } |
ForEach-Object { $_.Timeseries.Data.TimeseriesDataValues } |
Measure-Object -Property Total -Sum).Sum
平均延迟ms = [math]::Round(
($metrics | Where-Object { $_.Name -eq 'BackendRequestLatency' } |
ForEach-Object { $_.Timeseries.Data.TimeseriesDataValues } |
Measure-Object -Property Average -Average).Average, 2)
缓存命中率pct = [math]::Round(
($metrics | Where-Object { $_.Name -eq 'CacheHitRatio' } |
ForEach-Object { $_.Timeseries.Data.TimeseriesDataValues } |
Measure-Object -Property Average -Average).Average * 100, 2)
}

Write-Host "`n=== Front Door 周报 ==="
$report | Format-List

# 列出所有路由规则及缓存状态
Write-Host "`n=== 当前路由规则 ==="
foreach ($rule in $frontDoor.RoutingRules) {
$cacheStatus = if ($rule.Properties.RoutingRuleProperties.CacheConfiguration) {
"已启用 (有效期: $($rule.Properties.RoutingRuleProperties.CacheConfiguration.CacheDuration))"
} else {
'未启用'
}
[PSCustomObject]@{
规则名称 = $rule.Name
匹配路径 = ($rule.Properties.RoutingRuleProperties.PatternsToMatch -join ', ')
缓存状态 = $cacheStatus
协议 = $rule.Properties.RoutingRuleProperties.AcceptedProtocols -join ', '
} | Format-Table -AutoSize
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== Front Door 周报 ===

时间范围 : 2026-02-10 00:00:00 ~ 2026-02-17 00:00:00
总请求数 : 1284350
平均延迟ms : 23.67
缓存命中率pct : 87.43

=== 当前路由规则 ===

规则名称 匹配路径 缓存状态 协议
-------- -------- -------- ----
defaultRouting /* 未启用 Https
staticAssetsRouting /static/*, /images/*, /css/*, /js/* 已启用 (有效期: P30D) Http, Https
apiCacheRouting /api/products/*, /api/catalog/* 已启用 (有效期: PT5M) Https

从报告中可以看到,静态资源缓存 30 天能覆盖绝大部分场景,而 API 数据用 5 分钟短缓存可以在数据新鲜度和性能之间取得平衡。缓存命中率 87% 意味着绝大部分请求在边缘节点就已响应,无需回源。

注意事项

  1. Front Door Standard/Premium 与经典版差异:经典版使用 Az.FrontDoor 模块,而 Standard/Premium 版使用 Az.Cdn 模块(对应 Microsoft.CDN/profiles 资源)。迁移前需确认当前使用的版本,两者的 cmdlet 和配置结构完全不同。

  2. WAF 策略的 Detection 模式调试:上线新规则前,建议先将 WAF 切换为 Detection 模式运行 3-7 天,观察日志中的误报情况。避免直接启用 Prevention 模式导致合法流量被阻断。

  3. 缓存键的查询参数处理CacheKeyQueryParameterStripAll 会忽略所有查询参数,即 /api/data?a=1/api/data?b=2 返回同一份缓存。如果 API 返回内容依赖查询参数(如分页、搜索),需要改用白名单模式。

  4. 后端健康探测间隔不宜过短IntervalInSeconds 设为 30 秒是推荐值,低于 10 秒会增加后端负载,尤其是后端为 Serverless 或按调用计费的服务时,探测流量本身会产生额外费用。

  5. SSL 证书的自动续期:Front Door 托管的 .azurefd.net 域名自带 SSL,但自定义域名需要绑定证书。建议使用 Azure Key Vault 管理证书并启用自动轮换,避免证书过期导致服务中断。

  6. 配置变更的传播延迟:Front Door 的配置变更需要在全球边缘节点传播,通常需要 5-10 分钟生效。自动化脚本中应加入等待逻辑(如轮询 ProvisioningState),不要假设配置立即生效。

PowerShell 技能连载 - Azure 虚拟网络管理

适用于 PowerShell 5.1 及以上版本

Azure 虚拟网络(Virtual Network,简称 VNet)是云端基础设施的基石,所有 Azure 资源之间的通信都建立在虚拟网络之上。无论是虚拟机、应用服务还是数据库,都需要通过 VNet 进行网络隔离与互联。对于运维工程师而言,掌握虚拟网络的创建、子网划分、安全组配置和对等连接等操作,是管理 Azure 环境的必备技能。

在多区域、多订阅的企业级架构中,网络配置的复杂度会迅速攀升。手动在 Azure Portal 中逐个配置子网和规则不仅耗时,还容易出现人为错误。通过 PowerShell 的 Az.Network 模块,我们可以将网络配置脚本化,实现版本控制和自动化部署,确保开发、测试、生产各环境的网络拓扑一致。

本文将从零开始演示如何使用 PowerShell 创建虚拟网络、管理子网、配置网络安全组以及建立 VNet 对等连接,帮助你构建一套可复用的网络管理脚本库。

前置准备:安装模块并连接 Azure

在操作虚拟网络之前,需要确保已安装 Az 模块并完成 Azure 认证。Az.Network 模块包含在 Az 总包中,无需单独安装。如果你在受限环境中只需要网络管理功能,也可以单独安装 Az.Network

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 安装 Az 模块(如果尚未安装)
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force

# 连接 Azure 账户(交互式登录)
Connect-AzAccount

# 查看当前订阅列表,选择目标订阅
$subscriptions = Get-AzSubscription
foreach ($sub in $subscriptions) {
Write-Host "名称: $($sub.Name) ID: $($sub.Id)"
}

# 选择目标订阅
Select-AzSubscription -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# 验证当前上下文
Get-AzContext | Select-Object Account, Subscription

执行结果示例:

1
2
3
4
5
6
名称: Visual Studio Enterprise  ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
名称: Production-Sub ID: b2c3d4e5-f6a7-8901-bcde-f12345678901

Account Subscription
------- ------------
user@example.com Production-Sub (b2c3d4e5-...)

连接成功后,我们就可以开始操作虚拟网络资源了。建议在生产操作前先在测试订阅中验证脚本。

创建虚拟网络和子网

虚拟网络的核心概念是地址空间(Address Space)和子网(Subnet)。地址空间定义了整个 VNet 的 IP 范围,子网则是从地址空间中划分出的更小网段,用于隔离不同类型的资源。

下面的脚本演示如何创建一个包含三个子网(前端、后端、数据层)的虚拟网络,这是典型的三层架构网络拓扑。

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
# 定义变量
$resourceGroup = "rg-network-demo"
$location = "EastAsia"
$vnetName = "vnet-demo"
$addressPrefix = "10.0.0.0/16"

# 创建资源组(如果不存在)
$rg = Get-AzResourceGroup -Name $resourceGroup -ErrorAction SilentlyContinue
if (-not $rg) {
$rg = New-AzResourceGroup -Name $resourceGroup -Location $location
Write-Host "已创建资源组: $resourceGroup"
}

# 定义子网配置
$subnetConfigs = @(
@{ Name = "snet-frontend"; Prefix = "10.0.1.0/24" }
@{ Name = "snet-backend"; Prefix = "10.0.2.0/24" }
@{ Name = "snet-data"; Prefix = "10.0.3.0/24" }
)

# 创建子网配置对象
$subnets = @()
foreach ($config in $subnetConfigs) {
$subnet = New-AzVirtualNetworkSubnetConfig `
-Name $config.Name `
-AddressPrefix $config.Prefix
$subnets += $subnet
Write-Host "已准备子网配置: $($config.Name) ($($config.Prefix))"
}

# 创建虚拟网络
$vnet = New-AzVirtualNetwork `
-Name $vnetName `
-ResourceGroupName $resourceGroup `
-Location $location `
-AddressPrefix $addressPrefix `
-Subnet $subnets

# 查看创建结果
$vnet | Select-Object Name, Location, AddressSpace
$vnet.Subnets | Select-Object Name, AddressPrefix

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
已创建资源组: rg-network-demo
已准备子网配置: snet-frontend (10.0.1.0/24)
已准备子网配置: snet-backend (10.0.2.0/24)
已准备子网配置: snet-data (10.0.3.0/24)

Name Location AddressSpace
---- -------- ------------
vnet-demo eastasia 10.0.0.0/16

Name AddressPrefix
---- -------------
snet-frontend 10.0.1.0/24
snet-backend 10.0.2.0/24
snet-data 10.0.3.0/24

可以看到,一个地址空间为 10.0.0.0/16 的虚拟网络已创建完成,内部包含三个独立的子网。每个子网最多可容纳 251 个可用 IP(扣除 Azure 保留的 5 个地址)。

配置网络安全组(NSG)

网络安全组(Network Security Group)是 Azure 中的软件防火墙,通过定义入站和出站规则来控制子网或网络接口级别的流量。在生产环境中,合理的 NSG 规则是保护应用安全的第一道防线。

下面演示如何创建 NSG 并配置常见的安全规则:允许 HTTP/HTTPS 入站、允许 SSH 仅来自特定 IP 段、拒绝所有其他入站流量。

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
# 创建网络安全组
$nsgName = "nsg-frontend"
$nsg = New-AzNetworkSecurityGroup `
-Name $nsgName `
-ResourceGroupName $resourceGroup `
-Location $location

# 允许 HTTP 入站(优先级 100)
$nsg | Add-AzNetworkSecurityRuleConfig `
-Name "Allow-HTTP" `
-Description "允许 HTTP 流量" `
-Access Allow `
-Protocol Tcp `
-Direction Inbound `
-Priority 100 `
-SourceAddressPrefix "Internet" `
-SourcePortRange "*" `
-DestinationAddressPrefix "10.0.1.0/24" `
-DestinationPortRange 80

# 允许 HTTPS 入站(优先级 110)
$nsg | Add-AzNetworkSecurityRuleConfig `
-Name "Allow-HTTPS" `
-Description "允许 HTTPS 流量" `
-Access Allow `
-Protocol Tcp `
-Direction Inbound `
-Priority 110 `
-SourceAddressPrefix "Internet" `
-SourcePortRange "*" `
-DestinationAddressPrefix "10.0.1.0/24" `
-DestinationPortRange 443

# 允许 SSH 仅来自公司 IP 段(优先级 200)
$nsg | Add-AzNetworkSecurityRuleConfig `
-Name "Allow-SSH-Corp" `
-Description "允许公司网段 SSH 访问" `
-Access Allow `
-Protocol Tcp `
-Direction Inbound `
-Priority 200 `
-SourceAddressPrefix "203.0.113.0/24" `
-SourcePortRange "*" `
-DestinationAddressPrefix "10.0.1.0/24" `
-DestinationPortRange 22

# 拒绝所有其他入站流量(优先级 4096)
$nsg | Add-AzNetworkSecurityRuleConfig `
-Name "Deny-All-Inbound" `
-Description "拒绝所有其他入站流量" `
-Access Deny `
-Protocol "*" `
-Direction Inbound `
-Priority 4096 `
-SourceAddressPrefix "*" `
-SourcePortRange "*" `
-DestinationAddressPrefix "*" `
-DestinationPortRange "*"

# 保存 NSG 配置
$nsg | Set-AzNetworkSecurityGroup

# 将 NSG 关联到前端子网
$vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupName $resourceGroup
Set-AzVirtualNetworkSubnetConfig `
-Name "snet-frontend" `
-VirtualNetwork $vnet `
-AddressPrefix "10.0.1.0/24" `
-NetworkSecurityGroup $nsg

$vnet | Set-AzVirtualNetwork

# 查看生效的安全规则
$nsg.SecurityRules | Select-Object Name, Priority, Access, Direction, `
@{N='Source';E={$_.SourceAddressPrefix}}, `
@{N='DestPort';E={$_.DestinationPortRange}} |
Sort-Object Priority |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
Name             Priority Access Direction Source       DestPort
---- -------- ------ --------- ------ --------
Allow-HTTP 100 Allow Inbound Internet 80
Allow-HTTPS 110 Allow Inbound Internet 443
Allow-SSH-Corp 200 Allow Inbound 203.0.113.0/24 22
Deny-All-Inbound 4096 Deny Inbound * *

NSG 规则按照优先级数字从小到大依次匹配,数字越小优先级越高。一旦匹配到允许或拒绝规则,后续规则不再评估。因此,将最常见的流量规则设为高优先级可以提升网络性能。

建立 VNet 对等连接

在实际项目中,不同团队或不同环境往往使用独立的虚拟网络。VNet 对等连接(Peering)允许两个虚拟网络直接通信,流量在 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 创建第二个虚拟网络
$vnet2Name = "vnet-shared"
$vnet2Prefix = "10.1.0.0/16"
$snet2Name = "snet-shared-services"
$snet2Prefix = "10.1.1.0/24"

# 创建子网配置
$subnet2 = New-AzVirtualNetworkSubnetConfig `
-Name $snet2Name `
-AddressPrefix $snet2Prefix

# 创建第二个虚拟网络
$vnet2 = New-AzVirtualNetwork `
-Name $vnet2Name `
-ResourceGroupName $resourceGroup `
-Location $location `
-AddressPrefix $vnet2Prefix `
-Subnet $subnet2

Write-Host "已创建虚拟网络: $vnet2Name ($vnet2Prefix)"

# 重新获取两个 VNet 的最新对象
$vnet1 = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupName $resourceGroup
$vnet2 = Get-AzVirtualNetwork -Name $vnet2Name -ResourceGroupName $resourceGroup

# 添加从 vnet-demo 到 vnet-shared 的对等连接
Add-AzVirtualNetworkPeering `
-Name "peer-demo-to-shared" `
-VirtualNetwork $vnet1 `
-RemoteVirtualNetworkId $vnet2.Id `
-AllowVirtualNetworkAccess `
-AllowForwardedTraffic

# 添加从 vnet-shared 到 vnet-demo 的对等连接(双向)
Add-AzVirtualNetworkPeering `
-Name "peer-shared-to-demo" `
-VirtualNetwork $vnet2 `
-RemoteVirtualNetworkId $vnet1.Id `
-AllowVirtualNetworkAccess `
-AllowForwardedTraffic

# 验证对等连接状态
$peerings = Get-AzVirtualNetworkPeering -VirtualNetworkName $vnetName -ResourceGroupName $resourceGroup
foreach ($peering in $peerings) {
Write-Host "对等名称: $($peering.Name)"
Write-Host " 远程 VNet: $($peering.RemoteVirtualNetwork.Id.Split('/')[-1])"
Write-Host " 连接状态: $($peering.PeeringState)"
Write-Host ""
}

执行结果示例:

1
2
3
4
5
6
7
8
9
已创建虚拟网络: vnet-shared (10.1.0.0/16)

对等名称: peer-demo-to-shared
远程 VNet: vnet-shared
连接状态: Connected

对等名称: peer-shared-to-demo
远程 VNet: vnet-demo
连接状态: Connected

对等连接的状态为 Connected 表示双向连接已建立成功。对等连接是双向的,必须分别在两个 VNet 上各添加一条对等配置。如果只添加一端,状态会显示为 Initiated

批量查询网络资源

在大型 Azure 环境中,定期审计网络配置是安全合规的重要环节。下面的脚本演示如何批量查询订阅中的所有虚拟网络、子网和 NSG,并生成一份网络拓扑摘要报告。

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
# 获取当前订阅中所有虚拟网络
$vnets = Get-AzVirtualNetwork

$report = @()
foreach ($vnet in $vnets) {
foreach ($subnet in $vnet.Subnets) {
$nsgName = "无"
if ($subnet.NetworkSecurityGroup) {
$nsgName = $subnet.NetworkSecurityGroup.Id.Split('/')[-1]
}

$row = [PSCustomObject]@{
VNet = $vnet.Name
Location = $vnet.Location
VNetPrefix = ($vnet.AddressSpace.AddressPrefixes -join ", ")
Subnet = $subnet.Name
SubnetPrefix = $subnet.AddressPrefix
NSG = $nsgName
}
$report += $row
}
}

# 按虚拟网络名称排序输出报告
$report | Sort-Object VNet, Subnet | Format-Table -AutoSize

# 统计摘要信息
Write-Host "`n--- 网络资源摘要 ---"
Write-Host "虚拟网络总数: $($vnets.Count)"
Write-Host "子网总数: $(($report).Count)"

# 检查未关联 NSG 的子网(潜在安全风险)
$noNsg = $report | Where-Object { $_.NSG -eq "无" }
if ($noNsg) {
Write-Host "`n[警告] 以下子网未关联网络安全组:" -ForegroundColor Yellow
foreach ($item in $noNsg) {
Write-Host " - $($item.VNet)/$($item.Subnet)" -ForegroundColor Yellow
}
} else {
Write-Host "`n[OK] 所有子网均已关联网络安全组" -ForegroundColor Green
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VNet       Location VNetPrefix  Subnet          SubnetPrefix NSG
---- -------- ---------- ------ ------------ ---
vnet-demo eastasia 10.0.0.0/16 snet-frontend 10.0.1.0/24 nsg-frontend
vnet-demo eastasia 10.0.0.0/16 snet-backend 10.0.2.0/24
vnet-demo eastasia 10.0.0.0/16 snet-data 10.0.3.0/24
vnet-shared eastasia 10.1.0.0/16 snet-shared-services 10.1.1.0/24

--- 网络资源摘要 ---
虚拟网络总数: 2
子网总数: 4

[警告] 以下子网未关联网络安全组:
- vnet-demo/snet-backend
- vnet-demo/snet-data
- vnet-shared/snet-shared-services

这个审计脚本可以快速发现未配置安全组的子网。在生产环境中,建议将此类脚本集成到定期巡检流程中,或配合 Azure Automation 定时执行。

清理示例资源

完成测试后,及时清理资源可以避免产生不必要的费用。以下命令会删除本文创建的所有资源。

1
2
3
4
5
# 删除资源组及其中的所有资源
Remove-AzResourceGroup -Name $resourceGroup -Force -AsJob

# 查看删除任务状态
Get-Job | Select-Object Name, State

执行结果示例:

1
2
3
Name           State
---- -----
LongRunningJob Completed

-AsJob 参数使删除操作在后台执行,适合清理大量资源时使用。删除资源组是不可逆操作,请确保选择了正确的资源组。

注意事项

  1. 地址空间规划:创建 VNet 后虽然可以添加新的地址空间,但修改已有子网的地址前缀可能导致连接中断。建议在规划阶段预留足够的 IP 空间,考虑未来扩容需求。

  2. NSG 规则优先级:Azure 按优先级数字从小到大匹配规则,范围是 100-4096。建议在规则之间预留间隔(如 100、200、300),方便后续插入新规则而无需调整现有优先级。

  3. 对等连接不可传递:如果 VNet A 与 VNet B 对等,VNet B 与 VNet C 对等,VNet A 和 VNet C 之间不会自动互通。需要三方之间各自建立对等连接,或者使用 Azure 防火墙 / VPN 网关进行路由。

  4. 子网大小限制:Azure 在每个子网中保留 5 个 IP 地址不可使用(前 4 个和最后 1 个)。因此一个 /29 子网最多只有 3 个可用 IP,规划子网大小时需要考虑这一限制。

  5. 跨区域对等延迟:虽然 VNet 对等连接支持跨区域(Global Peering),但跨区域流量会产生额外费用且延迟略高于同区域对等。对延迟敏感的应用建议部署在同一区域。

  6. 幂等性脚本设计:生产环境中的网络管理脚本应具备幂等性——即多次运行结果一致。在创建资源前先用 Get-Az* 检查是否已存在,避免重复创建导致报错。

PowerShell 技能连载 - Azure 负载均衡器管理

适用于 PowerShell 5.1 及以上版本

在云原生架构中,负载均衡器是保障服务高可用的核心组件。Azure 负载均衡器(Azure Load Balancer)作为 L4 层负载均衡服务,能够在多个虚拟机或实例之间分发入站和出站流量,确保应用在高峰期依然稳定运行。无论是面向互联网的公共服务,还是内部微服务间的通信,负载均衡器都扮演着不可替代的角色。

对于运维工程师而言,手动在 Azure 门户中配置负载均衡器不仅耗时,而且容易出错。通过 PowerShell 的 Az 模块,我们可以将负载均衡器的创建、规则配置、健康探测以及后端池管理全部自动化。这种方式不仅提升了效率,还能将配置纳入版本控制,实现基础设施即代码(IaC)的最佳实践。

本文将围绕 Azure 负载均衡器的日常管理场景,介绍如何使用 PowerShell 完成从创建到监控的完整操作流程,帮助你构建可重复、可审计的负载均衡器管理方案。

创建负载均衡器及其前端 IP 配置

在创建负载均衡器之前,需要先准备好资源组和公共 IP 地址。以下代码展示了如何通过 PowerShell 一步到位地创建一个标准 SKU 的负载均衡器,并配置前端 IP 池和后端地址池。

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
# 定义变量
$rgName = "myResourceGroup"
$location = "EastAsia"
$publicIpName = "myLoadBalancerPublicIP"
$lbName = "myLoadBalancer"

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

# 创建公共 IP 地址
$publicIp = New-AzPublicIpAddress `
-ResourceGroupName $rgName `
-Name $publicIpName `
-Location $location `
-AllocationMethod Static `
-Sku Standard

# 创建前端 IP 配置
$frontendIp = New-AzLoadBalancerFrontendIpConfig `
-Name "myFrontendPool" `
-PublicIpAddress $publicIp

# 创建后端地址池
$backendPool = New-AzLoadBalancerBackendAddressPoolConfig `
-Name "myBackendPool"

# 创建负载均衡器
$lb = New-AzLoadBalancer `
-ResourceGroupName $rgName `
-Name $lbName `
-Location $location `
-Sku Standard `
-FrontendIpConfiguration $frontendIp `
-BackendAddressPool $backendPool

Write-Host "负载均衡器 '$lbName' 创建完成"
Write-Host "前端 IP: $($publicIp.IpAddress)"

执行结果示例:

1
2
3
4
5
6
7
8
ResourceGroupName : myResourceGroup
Location : eastasia
ProvisioningState : Succeeded
Sku : Standard
Name : myLoadBalancer

负载均衡器 'myLoadBalancer' 创建完成
前端 IP: 20.205.12.34

创建完成后,负载均衡器还只是一个空壳。接下来需要添加健康探测和负载均衡规则,才能真正开始分发流量。

配置健康探测与负载均衡规则

健康探测(Health Probe)用于定期检测后端实例的可用性。只有通过探测的实例才会接收流量。负载均衡规则则定义了前端端口到后端端口的映射关系以及分发策略。

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
$rgName = "myResourceGroup"
$lbName = "myLoadBalancer"
$lb = Get-AzLoadBalancer -ResourceGroupName $rgName -Name $lbName

# 添加 HTTP 健康探测
$probe = Add-AzLoadBalancerProbeConfig `
-LoadBalancer $lb `
-Name "httpProbe" `
-Protocol Http `
-Port 80 `
-RequestPath "/" `
-IntervalInSeconds 15 `
-ProbeCount 2

# 添加 TCP 负载均衡规则(将前端 80 端口映射到后端 80 端口)
$rule = Add-AzLoadBalancerRuleConfig `
-LoadBalancer $lb `
-Name "httpRule" `
-Protocol Tcp `
-FrontendPort 80 `
-BackendPort 80 `
-FrontendIpConfiguration $lb.FrontendIpConfigurations[0] `
-BackendAddressPool $lb.BackendAddressPools[0] `
-Probe $lb.Probes[0] `
-LoadDistribution Default `
-IdleTimeoutInMinutes 4 `
-EnableFloatingIp $false

# 将配置写入 Azure
Set-AzLoadBalancer -LoadBalancer $lb

Write-Host "健康探测和负载均衡规则配置完成"

执行结果示例:

1
2
3
4
5
6
7
Name              : myLoadBalancer
ResourceGroupName : myResourceGroup
Probes : {httpProbe}
LoadBalancingRules: {httpRule}
ProvisioningState : Succeeded

健康探测和负载均衡规则配置完成

这里有几个关键参数值得注意:IntervalInSeconds 设置为 15 秒表示每 15 秒探测一次;ProbeCount 设为 2 意味着连续两次探测失败后才会将实例标记为不健康。LoadDistribution 使用 Default 表示五元组哈希分发策略,适合大多数 Web 场景。

管理后端池与监控实例健康状态

后端池的管理是日常运维中最常见的操作。下面展示如何将虚拟机添加到后端池,以及如何查询各实例的健康状态。

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
$rgName = "myResourceGroup"
$lbName = "myLoadBalancer"
$lb = Get-AzLoadBalancer -ResourceGroupName $rgName -Name $lbName

# 获取要加入后端池的虚拟机网络接口
$vmNames = @("myVM01", "myVM02", "myVM03")
$backendPool = $lb.BackendAddressPools[0]

foreach ($vmName in $vmNames) {
$nic = Get-AzNetworkInterface |
Where-Object { $_.VirtualMachine.Id -like "*$vmName*" }

if ($nic) {
$nic.IpConfigurations[0].LoadBalancerBackendAddressPools = $backendPool
Set-AzNetworkInterface -NetworkInterface $nic
Write-Host "已将 '$vmName' 加入后端池"
} else {
Write-Host "警告: 未找到虚拟机 '$vmName' 的网络接口" -ForegroundColor Yellow
}
}

# 查询负载均衡器后端健康状态
$health = Get-AzLoadBalancerBackendAddressPool `
-ResourceGroupName $rgName `
-LoadBalancerName $lbName `
-Name "myBackendPool"

Write-Host "`n后端池当前配置:"
Write-Host "名称: $($health.Name)"
Write-Host "关联的网络接口数: $($health.NetworkInterfaces.Count)"

执行结果示例:

1
2
3
4
5
6
7
已将 'myVM01' 加入后端池
已将 'myVM02' 加入后端池
已将 'myVM03' 加入后端池

后端池当前配置:
名称: myBackendPool
关联的网络接口数: 3

通过 foreach 循环批量操作,可以轻松管理后端池中的实例。在生产环境中,建议将这段逻辑封装为可参数化的函数,方便在扩容或缩容时快速调用。

查询负载均衡器指标与运行状态

除了配置管理,监控负载均衡器的运行指标同样重要。以下代码通过 Get-AzMetric cmdlet 查询负载均衡器的流量指标,帮助你了解流量分布是否均匀。

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
$rgName = "myResourceGroup"
$lbName = "myLoadBalancer"

$lb = Get-AzLoadBalancer -ResourceGroupName $rgName -Name $lbName

# 查询过去 1 小时的 SYN 计数(新建连接数)指标
$endTime = Get-Date
$startTime = $endTime.AddHours(-1)

$metrics = Get-AzMetric `
-ResourceId $lb.Id `
-MetricName "SynCount" `
-StartTime $startTime `
-EndTime $endTime `
-TimeGrain 00:05:00 `
-AggregationType Total

Write-Host "负载均衡器 SYN 计数(过去 1 小时,每 5 分钟采样):"
Write-Host ("{0,-25} {1,15}" -f "时间", "SYN 计数")
Write-Host ("{0,-25} {1,15}" -f "----", "--------")

foreach ($ts in $metrics.Data) {
$timeStr = $ts.TimeStamp.ToString("yyyy-MM-dd HH:mm:ss")
$synCount = if ($ts.Total) { [math]::Round($ts.Total, 0) } else { 0 }
Write-Host ("{0,-25} {1,15}" -f $timeStr, $synCount)
}

# 获取 NAT 规则列表
$lb = Get-AzLoadBalancer -ResourceGroupName $rgName -Name $lbName
$inboundNatRules = $lb.InboundNatRules

if ($inboundNatRules) {
Write-Host "`n入站 NAT 规则:"
foreach ($rule in $inboundNatRules) {
Write-Host " 规则名称: $($rule.Name)"
Write-Host " 前端端口: $($rule.FrontendPort) -> 后端端口: $($rule.BackendPort)"
Write-Host " 协议: $($rule.Protocol)"
Write-Host ""
}
} else {
Write-Host "`n当前未配置入站 NAT 规则"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
负载均衡器 SYN 计数(过去 1 小时,每 5 分钟采样):
时间 SYN 计数
---- --------
2025-12-12 07:00:00 142
2025-12-12 07:05:00 158
2025-12-12 07:10:00 163
2025-12-12 07:15:00 151
2025-12-12 07:20:00 147
2025-12-12 07:25:00 172
2025-12-12 07:30:00 189
2025-12-12 07:35:00 195
2025-12-12 07:40:00 210
2025-12-12 07:45:00 198
2025-12-12 07:50:00 183
2025-12-12 07:55:00 176

当前未配置入站 NAT 规则

通过定期采集这些指标数据,你可以建立流量基线,在流量异常时及时发现并响应。结合 Azure Monitor 告警规则,还可以实现自动化的运维通知。

注意事项

  1. SKU 选择:Azure 负载均衡器有 Basic 和 Standard 两种 SKU。生产环境务必使用 Standard SKU,它支持可用性区域、更高的 SLA(99.99%)以及更精细的安全控制。Basic SKU 已不推荐用于生产环境。

  2. 健康探测参数调优IntervalInSecondsProbeCount 的组合决定了故障检测的灵敏度。过于激进的配置(如间隔 5 秒、计数 1)可能导致瞬时抖动误判;过于保守的配置(如间隔 30 秒、计数 5)则会让故障实例长时间接收流量。建议从 15 秒间隔、2 次失败阈值开始,根据业务特点调整。

  3. 负载分发策略LoadDistribution 参数有三种取值:Default(五元组哈希)、SourceIP(二元组哈希)、SourceIPProtocol(三元组哈希)。对于需要会话保持的场景(如 WebSocket 长连接),考虑使用 SourceIPProtocol;对于一般 Web 流量,Default 即可。

  4. 后端池操作幂等性:修改后端池配置时,Set-AzLoadBalancer 会整体替换配置。务必先 Get-AzLoadBalancer 获取最新状态,在现有对象上修改后再写回,避免覆盖其他并发的配置变更。

  5. 公共 IP 与 SKU 匹配:Standard SKU 的负载均衡器必须搭配 Standard SKU 的公共 IP 地址。如果尝试使用 Basic SKU 的公共 IP,创建时会报错。这一点在脚本变量初始化阶段就要确保一致。

  6. 权限与模块版本:运行本文中的脚本需要安装 Az.Network 模块(4.0 或更高版本),且当前账户需要在资源组上拥有 Network Contributor 或更高权限。使用 Get-InstalledModule -Name Az.Network 检查模块版本,必要时通过 Update-Module Az.Network 升级。