Structuring YAML Pipelines and nested variables in Azure DevOps

When managing pipelines for large and complex repositories with multiple ‘Platforms’, each containing multiple apps and services, then the folder structure and variable strategy can be complicated. However, if this is done right, then the payoff for template reuse is dramatic.

Here, I outline my approach on the pipeline folder and YAML structure only. The variable structure allows for a full set of naming conventions to easily default across all your projects apps and delegate standards to organisation and platform levels. This, ideally, leaves only app specific configurations for your dev teams to manage and worry about.

This strategy rests on top of my general approach to structuring a mono-repository. For more details on that see Mono-repository folder structures.

Table of Contents

Environments

Any pipeline and variable strategy should work across all environments. The goal here is to have a single pipeline deploy everywhere with almost zero changes. Let’s outline what we mean by environments.

For an enterprise project, I would normally have a minimum of 6 environments. Sometimes, usually when there is complex integration testing and dependent environments, then it can be many more. My standard set are:

  1. Development (dev)
  2. Quality Assurance (qa)
  3. Systems Integration Test (sit)
  4. User Acceptance Test (uat)
  5. Pre-production (pre)
  6. Production (prod)

Why and what are these environments for?

Each environment has a specific purpose and a suit of tests that should be conducted at that environment.

As we progress through the environments with our deployments, the environments become more secure. This can easily break code, so you want to find that out before UAT. You also want to validate that what you have built is secure and again, find out before the customer starts playing with it. This can be confirmed by specific security tests.

The environments data and infrastructure should also start to get closer to of production, both power and scale. It’s all very well when an API responds sub-second in dev with a thousand records, but will it still when in production with twenty million? That is what gets tested in Pre-production with a series of Performance, Load and Soak tests…and ideally some chaos testing too.

For this illustration and avoid too much repetition we are going to be only use just 3. DEV, QA and PROD.

Repo Folder Structure

For the full details on how I structure a repository see Mono-repository folder structures. Here we will focus on just the Pipelines and Pipeline Variables.

Personally, I place all my pipeline files in one place, but there may be a rationale for have the pipeline YAML closer to the code it will build, test, and deploy too.

The folder structure below illustrates what this structure may look like for a mono-repository with two ‘platforms’, each of which may have several micro-services and UI’s (e.g., multiple elements with independent development and deployment cadences and versions).

Variable YAML files and pipeline strategy

If the following strategy is followed correctly then only very minor changes will be required between environments, and pipelines will be much easier to maintain and reuse.

Azure DevOps has a Variable Library facility.

Although very convenient, there is currently no way to track changes to Libraries, so I limit their use to Key Vault reference Libraries. For all other pipeline variables, we use the variable YAML files.

For guidance on variable reuse in Azure DevOps see

This strategy does not apply to GitHub Actions. For guidance on variable reuse in GitHub Actions see

Variable Template Files Structures

The root of the Variables folder can be found at /pipelines/variables

The structures and preference order of where to place variables is as follows:

By nesting variable templates, and reusing variables throughout the hierarchy and combining this with standardized naming convention (For Azure Resource Naming convention see Cloud Resource Naming Convention (Azure)), you can configure a whole host of variables at the root common-vars.yml. This means that you will only need exceptions the further down the levels you go.

Example Variables

The following variables are typical examples. Here to illustrate the hierarchy we’ll follow the examples given in the Mono-repository folder structures.

So,

  • Organisation is ‘My Organisation’. Short-code: ‘myorg
  • The platform we will illustrate is ‘my-platform-1’. Short-code ‘mp1
  • The app we will illustrate is ‘microservice1’ on this platform. Short-code ‘micro1
  • We want everything to deploy to North Europe, short-code ‘ne

Top Level Common Variables

# These variables should work across all projects as defaults
variables:
  Location: "northeurope"
  LocationShortCode: "ne"
  OrgShortCode: "myorg"
  PlatformShortCode: "core"
  StorageAccountName: $(OrgShortcode)$(PlatformShortCode)sa$(EnvShortCode)$(LocationShortCode)  
  FunctionAppName: $(OrgShortcode)-$(PlatformShortCode)-func-$(AppName)-$(EnvShortCode)-$(LocationShortCode)
  FunctionAppResourceGroupName: $(OrgShortcode)-$(PlatformShortCode)-rg-$(EnvShortCode)-$(LocationShortCode)
  SQLServerName: $(OrgShortcode)-$(PlatformShortCode)-sql-$(EnvShortCode)-$(LocationShortCode).database.windows.net
  CosmosDbAccountName: $(OrgShortcode)-$(PlatformShortCode)-cosmos-$(EnvShortCode)-$(LocationShortCode)
  AppInsightsName: $(OrgShortcode)-$(PlatformShortCode)-appi-$(EnvShortCode)-$(LocationShortCode)
  KeyVaultName: $(OrgShortcode)-$(PlatformShortCode)-kv-$(EnvShortCode)-$(LocationShortCode)
  KeyVaultResourceGroupName: $(OrgShortcode)-$(PlatformShortCode)-rg-kv-$(EnvShortCode)-$(LocationShortCode)
  SonarCloudAccount: "My-SonarCloud-Acc@email.com"
  SonarCloudOrganisation: "mysonarcloudorg"
  WebAppName: $(OrgShortcode)-$(PlatformShortCode)-app-web-$(EnvShortCode)-$(LocationShortCode)
  ApiAppName: $(OrgShortcode)-$(PlatformShortCode)-app-$(AppName)-$(EnvShortCode)-$(LocationShortCode)
  PulumiBackendStorageAccountName: $(OrgShortcode)$(PlatformShortCode)sapulumi$(SubscriptionShortCode)$(LocationShortCode)
  PulumiBackendStorageContainerName: $(PulumiProjectName)-state
  PulumiBackendKeyVault: nerr-kv-pul-$(SubscriptionShortCode)-ne
  PulumiStackName: "$(EnvShortCode)"

You may be surprised to see so many variables for resources lower down the tree. These are really the templates that have their variables populated and/or overridden later.

We can define the names of most resources, by using a convention (see Cloud Resource Naming Convention (Azure)) and then setting the $(AppName) variable in the App Level variables file and the $(EnvShortCode) set in the top-level environment variables as shown below. This works with every function app in any platform in any environment.

e.g. FunctionAppName: $(OrgShortcode)-$(PlatformShortCode)-func-$(AppName)-$(EnvShortCode)-$(LocationShortCode)

All but the AppName variable can be set at the organisation of platform level.

Top Level Environment Variables

# These variables should work across all projects as defaults for the DEV Environments
variables:
  AzureSubscriptionConnection: "MYORG-Dev"
  EnvironmentName: "Development"
  EnvShortCode: "dev"
  SubscriptionShortCode: "dev"
  SubscriptionId: "a6f824a0-2a4b-4d53-9e87-8c34fee04158"
# These variables should work across all projects as defaults for the DEV Environments
variables:
  AzureSubscriptionConnection: "MYORG-Prod"
  EnvironmentName: "Production"
  EnvShortCode: "prod"
  SubscriptionShortCode: "prod"
  SubscriptionId: "affd2308-294c-42ec-916b-d1328ac7ee20"

These top-level variables flow down to the App and can be used in app level pipelines. See the example templates below.

Platform Level Common Variables

# Platform Common Variables
variables:
  PlatformName: "My Platform 1"
  PlatformShortCode: "mp1"
  PulumiProjectName: "mp1"
  PulumiWorkingDirectory: "infrastructure/my-platform1-1/mp1stack"

Platform Level Environment Variables

# Platform Environment Variables for DEV
variables:
  DevOpsEnvName: "MP1-DEV"
# Platform Environment Variables for PROD
variables:
  DevOpsEnvName: "MP1-PROD"

Workload or App Common Variables

# App Common Variables
variables:
  AppName: "micro1"
  AppSrcRootPath: "src/my-platform-1/microservice1/myorg.platform1.microservice1.api/"
  AppNamespace: "myorg.platform1.microservice1.api"
  DbConnectionString: "Data Source=$(SQLServerName);Initial Catalog=$(DatabaseName);Authentication=Active Directory Managed Identity"
  DatabaseName: "micro1db"
  SonarCloudProjectKey: "MyAzDoOrg_myorg_my-platform1_microservice1"
  SonarCloudProjectName: "myorg_my-platform1_microservice1"

When you look at the organisation level common variables, you will see they reference variables here, such as AppName.

Workload or App Environment Variables

# App DEV Variables
variables:
  MyDependencyURL: https://mydepedency-test.someapi.com
# App PROD Variables
variables:
  MyDependencyURL: https://mydepedency.someapi.com

If you push your variables to use nested variables and see if they can be closer to the root, you will find that there are very few variables to configure at the App environment level.

Using Variable Templates

To use the variable template files in a pipeline, we need to reference the correct ones in the correct place

Pipeline Level (top)

variables:
  # This variable template is common across the whole Organisation for all environments
  - template: /pipelines/variables/common.yaml

  # This variable template is common across the Platform for all environments
  - template: /pipelines/variables/my-platform-1/common.yaml

  # This variable template is common across the App components for all environments
  - template: /pipelines/variables/my-platform-1/microservice1/common.yaml

Stage Level (Environment)

Each environment is deployed in a stage. One after the other. See below for the full pipeline examples.

variables:
  # This variable template is common across the whole Organisation for a single environment
  - template: /pipelines/variables/common-vars-${{ env }}.yaml

  # This variable template is common across the whole Platform for a single environment
  - template: /pipelines/variables/my-platform-1/${{ env }}-vars.yaml

  # This variable template is common across the whole App components for a single environment
  - template: /pipelines/variables/my-platform-1/microservice1/${{ env }}-vars.yaml

Reference Variables in variable Templates

As the variable template files, are YAML pipeline templates, they in turn can reference other variable templates. In this way it is possible to have the variable templates encapsulate the hierarchy and then only reference the most specific one in the CICD pipeline itself.

# File: azure-pipelines.yml

variables:
- template: vars.yml  # Template reference

steps:
- script: echo My favorite vegetable is ${{ variables.favoriteVeggie }}.

App Configuration Variables

Application variables for Functions and Web App deployments can be kept in a separate variable file. This is usually so specific to the app, that it is likely to be in the app folder and called app-config-vars.yml.

Here the functions app configuration settings are listed as key-value pairs for the deployment step in a pipeline variable called ‘appSetting‘.

variables:
  appSettings: |
    -FUNCTIONS_WORKER_RUNTIME "dotnet-isolated" 
    -OrderDbOrdersContainer $(OrderDbOrdersContainerName) 
    -OrderDbOrderDetailsContainer $(OrderDbOrderDetailsContainerName) 
    -OrderDbDatabase $(OrderDbDatabaseName) 
    -OrderDbKey "@Microsoft.KeyVault(SecretUri=https://$(KeyVaultName).vault.azure.net/secrets/cosmosdb-primary-rw-key/)" 
    -OrderDbUri $(CosmosDbUri) 
    -AdTenantId $(AdTenantId) 
    -AdClientId $(AdClientId) 
    -AdClientSecret "@Microsoft.KeyVault(SecretUri=https://$(KeyVaultName).vault.azure.net/secrets/ad-client-secret-order/)" 

and referenced directly into a variable as follows:

variables:
  - name: FuncVariablesTemplate
    value: variables/my-platform-1/microservice1/app-config-vars.yml

and used like:

          steps:
          - ${{ if eq(parameters.DeployToSlots, true) }}:
            - task: AzureFunctionApp@1
              displayName: API Funcion App Deployment - $(AppNamespace)
              inputs:
                azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
                appType: 'functionApp'
                appName: '$(FunctionAppName)'
                deployToSlotOrASE: true
                resourceGroupName: '$(FunctionAppResourceGroupName)'
                slotName: 'staging'
                package: '$(Pipeline.Workspace)/$(AppName)/$(AppName).zip'
                appSettings: $(appSettings)
                deploymentMethod: 'auto'

Example Variable Templates

Although this seems complicated, I think the best way to understand the reasons for this setup is by example. Below is a function combined CICD pipeline and the templates it references.

The magic now, is that this setup will work for any Function in any Platform in any organisation, with pretty much only a few variable changes.

Another advantage is that the pipelines have effectively been codified leaving your dev teams to only manage a few very local variables and setting close to the App code…which they are best placed to understand.

Full Function Pipeline Example

A Typical CICD Pipeline for an Azure Function

The following is a combined Continuous Integration (CI) and Continuous Delivery (CD) pipeline that deploys to Dev, then QA, then Prod.

It uses four pipeline templates, which are given below.

  1. function-dotnet--ci-template.yml for the build stage
  2. functions-cd-job-template.yml in each Environment Stage to deploy the function
  3. This template in turn incorporates a template for
    • packaging postman API tests for the deployment called publish-postman-ci-template.yml
    • running postman API tests called postman-runner-ci-template.yml

and references all the necessary pipeline variable YAML files.

# Pipeline for deploying code and configuration to an Azure Function - myorg.my-pltform-1.microservice1

parameters:
  - name: buildConfiguration
    displayName: Build Configuration
    default: Release
    type: string
    values:
      - Release
      - Debug
  - name: RunApiTests
    displayName: Run the API integration tests with Postman
    default: false
    type: boolean
  - name: RunApimTests
    displayName: Run the APIM tests with Postman
    default: false
    type: boolean          

name: $(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)

variables:
  - template: /pipelines/variables/common-vars.yml
  - template: /pipelines/variables/my-platform-1/common-vars.yml
  - template: /pipelines/variables/my-platform-1/microservice1/common-vars.yml
  - name: FuncVariablesTemplate
    value: /pipelines/variables/my-platform-1/microservice1/app-config-vars.yml  
  - name: SolPath
    value: $(AppSrcRootPath)/$(AppNamespace).sln
  - name: mainFuncProjPath
    value: $(AppSrcRootPath)/$(AppNamespace)/$(AppNamespace).csproj
  - name: ApiCommand
    value: $(Build.SourcesDirectory)/testing/my-platform-1/postman/API/collections/microservice1.func-postman_collection.json -e $(Build.SourcesDirectory)/testing/my-platform-1/postman/API/environments/microservice1.func-postman_environment.json  --env-var baseUrl=https://$(FunctionAppName).azurewebsites.net/api -r htmlextra --reporters cli,junit,htmlextra --reporter-htmlextra-export $(System.DefaultWorkingDirectory)/Results/PostmantTest.html --reporter-htmlextra-browserTitle "Postman Test Summary"
  - name: ApimCommand
    value: $(Build.SourcesDirectory)/testing/my-platform-1/postman/APIM/collections/microservice1.apim-postman_collection.json -e $(Build.SourcesDirectory)/testing/my-platform-1/postman/APIM/environments/microservice1.apim-postman_environment.json  --env-var baseUrl=https://myorg-apim-$(EnvShortCode)-ne.azure-api.net/my-platform-1/microservice1/ --env-var ClientId=$(ClientId) --env-var TenantId=$(TenantId) --env-var Scope=$(Scope) --env-var SubscriptionKey=$(APIMSubscriptionKey) --env-var ClientSecret=$(ADClientSecret) -r htmlextra --reporters cli,junit,htmlextra --reporter-htmlextra-export $(System.DefaultWorkingDirectory)/Results/PostmantTest.html --reporter-htmlextra-browserTitle "Postman Test Summary"          


trigger:
  branches:
    include:
    - main
  paths:
    include:
    - src/my-platform-1/microservice1

stages:
- stage: CI
  displayName: Function CI
  jobs:
  - template: ../build/templates/function-dotnet--ci-template.yml
    parameters:
      buildConfiguration: ${{ parameters.buildConfiguration }}
      appName: $(AppName)
      appNamespace: $(AppNamespace)
      appSrcRootPath: $(AppSrcRootPath)
      solPath: $(SolPath)
      mainFuncProjPath: $(mainFuncProjPath)
      sonarProjectKey: $(SonarProjectKey)
      sonarProjectName: $(SonarProjectName)
      archiveName: $(AppName).zip
  - ${{ if eq(parameters.RunApiTests, true) }}:
    - template: ../build/templates/publish-postman-ci-template.yml


- stage: DEV
  displayName: Deploy to DEV
  condition: and(succeeded('CI'), ne(variables['Build.Reason'], 'PullRequest'))

  variables:
    - template: /pipelines/variables/common-vars-dev.yml
    - template: /pipelines/variables/my-platform-1/dev-vars.yml
    - template: /pipelines/variables/my-platform-1/microservice1/dev-vars.yml
    - group: KeyVault-my-platform-1-Dev
    - template: ${{ variables.FuncVariablesTemplate }}

  jobs:
  - template: ../release/templates/functions-cd-job-template.yml
    parameters:
      AzureSubscriptionConnection: ${{ variables.AzureSubscriptionConnection }}
      Environment: ${{ variables.DevOpsEnvName }}
      RunApiTests: ${{ parameters.RunApiTests }}
      RunApimTests: ${{ parameters.RunApimTests }}
      ApiCommand : ${{ variables.ApiCommand }}
      ApimCommand : ${{ variables.ApimCommand }}      
      SetAdditionalSettings: true
      # DeployToSlots: false

- stage: QA
  displayName: Deploy to QA
  condition: and(succeeded('CI'), ne(variables['Build.Reason'], 'PullRequest'))

  variables:
    - template: /pipelines/variables/common-vars-qa.yml
    - template: /pipelines/variables/my-platform-1/qa-vars.yml
    - template: /pipelines/variables/my-platform-1/microservice1/qa-vars.yml
    - group: KeyVault-my-platform-1-QA
    - template: ${{ variables.FuncVariablesTemplate }}

  jobs:
  - template: ../release/templates/functions-cd-job-template.yml
    parameters:
      AzureSubscriptionConnection: ${{ variables.AzureSubscriptionConnection }}
      Environment: ${{ variables.DevOpsEnvName }}
      RunApiTests: ${{ parameters.RunApiTests }}
      RunApimTests: ${{ parameters.RunApimTests }}
      SetAdditionalSettings: true
      # DeployToSlots: false      

- stage: PROD
  displayName: Deploy to PROD
  condition: and(succeeded('CI'), ne(variables['Build.Reason'], 'PullRequest'))

  variables:
    - template: /pipelines/variables/common-vars-prod.yml
    - template: /pipelines/variables/my-platform-1/prod-vars.yml
    - template: /pipelines/variables/my-platform-1/microservice1/prod-vars.yml
    - group: KeyVault-my-platform-1-PROD
    - template: ${{ variables.FuncVariablesTemplate }}

  jobs:
  - template: ../release/templates/functions-cd-job-template.yml
    parameters:
      AzureSubscriptionConnection: ${{ variables.AzureSubscriptionConnection }}
      Environment: ${{ variables.DevOpsEnvName }}
      RunApiTests: ${{ parameters.RunApiTests }}
      RunApimTests: ${{ parameters.RunApimTests }}
      SetAdditionalSettings: true
      # DeployToSlots: false      

.NET Azure Function CI (build) Pipeline TEMPLATE

This builds and runs the unit tests. It also includes a SonarCloud code scan and publish step and Whitesource Bolt open-source library checks. There is also a step that ensures all the Nugets have been consolidated to the same version.

This template has all the necessary parameters to be used across all projects.

parameters:
  - name: buildConfiguration
    displayName: Build Configuration
    default: Release
    type: string
    values:
      - Release
      - Debug
  - name: appName
    type: string
  - name: appNameSpace
    type: string
  - name: appSrcRootPath
    type: string
  - name: solPath
    type: string
  - name: mainFuncProjPath
    type: string
  - name: sonarProjectKey
    type: string
  - name: sonarProjectName
    type: string
  - name: archiveName
    type: string
  - name: zipUsingPublishTask
    displayName: Zip Using Publish Task
    default: true
    type: boolean

jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: ubuntu-latest
    steps:
    - task: SonarCloudPrepare@1
      displayName: Prepare for SonarCloud Scan
      inputs:
        SonarCloud: $(SonarCloudAccount)
        organization: $(SonarCloudOrganisation)
        scannerMode: 'MSBuild'
        projectKey: $(SonarCloudProjectKey)
        projectName: $(SonarCloudProjectName)
        extraProperties: |
            sonar.projectBaseDir=$(Build.SourcesDirectory)
            sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/coverage/coverage.opencover.xml            
    #       sonar.exclusions=${{parameters.envSonarExclusions}}

    # Check that all Nuget Packages are Consolidated
    - task: DotNetCoreCLI@2
      displayName: Install dotnet-consolidate
      inputs:
        command: 'custom'
        custom: 'tool'
        arguments: 'install dotnet-consolidate --global --version 2.0.0'

    - task: DotNetCoreCLI@2
      displayName: Ensure Consolidated Packages
      inputs:
        command: 'custom'
        custom: 'consolidate'
        arguments: '-s $(solPath)'

    - task: DotNetCoreCLI@2
      displayName: Build Solution
      inputs:
        projects: $(solPath)
        arguments: '--configuration ${{ parameters.buildConfiguration }}' 

    - task: DotNetCoreCLI@2
      displayName: Run Unit Tests
      inputs:
        command: test
        projects: $(solPath)
        publishTestResults: true 
        arguments: '--configuration ${{ parameters.buildConfiguration }} /p:CollectCoverage=true /p:MergeWith=$(Build.SourcesDirectory)/coverage/coverage.json /p:CoverletOutput=$(Build.SourcesDirectory)/coverage/ "/p:CoverletOutputFormat=\"json,opencover,Cobertura\""'
        nobuild: true

    - task: SonarCloudAnalyze@1
      displayName: Sonar Cloud Analysis

    - task: PublishCodeCoverageResults@1
      displayName: Publish Code Coverage
      inputs:
        codeCoverageTool: Cobertura
        summaryFileLocation: $(Build.SourcesDirectory)/coverage/coverage.cobertura.xml
        failIfCoverageEmpty: false

    - task: SonarCloudPublish@1
      displayName: Sonar Cloud - Publish Result
      inputs:
        pollingTimeoutSec: '300'

    - task: DotNetCoreCLI@2
      displayName: Publish API Function App
      inputs:
        command: publish
        arguments: --configuration ${{ parameters.buildConfiguration }} --output $(build.artifactstagingdirectory)/${{ parameters.appName }}
        projects: $(mainFuncProjPath)
        publishWebProjects: false
        modifyOutputPath: false
        zipAfterPublish: ${{ parameters.zipUsingPublishTask }}

    - task: ArchiveFiles@2
      displayName: "Archive Files"
      condition: and(succeeded(), eq(${{ parameters.zipUsingPublishTask }}, false))
      inputs:
        rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.appName }}'
        includeRootFolder: false
        archiveType: 'zip'
        archiveFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/${{ parameters.archiveName }}'
        replaceExistingArchive: true

    - task: WhiteSource@21
      displayName: White Source Security Scan
      inputs:
        cwd: '${{ parameters.appSrcRootPath }}'
        projectName: ${{ parameters.appName }}
      # An error should be a warning rather than a fail 
      continueOnError: true 

    - publish: $(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/${{ parameters.archiveName }}
      displayName: Publish Build Artifact
      artifact: ${{ parameters.appName }}

.NET Azure Function CD (Deployment) Pipeline TEMPLATE

This template is used to deploy an Azure Function App. This same template can be used to deploy to all environments, just by changing the parameter values. There are some extra options to run Postman Collection API Tests using Newton either directly against the Function or via the API Manager. This can be used to verify the staging slot directly, prior to doing a swap-slot. If the test fails, then you don’t continue with making the latest changes live. If the direct tests succeed, then you can swap-slot and verify it’s still good through the API manager.

# Job Template for creating or updating an Api

parameters:
  - name: AzureSubscriptionConnection
    displayName: Azure Subscription Connection
    type: string
  - name: Environment
    displayName: The environment in Azure DevOps
    type: string
  - name: RunApiTests
    displayName: Run the API Tests
    type: boolean
    default: true
  - name: RunApimTests
    displayName: Run the APIM Tests
    type: boolean
    default: true
  - name: SetAdditionalSettings
    displayName: Set additional app settings
    type: boolean
    default: false
  - name: DeployToSlots
    displayName: Deploy Function App to Slots
    type: boolean
    default: true
  - name: ApiCommand
    displayName: API Test Command
    type: string
    default: ' '
  - name: ApimCommand
    displayName: API Test Command
    type: string
    default: ' '
    
jobs:
  - deployment: Deploy_Function_App
    displayName: Deploy Function App
    pool:
      vmImage: ubuntu-latest
    environment: ${{ parameters.Environment }}
    strategy:
      runOnce:
        deploy:
          steps:
          - ${{ if eq(parameters.DeployToSlots, true) }}:
            - task: AzureFunctionApp@1
              displayName: API Funcion App Deployment - $(AppNamespace)
              inputs:
                azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
                appType: 'functionApp'
                appName: '$(FunctionAppName)'
                deployToSlotOrASE: true
                resourceGroupName: '$(FunctionAppResourceGroupName)'
                slotName: 'staging'
                package: '$(Pipeline.Workspace)/$(AppName)/$(AppName).zip'
                appSettings: $(appSettings)
                deploymentMethod: 'auto'
          
          - ${{ if eq(parameters.DeployToSlots, false) }}:
            - task: AzureFunctionApp@1
              displayName: API Funcion App Deployment - $(AppNamespace)
              inputs:
                azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
                appType: 'functionApp'
                appName: $(FunctionAppName)
                resourceGroupName: $(FunctionAppResourceGroupName)
                package: $(Pipeline.Workspace)/$(AppName)/$(AppName).zip
                appSettings: $(appSettings)
                deploymentMethod: 'auto'
          
          - ${{ if eq(parameters.SetAdditionalSettings, true) }}:
            - task: AzureAppServiceSettings@1
              displayName: Additional Slot Settings
              inputs:
                azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
                appName: '$(FunctionAppName)'
                resourceGroupName: '$(FunctionAppResourceGroupName)'
                slotName: 'staging'
                appSettings: $(additionalAppSettings)
                connectionStrings: $(additionalConnSettings)
                generalSettings: $(additionalGeneralSettings)

          - ${{ if eq(parameters.DeployToSlots, true) }}:
            - task: AzureAppServiceManage@0
              displayName: 'Swap staging slot for ${{ parameters.Environment }}'
              inputs:
                azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
                Action: 'Swap Slots'
                WebAppName: '$(FunctionAppName)'
                ResourceGroupName: '$(FunctionAppResourceGroupName)'
                SourceSlot: 'staging'
                PreserveVnet: true      

  - job: APIPostmanTesting
    displayName : 'Api Postman Testing on ${{ parameters.Environment }}'
    condition:  and(succeeded(), eq('${{ parameters.RunApiTests }}', 'true'))
    dependsOn: Deploy_Function_App
    pool:
      vmImage: 'ubuntu-latest'
    steps:
     - template: /pipelines/build/templates/postman-runner-ci-template.yml
       parameters:
         SourcePath : ${{ parameters.ApiCommand }}

  - job: APIMPostmanTesting
    displayName : 'APIM Postman Testing on ${{ parameters.Environment }}'
    condition:  and(succeeded(), eq('${{ parameters.RunApimTests }}', 'true'))
    dependsOn: Deploy_Function_App
    pool:
      vmImage: 'ubuntu-latest'
    steps:
     - template: /pipelines/build/templates/postman-runner-ci-template.yml
       parameters:
         SourcePath : ${{ parameters.ApimCommand }}
    

Postman API Test Publish Pipeline

This job just assembles the test assets to make them accessible for the deployment stages

# Job Template for packaging the Postman Test assets for pipelines
jobs:
  - job: PostmanBuild
    dependsOn: Build
    condition: ne(variables['Build.Reason'], 'PullRequest')
    displayName: Publish Postman Artifacts
    pool:
      vmImage: ubuntu-latest
    steps:
    - task: CopyFiles@2
      inputs:
        SourceFolder: 'testing/$(PlatformShortCode)/postman'
        Contents: '**'
        TargetFolder: '$(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/postman'

    - publish: $(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/postman
      displayName: Publish Build Artifact
      artifact: postman

Postman API Test Pipeline

For completeness I have included the Postman Tests template

parameters:
  - name: SourcePath
    displayName: Source path for the postman collection
    type: string

steps:
  - checkout: self
  - task: CmdLine@2 
  - script: |     
     npm install -g newman
     npm install -g newman-reporter-junitfull
     npm install -g newman-reporter-htmlextra     
    workingDirectory: '$(System.DefaultWorkingDirectory)'
    displayName: 'Installing Newman'    
  - task: CmdLine@2
  - script: 'newman run  ${{ parameters.SourcePath }}'
    workingDirectory: '$(System.DefaultWorkingDirectory)'
    displayName: Running the Postman API Testing $(AppName) - $(EnvShortCode) 
    continueOnError: true
  
  - task: PublishTestResults@2
    inputs:
      testResultsFormat: 'JUnit'
      testResultsFiles: '$(System.DefaultWorkingDirectory)/Results/test.html'
      failTaskOnFailedTests: true
      testRunTitle: API $(AppName) - $(EnvShortCode) - Postman test results
    displayName: API $(AppName) - Postman test results     
  - task: UploadPostmanHtmlReport@1
    displayName: Postman HTML Report For $(AppName) - $(EnvShortCode)
    inputs:
      cwd: '$(System.DefaultWorkingDirectory)/Results'
      tabName: 'Postman'

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.