如何在VSTS构建的Azure PowerShell中并行执行Start-AzureVM?
解决Azure DevOps中PowerShell并行启动DevTest Lab虚拟机的上下文问题
你遇到的问题确实很典型:Start-Job创建的独立PowerShell进程不会继承父进程的Azure登录上下文,所以子作业里的Start-AzureRmVM会因为未授权失败。而且受限于托管代理和Azure DevOps任务并行的限制,我们可以用PowerShell Runspace池来实现并行,同时复用已有的Azure登录会话,不需要重新认证。
下面是两种可行的解决方案,优先推荐Runspace池的方式:
方案一:使用RunspacePool实现高效并行(推荐)
Runspace是PowerShell的执行环境,RunspacePool可以让我们在同一个进程内创建多个并行执行的Runspace,它们能共享父进程的Azure上下文,不需要额外登录。这种方式比Start-Job更轻量、性能更好。
修改后的脚本示例:
[cmdletbinding()] param ( [ValidateSet("Start","Stop")][string]$Action, $labName = "DevTestLab", $labResourceGroup = "DevTestLabRG" ) if ($Action -eq "Start") { Write-Verbose "Starting the domain controller first" # 启动域控制器并等待就绪 $dcVM = Get-AzureRmResource | Where-Object { $_.Name -match "dc" -and $_.ResourceType -eq "Microsoft.Compute/virtualMachines" -and $_.ResourceGroupName -match $labResourceGroup } $dcVM | Start-AzureRmVM # 等待DC启动完成(可选,但建议加,避免其他VM依赖DC时出问题) do { Start-Sleep -Seconds 10 $dcStatus = Get-AzureRmVM -Name $dcVM.Name -ResourceGroupName $dcVM.ResourceGroupName -Status } while ($dcStatus.Statuses.Code -notcontains "PowerState/running") Write-Verbose "Starting other machines using RunspacePool" $nonDCVMs = Get-AzureRmResource | Where-Object { $_.Name -notmatch "dc" -and $_.ResourceType -eq "Microsoft.Compute/virtualMachines" -and $_.ResourceGroupName -match $labResourceGroup } # 创建Runspace池,设置并行度(根据代理性能调整,比如4) $runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, 4) $runspacePool.Open() $jobs = @() foreach ($vm in $nonDCVMs) { $ps = [PowerShell]::Create().AddScript({ param($vmObj) Start-AzureRmVM -Name $vmObj.Name -ResourceGroupName $vmObj.ResourceGroupName }).AddArgument($vm) $ps.RunspacePool = $runspacePool $jobs += [PSCustomObject]@{ PowerShell = $ps Result = $ps.BeginInvoke() } } # 等待所有Runspace完成 foreach ($job in $jobs) { $job.PowerShell.EndInvoke($job.Result) $job.PowerShell.Dispose() } # 清理Runspace池 $runspacePool.Close() $runspacePool.Dispose() }
关键说明:
- RunspacePool在同一个进程内运行,直接复用父进程已有的Azure登录上下文,不需要额外认证
- 可以通过
CreateRunspacePool的参数控制并行数量(第一个是最小Runspace数,第二个是最大),避免代理资源耗尽 - 相比
Start-Job,Runspace的开销更小,执行效率更高
方案二:在Start-Job中重新导入Azure上下文
如果不想改用Runspace,也可以在父进程中导出Azure登录上下文,然后在子作业里导入。这种方式需要处理临时文件,但实现起来更简单:
修改后的脚本片段:
if ($Action -eq "Start") { Write-Verbose "Starting the domain controller first" # 启动DC逻辑不变... # 导出当前Azure上下文到临时文件 $contextPath = Join-Path $env:TEMP "azure_context.json" Save-AzureRmContext -Path $contextPath -Force Write-Verbose "Starting other machines in the lab as background jobs" foreach ($AzureRMResource in Get-AzureRmResource | Where-Object { $_.Name -notmatch "dc" -and $_.ResourceType -eq "Microsoft.Compute/virtualMachines" -and $_.ResourceGroupName -match $labResourceGroup } ) { Start-Job { param($vmResource, $ctxPath) # 导入Azure上下文 Import-AzureRmContext -Path $ctxPath Start-AzureRmVM -Name $vmResource.Name -ResourceGroupName $vmResource.ResourceGroupName } -ArgumentList $AzureRMResource, $contextPath } # 等待所有作业完成并清理 Get-Job | Wait-Job Get-Job | Receive-Job Get-Job | Remove-Job # 删除临时上下文文件 Remove-Item $contextPath -Force }
关键说明:
Save-AzureRmContext会把当前登录的Azure会话保存到文件,子作业通过Import-AzureRmContext加载- 注意临时文件的权限,托管代理的TEMP目录是安全的,作业完成后要删除避免残留
- 这种方式每个子作业都会重新加载上下文,开销比Runspace大,适合VM数量不多的场景
额外建议
- 不管用哪种方式,都建议给DC添加启动后的等待逻辑(比如检查PowerState),避免其他VM依赖DC但DC还未完全就绪的问题
- 可以根据托管代理的CPU/内存情况调整并行数量,避免资源过载
- 如果使用的是较新的Az模块(代替AzureRm),只需要把
AzureRm相关的命令替换成Az即可,逻辑完全一致
内容的提问来源于stack exchange,提问作者Mark Allison
相关产品推荐
相关产品推荐

