In Part 1 I went through the idea of a base template to handle dynamic multistage pipelines and ended with a template to deal with multiple subscriptions and environments. In this part I am going to show real use by deploying some IaC (Infrastructure as Code) to create an Web App in Azure and then deploying an application to the Web App using that base template.
To recap, the base template looked liked this with one difference, I’ve added a QA environment:
parameters:
- name: environments
type: object
default:
- name: 'dev'
subscriptions:
- subscription: 'Dev Subscription'
regions:
- location: 'eastus'
locationShort: 'eus'
- name: 'qa'
subscriptions:
- subscription: 'QA Subscription'
regions:
- location: 'eastus'
locationShort: 'eus'
- location: 'westus'
locationShort: 'wus'
- name: 'prod'
subscriptions:
- subscription: 'Prod Subscription'
regions:
- location: 'eastus'
locationShort: 'eus'
- location: 'westus'
locationShort: 'wus'
- name: buildSteps
type: stepList
default: []
- name: releaseSteps
type: stepList
default: []
- name: customReleaseTemplate
type: string
default: ''
- name: variableGroupPrefix
type: string
default: ''
- name: variableFilePrefix
type: string
default: ''
stages:
- stage: build
displayName: 'Build/Package Code or IaC'
jobs:
- job: build
displayName: 'Build/Package Code'
steps: ${{ parameters.buildSteps }}
- ${{ each env in parameters.environments }}:
- stage: ${{ env.name }}
displayName: 'Deploy to ${{ env.name }}'
condition: succeeded()
variables:
- ${{ if ne(parameters.variableFilePrefix, '') }}:
- template: ${{ parameters.variableFilePrefix }}_${{ env.name }}.yml@self
- ${{ if ne(parameters.variableGroupPrefix, '') }}:
- group: ${{ parameters.variableGroupPrefix }}_${{ env.name }}
jobs:
- ${{ each sub in env.subscriptions }}:
- ${{ each region in sub.regions }}:
- ${{ if ne(parameters.customReleaseTemplate, '') }}:
- template: ${{ parameters.customReleaseTemplate }}
parameters:
env: ${{ env.name }}
location: ${{ region.location }}
locationShort: ${{ region.locationShort }}
subscription: ${{ sub.subscription }}
- ${{ else }}:
- deployment: deploy_${{ region.locationShort }}
displayName: 'Deploy app to ${{ env.name }} in ${{ region.location }}'
environment: ${{ env.name }}_${{ region.locationShort }}
strategy:
runOnce:
deploy:
steps:
- ${{ parameters.releaseSteps }}
My repository for this contains several folders, one called infrastructure which contains my IaC code (in this case Bicep) and one called src which holds a very basic .NET 6 web app.
Deploy IaC
As most of the configuration is in the base template this pipeline is going to extend the environments template and then add steps to package up the infrastructure and run the IaC via a custom release template deploy-infrastructure.yml
.
trigger: none
pr: none
pool:
vmImage: 'ubuntu-latest'
resources:
repositories:
- repository: templates
type: git
name: shared-templates
ref: main
extends:
template: environments.yml@templates
parameters:
buildSteps:
- task: CopyFiles@2
displayName: 'Copy Files to output'
inputs:
contents: |
**/infrastructure/**
targetFolder: $(Build.ArtifactStagingDirectory)
- task: ArchiveFiles@2
displayName: 'Archive Infrastructure'
inputs:
rootFolderOrFile: $(Build.ArtifactStagingDirectory)
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/infrastructure$(Build.BuildId).zip'
- publish: $(Build.ArtifactStagingDirectory)/infrastructure$(Build.BuildId).zip
displayName: 'Upload Infrastructure package'
artifact: infrastructure
customReleaseTemplate: deploy-infrastructure.yml@self
The custom release template is mainly used so that I can dynamically load in some parameters files that will build the parameters JSON file for Bicep to use (if you want to know how I create the parameters JSON see my post on Passing Parameters to Bicep Technique #3).
deploy-infrastructure.yml
parameters:
- name: env
type: string
- name: location
type: string
- name: locationShort
type: string
- name: subscription
type: string
jobs:
- deployment: deploy_${{ parameters.locationShort }}
displayName: 'Deploy app to ${{ parameters.env }} in ${{ parameters.location }}'
environment: ${{ parameters.env }}_${{ parameters.locationShort }}
variables:
resourceGroupName: 'myapp-${{ parameters.env }}-${{ parameters.locationShort }}-rg'
templateFileName: 'infrastructure/main.bicep'
templateParametersFileName: 'infrastructure/main.parameters.json'
strategy:
runOnce:
deploy:
steps:
- task: ExtractFiles@1
displayName: 'Extract Infrastructure'
inputs:
archiveFilePatterns: '$(Pipeline.Workspace)/**/*.zip'
destinationFolder: $(Build.SourcesDirectory)
cleanDestinationFolder: false
- template: params_${{ parameters.env }}.yml
parameters:
linuxFxVersion: 'DOTNETCORE|6.0'
env: ${{ parameters.env }}
appname: 'myapp'
- task: AzureCLI@2
displayName: 'Deploy Infrastructure'
inputs:
azureSubscription: ${{ parameters.subscription }}
scriptType: 'pscore'
scriptLocation: inlineScript
inlineScript: |
az group create --name $(resourceGroupName) --location ${{ parameters.location }}
az deployment group create --name "infrastructure-deploy" --resource-group $(resourceGroupName) --template-file $(templateFileName) --parameters $(templateParametersFileName)
The parameters template will create the parameters JSON file, each file is very similar but the main difference is the sku being used, dev has ‘F1’, qa has ‘B1’ and prod has ‘S1’. This is what the prod parameters file looks like:
parameters:
- name: sku
type: string
default: 'S1'
- name: linuxFxVersion
type: string
- name: env
type: string
- name: appname
type: string
- name: tags
type: object
default:
environment: 'Production'
project: 'myproj'
steps:
- template: createParametersFile.yml@templates
parameters:
paramsJson: '${{ convertToJson(parameters) }}'
parameterFilePath: $(templateParametersFileName)
Deploy Application
As with the deployment of IaC most of the configuration is in the base template and so again this pipeline is quite small.
trigger: none
pr: none
pool:
vmImage: 'ubuntu-latest'
resources:
repositories:
- repository: templates
type: git
name: shared-templates
ref: main
extends:
template: environments.yml@templates
parameters:
buildSteps:
- script: |
cd src
dotnet build --configuration Release
displayName: 'dotnet build Release'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish'
inputs:
arguments: --configuration Release -o $(Build.ArtifactStagingDirectory)
command: 'publish'
publishWebProjects: true
- publish: $(Build.ArtifactStagingDirectory)
artifact: code
displayName: 'Publish Code'
customReleaseTemplate: deploy-app.yml@self
deploy-app.yml
parameters:
- name: env
type: string
- name: location
type: string
- name: locationShort
type: string
- name: subscription
type: string
jobs:
- deployment: deploy_${{ parameters.locationShort }}
displayName: 'Deploy app to ${{ parameters.env }} in ${{ parameters.location }}'
environment: ${{ parameters.env }}_${{ parameters.locationShort }}
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: '${{ parameters.subscription }}'
appType: 'webAppLinux'
appName: 'app-myapp-${{ parameters.env }}-${{ parameters.locationShort }}'
package: '$(Pipeline.Workspace)/**/*.zip'
Conclusion
The infrastructure and application both run the same steps and so have the same stages output

Resource Groups and Web Apps created by the IaC






Using this type of template provides a very powerful base template and allows the pipelines to focus on building and deploying removing the need to worry too much about the environments that are needed.
I hope showing this has been helpful and who knows it may spark some other ideas and ways of using base templates. Happy deployments 🙂