PowerShell 技能连载 - Azure Container Apps 环境管理

适用于 PowerShell 7.0 及以上版本,需要 Az.ContainerApp 模块

Azure Container Apps 的”环境”(Managed Environment)是整个平台的核心组织单元。每个环境相当于一个轻量级的 Kubernetes 集群边界,定义了容器应用之间的网络拓扑、日志目标和共享基础设施。与直接操作 AKS 不同,环境的网络配置、内部流量路由和 Dapr 组件都通过声明式的资源模型管理,运维人员无需关心底层节点维护。

在微服务架构中,环境级别的配置往往比单个应用更为重要。合理规划 VNet 集成可以确保服务间通信不经过公网;精确的流量分割策略可以实现蓝绿发布和金丝雀部署;Dapr 服务网格则统一了服务发现、状态管理和发布订阅等横切关注点。通过 PowerShell 脚本化这些配置,不仅提高了可重复性,也便于纳入 CI/CD 流水线进行审计和回滚。

本文将围绕 Container Apps 环境管理的三个核心场景展开:环境与网络配置、微服务部署与流量管理、以及 Dapr 集成与服务网格。

Container Apps 环境与联网

每个 Container Apps 环境都需要一个虚拟网络来承载内部流量。下面的脚本创建了一个带有自定义子网的 Managed Environment,并配置了内部负载均衡器模式,确保所有入站流量仅通过环境内部 IP 可达,不暴露公网端点。这种”内部环境”模式特别适合企业内部 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
# 安装并导入 ContainerApp 模块
Install-Module -Name Az.ContainerApp -Force -Scope CurrentUser
Import-Module Az.ContainerApp

# 连接到 Azure 账户
Connect-AzAccount -SubscriptionId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

# 定义资源组与环境参数
$resourceGroup = 'rg-containerapps-prod'
$location = 'eastasia'
$envName = 'cae-microservice-prod'

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

# 创建专用的虚拟网络和子网
# Container Apps 环境要求子网至少 /23 大小
$vnet = New-AzVirtualNetwork `
-ResourceGroupName $resourceGroup `
-Location $location `
-Name 'vnet-containerapps' `
-AddressPrefix '10.0.0.0/16'

$subnet = Add-AzVirtualNetworkSubnetConfig `
-Name 'snet-containerapps' `
-AddressPrefix '10.0.0.0/23' `
-VirtualNetwork $vnet

$vnet = Set-AzVirtualNetwork -VirtualNetwork $vnet
$subnetId = $vnet.Subnets | Where-Object { $_.Name -eq 'snet-containerapps' } | Select-Object -ExpandProperty Id

# 创建 Managed Environment(内部模式)
$managedEnv = New-AzContainerAppManagedEnv `
-ResourceGroupName $resourceGroup `
-Location $location `
-Name $envName `
-SubnetResourceId $subnetId `
-AppInsightsConfigurationIngestionKey $null `
-AppLogsConfigurationDestination 'log-analytics'

# 为环境关联 Log Analytics 工作区
$workspace = New-AzOperationalInsightsWorkspace `
-ResourceGroupName $resourceGroup `
-Location $location `
-Name 'law-containerapps-prod'

$managedEnv | Update-AzContainerAppManagedEnv `
-AppLogsLogAnalyticsConfigurationCustomerId $workspace.CustomerId `
-AppLogsLogAnalyticsConfigurationSharedKey $workspace.GetSharedKeys().PrimarySharedKey

# 查看环境信息
$managedEnv | Format-List Name, Location, ProvisioningState, DefaultDomain
1
2
3
4
Name              : cae-microservice-prod
Location : eastasia
ProvisioningState : Succeeded
DefaultDomain : pleasantsea-xxxxxxxx.eastasia.azurecontainerapps.io

创建完环境后,可以进一步配置日志目标、添加自定义域和证书。下面的示例展示如何在环境中注册自定义域并绑定 SSL 证书,使内部服务通过企业域名访问。

1
2
3
4
5
6
7
# 获取环境详情
$env = Get-AzContainerAppManagedEnv `
-ResourceGroupName $resourceGroup `
-Name $envName

# 查看环境支持的可用区域冗余
$env | Select-Object Name, ZoneRedundant, VnetConfigurationInternal
1
2
3
Name                : cae-microservice-prod
ZoneRedundant : True
VnetConfigurationInternal : True

微服务部署与流量管理

在同一个环境中部署多个微服务时,流量管理是保障发布安全和系统稳定性的关键。下面的脚本演示了如何部署一个 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
# 定义容器应用参数
$appName = 'ca-api-gateway'
$imageV1 = 'myregistry.azurecr.io/api-gateway:v1.0.0'
$imageV2 = 'myregistry.azurecr.io/api-gateway:v1.1.0'
$acrLoginServer = 'myregistry.azurecr.io'

# 获取 ACR 凭据用于拉取镜像
$acr = Get-AzContainerRegistry -ResourceGroupName $resourceGroup -Name 'myregistry'
$acrCred = Get-AzContainerRegistryCredential -Registry $acr

# 部署初始版本 v1.0.0
New-AzContainerApp `
-ResourceGroupName $resourceGroup `
-Name $appName `
-Location $location `
-ManagedEnvironmentId $env.Id `
-ConfigurationRegistryServer $acrLoginServer `
-ConfigurationRegistryUser $acrCred.Username `
-ConfigurationRegistryPasswordSecretRef 'acr-password' `
-Secret 'acr-password'=$acrCred.Password `
-Image $imageV1 `
-TargetPort 8080 `
-IngressTargetPort 8080 `
-IngressExternal:$false `
-Cpu 0.5 `
-Memory '1.0Gi' `
-MinReplica 1 `
-MaxReplica 5

# 创建新修订版 v1.1.0,并配置 20% 流量
New-AzContainerAppRevision `
-ResourceGroupName $resourceGroup `
-Name $appName `
-Image $imageV2 `
-TrafficWeight 20 `
-RevisionSuffix 'v1-1-0'

# 验证流量分割状态
$trafficConfig = Get-AzContainerApp `
-ResourceGroupName $resourceGroup `
-Name $appName

$trafficConfig.TrafficWeight | Format-Table RevisionName, Weight, LatestRevision
1
2
3
4
RevisionName                           Weight LatestRevision
------------ ------ --------------
ca-api-gateway--v1-0-0-xxxxxxxx 80 False
ca-api-gateway--v1-1-0-xxxxxxxx 20 True

自动扩缩容是容器应用的核心能力。下面的脚本配置基于 HTTP 并发请求数的扩缩容规则,并设置 CPU 使用率作为辅助指标。当请求量突增时,实例数会自动扩展到上限;流量回落后自动缩容以节约成本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 配置自定义扩缩容规则
$scaleRule = New-AzContainerAppScaleRuleObject `
-Name 'http-concurrency' `
-CustomType 'http' `
-CustomMetadata 'concurrency'='100'

# 更新容器应用的扩缩容配置
Update-AzContainerApp `
-ResourceGroupName $resourceGroup `
-Name $appName `
-MinReplica 2 `
-MaxReplica 20 `
-ScaleRule $scaleRule

# 查看当前修订版的扩缩容配置
$app = Get-AzContainerApp -ResourceGroupName $resourceGroup -Name $appName
$app.Template.Scale | Format-List MinReplica, MaxReplica
1
2
MinReplica : 2
MaxReplica : 20

Dapr 集成与服务网格

Dapr(Distributed Application Runtime)为微服务提供了标准化的服务间调用、状态管理和发布订阅能力。Container Apps 原生集成 Dapr,无需额外安装 Sidecar。下面的脚本演示了如何在环境中启用 Dapr,配置状态存储和服务间调用组件。

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
# 在环境中添加 Dapr 状态存储组件(使用 Azure Cosmos DB)
$cosmosAccount = 'cosmos-microservice-prod'
$cosmosDb = 'statestore'
$cosmosKey = (Get-AzCosmosDBAccountKey `
-ResourceGroupName $resourceGroup `
-Name $cosmosAccount).PrimaryMasterKey

# 创建 Dapr 组件:状态存储
New-AzContainerAppManagedEnvDaprComponent `
-ResourceGroupName $resourceGroup `
-EnvName $envName `
-Name 'statestore' `
-ComponentType 'state.azure.cosmosdb' `
-Version 'v1' `
-Metadata 'url'="https://$cosmosAccount.documents.azure.com:443/" `
-Metadata 'database'=$cosmosDb `
-Metadata 'collection'='state' `
-Secret 'masterKey'=$cosmosKey `
-SecretRef 'masterKey'

# 创建 Dapr 组件:发布订阅(使用 Azure Service Bus)
$servicebusKey = (Get-AzServiceBusKey `
-ResourceGroupName $resourceGroup `
-NamespaceName 'sb-microservice-prod' `
-QueueName 'orders' `
-AuthorizationRuleName 'RootManageSharedAccessKey').PrimaryKey

$sbConnStr = "Endpoint=sb://sb-microservice-prod.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=$servicebusKey"

New-AzContainerAppManagedEnvDaprComponent `
-ResourceGroupName $resourceGroup `
-EnvName $envName `
-Name 'pubsub-orders' `
-ComponentType 'pubsub.azure.servicebus' `
-Version 'v1' `
-Metadata 'connectionString'=$sbConnStr `
-Scopes 'ca-order-service','ca-notification-service'

# 部署启用 Dapr 的订单服务
New-AzContainerApp `
-ResourceGroupName $resourceGroup `
-Name 'ca-order-service' `
-Location $location `
-ManagedEnvironmentId $env.Id `
-Image "$acrLoginServer/order-service:v1.0.0" `
-ConfigurationRegistryServer $acrLoginServer `
-ConfigurationRegistryUser $acrCred.Username `
-ConfigurationRegistryPasswordSecretRef 'acr-password' `
-DaprEnabled `
-DaprAppId 'order-service' `
-DaprAppPort 8080 `
-DaprAppProtocol 'http' `
-TargetPort 8080 `
-IngressTargetPort 8080 `
-IngressExternal:$false `
-EnvVar 'ASPNETCORE_ENVIRONMENT'='Production'

# 部署启用 Dapr 的通知服务(订阅订单事件)
New-AzContainerApp `
-ResourceGroupName $resourceGroup `
-Name 'ca-notification-service' `
-Location $location `
-ManagedEnvironmentId $env.Id `
-Image "$acrLoginServer/notification-service:v1.0.0" `
-ConfigurationRegistryServer $acrLoginServer `
-ConfigurationRegistryUser $acrCred.Username `
-ConfigurationRegistryPasswordSecretRef 'acr-password' `
-DaprEnabled `
-DaprAppId 'notification-service' `
-DaprAppPort 8080 `
-DaprAppProtocol 'http' `
-TargetPort 8080 `
-IngressTargetPort 8080 `
-IngressExternal:$false

# 列出环境中所有 Dapr 组件
Get-AzContainerAppManagedEnvDaprComponent `
-ResourceGroupName $resourceGroup `
-EnvName $envName | Format-Table Name, ComponentType, Version
1
2
3
4
Name            ComponentType              Version
---- ------------- -------
statestore state.azure.cosmosdb v1
pubsub-orders pubsub.azure.servicebus v1

部署完成后,订单服务可以通过 Dapr 的服务调用 API 与通知服务通信,无需硬编码服务地址。下面的脚本验证 Dapr Sidecar 的健康状态,并测试服务间调用链路。

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
# 获取订单服务的内部 FQDN
$orderApp = Get-AzContainerApp `
-ResourceGroupName $resourceGroup `
-Name 'ca-order-service'

$internalUrl = "http://{0}:8080/v1.0/invoke/notification-service/method/health" -f `
$orderApp.Configuration.Ingress.Fqdn

# 通过 Dapr Sidecar 测试服务间调用
$envId = (Get-AzContainerAppManagedEnv `
-ResourceGroupName $resourceGroup `
-Name $envName).StaticIp

Write-Host "环境静态 IP: $envId"
Write-Host "订单服务内部域名: $($orderApp.Configuration.Ingress.Fqdn)"

# 列出环境中的所有容器应用及其 Dapr 配置
$apps = Get-AzContainerApp -ResourceGroupName $resourceGroup
$apps | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
DaprEnabled = $_.Template.Dapr.Enabled
DaprAppId = $_.Template.Dapr.AppId
Replicas = "{0}-{1}" -f $_.Template.Scale.MinReplica, $_.Template.Scale.MaxReplica
}
} | Format-Table -AutoSize
1
2
3
4
5
6
7
8
环境静态 IP: 10.0.0.4
订单服务内部域名: ca-order-service.internal.pleasantsea-xxxxxxxx.eastasia.azurecontainerapps.io

Name DaprEnabled DaprAppId Replicas
---- ----------- ---------- --------
ca-api-gateway False 2-20
ca-order-service True order-service 1-10
ca-notification-service True notification-service 1-10

注意事项

  1. 子网大小要求:Container Apps 环境要求关联子网至少为 /23(512 个 IP),在规划 VNet 地址空间时需预留足够的 IP 范围,避免因 IP 耗尽导致新应用无法部署。

  2. 内部环境模式:启用 VnetConfigurationInternal 后,环境不提供公网入口。如需外部访问,必须额外部署 Application Gateway 或 Front Door 作为反向代理,并将其后端池指向环境的内部 IP。

  3. Dapr 组件的作用域:通过 Scopes 参数限制哪些容器应用可以使用某个 Dapr 组件。不加限制的组件对所有启用 Dapr 的应用可见,可能导致意外的依赖关系和安全风险。

  4. 流量分割的修订版管理:每个流量权重条目对应一个活跃修订版。当所有流量切换到新版本后,旧修订版不会自动删除,需要手动清理以释放资源配额。可使用 Remove-AzContainerAppRevision 清理不再使用的版本。

  5. 扩缩容冷启动MinReplica 设为 0 虽然可以节省成本,但会导致冷启动延迟(通常 5-30 秒)。对于延迟敏感型 API 网关或前端服务,建议设置 MinReplica 至少为 1。

  6. 密钥与连接字符串安全:Dapr 组件中的敏感信息(如数据库密钥、连接字符串)应通过 SecretSecretRef 传递,避免明文出现在脚本中。更推荐将密钥存储在 Azure Key Vault 中,通过 Managed Identity 引用。

PowerShell 技能连载 - Azure API Management 管理

适用于 PowerShell 7.0 及以上版本

在微服务架构中,API 网关是连接前端应用与后端服务的关键枢纽。Azure API Management(APIM)作为微软云平台的企业级 API 网关,提供了 API 发布、流量控制、安全认证、请求转换和用量分析等一站式能力。无论是对外暴露开放 API,还是在内部微服务之间做统一入口,APIM 都能胜任。

然而 APIM 的配置项非常庞杂——API 定义、操作策略、产品打包、订阅密钥、后端服务池、开发者门户,每一项都需要精确配置。在多环境(开发、测试、生产)部署场景下,手动在 Azure 门户中点选配置既容易出错,也难以做到版本追溯。

通过 PowerShell 的 Az.ApiManagement 模块,我们可以将 APIM 的全部配置写成脚本,实现基础设施即代码(IaC)。这不仅保证了环境之间的一致性,还能将配置纳入 Git 版本控制,配合 CI/CD 管线实现 API 管理的自动化交付。本文将从实例管理、策略配置和产品运营三个维度,展示如何用 PowerShell 高效管理 APIM。

APIM 实例与 API 管理

首先,我们来看如何创建 APIM 实例并导入 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
# 连接 Azure 账户
Connect-AzAccount -Subscription 'MySubscription'

# 定义变量
$resourceGroup = 'rg-apim-demo'
$location = 'eastasia'
$apimName = 'apim-demo-001'
$apiName = 'user-service-api'

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

# 创建 APIM 实例(开发者层级,适合测试)
$apim = New-AzApiManagement `
-ResourceGroupName $resourceGroup `
-Name $apimName `
-Location $location `
-Organization 'Vichamp Corp' `
-AdminEmail 'admin@vichamp.com' `
-Sku 'Developer' `
-Capacity 1

$context = $apim.Context

# 从 OpenAPI 规范导入 API
$openApiSpec = @'
{
"openapi": "3.0.1",
"info": { "title": "User Service", "version": "1.0.0" },
"paths": {
"/users": {
"get": {
"operationId": "list-users",
"responses": { "200": { "description": "OK" } }
},
"post": {
"operationId": "create-user",
"responses": { "201": { "description": "Created" } }
}
},
"/users/{id}": {
"get": {
"operationId": "get-user",
"responses": { "200": { "description": "OK" } }
}
}
}
}
'@

$specPath = Join-Path $env:TEMP 'user-service-openapi.json'
$openApiSpec | Set-Content -Path $specPath -Encoding utf8

# 导入 API,设置后端 URL
Import-AzApiManagementApi `
-Context $context `
-ApiId $apiName `
-SpecificationFormat 'OpenApiJson' `
-SpecificationPath $specPath `
-Path 'users' `
-ServiceUrl 'https://user-svc.internal.vichamp.com/api/v1'

# 配置后端服务(支持负载均衡和服务发现)
$backendId = 'user-service-backend'
New-AzApiManagementBackend `
-Context $context `
-BackendId $backendId `
-Url 'https://user-svc.internal.vichamp.com/api/v1' `
-Protocol 'http' `
-Description 'User microservice backend'

# 将 API 操作绑定到后端
Set-AzApiManagementApi `
-Context $context `
-ApiId $apiName `
-ApiIdSet $apiName `
-ServiceUrl 'https://user-svc.internal.vichamp.com/api/v1'

Write-Host "APIM 实例 '$apimName' 创建完成,API '$apiName' 已导入" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
7
8
9
确认
资源组 'rg-apim-demo' 已存在,是否覆盖?
[Y] 是 [N] 否 [S] 挂起 [?] 帮助 (默认值为“Y”):

ResourceId : /subscriptions/xxxx/resourceGroups/rg-apim-demo
Location : eastasia
ProvisioningState : Succeeded

APIM 实例 'apim-demo-001' 创建完成,API '$apiName' 已导入

策略配置与安全

APIM 的核心能力在于策略(Policy)。策略是一段 XML 配置,可以在请求和响应的各个阶段注入逻辑。下面展示如何配置速率限制、JWT 验证和请求转换策略:

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
$context = (Get-AzApiManagement -ResourceGroupName 'rg-apim-demo' -Name 'apim-demo-001').Context

# --- 策略 1:速率限制(API 级别) ---
$rateLimitPolicy = @'
<policies>
<inbound>
<rate-limit calls="100" renewal-period="60" />
<rate-limit-by-key calls="20" renewal-period="60"
counter-key="@(context.Request.IpAddress)" />
</inbound>
</policies>
'@

Set-AzApiManagementPolicy `
-Context $context `
-ApiId 'user-service-api' `
-PolicyValue $rateLimitPolicy

Write-Host "速率限制策略已应用:全局 100 次/分钟,单 IP 20 次/分钟" -ForegroundColor Cyan

# --- 策略 2:JWT 验证(操作级别) ---
$jwtPolicy = @'
<policies>
<inbound>
<validate-jwt header-name="Authorization"
failed-validation-httpcode="401"
failed-validation-error-message="Unauthorized. Invalid token.">
<openid-config url="https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration" />
<required-claims>
<claim name="aud" match="all">
<value>api://user-service</value>
</claim>
<claim name="roles" match="any">
<value>API.Reader</value>
<value>API.Writer</value>
</claim>
</required-claims>
</validate-jwt>
</inbound>
</policies>
'@

Set-AzApiManagementPolicy `
-Context $context `
-ApiId 'user-service-api' `
-OperationId 'get-user' `
-PolicyValue $jwtPolicy

Write-Host "JWT 验证策略已应用到 get-user 操作" -ForegroundColor Cyan

# --- 策略 3:请求头转换与响应缓存 ---
$transformPolicy = @'
<policies>
<inbound>
<set-header name="X-Request-Source" exists-action="override">
<value>APIM-Gateway</value>
</set-header>
<set-header name="X-Correlation-Id" exists-action="skip">
<value>@(Guid.NewGuid().ToString())</value>
</set-header>
<rewrite-uri template="/users/{id}/profile?fields=basic" />
</inbound>
<outbound>
<cache-store duration="300" />
<set-header name="X-Response-Time" exists-action="override">
<value>@(DateTime.UtcNow.ToString("O"))</value>
</set-header>
</outbound>
</policies>
'@

Set-AzApiManagementPolicy `
-Context $context `
-ApiId 'user-service-api' `
-OperationId 'get-user' `
-PolicyValue $transformPolicy

Write-Host "请求转换与缓存策略已应用" -ForegroundColor Cyan

# 导出当前 API 的完整策略用于版本控制
$policyXml = Get-AzApiManagementPolicy `
-Context $context `
-ApiId 'user-service-api'

$policyXml.Save("$(Join-Path $PWD 'policies\user-service-api-policy.xml')")
Write-Host "策略已导出到文件,可纳入 Git 版本控制" -ForegroundColor Green

执行结果示例:

1
2
3
4
速率限制策略已应用:全局 100 次/分钟,单 IP 20 次/分钟
JWT 验证策略已应用到 get-user 操作
请求转换与缓存策略已应用
策略已导出到文件,可纳入 Git 版本控制

产品管理与开发者门户

APIM 中的”产品”(Product)是 API 的打包和发布单元。开发者通过订阅产品来获取 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
87
88
89
90
91
92
93
94
$context = (Get-AzApiManagement -ResourceGroupName 'rg-apim-demo' -Name 'apim-demo-001').Context

# --- 创建产品并关联 API ---
$productId = 'user-service-basic'
$productTitle = 'User Service - Basic Tier'

New-AzApiManagementProduct `
-Context $context `
-ProductId $productId `
-Title $productTitle `
-Description '基础套餐:提供用户查询和创建功能,速率限制 100 次/分钟' `
-LegalTerm '使用本 API 即表示同意服务条款 v2.0' `
-SubscriptionRequired $true `
-ApprovalRequired $false `
-State 'Published'

# 将 API 添加到产品
Add-AzApiManagementApiToProduct `
-Context $context `
-ProductId $productId `
-ApiId 'user-service-api'

Write-Host "产品 '$productTitle' 已创建并发布" -ForegroundColor Cyan

# --- 创建开发者账户并生成订阅 ---
$userId = 'dev-user-001'
$email = 'developer@partner.com'

New-AzApiManagementUser `
-Context $context `
-UserId $userId `
-FirstName 'Alice' `
-LastName 'Chen' `
-Email $email `
-Password (ConvertTo-SecureString 'P@ssw0rd!' -AsPlainText -Force) `
-State 'Active'

# 为开发者创建订阅
$subscription = New-AzApiManagementSubscription `
-Context $context `
-ProductId $productId `
-Name "Alice 的订阅 - $productTitle" `
-UserId $userId `
-PrimaryKey (New-Guid).ToString() `
-SecondaryKey (New-Guid).ToString() `
-State 'Active'

Write-Host "开发者 $email 已订阅产品,订阅 ID: $($subscription.SubscriptionId)" -ForegroundColor Cyan

# --- 用量分析报告 ---
# 获取按 API 汇总的请求统计
$reportSasToken = Get-AzApiManagementSsoToken `
-ResourceGroupName 'rg-apim-demo' `
-Name 'apim-demo-001'

Write-Host "`n--- API 用量报告(最近 30 天)---" -ForegroundColor Yellow

$apis = Get-AzApiManagementApi -Context $context

foreach ($api in $apis) {
# 获取 API 级别的指标
$metrics = Get-AzApiManagementDiagnostic `
-Context $context `
-ApiId $api.ApiId `
-DiagnosticId 'applicationinsights' `
-ErrorAction SilentlyContinue

$operations = Get-AzApiManagementOperation -Context $context -ApiId $api.ApiId

[PSCustomObject]@{
API名称 = $api.Name
路径前缀 = $api.Path
操作数量 = $operations.Count
协议 = ($api.Protocols -join ', ')
后端URL = $api.ServiceUrl
状态 = $api.IsCurrent
}
}

# 导出订阅密钥列表(运维审计用)
$allSubscriptions = Get-AzApiManagementSubscription -Context $context

$subscriptionReport = $allSubscriptions | ForEach-Object {
[PSCustomObject]@{
订阅名称 = $_.Name
产品ID = $_.ProductId
创建时间 = $_.CreatedDate
状态 = $_.State
过期时间 = $_.EndDate
}
}

$subscriptionReport | Format-Table -AutoSize
Write-Host "共 $($subscriptionReport.Count) 个活跃订阅" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
产品 'User Service - Basic Tier' 已创建并发布
开发者 developer@partner.com 已订阅产品,订阅 ID: sub-20260206-001

--- API 用量报告(最近 30 天)---

API名称 路径前缀 操作数量 协议 后端URL 状态
------ -------- -------- ---- ------- ----
User Service users 3 https https://user-svc.internal.vichamp.com/api/v1 True

订阅名称 产品ID 创建时间 状态 过期时间
-------- ------ -------- ---- --------
Alice 的订阅 - User Service... user-service-basic 2026/2/6 10:30:00 Active
共 1 个活跃订阅

注意事项

  1. SKU 选择:开发者层级(Developer)仅用于非生产环境,不提供 SLA 保证。生产环境应选择基本(Basic)、标准(Standard)或高级(Premium)层级,高级层支持多区域部署和虚拟网络集成。
  2. 策略 XML 格式:APIM 策略是严格的 XML 文档,拼写错误或标签未闭合会导致整个 API 无法响应。建议在本地用 XML 校验工具验证后再部署,并将策略文件纳入 Git 版本控制。
  3. 订阅密钥安全New-AzApiManagementSubscription 生成的主密钥和次密钥是访问 API 的凭证,切勿硬编码在脚本中,应存储在 Azure Key Vault 或环境变量中。
  4. 策略继承顺序:APIM 策略按”全局 → 产品 → API → 操作”的层级继承,子级策略会覆盖父级同名的策略节点。配置时务必明确各层级的职责边界,避免策略冲突。
  5. 后端健康检查:生产环境建议配置后端服务池(Backend Pool)和健康探针(Health Probe),当某个后端实例不可用时 APIM 能自动切换,提高服务可用性。
  6. 成本监控:APIM 按层级和容量计费,高级层每个单元约 2,800 美元/月。建议在非工作时间缩减容量(Premium 层支持弹性扩缩),并设置 Azure 成本告警防止超支。

PowerShell 技能连载 - REST API 开发

适用于 PowerShell 7.0 及以上版本

当我们谈论 PowerShell 与 REST API 时,通常是在讨论如何用 Invoke-RestMethod 调用外部服务。但 PowerShell 的能力不止于此——借助 .NET 的 HttpListener 类或社区开发的 Web 框架,你完全可以用 PowerShell 构建自己的 REST API 服务。

这种能力在内部工具场景中尤为实用。比如 CI/CD 流水线需要一个状态查询端点、监控系统需要一个数据聚合接口、第三方 Webhook 需要一个接收器——这些都不需要引入一整套 ASP.NET 或 Node.js 项目,几十行 PowerShell 脚本就能搞定。本文将从底层到高层,逐步演示三种方案。

使用 HttpListener 创建基础 REST API

System.Net.HttpListener 是 .NET 内置的 HTTP 服务器类,无需安装任何额外模块即可使用。下面这段代码创建了一个支持 GET 和 POST 路由的简易 API,返回 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
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
using namespace System.Net
using namespace System.Text

# 创建 API 数据存储
$script:dataStore = @{
tasks = [System.Collections.ArrayList]::new()
1..5 | ForEach-Object {
@{ id = $_; title = "任务 $_"; status = if ($_ -le 3) { 'done' } else { 'pending' } }
}
}

# 路由表
$routes = @{
'GET /api/tasks' = {
param($ctx)
$json = $script:dataStore.tasks | ConvertTo-Json -Depth 3
Send-JsonResponse $ctx $json 200
}
'GET /api/tasks/id' = {
param($ctx)
$id = [int]($ctx.Request.Url.Segments | Select-Object -Last 1)
$task = $script:dataStore.tasks | Where-Object { $_.id -eq $id }
if ($task) {
Send-JsonResponse $ctx ($task | ConvertTo-Json) 200
} else {
Send-JsonResponse $ctx '{"error":"任务不存在"}' 404
}
}
'POST /api/tasks' = {
param($ctx)
$reader = [StreamReader]::new($ctx.Request.InputStream)
$body = $reader.ReadToEnd()
$reader.Close()
$newTask = $body | ConvertFrom-Json
$script:dataStore.tasks.Add(@{
id = ($script:dataStore.tasks.Count + 1)
title = $newTask.title
status = 'pending'
}) | Out-Null
Send-JsonResponse $ctx '{"message":"任务已创建"}' 201
}
}

# 响应辅助函数
function Send-JsonResponse {
param([HttpListenerContext]$Context, [string]$Body, [int]$StatusCode)
$buffer = [Encoding]::UTF8.GetBytes($Body)
$Context.Response.StatusCode = $StatusCode
$Context.Response.ContentType = 'application/json; charset=utf-8'
$Context.Response.ContentLength64 = $buffer.Length
$Context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
$Context.Response.Close()
}

# 启动监听器
$listener = [HttpListener]::new()
$listener.Prefixes.Add('http://localhost:8080/')
$listener.Start()
Write-Host "API 服务已启动: http://localhost:8080/" -ForegroundColor Green

try {
while ($listener.IsListening) {
$ctx = $listener.GetContext()
$method = $ctx.Request.HttpMethod
$path = $ctx.Request.Url.AbsolutePath.TrimEnd('/')
Write-Host " [$method] $path" -ForegroundColor Cyan

# 路由匹配
$routeKey = "$method $path"
$handler = $routes[$routeKey]
if (-not $handler -and $path -match '/api/tasks/\d+$') {
$handler = $routes['GET /api/tasks/id']
}

if ($handler) {
& $handler $ctx
} else {
Send-JsonResponse $ctx '{"error":"路由不存在"}' 404
}
}
} finally {
$listener.Stop()
Write-Host "API 服务已停止" -ForegroundColor Yellow
}

在另一个终端中测试 API:

1
2
3
4
5
6
7
8
# 查询所有任务
Invoke-RestMethod http://localhost:8080/api/tasks

# 查询单个任务
Invoke-RestMethod http://localhost:8080/api/tasks/2

# 创建新任务
Invoke-RestMethod http://localhost:8080/api/tasks -Method Post -Body '{"title":"新任务"}' -ContentType 'application/json'

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id title  status
-- ----- ------
1 任务 1 done
2 任务 2 done
3 任务 3 done
4 任务 4 pending
5 任务 5 pending

id title status
-- ----- ------
2 任务 2 done

message
-------
任务已创建

使用 Pode 框架构建完整 API

HttpListener 虽然灵活,但需要手动处理路由、中间件等逻辑。Pode 是一个专为 PowerShell 设计的跨平台 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
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
# 安装 Pode 模块(首次使用)
Install-Module Pode -Force -Scope CurrentUser

# 创建 api-server.ps1
Start-PodeServer {
# 监听端口
Add-PodeEndpoint -Address localhost -Port 3000 -Protocol Http

# 配置日志
New-PodeLoggingMethod -File -Name 'api-errors' | Enable-PodeErrorLogging
New-PodeLoggingMethod -File -Name 'api-requests' | New-PodeRequestLogging

# JWT 认证中间件
New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'jwt-auth' -Sessionless {
param($token)
try {
$payload = $token | Invoke-RestMethod -Uri 'https://your-idp/.well-known/jwks.json'
# 简化演示:直接验证 token 非空
if ([string]::IsNullOrEmpty($token)) {
return @{ Message = 'Token 无效' }
}
return @{ User = @{ Name = 'api-user'; Role = 'admin' } }
} catch {
return @{ Message = "认证失败: $_" }
}
}

# 全局中间件:请求计时
Add-PodeMiddleware -Name 'timer' -ScriptBlock {
$WebEvent.Metadata['StartTime'] = [datetime]::UtcNow
}

# 全局中间件:CORS 支持
Add-PodeMiddleware -Name 'cors' -ScriptBlock {
$WebEvent.Response.Headers.Add('Access-Control-Allow-Origin', '*')
$WebEvent.Response.Headers.Add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
if ($WebEvent.Method -eq 'OPTIONS') {
Set-PodeResponseStatus -Code 204
}
}

# 健康检查端点(无需认证)
Add-PodeRoute -Method Get -Path '/health' -ScriptBlock {
Write-PodeJsonResponse -Value @{
status = 'healthy'
uptime = (Get-Date) - $PodeContext.Server.StartTime
version = '1.0.0'
hostname = $env:COMPUTERNAME
}
}

# GET /api/items - 获取列表
Add-PodeRoute -Method Get -Path '/api/items' -Authentication 'jwt-auth' -ScriptBlock {
$page = [int]($WebEvent.Query['page'] ?? '1')
$size = [int]($WebEvent.Query['size'] ?? '20')
Write-PodeJsonResponse -Value @{
data = @(
@{ id = 1; name = '项目 A'; status = 'active' }
@{ id = 2; name = '项目 B'; status = 'archived' }
@{ id = 3; name = '项目 C'; status = 'active' }
)
page = $page
size = $size
total = 3
}
}

# POST /api/items - 创建项目
Add-PodeRoute -Method Post -Path '/api/items' -Authentication 'jwt-auth' -ScriptBlock {
$body = $WebEvent.Data
if (-not $body.name) {
Set-PodeResponseStatus -Code 400
Write-PodeJsonResponse -Value @{ error = 'name 字段必填' }
return
}
Write-PodeJsonResponse -Value @{
id = Get-Random -Maximum 1000
name = $body.name
status = 'created'
message = '项目创建成功'
} -StatusCode 201
}

# 静态文件托管
Add-PodeStaticRoute -Path '/docs' -Source './public'

Write-Host "Pode API 服务运行在 http://localhost:3000" -ForegroundColor Green
}

启动服务后测试各端点:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 健康检查
Invoke-RestMethod http://localhost:3000/health | Format-List

# 获取项目列表(带认证头)
$headers = @{ Authorization = 'Bearer your-jwt-token-here' }
Invoke-RestMethod http://localhost:3000/api/items -Headers $headers

# 分页查询
Invoke-RestMethod 'http://localhost:3000/api/items?page=1&size=10' -Headers $headers

# 创建新项目
$body = @{ name = '项目 D' } | ConvertTo-Json
Invoke-RestMethod http://localhost:3000/api/items -Method Post -Headers $headers -Body $body -ContentType 'application/json'

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
status  : healthy
uptime : 00:15:32.0180000
version : 1.0.0
hostname: SERVER01

data : {@{id=1; name=项目 A; status=active}, @{id=2; name=项目 B;
status=archived}, @{id=3; name=项目 C; status=active}}
page : 1
size : 20
total : 3

id : 847
name : 项目 D
status : created
message : 项目创建成功

API 部署与运维

将 API 从开发脚本变成生产服务,需要考虑日志记录、健康检查、自动重启等运维需求。以下脚本展示了如何将 Pode API 注册为 Windows 服务,并添加运维监控端点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
# 将 API 注册为 Windows 服务
function Register-ApiService {
param(
[string]$Name = 'PowershellApi',
[string]$ScriptPath = 'C:\api\server.ps1'
)

# 使用 NSSM(Non-Sucking Service Manager)注册服务
$nssmPath = 'C:\tools\nssm.exe'
if (-not (Test-Path $nssmPath)) {
Write-Warning '请先安装 NSSM: choco install nssm'
return
}

# 注册服务
& $nssmPath install $Name pwsh.exe "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
& $nssmPath set $Name DisplayName "PowerShell REST API Service"
& $nssmPath set $Name Description "内部工具 REST API 服务"
& $nssmPath set $Name Start SERVICE_AUTO_START
& $nssmPath set $Name AppDirectory (Split-Path $ScriptPath)
& $nssmPath set $Name AppStdout (Join-Path (Split-Path $ScriptPath) 'stdout.log')
& $nssmPath set $Name AppStderr (Join-Path (Split-Path $ScriptPath) 'stderr.log')
& $nssmPath set $Name AppRotateFiles 1
& $nssmPath set $Name AppRotateBytes 10485760

Write-Host "服务 [$Name] 已注册,使用以下命令管理:" -ForegroundColor Green
Write-Host " 启动: nssm start $Name"
Write-Host " 停止: nssm stop $Name"
Write-Host " 状态: nssm status $Name"
Write-Host " 删除: nssm remove $Name confirm"
}

# 运维监控脚本:定时检查 API 可用性
function Test-ApiHealth {
param(
[string]$BaseUrl = 'http://localhost:3000',
[int]$IntervalSeconds = 30
)

$logFile = "C:\api\health-$(Get-Date -Format 'yyyyMMdd').log"

while ($true) {
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
try {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$response = Invoke-RestMethod "$BaseUrl/health" -TimeoutSec 5
$sw.Stop()
$latency = $sw.ElapsedMilliseconds

$logEntry = "[$timestamp] OK | latency: ${latency}ms | uptime: $($response.uptime)"
Write-Host $logEntry -ForegroundColor Green
Add-Content -Path $logFile -Value $logEntry

} catch {
$logEntry = "[$timestamp] FAIL | error: $($_.Exception.Message)"
Write-Host $logEntry -ForegroundColor Red
Add-Content -Path $logFile -Value $logEntry

# 可选:自动重启服务
# Restart-Service -Name 'PowershellApi' -Force
}

Start-Sleep -Seconds $IntervalSeconds
}
}

# 一键部署函数
function Deploy-ApiService {
param([string]$ApiDir = 'C:\api')

# 创建目录
$dirs = @($ApiDir, "$ApiDir\logs", "$ApiDir\public")
$dirs | ForEach-Object {
if (-not (Test-Path $_)) {
New-Item -ItemType Directory -Path $_ -Force | Out-Null
}
}

# 部署最新代码
Copy-Item '.\api-server.ps1' "$ApiDir\server.ps1" -Force
Copy-Item '.\public\*' "$ApiDir\public\" -Recurse -Force

# 注册并启动服务
Register-ApiService -ScriptPath "$ApiDir\server.ps1"

Write-Host "部署完成!API 地址: http://localhost:3000" -ForegroundColor Green
Write-Host "健康检查: http://localhost:3000/health" -ForegroundColor Cyan
Write-Host "API 文档: http://localhost:3000/docs" -ForegroundColor Cyan
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
服务 [PowershellApi] 已注册,使用以下命令管理:
启动: nssm start PowershellApi
停止: nssm stop PowershellApi
状态: nssm status PowershellApi
删除: nssm remove PowershellApi confirm

[2026-01-14 10:00:00] OK | latency: 12ms | uptime: 00:45:12.0000000
[2026-01-14 10:00:30] OK | latency: 8ms | uptime: 00:45:42.0000000
[2026-01-14 10:01:00] OK | latency: 15ms | uptime: 00:46:12.0000000
[2026-01-14 10:01:30] FAIL | error: Unable to connect to the remote server
[2026-01-14 10:02:00] OK | latency: 10ms | uptime: 00:00:30.0000000

部署完成!API 地址: http://localhost:3000
健康检查: http://localhost:3000/health
API 文档: http://localhost:3000/docs

注意事项

  1. HttpListener 权限:在 Windows 上监听非 localhost 地址时,需要以管理员身份运行 netsh http add urlacl url=http://+:8080/ user=Everyone 来预留 URL 命名空间,否则会抛出”拒绝访问”异常。

  2. 并发处理HttpListenerGetContext() 是同步阻塞调用,只能一次处理一个请求。生产环境应使用 BeginGetContext / EndGetContext 异步模式,或者直接采用 Pode 框架——它内部已处理好并发问题。

  3. HTTPS 支持:生产环境务必启用 TLS。Pode 支持 Add-PodeEndpoint -ProtocolHttps -Certificate 参数直接绑定证书;HttpListener 则需要通过 netsh 绑定 SSL 证书到端口。

  4. 内存泄漏防范:长时间运行的 API 服务需要注意 IDisposable 对象(如 HttpListenerStreamReader)的释放。使用 try/finally 确保资源被正确清理,或使用 using 语句(PowerShell 7.0+ 支持)。

  5. 错误处理与日志:永远不要让未捕获的异常终止整个服务进程。在路由处理器中使用 try/catch,并将错误写入结构化日志文件,方便后续通过 ELK 或 Splunk 等工具分析。

  6. 跨平台部署:如果需要在 Linux 上运行,HttpListener 方案可以工作(依赖 .NET 的 Unix Socket 实现),但更推荐使用 Pode——它原生支持 Linux,可以搭配 systemd 或 Docker 容器化部署,实现与 Windows 服务对等的运维体验。

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 或至少检查模块版本是否满足要求。