Getting started with Azure DevOps YAML Pipelines (Part 2)

Getting started with Azure DevOps YAML Pipelines (Part 2)
Photo by T K / Unsplash

In the previous article, you learned the basics of creating YAML-based pipelines in Azure DevOps. Now somewhere down your journey, you came to a point where:

  • You keep writing values repeatedly in your code
  • You have logic or a set of tasks that also exist in another pipeline code
  • You keep updating your pipeline code when using a different set of values.

Now you ask yourself: is it possible to store these somewhere so I don't have to repeat myself?

Well, yes it's possible. The answers are (assuming the list above are questions):

  • Variables
  • Runtime Parameters
  • Templates

Variables?

Variables, like in your typical programming concept, is a way to referrence a value across multiple parts of your pipeline. This means you only need to change the variable if you need to change the values being used in your pipeline. For example, if you're pipeline performs an API request to a server multiple times across the run, you only need to referrence the URL variable in those tasks, and only change the variable value if the URL changes somehow. No more copy-paste!

If your pipeline looks like this:

pool:
  vmImage: 'ubuntu-latest'

steps:
- bash: |
    curl https://jsonplaceholder.typicode.com/posts | jq '.[] | select(.userId == 1)'
  displayName: 'Query userId 1'
- bash: |
    curl https://jsonplaceholder.typicode.com/posts | jq '.[] | select(.userId == 2)'
  displayName: 'Query userId 2'

You can change it to look like this instead to make it cleaner and maintainable:

pool:
  vmImage: 'ubuntu-latest'

variables:
  apiUrl: 'https://jsonplaceholder.typicode.com/posts'
  anotherVar: '1'
  yetAnotherVar: 2

steps:
- bash: |
    curl $(apiUrl) | jq '.[] | select(.userId == 1)'
  displayName: 'Query userId 1'
- bash: |
    curl $(apiUrl) | jq '.[] | select(.userId == 2)'
  displayName: 'Query userId 2'

To declare a variable, you just add variables to your pipeline, and add the variables in key/value pair format under it. Variables are referenced on your pipeline using the syntax $(varName)

You typically add the variable on the root level, but can declare this anywhere in your pipeline (stage level, job level), but variable scoping applies, so if you have multiple variables of the same name, the more locally scoped value is used (job level variable overrides the value declared at the root level).

pool:
  vmImage: 'ubuntu-latest'

variables:
  apiUrl: 'https://jsonplaceholder.typicode.com/posts'
  anotherVar: '1'
  yetAnotherVar: 2

stages:
  - stage: GetTODOS
    variables:
      # this will be used instead since it's locally scoped
      apiUrl: 'https://jsonplaceholder.typicode.com/todos'
    displayName: Perform 
    jobs:
    - job: GetTODO
      displayName: Get TODOs instead
      steps:
      - bash: |
          curl $(apiUrl) | jq '.[] | select(.userId == 1)'
        displayName: 'Query userId 1'
      - bash: |
          curl $(apiUrl) | jq '.[] | select(.userId == 2)'
        displayName: 'Query userId 2'
##### THE REST OF THE PIPELINE BELOW #####

Okay, so now you know about variables. But these are read-only, and obviously, if you need to change the values, you need to edit the pipeline and commit again before you make another pipeline run.

How about Runtime Parameters?

Runtime parameters solve the problem of making your variables or values configurable with every run, instead of editing the pipeline file over and over again. When using them, a form is provided run, allowing you to suppply the values right before the pipeline executes.

Adding parameters is a matter of adding the parameters property on the root level of your config, and adding an object for each parameter you want the pipeline to accept. Referencing them afterwards on the pipeline should look like ${{ parameters.paramName }}

pool:
  vmImage: 'ubuntu-latest'

parameters:
- name: apiUrl
  default: 'https://jsonplaceholder.typicode.com/posts'
  type: string

steps:
- bash: |
    curl ${{ parameters.apiUrl }} | jq '.[] | select(.userId == 1)'
  displayName: 'Query userId 1'
- bash: |
    curl ${{ parameters.apiUrl }} | jq '.[] | select(.userId == 2)'
  displayName: 'Query userId 2'

Azure Pipelines accept multiple parameter types, not just string, and you can refer to the official documentation on Runtime Parameters for more examples.

And Templates?

Templates, on the other hand, is a way to reuse parts of your pipeline, or a way to share logic and other similar pipeline aspects to other pipelines. A common use of templates is for inserting tasks and variables, see the following example:

*tasks.yml

steps:
- bash: |
    curl https://jsonplaceholder.typicode.com/posts | jq '.[] | select(.userId == 1)'
  displayName: 'Query userId 1'

*vars.yml

variables:
  anotherVar: '1'
  yetAnotherVar: 2

*pipeline.yml

pool:
  vmImage: 'ubuntu-latest'

parameters:
- name: apiUrl
  default: 'https://jsonplaceholder.typicode.com/posts'
  type: string

jobs:
- job: GetTODO
  displayName: Get TODOs
  steps:
  - template: tasks.yml

*pipeline2.yml

pool:
  vmImage: 'ubuntu-latest'

variables:
- template: vars.yml

stages:
  - stage: GetTODOS
    displayName: Perform 
    jobs:
    - job: GetTODO
      displayName: Get TODOs instead
      steps:
      - template: tasks.yml
    # you can even reuse templates from the same pipeline
    # and add tasks before and after the tasks template
    - job: GetTODO2
      displayName: Get TODOs instead
      steps:
      - template: tasks.yml
      - bash:
        echo $(anotherVar) # should print 1

Resources