适用于 PowerShell 7.0 及以上版本
在传统运维模式中,自动化脚本通常运行在虚拟机或容器上,无论脚本是否执行,底层计算资源都在持续消耗成本。对于定时清理、事件响应、API 集成等轻量级任务来说,这种模式既浪费资源又增加了运维负担——你不仅要维护脚本本身,还要操心服务器的补丁更新、高可用和扩缩容。
Azure Functions 是微软 Azure 提供的无服务器(Serverless)计算平台,支持使用 PowerShell 作为运行时语言。函数只在被事件触发时才执行,按实际运行时间和调用次数计费,空闲时不产生任何费用。PowerShell 运行时内置了 Az 模块和大量常用 cmdlet,让你可以直接在云端编写和运行自动化逻辑,无需自行管理基础设施。
本文将从三个实战场景出发:搭建 Azure Functions PowerShell 项目并编写 HTTP 触发器函数、使用定时触发器和队列触发器处理周期性与事件驱动任务,以及部署、监控与错误处理的最佳实践。
项目结构与 HTTP 触发器函数
Azure Functions PowerShell 项目有固定的目录结构。function.json 定义触发器绑定,run.ps1 是入口脚本。以下示例展示如何使用命令行工具初始化项目,并编写一个接收 HTTP 请求、查询 Azure 资源状态的 HTTP 触发器函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
func init PsFuncDemo --worker-runtime powershell --managed-dependencies
Set-Location PsFuncDemo func new --name Get-ResourceStatus --template "HTTP trigger" --authlevel "function"
Get-ChildItem -Recurse | Where-Object { -not $_.PSIsContainer } | Select-Object FullName | Format-Table -AutoSize
|
生成的项目中,Get-ResourceStatus/run.ps1 是函数入口,Get-ResourceStatus/function.json 定义绑定。下面我们来改造 run.ps1,实现一个查询 Azure 资源组状态的 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
|
using namespace System.Net
param($Request, $TriggerMetadata)
$ResourceGroupName = $Request.Query.ResourceGroup if (-not $ResourceGroupName) { $ResourceGroupName = $Request.Body.ResourceGroup }
if (-not $ResourceGroupName) { Push-OutputBinding -Name Response -Value ( [HttpResponseContext]@{ StatusCode = [HttpStatusCode]::BadRequest Body = @{ error = "请提供 ResourceGroup 参数" usage = "GET /api/Get-ResourceStatus?ResourceGroup=<名称>" } } ) return }
try { Connect-AzAccount -Identity -ErrorAction Stop | Out-Null
$rg = Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction Stop
$resources = Get-AzResource -ResourceGroupName $ResourceGroupName | Select-Object Name, ResourceType, Location, Tags
$result = @{ status = 'success' resourceGroup = $rg.ResourceGroupName location = $rg.Location provisioning = $rg.ProvisioningState resourceCount = $resources.Count resources = $resources queriedAt = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssZ') }
Push-OutputBinding -Name Response -Value ( [HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = $result } ) } catch { Push-OutputBinding -Name Response -Value ( [HttpResponseContext]@{ StatusCode = [HttpStatusCode]::InternalServerError Body = @{ status = 'error' error = $_.Exception.Message } } ) }
|
以下是 function.json 的绑定配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { "bindings": [ { "authLevel": "function", "type": "httpTrigger", "direction": "in", "name": "Request", "methods": ["get", "post"] }, { "type": "http", "direction": "out", "name": "Response" } ] }
|
执行结果示例:
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
| FullName -------- /Users/demo/PsFuncDemo/.gitignore /Users/demo/PsFuncDemo/host.json /Users/demo/PsFuncDemo/local.settings.json /Users/demo/PsFuncDemo/requirements.psd1 /Users/demo/PsFuncDemo/profile.ps1 /Users/demo/PsFuncDemo/extensions.csproj /Users/demo/PsFuncDemo/Get-ResourceStatus/run.ps1 /Users/demo/PsFuncDemo/Get-ResourceStatus/function.json
HTTP/1.1 200 OK Content-Type: application/json
{ "status": "success", "resourceGroup": "rg-demo", "location": "eastasia", "provisioning": "Succeeded", "resourceCount": 5, "resources": [ { "Name": "vm-web-01", "ResourceType": "Microsoft.Compute/virtualMachines", "Location": "eastasia", "Tags": { "env": "production" } }, ... ], "queriedAt": "2025-12-26T08:30:15Z" }
|
定时触发器与队列触发器
除了 HTTP 触发器,Azure Functions 还支持定时触发器(Timer Trigger)和队列触发器(Queue Trigger),分别用于周期性任务和消息驱动的异步处理。以下示例分别创建一个每天凌晨执行的资源清理函数和一个监听存储队列的自动处理函数。
1 2 3 4 5
| func new --name Daily-Cleanup --template "Timer trigger"
|
Daily-Cleanup/function.json 配置:
1 2 3 4 5 6 7 8 9 10
| { "bindings": [ { "name": "Timer", "type": "timerTrigger", "direction": "in", "schedule": "0 0 2 * * *" } ] }
|
Daily-Cleanup/run.ps1 内容:
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
| param($Timer, $TriggerMetadata)
$startTime = Get-Date Write-Host "[$startTime] 每日清理任务启动"
Connect-AzAccount -Identity | Out-Null
$cutoffDate = (Get-Date).AddDays(-30) $snapshots = Get-AzSnapshot | Where-Object { $_.TimeCreated -lt $cutoffDate }
$cleanedCount = 0 foreach ($snapshot in $snapshots) { Write-Host "删除过期快照: $($snapshot.Name) (创建于 $($snapshot.TimeCreated))" Remove-AzSnapshot -ResourceGroupName $snapshot.ResourceGroupName ` -SnapshotName $snapshot.Name -Force $cleanedCount++ }
$emptyGroups = Get-AzResourceGroup | Where-Object { $resources = Get-AzResource -ResourceGroupName $_.ResourceGroupName $resources.Count -eq 0 -and $_.ResourceGroupName -notmatch '^rg-shared-|^rg-network-' }
$removedGroups = 0 foreach ($group in $emptyGroups) { Write-Host "删除空资源组: $($group.ResourceGroupName)" Remove-AzResourceGroup -Name $group.ResourceGroupName -Force $removedGroups++ }
$duration = ((Get-Date) - $startTime).ToString('hh\:mm\:ss') Write-Host "清理完成。删除快照: $cleanedCount 个,删除空资源组: $removedGroups 个,耗时: $duration"
|
接下来创建队列触发器函数:
1 2
| func new --name Process-QueueMessage --template "Azure Queue Storage trigger"
|
Process-QueueMessage/function.json 配置:
1 2 3 4 5 6 7 8 9 10 11
| { "bindings": [ { "name": "QueueItem", "type": "queueTrigger", "direction": "in", "queueName": "automation-tasks", "connection": "AzureWebJobsStorage" } ] }
|
Process-QueueMessage/run.ps1 内容:
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
| param($QueueItem, $TriggerMetadata)
$message = $QueueItem | ConvertFrom-Json Write-Host "收到队列消息,操作类型: $($message.action)"
switch ($message.action) { 'restart-vm' { Write-Host "重启虚拟机: $($message.vmName) in $($message.resourceGroup)" Connect-AzAccount -Identity | Out-Null Restart-AzVM -ResourceGroupName $message.resourceGroup ` -Name $message.vmName -NoWait Write-Host "已发送重启指令" }
'scale-webapp' { Write-Host "调整 Web 应用规模: $($message.appName) -> $($message.tier)" Connect-AzAccount -Identity | Out-Null $app = Get-AzWebApp -ResourceGroupName $message.resourceGroup ` -Name $message.appName $app.ServerFarmId = $message.newPlanId Set-AzWebApp -WebApp $app | Out-Null Write-Host "Web 应用规模已调整" }
'send-notification' { Write-Host "发送通知: $($message.subject)" $body = @{ title = $message.subject message = $message.body channel = $message.channel } | ConvertTo-Json -Compress
Invoke-RestMethod -Uri $message.webhookUrl ` -Method Post ` -Body $body ` -ContentType 'application/json' Write-Host "通知已发送" }
default { Write-Warning "未知的操作类型: $($message.action)" } }
|
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| # 定时清理任务日志 [12/26/2025 02:00:00] 每日清理任务启动 [12/26/2025 02:00:01] 删除过期快照: snap-backup-20251120 (创建于 11/20/2025) [12/26/2025 02:00:03] 删除过期快照: snap-backup-20251115 (创建于 11/15/2025) [12/26/2025 02:00:04] 删除过期快照: snap-backup-20251110 (创建于 11/10/2025) [12/26/2025 02:00:05] 删除空资源组: rg-test-deploy-20251201 [12/26/2025 02:00:07] 清理完成。删除快照: 3 个,删除空资源组: 1 个,耗时: 00:00:07
# 队列触发器处理日志 收到队列消息,操作类型: restart-vm 重启虚拟机: vm-api-02 in rg-production 已发送重启指令
收到队列消息,操作类型: send-notification 发送通知: 扩容完成通知 通知已发送
收到队列消息,操作类型: unknown-action 警告: 未知的操作类型: unknown-action
|
部署、监控与错误处理最佳实践
函数开发完成后,需要部署到 Azure 并建立完善的监控和错误处理机制。以下脚本展示了使用 PowerShell 完成函数应用的创建、代码部署、日志查询以及结构化错误处理的完整流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
|
$ResourceGroup = 'rg-func-demo' $Location = 'eastasia' $FuncAppName = 'func-ps-automation-2025' $StorageAccount = "stpsfunc$(Get-Random -Maximum 9999)"
New-AzResourceGroup -Name $ResourceGroup -Location $Location -Force
New-AzStorageAccount ` -ResourceGroupName $ResourceGroup ` -Name $StorageAccount ` -Location $Location ` -SkuName Standard_LRS ` -Kind StorageV2
$plan = New-AzAppServicePlan ` -ResourceGroupName $ResourceGroup ` -Name "$FuncAppName-plan" ` -Location $Location ` -Tier Consumption ` -NumberofWorkers 1
New-AzFunctionApp ` -ResourceGroupName $ResourceGroup ` -Name $FuncAppName ` -StorageAccountName $StorageAccount ` -Runtime PowerShell ` -RuntimeVersion '7.4' ` -PlanName $plan.ServerFarmName ` -FunctionsVersion 4 ` -ManagedIdentity SystemAssigned
$identity = Get-AzWebApp -ResourceGroupName $ResourceGroup -Name $FuncAppName $principalId = $identity.Identity.PrincipalId New-AzRoleAssignment ` -ObjectId $principalId ` -RoleDefinitionName 'Reader' ` -Scope "/subscriptions/$((Get-AzContext).Subscription.Id)"
Publish-AzWebApp ` -ResourceGroupName $ResourceGroup ` -Name $FuncAppName ` -ArchivePath (Resolve-Path './PsFuncDemo.zip')
Write-Host "函数应用已部署: $FuncAppName"
$query = @" AppTraces | where AppRoleName == '$FuncAppName' | where TimeGenerated > ago(1h) | project TimeGenerated, MessageSeverity, Message | order by TimeGenerated desc "@
$results = Invoke-AzOperationalInsightsQuery ` -WorkspaceId $env:LOG_ANALYTICS_WORKSPACE_ID ` -Query $query
$results.Results | Format-Table -AutoSize
$stats = Get-AzFunctionApp ` -ResourceGroupName $ResourceGroup ` -Name $FuncAppName
Write-Host "函数应用状态: $($stats.State)" Write-Host "运行时版本: $($stats.SiteConfig.PowerShellVersion)"
|
接下来在函数代码中加入结构化错误处理和重试逻辑。以下是 profile.ps1 中的全局错误处理配置,以及一个带重试机制的辅助函数:
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
|
$ErrorActionPreference = 'Stop'
$ExecutionContext.SessionState.Module.OnRemove = { Write-Host "函数执行结束,清理资源" }
function Invoke-WithRetry { [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock,
[int]$MaxRetries = 3, [int]$DelaySeconds = 5, [string]$OperationName = '操作' )
$attempt = 0 while ($attempt -lt $MaxRetries) { $attempt++ try { $result = & $ScriptBlock Write-Host "[$OperationName] 第 $attempt 次尝试成功" return $result } catch { $errorMessage = $_.Exception.Message Write-Warning "[$OperationName] 第 $attempt 次尝试失败: $errorMessage"
if ($attempt -ge $MaxRetries) { Write-Error "[$OperationName] 已达最大重试次数 ($MaxRetries),操作失败" throw }
$waitTime = $DelaySeconds * [Math]::Pow(2, $attempt - 1) Write-Host "等待 $waitTime 秒后重试..." Start-Sleep -Seconds $waitTime } } }
|
执行结果示例:
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
| ResourceGroupName : rg-func-demo Location : eastasia ProvisioningState : Succeeded
StorageAccountName : stpsfunc4821 Kind : StorageV2 Sku : Standard_LRS
Name : func-ps-automation-2025 State : Running Runtime : PowerShell RuntimeVersion : 7.4 FunctionsVersion : 4 ManagedIdentity : SystemAssigned
函数应用已部署: func-ps-automation-2025
TimeGenerated MessageSeverity Message -------------- --------------- ------- 2025-12-26T02:00:07Z Information 清理完成。删除快照: 3 个,删除空资源组: 1 个,耗时: 00:00:07 2025-12-26T02:00:05Z Information 删除空资源组: rg-test-deploy-20251201 2025-12-26T02:00:03Z Information 删除过期快照: snap-backup-20251120 (创建于 11/20/2025) 2025-12-26T01:30:12Z Information 收到队列消息,操作类型: restart-vm
函数应用状态: Running 运行时版本: 7.4
[查询资源组] 第 1 次尝试失败: Resource group 'rg-production' not found. 等待 5 秒后重试... [查询资源组] 第 2 次尝试失败: Resource group 'rg-production' not found. 等待 10 秒后重试... [查询资源组] 第 3 次尝试成功
|
注意事项
使用 Consumption 计划控制成本:开发和测试阶段使用 Consumption(消耗量)计划,按实际执行时间和调用次数计费,空闲时不收费。只有当函数需要长时间运行(超过 10 分钟)或需要稳定的低延迟响应时,才考虑升级到 Premium 计划。
启用托管标识替代密钥认证:为函数应用开启系统分配的托管标识,并通过 RBAC 授予最小权限。这比在应用设置中存储服务主体密钥更安全,也避免了密钥轮换的运维负担。
注意冷启动影响:Consumption 计划的函数在长时间空闲后会经历冷启动(Cold Start),首次调用响应时间可能较长。对于对延迟敏感的场景,可以在 host.json 中配置 minimumConcurrency 或使用 Premium 计划保持常驻实例。
利用 requirements.psd1 管理模块依赖:PowerShell Functions 的模块依赖在 requirements.psd1 中声明,部署时会自动安装。不要在 run.ps1 中使用 Install-Module,这不仅会增加冷启动时间,还可能因网络问题导致函数启动失败。
为队列触发器实现幂等处理:队列消息可能被重复投递(至少一次语义),因此队列触发器函数必须具备幂等性。处理逻辑应先检查目标状态是否已经符合预期,避免重复执行产生副作用(如重复发送通知、重复创建资源)。
配置 Application Insights 收集运行日志:创建函数应用时务必关联 Application Insights 资源,它自动采集函数执行的请求追踪、异常信息和依赖项调用。配合 Log Analytics 查询,可以快速定位线上问题并设置告警规则,实现从开发到运维的全链路可观测性。