【问题标题】:Azure DevOps Azure CLI task with PowerShell script and parallel ForEach-Object execution: no output on failure带有 PowerShell 脚本和并行 ForEach-Object 执行的 Azure DevOps Azure CLI 任务:失败时没有输出
【发布时间】:2021-10-29 19:51:53
【问题描述】:

为了快速扩展功能应用程序,我们希望能够通过 IaC 部署它们,然后在其上部署代码包。不幸的是,这在 Azure DevOps 中无法通过 YAML 管道动态实现,因此我不得不求助于 Azure CLI。

您可以在下面看到我想出的 PowerShell 脚本,用于将代码部署到我事先通过 Terraform 部署的函数应用程序池中。为了加快速度,我打开了ForEach-Object 循环的并行处理,因为单个实例之间没有依赖关系。这在一定程度上也可以正常工作,但由于 Azure CLI 的古怪,我遇到了麻烦。将非错误信息写入 StdErr 似乎是设计使然。这与其他一些奇怪的行为相结合会导致以下情况:

  • 按顺序运行通常可以完美运行,如果出现问题,我会看到任何错误输出。我也不需要设置powerShellErrorActionPreference: 'continue'。这当然会大大减慢部署速度。
  • 如果没有设置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)
          }

希望这可以帮助每个想要实现类似目标的人。

【问题讨论】:

  • 脚本很好用。我有两个问题?您是否尝试在本地运行它?您是否考虑过将运行拆分为单独的作业,以便每个作业处理一个功能?
  • @KrzysztofMadej 谢谢。是的,其实我是先在本地开发脚本的。我刚刚获取了当前版本并再次在本地运行。仍然按预期工作。没有错误。将其拆分为单独的工作对我的情况有何帮助?当我们谈论大约 2-6 次并行执行时,多个作业比一个具有多个子作业的作业有什么优势吗?
  • 单独的工作,实际上意味着单独的机器 - 如果 azure cli 调用之间存在奇怪的交互,这可能会有所帮助 - 这不应该发生,但我们就在我们所在的地方。
  • @KrzysztofMadej 正如我试图在我的帖子中描述的那样,这是不可能的。我首选的实现方式是创建一个 YAML 数组,其中每个部署都作为单独的条目。但为此,Azure Pipelines 需要预先了解这些元素。无法动态创建 YAML 数组。因此,管道中的单独作业是不可能的。此外,部署本身现在工作正常。只是 Azure CLI 的报告与 Azure CLI 任务如何处理导致所述问题的 StdErr 输出相结合。
  • 好奇:代理上安装了哪个版本的 PowerShell Core?我相信-Parallel 是在 PowerShell 7.0 Preview 3(及更高版本)中添加的。

标签: powershell azure-devops azure-cli


【解决方案1】:

看来谜团已经解开了。

TLDR;

如果您想要正确处理错误,请从所有 Azure CLI 调用中删除 --verbose,因为详细输出总是写入 StdErr,即使在设置环境变量 AZURE_CORE_ONLY_SHOW_ERRORS 时也是如此。

说明

我通过向该脚本添加一个不相关的功能偶然发现了解决方案,并注意到在某些情况下,ChildJobs 的最后一个输出没有被收集。我最初认为这是 Azure DevOps 任务的一个怪癖,但发现当我在 VSCode 中本地调试输出时也会发生这种情况。

这导致我为while 循环添加另一个条件,以确保为我提供最终输出。我将相应地在我的初始帖子中更新脚本。最后配备了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

我希望这个答案可以帮助其他遇到同样奇怪行为的人。编码愉快。

【讨论】:

    猜你喜欢
    • 2020-08-27
    • 1970-01-01
    • 2021-02-03
    • 1970-01-01
    • 2020-07-19
    • 2020-10-07
    • 1970-01-01
    • 1970-01-01
    • 2020-03-02
    相关资源
    最近更新 更多