练习 - 部署 Web 应用程序

已完成

你任职于一家玩具公司,你们的网站开发团队已将网站的最新版本提交到了 Git 存储库。 现在,你已准备好更新管道以生成网站,并将其部署到 Azure 应用服务。

在此过程中,你将执行以下任务:

  • 为生成作业添加新的管道模板。
  • 更新管道以包含生成作业。
  • 添加新的版本验收测试。
  • 更新部署阶段以部署应用程序。
  • 运行管道。

为生成作业添加管道模板

你将添加一个新的作业定义,其中包含生成网站应用程序所需的步骤。

  1. 打开 Visual Studio Code。

  2. 在 deploy/pipeline-templates 文件夹中创建一个名为 build.yml 的新文件。

    Screenshot of Visual Studio Code Explorer, with the pipeline-templates folder and the 'build.yml' file shown.

  3. 将以下内容添加到 build.yml 管道模板文件中:

    jobs:
    - job: Build
      displayName: Build application and database
      pool:
        vmImage: windows-latest
    
      steps:
    
      # Build, copy, and publish the website.
      - task: DotNetCoreCLI@2
        displayName: Build publishable website
        inputs:
          command: 'publish'
          publishWebProjects: true
    
      - task: CopyFiles@2
        displayName: Copy publishable website
        inputs:
          sourceFolder: '$(Build.SourcesDirectory)/src/ToyCompany/ToyCompany.Website/bin'
          contents: '**/publish.zip'
          targetFolder: '$(Build.ArtifactStagingDirectory)/website'
          flattenFolders: true
    
      - task: PublishBuildArtifacts@1
        displayName: Publish website as pipeline artifact
        inputs:
          pathToPublish: '$(Build.ArtifactStagingDirectory)/website'
          artifactName: 'website'
    

    该作业运行生成步骤,将网站应用程序的源代码转换为准备在 Azure 中运行的编译文件。 然后,该作业将编译后的工件复制到临时暂存文件夹,并将其发布为管道工件。

  4. 保存对该文件所做的更改。

重命名第一个管道阶段,并添加生成作业

  1. 打开 deploy 文件夹中的 azure-pipelines.yml 文件。

  2. 修改 Lint 阶段。 将其重命名为“Build”,并添加使用你创建的 build.yml 管道模板的生成作业

    trigger:
      batch: true
      branches:
        include:
        - main
    
    pool:
      vmImage: ubuntu-latest
    
    stages:
    
    - stage: Build
      jobs:
      # Build the Visual Studio solution.
      - template: pipeline-templates/build.yml
    
      # Lint the Bicep file.
      - template: pipeline-templates/lint.yml
    
    # Deploy to the test environment.
    - template: pipeline-templates/deploy.yml
      parameters:
        environmentType: Test
    
    # Deploy to the production environment.
    - template: pipeline-templates/deploy.yml
      parameters:
        environmentType: Production
    
  3. 保存对该文件所做的更改。

更新版本验收测试文件

网站开发人员向网站添加了运行状况终结点。 此终结点检查网站是否联机,以及其能否访问数据库。 在这里,你将添加新的版本验收测试,以从部署管道调用运行状况检查。

  1. 打开 deploy 文件夹中的 Website.Tests.ps1 文件。

  2. 添加调用运行状况检查的新测试用例。 如果响应代码不是表示成功的 200,则测试用例失败。

    param(
      [Parameter(Mandatory)]
      [ValidateNotNullOrEmpty()]
      [string] $HostName
    )
    
    Describe 'Toy Website' {
    
        It 'Serves pages over HTTPS' {
          $request = [System.Net.WebRequest]::Create("https://$HostName/")
          $request.AllowAutoRedirect = $false
          $request.GetResponse().StatusCode |
            Should -Be 200 -Because "the website requires HTTPS"
        }
    
        It 'Does not serves pages over HTTP' {
          $request = [System.Net.WebRequest]::Create("http://$HostName/")
          $request.AllowAutoRedirect = $false
          $request.GetResponse().StatusCode | 
            Should -BeGreaterOrEqual 300 -Because "HTTP is not secure"
        }
    
        It 'Returns a success code from the health check endpoint' {
          $response = Invoke-WebRequest -Uri "https://$HostName/health" -SkipHttpErrorCheck
          Write-Host $response.Content
          $response.StatusCode |
            Should -Be 200 -Because "the website and configuration should be healthy"
        }
    
    }
    
  3. 保存对该文件所做的更改。

将输出添加到 Bicep 文件

你需要添加一个将网站发布到 Azure 应用服务的部署步骤,但该发布步骤需要使用该应用服务应用的名称。 在这里,你将应用名称公开为 Bicep 文件的输出。

  1. 打开 deploy 文件夹中的 main.bicep 文件。

  2. 在文件内容的末尾,添加该应用服务应用的名称作为输出。

    output appServiceAppName string = appServiceApp.name
    output appServiceAppHostName string = appServiceApp.properties.defaultHostName
    
  3. 保存对该文件所做的更改。

更新部署阶段

  1. 打开 deploy/pipeline-templates 文件夹中的 deploy.yml 文件。

  2. 在部署阶段的部署作业的定义(第 59 行附近)中,将作业配置为使用 Windows 托管代理池:

    - stage: Deploy_${{parameters.environmentType}}
      displayName: Deploy (${{parameters.environmentType}} Environment)
      jobs:
      - deployment: DeployWebsite
        displayName: Deploy website
        pool:
          vmImage: windows-latest
        variables:
        - group: ToyWebsite${{parameters.environmentType}}
        environment: ${{parameters.environmentType}}
        strategy:
    

    稍后添加的一些用以处理数据库的管道步骤将需要运行 Windows 操作系统。 可以为管道中的不同作业使用不同的代理池,以便其他作业继续使用 Ubuntu Linux 管道代理池。

  3. 在部署作业的 SaveDeploymentOutputs 步骤中,添加一个新的管道变量,其值为 Bicep 部署输出的应用名称:

    - bash: |
        echo "##vso[task.setvariable variable=appServiceAppName]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppName.value')"
        echo "##vso[task.setvariable variable=appServiceAppHostName;isOutput=true]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppHostName.value')"
      name: SaveDeploymentOutputs
      displayName: Save deployment outputs into variables
      env:
        DEPLOYMENT_OUTPUTS: $(deploymentOutputs)
    

    请注意,appServiceAppHostName 变量应用了 isOutput=true 属性,因为该变量用于版本验收测试阶段。 appServiceAppName 变量将在同一管道阶段和作业中设置和使用。 因此,它不需要 isOutput=true 设置。

  4. 在部署作业内容的末尾,添加一个新步骤以将应用部署到 Azure 应用服务:

    steps:
      - checkout: self
    
      - task: AzureResourceManagerTemplateDeployment@3
        name: DeployBicepFile
        displayName: Deploy Bicep file
        inputs:
          connectedServiceName: ToyWebsite${{parameters.environmentType}}
          deploymentName: $(Build.BuildNumber)
          location: ${{parameters.deploymentDefaultLocation}}
          resourceGroupName: $(ResourceGroupName)
          csmFile: deploy/main.bicep
          overrideParameters: >
            -environmentType $(EnvironmentType)
            -reviewApiUrl $(ReviewApiUrl)
            -reviewApiKey $(ReviewApiKey)
          deploymentOutputs: deploymentOutputs
    
      - bash: |
          echo "##vso[task.setvariable variable=appServiceAppName]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppName.value')"
          echo "##vso[task.setvariable variable=appServiceAppHostName;isOutput=true]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppHostName.value')"
        name: SaveDeploymentOutputs
        displayName: Save deployment outputs into variables
        env:
          DEPLOYMENT_OUTPUTS: $(deploymentOutputs)
    
      - task: AzureRmWebAppDeployment@4
        name: DeployWebsiteApp
        displayName: Deploy website
        inputs:
          appType: webApp
          ConnectionType: AzureRM
          azureSubscription: ToyWebsite${{parameters.environmentType}}
          ResourceGroupName: $(ResourceGroupName)
          WebAppName: $(appServiceAppName)
          Package: '$(Pipeline.Workspace)/website/publish.zip'
    

    注意

    请注意 YAML 文件的缩进,确保新部署步骤与 DeployBicepFile 步骤在同一级别缩进。 如果不确定,请从下一步的示例中复制整个 deploy.yml 文件内容。

    请注意,在管道定义中未显式下载工件。 由于使用了部署作业,Azure Pipelines 会自动下载工件。

验证 deploy.yml 文件内容,并提交更改

  1. 验证 deploy.yml 文件是否如以下示例所示:

    parameters:
    - name: environmentType
      type: string
    - name: deploymentDefaultLocation
      type: string
      default: westus3
    
    stages:
    
    - ${{ if ne(parameters.environmentType, 'Production') }}:
      - stage: Validate_${{parameters.environmentType}}
        displayName: Validate (${{parameters.environmentType}} Environment)
        jobs:
        - job: ValidateBicepCode
          displayName: Validate Bicep code
          variables:
          - group: ToyWebsite${{parameters.environmentType}}
          steps:
            - task: AzureResourceManagerTemplateDeployment@3
              name: RunPreflightValidation
              displayName: Run preflight validation
              inputs:
                connectedServiceName: ToyWebsite${{parameters.environmentType}}
                location: ${{parameters.deploymentDefaultLocation}}
                deploymentMode: Validation
                resourceGroupName: $(ResourceGroupName)
                csmFile: deploy/main.bicep
                overrideParameters: >
                  -environmentType $(EnvironmentType)
                  -reviewApiUrl $(ReviewApiUrl)
                  -reviewApiKey $(ReviewApiKey)
    
    - ${{ if eq(parameters.environmentType, 'Production') }}:
      - stage: Preview_${{parameters.environmentType}}
        displayName: Preview (${{parameters.environmentType}} Environment)
        jobs:
        - job: PreviewAzureChanges
          displayName: Preview Azure changes
          variables:
          - group: ToyWebsite${{parameters.environmentType}}
          steps:
            - task: AzureCLI@2
              name: RunWhatIf
              displayName: Run what-if
              inputs:
                azureSubscription: ToyWebsite${{parameters.environmentType}}
                scriptType: 'bash'
                scriptLocation: 'inlineScript'
                inlineScript: |
                  az deployment group what-if \
                    --resource-group $(ResourceGroupName) \
                    --template-file deploy/main.bicep \
                    --parameters environmentType=$(EnvironmentType) \
                                 reviewApiUrl=$(ReviewApiUrl) \
                                 reviewApiKey=$(ReviewApiKey)
    
    - stage: Deploy_${{parameters.environmentType}}
      displayName: Deploy (${{parameters.environmentType}} Environment)
      jobs:
      - deployment: DeployWebsite
        displayName: Deploy website
        pool:
          vmImage: windows-latest
        variables:
        - group: ToyWebsite${{parameters.environmentType}}
        environment: ${{parameters.environmentType}}
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
    
                - task: AzureResourceManagerTemplateDeployment@3
                  name: DeployBicepFile
                  displayName: Deploy Bicep file
                  inputs:
                    connectedServiceName: ToyWebsite${{parameters.environmentType}}
                    deploymentName: $(Build.BuildNumber)
                    location: ${{parameters.deploymentDefaultLocation}}
                    resourceGroupName: $(ResourceGroupName)
                    csmFile: deploy/main.bicep
                    overrideParameters: >
                      -environmentType $(EnvironmentType)
                      -reviewApiUrl $(ReviewApiUrl)
                      -reviewApiKey $(ReviewApiKey)
                    deploymentOutputs: deploymentOutputs
    
                - bash: |
                    echo "##vso[task.setvariable variable=appServiceAppName]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppName.value')"
                    echo "##vso[task.setvariable variable=appServiceAppHostName;isOutput=true]$(echo $DEPLOYMENT_OUTPUTS | jq -r '.appServiceAppHostName.value')"
                  name: SaveDeploymentOutputs
                  displayName: Save deployment outputs into variables
                  env:
                    DEPLOYMENT_OUTPUTS: $(deploymentOutputs)
    
                - task: AzureRmWebAppDeployment@4
                  name: DeployWebsiteApp
                  displayName: Deploy website
                  inputs:
                    appType: webApp
                    ConnectionType: AzureRM
                    azureSubscription: ToyWebsite${{parameters.environmentType}}
                    ResourceGroupName: $(ResourceGroupName)
                    WebAppName: $(appServiceAppName)
                    Package: '$(Pipeline.Workspace)/website/publish.zip'
    
    - stage: SmokeTest_${{parameters.environmentType}}
      displayName: Smoke Test (${{parameters.environmentType}} Environment)
      jobs:
      - job: SmokeTest
        displayName: Smoke test
        variables:
          appServiceAppHostName: $[ stageDependencies.Deploy_${{parameters.environmentType}}.DeployWebsite.outputs['DeployWebsite.SaveDeploymentOutputs.appServiceAppHostName'] ]
        steps:
          - task: PowerShell@2
            name: RunSmokeTests
            displayName: Run smoke tests
            inputs:
              targetType: inline
              script: |
                $container = New-PesterContainer `
                  -Path 'deploy/Website.Tests.ps1' `
                  -Data @{ HostName = '$(appServiceAppHostName)' }
                Invoke-Pester `
                  -Container $container `
                  -CI
    
          - task: PublishTestResults@2
            name: PublishTestResults
            displayName: Publish test results
            condition: always()
            inputs:
              testResultsFormat: NUnit
              testResultsFiles: 'testResults.xml'
    
  2. 保存对该文件所做的更改。

  3. 在 Visual Studio Code 终端中,运行以下命令来提交更改并将其推送到 Git 存储库:

    git add .
    git commit -m "Build and deploy website application"
    git push
    

运行管道

  1. 在浏览器中,转到“管道”。

  2. 选择管道的最新运行。

    Screenshot of Azure DevOps showing the pipeline run list. The latest pipeline run is highlighted.

    等待生成阶段成功完成。

    管道在运行“验证(测试环境)”阶段之前暂停,因为管道需要权限才能使用阶段引用的变量组。 你需要批准管道对变量组的访问权限,因为这是你首次在此项目中运行管道。 再次运行管道时,就无需再批准对同一变量组的访问权限。

  3. 选择“视图”。

    Screenshot of Azure DevOps showing the pipeline run paused at the Validate stage. Permission is required to continue. The View button is highlighted.

  4. 选择“允许”。

    Screenshot of Azure DevOps showing that the pipeline needs permission to use the ToyWebsiteTest variable group. The Permit button is highlighted.

  5. 选择“允许”。

    Screenshot of Azure DevOps showing the permission confirmation interface. The Permit button is highlighted.

    “验证(测试环境)”阶段已成功完成。

    管道继续运行,“部署(测试环境)”阶段成功完成。 管道然后运行“版本验收测试(测试环境)”阶段,但版本验收测试阶段失败。

  6. 选择“版本验收测试(测试环境)”阶段,打开管道日志。

    Screenshot of Azure DevOps showing the pipeline run's Smoke Test stage for the test environment. The status shows that the stage failed.

  7. 选择“运行版本验收测试”步骤,查看管道日志的关联部分。

    Screenshot of Azure DevOps showing the pipeline run log, with the output of the smoke test displayed. The JSON health test result is highlighted.

    请注意,管道日志包含运行状况检查响应。 响应表明应用程序与 Azure SQL 数据库的通信存在问题。 数据库尚未部署或配置,因此网站还无法对其进行访问。 在下一个练习中,你将修复此配置问题。