Exercise - Add multiple environments to your pipeline

Completed

Now you're ready to update your pipeline to deploy to both your test and production environments. In this unit, you'll update your pipeline to use templates so that you can reuse the stages across the environments.

During the process, you'll:

  • Add a pipeline template for the lint stage.
  • Add a pipeline template that defines the stages required to deploy to any environment.
  • Update your pipeline to use the templates.
  • Run your pipeline and view the results.

Add a pipeline template for the lint stage

The lint stage happens only once during the pipeline run, regardless of how many environments the pipeline deploys to. So, you don't really need to use templates for the lint stage. But to keep your main pipeline definition file simple and easy to read, you decide to define the lint stage in a template.

  1. In Visual Studio Code, create a new folder named pipeline-templates inside the deploy folder.

  2. Create a new file in the pipeline-templates folder named lint.yml.

    Screenshot of Visual Studio Code Explorer, with the pipeline-templates folder and the lint dot Y M L file.

  3. Paste the following pipeline template definition into the file:

    jobs:
    - job: LintCode
      displayName: Lint code
      steps:
        - script: |
            az bicep build --file deploy/main.bicep
          name: LintBicepCode
          displayName: Run Bicep linter
    

    The lint stage is the same as the lint stage already in the pipeline, but now it's in a separate pipeline template file.

  4. Save your changes and close the file.

Add a pipeline template for deployment

Create a pipeline template that defines all of the stages required to deploy each of your environments. You use template parameters to specify the settings that might differ between environments.

  1. Create a new file in the pipeline-templates folder named deploy.yml.

    Screenshot of Visual Studio Code Explorer, with the pipeline-templates folder and the deploy dot YML file.

    This file represents all of the deployment activities that run for each of your environments.

  2. Paste the following pipeline template parameters into the file:

    parameters:
    - name: environmentType
      type: string
    - name: resourceGroupName
      type: string
    - name: serviceConnectionName
      type: string
    - name: deploymentDefaultLocation
      type: string
      default: westus3
    

    Note

    When you start to work with your YAML file in Visual Studio Code, you might see some red squiggly lines telling you there's a problem. This is because the Visual Studio Code extension for YAML files sometimes incorrectly guesses the file's schema.

    You can ignore the problems that the extension reports. Or if you prefer, you can add the following code to the top of the file to suppress the extension's guessing:

    # yaml-language-server: $schema=./deploy.yml
    
  3. Below the parameters, paste the definition of the validation stage:

    stages:
    
    - ${{ if ne(parameters.environmentType, 'Production') }}:
      - stage: Validate_${{parameters.environmentType}}
        displayName: Validate (${{parameters.environmentType}} Environment)
        jobs:
        - job: ValidateBicepCode
          displayName: Validate Bicep code
          steps:
            - task: AzureResourceManagerTemplateDeployment@3
              name: RunPreflightValidation
              displayName: Run preflight validation
              inputs:
                connectedServiceName: ${{parameters.serviceConnectionName}}
                location: ${{parameters.deploymentDefaultLocation}}
                deploymentMode: Validation
                resourceGroupName: ${{parameters.resourceGroupName}}
                csmFile: deploy/main.bicep
                overrideParameters: >
                  -environmentType ${{parameters.environmentType}}
    

    Notice that a condition is applied to this stage. It runs only for non-production environments.

    Also notice that the stage identifier includes the value of the environmentType parameter. This parameter ensures that every stage in your pipeline has a unique identifier. The stage also has a displayName property to create a well-formatted name for you to read.

  4. Below the validation stage, paste the definition of the preview stage:

    - ${{ if eq(parameters.environmentType, 'Production') }}:
      - stage: Preview_${{parameters.environmentType}}
        displayName: Preview (${{parameters.environmentType}} Environment)
        jobs:
        - job: PreviewAzureChanges
          displayName: Preview Azure changes
          steps:
            - task: AzureCLI@2
              name: RunWhatIf
              displayName: Run what-if
              inputs:
                azureSubscription: ${{parameters.serviceConnectionName}}
                scriptType: 'bash'
                scriptLocation: 'inlineScript'
                inlineScript: |
                  az deployment group what-if \
                    --resource-group ${{parameters.resourceGroupName}} \
                    --template-file deploy/main.bicep \
                    --parameters environmentType=${{parameters.environmentType}}
    

    Notice that this stage has a condition applied too, but it's the opposite of the validation stage's condition. The preview stage runs only for the production environment.

  5. Below the preview stage, paste the definition of the deploy stage:

    - stage: Deploy_${{parameters.environmentType}}
      displayName: Deploy (${{parameters.environmentType}} Environment)
      jobs:
      - deployment: DeployWebsite
        displayName: Deploy website
        environment: ${{parameters.environmentType}}
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
    
                - task: AzureResourceManagerTemplateDeployment@3
                  name: DeployBicepFile
                  displayName: Deploy Bicep file
                  inputs:
                    connectedServiceName: ${{parameters.serviceConnectionName}}
                    deploymentName: $(Build.BuildNumber)
                    location: ${{parameters.deploymentDefaultLocation}}
                    resourceGroupName: ${{parameters.resourceGroupName}}
                    csmFile: deploy/main.bicep
                    overrideParameters: >
                      -environmentType ${{parameters.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)
    
  6. Below the deploy stage, paste the definition of the smoke test stage:

    - 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'
    

    Notice that the appServiceAppHostName variable definition incorporates the environmentType parameter when it refers to the stage that published the host name. This parameter ensures that each smoke test stage runs against the correct environment.

  7. Verify that your deploy.yml file now looks like the following example:

    parameters:
    - name: environmentType
      type: string
    - name: resourceGroupName
      type: string
    - name: serviceConnectionName
      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
          steps:
            - task: AzureResourceManagerTemplateDeployment@3
              name: RunPreflightValidation
              displayName: Run preflight validation
              inputs:
                connectedServiceName: ${{parameters.serviceConnectionName}}
                location: ${{parameters.deploymentDefaultLocation}}
                deploymentMode: Validation
                resourceGroupName: ${{parameters.resourceGroupName}}
                csmFile: deploy/main.bicep
                overrideParameters: >
                  -environmentType ${{parameters.environmentType}}
    
    - ${{ if eq(parameters.environmentType, 'Production') }}:
      - stage: Preview_${{parameters.environmentType}}
        displayName: Preview (${{parameters.environmentType}} Environment)
        jobs:
        - job: PreviewAzureChanges
          displayName: Preview Azure changes
          steps:
            - task: AzureCLI@2
              name: RunWhatIf
              displayName: Run what-if
              inputs:
                azureSubscription: ${{parameters.serviceConnectionName}}
                scriptType: 'bash'
                scriptLocation: 'inlineScript'
                inlineScript: |
                  az deployment group what-if \
                    --resource-group ${{parameters.resourceGroupName}} \
                    --template-file deploy/main.bicep \
                    --parameters environmentType=${{parameters.environmentType}}
    
    - stage: Deploy_${{parameters.environmentType}}
      displayName: Deploy (${{parameters.environmentType}} Environment)
      jobs:
      - deployment: DeployWebsite
        displayName: Deploy website
        environment: ${{parameters.environmentType}}
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
    
                - task: AzureResourceManagerTemplateDeployment@3
                  name: DeployBicepFile
                  displayName: Deploy Bicep file
                  inputs:
                    connectedServiceName: ${{parameters.serviceConnectionName}}
                    deploymentName: $(Build.BuildNumber)
                    location: ${{parameters.deploymentDefaultLocation}}
                    resourceGroupName: ${{parameters.resourceGroupName}}
                    csmFile: deploy/main.bicep
                    overrideParameters: >
                      -environmentType ${{parameters.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_${{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'
    
  8. Save your changes to the file.

Update the pipeline definition to use the templates

  1. Open the azure-pipelines.yml file.

  2. Update the file to use the new templates by replacing the contents with the following code:

    trigger:
      batch: true
      branches:
        include:
        - main
    
    pool:
      vmImage: ubuntu-latest
    
    stages:
    
    # Lint the Bicep file.
    - stage: Lint
      jobs: 
      - template: pipeline-templates/lint.yml
    
    # Deploy to the test environment.
    - template: pipeline-templates/deploy.yml
      parameters:
        environmentType: Test
        resourceGroupName: ToyWebsiteTest
        serviceConnectionName: ToyWebsiteTest
    
    # Deploy to the production environment.
    - template: pipeline-templates/deploy.yml
      parameters:
        environmentType: Production
        resourceGroupName: ToyWebsiteProduction
        serviceConnectionName: ToyWebsiteProduction
    

    This pipeline runs the lint stage once. Then it uses the deploy.yml template file twice: once per environment. This keeps the pipeline definition clear and easy to understand. Also, the comments help explain what's happening.

  3. Save your changes.

  4. Commit and push your changes to your Git repository by running the following commands in the Visual Studio Code terminal:

    git add .
    git commit -m "Add pipeline templates"
    git push
    

View the pipeline run

  1. In your browser, go to Pipelines.

  2. Select the most recent run of your pipeline.

    Notice that the pipeline run now shows all the stages that you defined in the YAML file. You might need to scroll horizontally to see them all.

    Screenshot of Azure Pipelines that shows the pipeline run stages.

  3. Wait for the pipeline to pause before the Deploy (Production Environment) stage. It might take a few minutes for the pipeline to reach this point.

    Screenshot of Azure Pipelines that shows the pipeline run paused for approval.

  4. Approve the deployment to the production environment by selecting the Review button.

  5. Select the Approve button.

    Screenshot of the Azure DevOps interface that shows the pipeline approval page and the Approve button.

    Wait for the pipeline to finish running.

  6. Select the Tests tab to show the test results from this pipeline run.

    Notice that there are now four test results. The smoke test runs on both the test and production environments, so you see the results for both sets of tests.

    Screenshot of Azure Pipelines that shows the page for pipeline run tests, with four test results.

  7. Select Pipelines > Environments.

  8. Select the Production environment.

  9. Notice that on the environment details screen, you see an overview of the production environment's deployment history.

    Screenshot of Azure Pipelines that shows the production environment, with the deployment history showing a single deployment.

  10. Select the deployment, and select the Changes tab.

    Notice that Changes tab shows you the list of commits included in the deployment. This information helps you to see exactly what has changed in your environment over time.

    Screenshot of Azure Pipelines that shows the production environment's deployment details, with a list of commits.

  11. In your browser, go to the Azure portal.

  12. Go to the ToyWebsiteProduction resource group.

  13. In the list of resources, open the Azure App Service app.

    Screenshot of the Azure portal that shows the production App Service app and the App Service plan SKU details.

    Notice that the type of App Service plan is S1.

  14. Go to the App Service app in the ToyWebsiteTest resource group.

    Notice that the type of App Service plan is F1. The two environments use different settings, as you defined in your Bicep file.