This article will talk about YAML templates. By using these templates, you can make your code reusable and simplify sharing code across multiple pipelines.
If you are new to CI/CD, you might be familiar with blocks of code repeated multiple times in a single pipeline or deploying applications in various environments. The problem with code duplication is that block refactors must be propagated to each duplicate. Because of this, both the chance of human error and the time required to develop the pipeline increase linearly as the number of duplicates.
Using YAML templates, you can define reusable content, logic, and parameters in separate templates that load into your pipeline at runtime. You can divide these templates into one of these four categories,
- Stage - define a set of stages for related jobs
- Job - define a collection of steps run by an agent
- Step - define a linear sequence of operations for a job
- Variable - alternative to hardcoded values or variable groups
Example
Suppose you have a repository that needs to be automated through CI/CD pipelines with the following structure,
.
├── api
│ ├── dockerfile
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ └── server.js
├── frontend
│ ├── dockerfile
│ ├── node_modules
│ ├── package.json
│ ├── public
│ ├── src
│ └── yarn.lock
└── pipelines
└── azure-pipelines.yml
Without using a template, you will have to add the tasks to build, test, and deploy the docker image of the Node.JS API. Then you will have to perform the same tasks for the React.JS front-end. The result will be something like this,
trigger:
- mainpool:
vmImage: 'ubuntu-latest'stages:
- stage: Build
displayName: Build stage
jobs:
- job: Build_api
displayName: Build API
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: |
npm install
workingDirectory: '$(Build.Repository.LocalPath)/api'
displayName: 'npm install'- job: Build_frontend
displayName: Build Frontend
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: |
npm install
npm run build
workingDirectory: '$(Build.Repository.LocalPath)/frontend'
displayName: 'npm install and build'- stage: Test
displayName: Test stage
jobs:
- job: Test_api
displayName: Test API
steps:
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'SonarCloud training connection'
organization: 'gtrekter'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'GTRekter_Training'
cliProjectName: 'Training'
cliSources: '$(Build.Repository.LocalPath)/api'
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
inputs:
pollingTimeoutSec: '300' - job: Test_frontend
displayName: Test Frontend
steps:
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'SonarCloud training connection'
organization: 'gtrekter'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'GTRekter_Training'
cliProjectName: 'Training'
cliSources: '$(Build.Repository.LocalPath)/frontend'
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
inputs:
pollingTimeoutSec: '300'- stage: Build_dockerimage
displayName: Build and push image to ACR stage
jobs:
- job: Build_api_dockerimage
displayName: Build and push API image
steps:
- task: Docker@2
displayName: Build and push an image to ACR
inputs:
containerRegistry: 'ACR training connection'
repository: 'training-frontend'
command: 'buildAndPush'
Dockerfile: '$(Build.SourcesDirectory)/api/dockerfile'
tags: '$(Build.BuildId)' - job: Build_frontend_dockerimage
displayName: Build and push frontend image
steps:
- task: Docker@2
displayName: Build and push an image to ACR
inputs:
containerRegistry: 'ACR training connection'
repository: 'training-frontend'
command: 'buildAndPush'
Dockerfile: '$(Build.SourcesDirectory)/frontend/dockerfile'
tags: '$(Build.BuildId)'
The results of running the pipeline will be the following,
Suppose that you must update the pipeline by, for example, adding unit tests or changing the SonarCloud connection. If you aren’t using a template, you must repeat the same operation for each duplicate.
By using YAML templates, you can split the pipeline into multiple, more manageable components stored in different files. The structure of the project would then be the following,
.
├── README.md
├── api
│ ├── dockerfile
│ ├── package-lock.json
│ ├── package.json
│ └── server.js
├── frontend
│ ├── dockerfile
│ ├── node_modules
│ ├── package.json
│ ├── public
│ ├── src
│ └── yarn.lock
└── pipelines
├── build-template.yml
├── docker-template.yml
├── pipeline.yml
└── sonarcloud-template.yml
In this case, the pipeline would be,
trigger:
- mainpool:
vmImage: 'ubuntu-latest'stages:
- stage: Build
displayName: Build stage
jobs:
- job: Build_api
displayName: Build API
steps:
- template: ./build-template.yml
parameters:
workingDirectory: '$(Build.Repository.LocalPath)/api'- job: Build_frontend
displayName: Build Frontend
steps:
- template: ./build-template.yml
parameters:
workingDirectory: '$(Build.Repository.LocalPath)/frontend'
buildProject: true- stage: Test
displayName: Test stage
jobs:
- job: Test_api
displayName: Test API
steps:
- template: ./sonarcloud-template.yml
parameters:
workingDirectory: '$(Build.Repository.LocalPath)/api'- job: Test_frontend
displayName: Test Frontend
steps:
- template: sonarcloud-template.yml
parameters:
workingDirectory: '$(Build.Repository.LocalPath)/frontend'- stage: Build_dockerimage
displayName: Build and push image to ACR stage
jobs:
- job: Build_api_dockerimage
displayName: Build and push API image
steps:
- template: ./docker-template.yml
parameters:
workingDirectory: '$(Build.Repository.LocalPath)/api/dockerfile'- job: Build_frontend_dockerimage
displayName: Build and push frontend image
steps:
- template: ./docker-template.yml
parameters:
workingDirectory: '$(Build.Repository.LocalPath)/frontend/dockerfile'
As you can see, each of the steps dedicated to a particular activity is collected in a dedicated file.
build-template.yml
parameters:
- name: workingDirectory
type: string
default: ''
- name: buildProject
type: boolean
default: falsesteps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: |
npm install
workingDirectory: '${{ parameters.workingDirectory }}'
displayName: 'npm install'
- script: |
npm install
workingDirectory: '${{ parameters.workingDirectory }}'
condition: ${{ parameters.buildProject }}
displayName: 'npm build'
sonarcube-template.yml
parameters:
- name: workingDirectory
type: string
default: ''steps:
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'SonarCloud training connection'
organization: 'gtrekter'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'GTRekter_Training'
cliProjectName: 'Training'
cliSources: '${{ parameters.workingDirectory }}'
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
inputs:
pollingTimeoutSec: '300'
docker-template.yml
parameters:
- name: workingDirectory
type: string
default: ''steps:
- task: Docker@2
displayName: Build and push an image to ACR
inputs:
containerRegistry: 'ACR training connection'
repository: 'training-api'
command: 'buildAndPush'
Dockerfile: ${{ parameters.workingDirectory }}'
tags: '$(Build.BuildId)'
The results of running the pipeline will be the following,
Limitations
Azure pipelines support 100 separate template files in a single pipeline. It supports a maximum of 20 template nesting levels.
Wrap Up
In conclusion, YAML templates will keep your pipeline simpler to edit. This allows you to focus on application-specific tasks. However, be aware that a simple change to a template will affect all pipelines that use that template.
Resources