使用 PowerShell 脚本和并行 ForEach-Object 执行的 Azure DevOps Azure CLI 任务:失败时无输出
Azure DevOps Azure CLI task with PowerShell script and parallel ForEach-Object execution: no output on failure
为了快速扩展函数应用程序,我们希望能够通过 IaC 部署它们,然后将代码包部署到它上面。不幸的是,这在 Azure DevOps 中使用 YAML 管道是不可能的,所以我不得不求助于使用 Azure CLI。
下面是我想出的 PowerShell 脚本,用于将代码部署到我事先通过 Terraform 部署的函数应用程序池中。为了加快速度,我打开了 ForEach-Object
循环的并行处理,因为单个实例之间没有依赖关系。这在一定程度上也能正常工作,但由于 Azure CLI 的古怪之处,我遇到了麻烦。将非错误信息写入 StdErr 似乎是设计使然。这与其他一些奇怪的行为相结合导致了以下情况:
- 运行 sequentially 通常可以完美运行,如果出现问题,我会看到任何错误输出。我也不需要设置
powerShellErrorActionPreference: 'continue'
。这当然会显着减慢部署速度。
- 运行 并行失败 always 没有设置
powerShellErrorActionPreference: 'continue'
。失败的原因没有输出到控制台。即使没有真正的错误发生,这似乎也会发生,因为 continue
也没有错误输出到控制台。如果管道在实际错误的情况下失败(应该通过检查 ChildJobs
的状态来处理 - 但事实并非如此。 ,这将不是问题
所以我进退两难。有没有人看到我的实施中的缺陷?非常感谢任何建议。
- task: AzureCLI@2
displayName: 'Functions deployment'
env:
AZURE_CORE_ONLY_SHOW_ERRORS: 'True'
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
ARM_CLIENT_ID: $(AzureApplicationId)
ARM_CLIENT_SECRET: $(AzureApplicationSecret)
ARM_SUBSCRIPTION_ID: $(AzureSubscriptionId)
ARM_TENANT_ID: $(AzureTenantId)
inputs:
azureSubscription: 'MySubscription'
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
Write-Output -InputObject "INFO: Get Function App names"
$appNames = terragrunt output -json all_functionapp_names | ConvertFrom-Json
Write-Output -InputObject "INFO: Loop over Function Apps"
$jobs = $appNames | ForEach-Object -Parallel {
$name = $_
try
{
Write-Output -InputObject "INFO: $name`: start slot"
az functionapp start --resource-group $(ResourceGroup) --name "$name" --slot Stage --verbose
Write-Output -InputObject "INFO: $name`: deploy into slot"
az functionapp deploy --resource-group $(ResourceGroup) --name "$name" --slot Stage --src-path "$(System.ArtifactsDirectory)/drop/MyCodePackage.zip" --type zip --verbose
Write-Output -InputObject "INFO: $name`: deploy app settings"
az functionapp config appsettings set --resource-group $(ResourceGroup) --name "$name" --slot Stage --settings "@$(Build.ArtifactStagingDirectory)/appsettings.json" --verbose
Write-Output -InputObject "INFO: $name`: swap slot with production"
az functionapp deployment slot swap --resource-group $(ResourceGroup) --name "$name" --slot Stage --action swap --verbose
}
catch
{
Write-Output -InputObject "ERROR: $name`: An error occured during deployment"
Write-Output -InputObject ($_.Exception | Format-List -Force)
}
finally
{
try
{
Write-Output -InputObject "INFO: $name`: stop slot"
az functionapp stop --resource-group $(ResourceGroup) --name "$name" --slot Stage --verbose
}
catch
{
Write-Output -InputObject "ERROR: $name`: could not stop slot"
}
}
} -AsJob
[int]$pollingInterval = 10
[int]$elapsedSeconds = 0
while ($jobs.State -eq "Running") {
$jobs.ChildJobs | ForEach-Object {
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject "INFO: $($_.Name) output [$($elapsedSeconds)s]"
Write-Output -InputObject "---------------------------------"
$_ | Receive-Job
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject ""
}
$elapsedSeconds += $pollingInterval
[Threading.Thread]::Sleep($pollingInterval * 1000)
}
$jobs.ChildJobs | Where-Object { $_.JobStateInfo.State -eq "Failed" } | ForEach-Object {
Write-Output -InputObject "ERROR: At least one of the deployments failed with the following reason:"
Write-Output -InputObject $_.JobStateInfo.Reason
}
if ($jobs.State -eq "Failed")
{
exit 1
}
else
{
exit 0
}
powerShellErrorActionPreference: 'continue'
workingDirectory: './infrastructure/environments/$(TerraFormEnvironmentName)'
编辑 1
要从 ChildJobs
获取所有输出,我必须像这样更改代码:
[int]$pollingInterval = 10
[int]$elapsedSeconds = 0
$lastResultsRead = false
while ($jobs.State -eq "Running" -or !$lastResultsRead)
{
$lastResultsRead = $jobs.State -ne "Running"
$jobs.ChildJobs | ForEach-Object {
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject "INFO: $($_.Name) output [$($elapsedSeconds)s]"
Write-Output -InputObject "---------------------------------"
$_ | Receive-Job
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject ""
}
$elapsedSeconds += $pollingInterval
if (!$lastResultsRead)
{
[Threading.Thread]::Sleep($pollingInterval * 1000)
}
希望这可以帮助每个想要实现类似目标的人。
看来谜底已经解开了
TLDR;
如果您想要正确的错误处理,请从所有 Azure CLI 调用中删除 --verbose
,因为即使在设置环境变量 AZURE_CORE_ONLY_SHOW_ERRORS
.
说明
我通过向该脚本添加不相关的功能偶然发现了解决方案,并注意到在某些情况下未收集 ChildJobs
的最后输出。我最初认为这是 Azure DevOps 任务的一个怪癖,但发现当我在 VSCode.
中本地调试输出时也会发生这种情况
这导致我为 while
循环添加另一个条件,以确保为我提供最终输出。我将相应地更新初始 post 中的脚本。最后配备了 ChildJobs
中发生的事情的全貌,我设置了一个单独的测试管道,我将 运行 不同的测试用例来找到罪魁祸首。很快我注意到带走 --verbose
可以防止任务失败。无论是否设置 AZURE_CORE_ONLY_SHOW_ERRORS
,都会发生这种情况。所以我试了一下 --only-show-errors
选项,它应该与环境变量有相同的结果,尽管只是在一次 Azure CLI 调用中。由于我现在可以使用完整的输出,所以我终于可以看到 --verbose
和 --only-show-errors
不能结合使用的消息。就这样解决了。 --verbose
不得不走了。它所添加的只是命令 运行 多长时间的信息。我想我们可以没有它。
附带说明:与此同时,我发现 ForEach-Object -Parallel {} -AsJob
正在大量使用 PowerShell 运行 空格。这意味着无法以典型方式从 VSCode 中调试它。我发现了一个视频,在这种情况下可能会有所帮助:https://www.youtube.com/watch?v=O-dksknPQBw
我希望这个回答能帮助那些因同样的 st运行ge 行为而绊倒的人。编码愉快。
为了快速扩展函数应用程序,我们希望能够通过 IaC 部署它们,然后将代码包部署到它上面。不幸的是,这在 Azure DevOps 中使用 YAML 管道是不可能的,所以我不得不求助于使用 Azure CLI。
下面是我想出的 PowerShell 脚本,用于将代码部署到我事先通过 Terraform 部署的函数应用程序池中。为了加快速度,我打开了 ForEach-Object
循环的并行处理,因为单个实例之间没有依赖关系。这在一定程度上也能正常工作,但由于 Azure CLI 的古怪之处,我遇到了麻烦。将非错误信息写入 StdErr 似乎是设计使然。这与其他一些奇怪的行为相结合导致了以下情况:
- 运行 sequentially 通常可以完美运行,如果出现问题,我会看到任何错误输出。我也不需要设置
powerShellErrorActionPreference: 'continue'
。这当然会显着减慢部署速度。 - 运行 并行失败 always 没有设置
powerShellErrorActionPreference: 'continue'
。失败的原因没有输出到控制台。即使没有真正的错误发生,这似乎也会发生,因为continue
也没有错误输出到控制台。如果管道在实际错误的情况下失败(应该通过检查ChildJobs
的状态来处理 - 但事实并非如此。 ,这将不是问题
所以我进退两难。有没有人看到我的实施中的缺陷?非常感谢任何建议。
- task: AzureCLI@2
displayName: 'Functions deployment'
env:
AZURE_CORE_ONLY_SHOW_ERRORS: 'True'
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
ARM_CLIENT_ID: $(AzureApplicationId)
ARM_CLIENT_SECRET: $(AzureApplicationSecret)
ARM_SUBSCRIPTION_ID: $(AzureSubscriptionId)
ARM_TENANT_ID: $(AzureTenantId)
inputs:
azureSubscription: 'MySubscription'
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
Write-Output -InputObject "INFO: Get Function App names"
$appNames = terragrunt output -json all_functionapp_names | ConvertFrom-Json
Write-Output -InputObject "INFO: Loop over Function Apps"
$jobs = $appNames | ForEach-Object -Parallel {
$name = $_
try
{
Write-Output -InputObject "INFO: $name`: start slot"
az functionapp start --resource-group $(ResourceGroup) --name "$name" --slot Stage --verbose
Write-Output -InputObject "INFO: $name`: deploy into slot"
az functionapp deploy --resource-group $(ResourceGroup) --name "$name" --slot Stage --src-path "$(System.ArtifactsDirectory)/drop/MyCodePackage.zip" --type zip --verbose
Write-Output -InputObject "INFO: $name`: deploy app settings"
az functionapp config appsettings set --resource-group $(ResourceGroup) --name "$name" --slot Stage --settings "@$(Build.ArtifactStagingDirectory)/appsettings.json" --verbose
Write-Output -InputObject "INFO: $name`: swap slot with production"
az functionapp deployment slot swap --resource-group $(ResourceGroup) --name "$name" --slot Stage --action swap --verbose
}
catch
{
Write-Output -InputObject "ERROR: $name`: An error occured during deployment"
Write-Output -InputObject ($_.Exception | Format-List -Force)
}
finally
{
try
{
Write-Output -InputObject "INFO: $name`: stop slot"
az functionapp stop --resource-group $(ResourceGroup) --name "$name" --slot Stage --verbose
}
catch
{
Write-Output -InputObject "ERROR: $name`: could not stop slot"
}
}
} -AsJob
[int]$pollingInterval = 10
[int]$elapsedSeconds = 0
while ($jobs.State -eq "Running") {
$jobs.ChildJobs | ForEach-Object {
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject "INFO: $($_.Name) output [$($elapsedSeconds)s]"
Write-Output -InputObject "---------------------------------"
$_ | Receive-Job
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject ""
}
$elapsedSeconds += $pollingInterval
[Threading.Thread]::Sleep($pollingInterval * 1000)
}
$jobs.ChildJobs | Where-Object { $_.JobStateInfo.State -eq "Failed" } | ForEach-Object {
Write-Output -InputObject "ERROR: At least one of the deployments failed with the following reason:"
Write-Output -InputObject $_.JobStateInfo.Reason
}
if ($jobs.State -eq "Failed")
{
exit 1
}
else
{
exit 0
}
powerShellErrorActionPreference: 'continue'
workingDirectory: './infrastructure/environments/$(TerraFormEnvironmentName)'
编辑 1
要从 ChildJobs
获取所有输出,我必须像这样更改代码:
[int]$pollingInterval = 10
[int]$elapsedSeconds = 0
$lastResultsRead = false
while ($jobs.State -eq "Running" -or !$lastResultsRead)
{
$lastResultsRead = $jobs.State -ne "Running"
$jobs.ChildJobs | ForEach-Object {
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject "INFO: $($_.Name) output [$($elapsedSeconds)s]"
Write-Output -InputObject "---------------------------------"
$_ | Receive-Job
Write-Output -InputObject "---------------------------------"
Write-Output -InputObject ""
}
$elapsedSeconds += $pollingInterval
if (!$lastResultsRead)
{
[Threading.Thread]::Sleep($pollingInterval * 1000)
}
希望这可以帮助每个想要实现类似目标的人。
看来谜底已经解开了
TLDR;
如果您想要正确的错误处理,请从所有 Azure CLI 调用中删除 --verbose
,因为即使在设置环境变量 AZURE_CORE_ONLY_SHOW_ERRORS
.
说明
我通过向该脚本添加不相关的功能偶然发现了解决方案,并注意到在某些情况下未收集 ChildJobs
的最后输出。我最初认为这是 Azure DevOps 任务的一个怪癖,但发现当我在 VSCode.
这导致我为 while
循环添加另一个条件,以确保为我提供最终输出。我将相应地更新初始 post 中的脚本。最后配备了 ChildJobs
中发生的事情的全貌,我设置了一个单独的测试管道,我将 运行 不同的测试用例来找到罪魁祸首。很快我注意到带走 --verbose
可以防止任务失败。无论是否设置 AZURE_CORE_ONLY_SHOW_ERRORS
,都会发生这种情况。所以我试了一下 --only-show-errors
选项,它应该与环境变量有相同的结果,尽管只是在一次 Azure CLI 调用中。由于我现在可以使用完整的输出,所以我终于可以看到 --verbose
和 --only-show-errors
不能结合使用的消息。就这样解决了。 --verbose
不得不走了。它所添加的只是命令 运行 多长时间的信息。我想我们可以没有它。
附带说明:与此同时,我发现 ForEach-Object -Parallel {} -AsJob
正在大量使用 PowerShell 运行 空格。这意味着无法以典型方式从 VSCode 中调试它。我发现了一个视频,在这种情况下可能会有所帮助:https://www.youtube.com/watch?v=O-dksknPQBw
我希望这个回答能帮助那些因同样的 st运行ge 行为而绊倒的人。编码愉快。