연습 - 파이프라인에 테스트 스테이지 추가

완료됨

장난감 회사의 보안 팀에서 HTTPS를 통해서만 웹 사이트에 액세스할 수 있는지 확인해 달라고 여러분에게 요청했습니다. 이 연습에서는 보안 팀의 요구 사항을 확인하는 스모크 테스트를 실행하도록 파이프라인을 구성합니다.

프로세스 중에 다음을 수행합니다.

  • 리포지토리에 테스트 스크립트를 추가합니다.
  • 파이프라인 정의를 업데이트하여 테스트 스테이지를 추가합니다.
  • 파이프라인을 실행하고 테스트가 실패하는지 확인합니다.
  • Bicep 파일을 수정하고 파이프라인이 성공적으로 실행되는지 확인합니다.

테스트 스크립트 추가

여기서는 HTTPS를 사용하면 웹 사이트에 액세스할 수 있고, 안전하지 않은 HTTP 프로토콜을 사용하면 액세스할 수 없는지 확인하는 테스트 스크립트를 추가합니다.

  1. Visual Studio Code에서 deploy 폴더에 Website.Tests.ps1이라는 새 파일을 만듭니다.

    Screenshot of Visual Studio Code Explorer, with the deploy folder and the test file shown.

  2. 다음 테스트 코드를 파일에 붙여넣습니다.

    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"
        }
    
    }
    

    이 코드는 Pester 테스트 파일입니다. $HostName이라는 매개 변수가 필요합니다. 호스트 이름에 대해 두 개의 테스트를 실행합니다.

    • HTTPS를 통해 웹 사이트에 연결해 봅니다. 서버가 연결 성공을 나타내는 200~299 사이의 HTTP 응답 상태 코드로 응답하면 테스트를 통과합니다.
    • HTTP를 통해 웹 사이트에 연결해 봅니다. 서버가 300 이상의 HTTP 응답 상태 코드로 응답하면 테스트를 통과합니다.

    이 연습에서는 테스트 파일의 세부 정보와 작동 방식을 꼭 이해할 필요는 없습니다. 관심이 있는 분들을 위해 요약 단원에 링크를 제공합니다.

Bicep 파일의 출력을 스테이지 출력 변수로 게시

이전 단계에서 만든 테스트 스크립트에는 테스트할 호스트 이름이 필요합니다. Bicep 파일에는 이미 출력이 포함되어 있지만, 스모크 테스트에 사용하려면 출력을 스테이지 출력 변수로 게시해야 합니다.

  1. Visual Studio Code의 deploy 폴더에서 azure-pipelines.yml 파일을 엽니다.

  2. 배포 스테이지에서 배포 단계를 업데이트하여 출력을 변수에 게시합니다.

    - task: AzureResourceManagerTemplateDeployment@3
      name: DeployBicepFile
      displayName: Deploy Bicep file
      inputs:
        connectedServiceName: $(ServiceConnectionName)
        deploymentName: $(Build.BuildNumber)
        location: $(deploymentDefaultLocation)
        resourceGroupName: $(ResourceGroupName)
        csmFile: deploy/main.bicep
        overrideParameters: >
          -environmentType $(EnvironmentType)
        deploymentOutputs: deploymentOutputs
    

    이제 배포 프로세스는 이전과 동일한 작업을 계속 사용하지만 배포의 출력은 deploymentOutputs라는 파이프라인 변수에 저장됩니다. 출력 변수의 형식은 JSON으로 지정됩니다.

  3. JSON 형식 출력을 파이프라인 변수로 변환하려면 배포 단계 아래에 다음 스크립트 단계를 추가합니다.

    - bash: |
        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)
    

    배포가 성공적으로 완료되면 스크립트는 Bicep 배포의 각 출력 값에 액세스합니다. 스크립트는 jq 도구를 사용하여 JSON 출력의 관련 부분에 액세스합니다. 그런 다음, Bicep 배포 출력과 동일한 이름의 스테이지 출력 변수에 값이 게시됩니다.

    참고

    Pester 및 jq는 Azure Pipelines의 Microsoft에서 호스트된 에이전트에 미리 설치되어 있습니다. 스크립트 단계에서 사용하기 위해 특별한 작업을 수행할 필요가 없습니다.

  4. 파일을 저장합니다.

파이프라인에 스모크 테스트 스테이지 추가

이제 테스트를 실행하는 스모크 테스트 스테이지를 추가할 수 있습니다.

  1. 파일 맨 아래에 SmokeTest 스테이지에 대한 다음 정의를 추가합니다.

    - stage: SmokeTest
      jobs:
      - job: SmokeTest
        displayName: Smoke test
        variables:
          appServiceAppHostName: $[ stageDependencies.Deploy.DeployWebsite.outputs['DeployWebsite.SaveDeploymentOutputs.appServiceAppHostName'] ]
    

    이 코드는 스테이지 및 작업을 정의합니다. 또한 이 단계에서는 appServiceAppHostName이라는 작업에 변수를 만듭니다. 이 변수는 이전 섹션에서 만든 출력 변수에서 해당 값을 가져옵니다.

  2. 파일 맨 아래에서 SmokeTest 스테이지에 다음 단계 정의를 추가합니다.

    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
    

    이 단계에서는 Pester 테스트 도구를 사용하여 앞에서 작성한 테스트 스크립트를 실행하는 PowerShell 스크립트를 실행합니다.

  3. 파일 맨 아래에서 SmokeTest 스테이지에 다음 단계 정의를 추가합니다.

    - task: PublishTestResults@2
      name: PublishTestResults
      displayName: Publish test results
      condition: always()
      inputs:
        testResultsFormat: NUnit
        testResultsFiles: 'testResults.xml'
    

    이 단계에서는 Pester에서 작성하는 테스트 결과 파일을 가져와서 파이프라인 테스트 결과로 게시합니다. 결과가 어떻게 표시되는지 잠시 후에 확인할 수 있습니다.

    단계 정의에는 condition: always()가 포함됩니다. 이 조건은 이전 단계가 실패하더라도 항상 테스트 결과를 게시해야 한다고 Azure Pipelines에 알립니다. 테스트가 실패하면 테스트 단계가 실패하게 되고, 일반적으로 단계가 실패하면 파이프라인이 실행을 중지하기 때문에 이 조건이 중요합니다.

  4. 파일을 저장합니다.

파이프라인 정의 확인 및 커밋

  1. azure-pipelines.yml 파일이 다음 코드와 같은지 확인합니다.

    trigger:
      batch: true
      branches:
        include:
        - main
    
    pool:
      vmImage: ubuntu-latest
    
    variables:
      - name: deploymentDefaultLocation
        value: westus3
    
    stages:
    
    - stage: Lint
      jobs:
      - job: LintCode
        displayName: Lint code
        steps:
          - script: |
              az bicep build --file deploy/main.bicep
            name: LintBicepCode
            displayName: Run Bicep linter
    
    - stage: Validate
      jobs:
      - job: ValidateBicepCode
        displayName: Validate Bicep code
        steps:
          - task: AzureResourceManagerTemplateDeployment@3
            name: RunPreflightValidation
            displayName: Run preflight validation
            inputs:
              connectedServiceName: $(ServiceConnectionName)
              location: $(deploymentDefaultLocation)
              deploymentMode: Validation
              resourceGroupName: $(ResourceGroupName)
              csmFile: deploy/main.bicep
              overrideParameters: >
                -environmentType $(EnvironmentType)
    
    - stage: Preview
      jobs:
      - job: PreviewAzureChanges
        displayName: Preview Azure changes
        steps:
          - task: AzureCLI@2
            name: RunWhatIf
            displayName: Run what-if
            inputs:
              azureSubscription: $(ServiceConnectionName)
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                az deployment group what-if \
                  --resource-group $(ResourceGroupName) \
                  --template-file deploy/main.bicep \
                  --parameters environmentType=$(EnvironmentType)
    
    - stage: Deploy
      jobs:
      - deployment: DeployWebsite
        displayName: Deploy website
        environment: Website
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
                - task: AzureResourceManagerTemplateDeployment@3
                  name: DeployBicepFile
                  displayName: Deploy Bicep file
                  inputs:
                    connectedServiceName: $(ServiceConnectionName)
                    deploymentName: $(Build.BuildNumber)
                    location: $(deploymentDefaultLocation)
                    resourceGroupName: $(ResourceGroupName)
                    csmFile: deploy/main.bicep
                    overrideParameters: >
                      -environmentType $(EnvironmentType)
                    deploymentOutputs: deploymentOutputs
    
                - bash: |
                    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)
    
    - stage: SmokeTest
      jobs:
      - job: SmokeTest
        displayName: Smoke test
        variables:
          appServiceAppHostName: $[ stageDependencies.Deploy.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. Visual Studio Code 터미널에서 다음 명령을 실행하여 변경 내용을 커밋하고 Git 리포지토리에 푸시합니다.

    git add .
    git commit -m "Add test stage"
    git push
    

파이프라인을 실행하고 테스트 결과 검토

  1. 브라우저에서 Pipelines로 이동합니다.

  2. 가장 최근에 실행한 파이프라인을 선택합니다.

    파이프라인이 린팅, 유효성 검사미리 보기 스테이지를 완료할 때까지 기다립니다. Azure Pipelines는 자동으로 페이지를 최신 상태로 업데이트하지만 가끔 페이지를 새로 고치는 것이 좋습니다.

  3. 검토 단추를 선택한 다음, 승인을 선택합니다.

    파이프라인 실행이 완료될 때까지 기다립니다.

  4. 배포 스테이지가 성공적으로 완료됩니다. SmokeTest 스테이지가 오류와 함께 완료됩니다.

    Screenshot of the Azure DevOps interface that shows the pipeline run stages. The SmokeTest stage reports failure.

  5. 테스트 탭을 선택합니다.

    Screenshot of the Azure DevOps interface that shows the pipeline run, with the Tests tab highlighted.

  6. 테스트 요약에서는 두 개의 테스트가 실행되었음을 볼 수 있습니다. 하나는 통과하고 하나는 실패했습니다. 실패한 테스트는 Toy Website.Does not serve pages over HTTP로 표시됩니다.

    Screenshot of the Azure DevOps interface that shows the pipeline run's test results, with the failed test highlighted.

    이 텍스트는 웹 사이트가 보안 팀의 요구 사항을 충족하도록 올바르게 구성되지 않았음을 나타냅니다.

Bicep 파일 업데이트

Bicep 정의가 보안 팀의 요구 사항을 충족하지 않는다는 것을 확인했으므로 문제를 해결해야 합니다.

  1. Visual Studio Code의 deploy 폴더에서 main.bicep 파일을 엽니다.

  2. Azure App Service 앱에 대한 정의를 찾아 properties 영역에 httpsOnly 속성을 포함하도록 업데이트합니다.

    resource appServiceApp 'Microsoft.Web/sites@2022-03-01' = {
      name: appServiceAppName
      location: location
      properties: {
        serverFarmId: appServicePlan.id
        httpsOnly: true
        siteConfig: {
          appSettings: [
            {
              name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
              value: applicationInsights.properties.InstrumentationKey
            }
            {
              name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
              value: applicationInsights.properties.ConnectionString
            }
          ]
        }
      }
    }
    
  3. 파일을 저장합니다.

  4. Visual Studio Code 터미널에서 다음 명령을 실행하여 변경 내용을 커밋하고 Git 리포지토리에 푸시합니다.

    git add .
    git commit -m "Configure HTTPS on website"
    git push
    

파이프라인 다시 실행

  1. 브라우저에서 Pipelines로 이동합니다.

  2. 가장 최근 실행을 선택합니다.

    파이프라인이 린팅, 유효성 검사미리 보기 스테이지를 완료할 때까지 기다립니다. Azure Pipelines는 자동으로 페이지를 최신 상태로 업데이트하지만 가끔 페이지를 새로 고치는 것이 좋습니다.

  3. 미리 보기 스테이지를 선택하고 가상 결과를 다시 검토합니다.

    가상 명령이 다음과 같은 httpsOnly 속성 값의 변경 내용을 탐지했습니다.

    Resource and property changes are indicated with these symbols:
      + Create
      ~ Modify
      = Nochange
    
    The deployment will update the following scope:
    
    Scope: /subscriptions/f0750bbe-ea75-4ae5-b24d-a92ca601da2c/resourceGroups/ToyWebsiteTest
    
      ~ Microsoft.Web/sites/toy-website-nbfnedv766snk [2021-01-15]
        + properties.siteConfig.localMySqlEnabled:   false
        + properties.siteConfig.netFrameworkVersion: "v4.6"
        ~ properties.httpsOnly:                      false => true
    
      = Microsoft.Insights/components/toywebsite [2020-02-02]
      = Microsoft.Storage/storageAccounts/mystoragenbfnedv766snk [2021-04-01]
      = Microsoft.Web/serverfarms/toy-website [2021-01-15]
    
    Resource changes: 1 to modify, 3 no change.
    
  4. 파이프라인 실행으로 돌아갑니다.

  5. 검토 단추를 선택한 다음, 승인을 선택합니다.

    파이프라인 실행이 완료될 때까지 기다립니다.

  6. SmokeTest 스테이지를 포함하여 전체 파이프라인이 성공적으로 완료됩니다. 성공이란 두 테스트를 모두 통과했다는 뜻입니다.

    Screenshot of the Azure DevOps interface that shows a successful pipeline run.

리소스 정리

이제 연습을 완료했으므로 리소스에 대한 요금이 청구되지 않도록 리소스를 제거할 수 있습니다.

Visual Studio Code 터미널에서 다음 명령을 실행합니다.

az group delete --resource-group ToyWebsiteTest --yes --no-wait

리소스 그룹은 백그라운드에서 삭제됩니다.

Remove-AzResourceGroup -Name ToyWebsiteTest -Force