Exercise - Add multiple environments to your workflow

Completed

Now you're ready to update your workflow to deploy to both your test and production environments. In this unit, you'll update your workflow to use called workflows so that you can reuse the jobs across the environments.

During the process, you'll:

  • Add a reusable workflow for the lint job.
  • Add a reusable workflow that defines the jobs required to deploy to any environment.
  • Update your workflow to use the called workflows.
  • Run your workflow and view the results.

Add a reusable workflow for the lint job

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

  1. In Visual Studio Code, create a new file in the .github/workflows folder named lint.yml.

    Screenshot of Visual Studio Code Explorer, with the dot github and workflows folders and the lint dot YML file.

  2. Paste the following workflow definition into the file:

    name: lint
    
    on:
      workflow_call:
    
    jobs:
      lint:
        name: Lint code
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v3
    
        - name: Lint code
          run: |
            az bicep build --file deploy/main.bicep
    

    The lint job is the same as the lint job already in the workflow, but now it's in a separate workflow file.

  3. Save your changes and close the file.

Add a reusable workflow for deployment

Create a reusable workflow that defines all of the jobs required to deploy each of your environments. You'll use inputs and secrets to specify the settings that might differ between environments.

  1. Create a new file in the .github/workflows folder named deploy.yml.

    Screenshot of Visual Studio Code Explorer, with the dot github and workflows folders 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 workflow name, trigger, inputs, and secrets into the file:

    name: deploy
    
    on:
      workflow_call:
        inputs:
          environmentType:
            required: true
            type: string
          resourceGroupName:
            required: true
            type: string
        secrets:
          AZURE_CLIENT_ID:
            required: true
          AZURE_TENANT_ID:
            required: true
          AZURE_SUBSCRIPTION_ID:
            required: true
    

    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 secrets, paste the definition of the validation job:

    jobs:
      validate:
         runs-on: ubuntu-latest
         steps:
         - uses: actions/checkout@v3
         - uses: azure/login@v1
           name: Sign in to Azure
           with:
            client-id: ${{ secrets.AZURE_CLIENT_ID }}
            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
         - if: inputs.environmentType != 'Production'
           uses: azure/arm-deploy@v1
           name: Run preflight validation
           with:
             deploymentName: ${{ github.run_number }}
             resourceGroupName: ${{ inputs.resourceGroupName }}
             template: ./deploy/main.bicep
             parameters: >
               environmentType=${{ inputs.environmentType }}
             deploymentMode: Validate
         - if: inputs.environmentType == 'Production'
           uses: azure/arm-deploy@v1
           name: Run what-if
           with:
             failOnStdErr: false
             resourceGroupName: ${{ inputs.resourceGroupName }}
             template: ./deploy/main.bicep
             parameters: >
               environmentType=${{ inputs.environmentType }}
             additionalArguments: --what-if
    

    Notice that a condition is applied to the jobs. The preflight validation runs only for non-production environments. The what-if operation runs only for the production environment. In the previous module in the learning path, you used separate jobs for these operations, but here you combine them to simplify the workflow.

    Tip

    YAML files are sensitive to indentation. Whether you type or paste this code, make sure your indentation is correct. Later in this exercise, you'll see the complete YAML workflow definition so that you can verify that your file matches.

  4. Below the validation job, paste the definition of the deploy job:

    deploy:
      needs: validate
      environment: ${{ inputs.environmentType }}
      runs-on: ubuntu-latest
      outputs:
        appServiceAppHostName: ${{ steps.deploy.outputs.appServiceAppHostName }}
      steps:
      - uses: actions/checkout@v3
      - uses: azure/login@v1
        name: Sign in to Azure
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - uses: azure/arm-deploy@v1
        id: deploy
        name: Deploy Bicep file
        with:
          failOnStdErr: false
          deploymentName: ${{ github.run_number }}
          resourceGroupName: ${{ inputs.resourceGroupName }}
          template: ./deploy/main.bicep
          parameters: >
            environmentType=${{ inputs.environmentType }}
    
  5. Below the deploy job, paste the definition of the smoke test job:

    smoke-test:
      runs-on: ubuntu-latest
      needs: deploy
      steps:
      - uses: actions/checkout@v3
      - run: |
          $container = New-PesterContainer `
            -Path 'deploy/Website.Tests.ps1' `
            -Data @{ HostName = '${{needs.deploy.outputs.appServiceAppHostName}}' }
          Invoke-Pester `
            -Container $container `
            -CI
        name: Run smoke tests
        shell: pwsh
    
  6. Verify that your deploy.yml file now looks like the following example:

    name: deploy
    
    on:
      workflow_call:
        inputs:
          environmentType:
            required: true
            type: string
          resourceGroupName:
            required: true
            type: string
        secrets:
          AZURE_CLIENT_ID:
            required: true
          AZURE_TENANT_ID:
            required: true
          AZURE_SUBSCRIPTION_ID:
            required: true
    
    jobs:
      validate:
         runs-on: ubuntu-latest
         steps:
         - uses: actions/checkout@v3
         - uses: azure/login@v1
           name: Sign in to Azure
           with:
            client-id: ${{ secrets.AZURE_CLIENT_ID }}
            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
         - if: inputs.environmentType != 'Production'
           uses: azure/arm-deploy@v1
           name: Run preflight validation
           with:
             deploymentName: ${{ github.run_number }}
             resourceGroupName: ${{ inputs.resourceGroupName }}
             template: ./deploy/main.bicep
             parameters: >
               environmentType=${{ inputs.environmentType }}
             deploymentMode: Validate
         - if: inputs.environmentType == 'Production'
           uses: azure/arm-deploy@v1
           name: Run what-if
           with:
             failOnStdErr: false
             resourceGroupName: ${{ inputs.resourceGroupName }}
             template: ./deploy/main.bicep
             parameters: >
               environmentType=${{ inputs.environmentType }}
             additionalArguments: --what-if
    
      deploy:
        needs: validate
        environment: ${{ inputs.environmentType }}
        runs-on: ubuntu-latest
        outputs:
          appServiceAppHostName: ${{ steps.deploy.outputs.appServiceAppHostName }}
        steps:
        - uses: actions/checkout@v3
        - uses: azure/login@v1
          name: Sign in to Azure
          with:
            client-id: ${{ secrets.AZURE_CLIENT_ID }}
            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
        - uses: azure/arm-deploy@v1
          id: deploy
          name: Deploy Bicep file
          with:
            failOnStdErr: false
            deploymentName: ${{ github.run_number }}
            resourceGroupName: ${{ inputs.resourceGroupName }}
            template: ./deploy/main.bicep
            parameters: >
              environmentType=${{ inputs.environmentType }}
    
      smoke-test:
        runs-on: ubuntu-latest
        needs: deploy
        steps:
        - uses: actions/checkout@v3
        - run: |
            $container = New-PesterContainer `
              -Path 'deploy/Website.Tests.ps1' `
              -Data @{ HostName = '${{needs.deploy.outputs.appServiceAppHostName}}' }
            Invoke-Pester `
              -Container $container `
              -CI
          name: Run smoke tests
          shell: pwsh
    
  7. Save your changes to the file.

Update the workflow definition to use the templates

  1. Open the workflow.yml file in the .github/workflows folder.

  2. Remove the contents of the env: section, including the two environment variables. You'll replace these with environment-specific variables soon.

  3. Remove the contents of the lint: job definition and replace it with the following code to use the lint.yml file you created earlier:

    # Lint the Bicep file.
    lint:
      uses: ./.github/workflows/lint.yml
    
  4. Delete everything in the file below the lint job that you updated.

  5. At the bottom of the file, add the following code to deploy to the test environment:

    # Deploy to the test environment.
    deploy-test:
      uses: ./.github/workflows/deploy.yml
      needs: lint
      with:
        environmentType: Test
        resourceGroupName: ToyWebsiteTest
      secrets:
        AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID_TEST }}
        AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
        AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    
  6. Below the code you just added, add the following code to deploy to the production environment:

    # Deploy to the production environment.
    deploy-production:
      uses: ./.github/workflows/deploy.yml
      needs: deploy-test
      with:
        environmentType: Production
        resourceGroupName: ToyWebsiteProduction
      secrets:
        AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID_PRODUCTION }}
        AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
        AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    

    Your updated workflow runs the lint job once. Then it uses the deploy.yml called workflow twice: once per environment. This keeps the workflow definition clear and easy to understand. The comments in the YAML file identify each job's target environment.

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

    name: deploy-toy-website-environments
    concurrency: toy-company
    
    on:
      push:
        branches:
          - main
      workflow_dispatch:
    
    permissions:
      id-token: write
      contents: read
    
    jobs:
    
      # Lint the Bicep file.
      lint:
        uses: ./.github/workflows/lint.yml
    
      # Deploy to the test environment.
      deploy-test:
        uses: ./.github/workflows/deploy.yml
        needs: lint
        with:
          environmentType: Test
          resourceGroupName: ToyWebsiteTest
        secrets:
          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID_TEST }}
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    
      # Deploy to the production environment.
      deploy-production:
        uses: ./.github/workflows/deploy.yml
        needs: deploy-test
        with:
          environmentType: Production
          resourceGroupName: ToyWebsiteProduction
        secrets:
          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID_PRODUCTION }}
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    
  8. Save your changes.

  9. 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 reusable workflows"
    git push
    
  10. Since this is the first time you push to the repository, you might be prompted to sign in.

    On Windows, type 1 to authenticate using a web browser, and select Enter.

    On macOS, select Authorize.

  11. A browser window appears. You might need to sign in to GitHub again. Select Authorize.

View the workflow run

  1. In your browser, go to Actions.

    The first run of your workflow, labeled Initial commit, is shown as a failure. GitHub automatically ran the workflow when you created the repository. It failed because the secrets weren't ready at that time. You can ignore this failure.

  2. Select the deploy-toy-website-environments workflow.

  3. Select the most recent run of your workflow.

    Notice that the workflow run now shows all the jobs that you defined in the YAML files.

    Some warnings are listed in the Annotations panel. All of these warnings are because of the way Bicep writes informational messages to the workflow log. You can ignore these warnings.

  4. Wait for the workflow to pause before the deploy-production / deploy job. It might take a few minutes for the workflow to reach this point.

    Screenshot of GitHub that shows the workflow run paused for approval.

  5. Approve the deployment to the production environment by selecting the Review deployments button.

  6. Select the Production environment, then select the Approve and deploy button.

    Screenshot of the GitHub interface that shows the workflow approval page and the Approve and deploy button.

    Wait for the workflow to finish running. The workflow completes successfully.

  7. Select Code.

  8. Select the Production deployment.

    Screenshot of GitHub that shows the Code page environment, with the production environment highlighted.

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

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

  10. Select the commit identifier.

    Notice that GitHub shows you the list of commits included in the deployment. This helps you to see the changes in your environment over time.

    Screenshot of GitHub 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 App Services toy website production 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.