Bicep Tips and Tricks | #1 | Template Versioning and Applying to Azure Resource Tags

The first of many

For those who know me well, starting a bicep tips and tricks series would not be a surprise to them. The moment the Bicep language was introduced, I knew I would be completely obsessed. I love writing bicep templates and even more the clever refinement to make them reusable, configurable, manageable, readable… I really enjoy sharing my experiences with Bicep, so here it begins on the wider front. From one Bicep nerd to another, I hope you find these tips useful, happy templating!

Why is versioning important

Versioning is a fundamental practice in software development that helps us manage change effectively. It provides a structured way to track and communicate updates, ensuring clarity and consistency throughout the development lifecycle.

  • Why apply a version to your templates?

    Applying versioning to templates ensures you can track changes, maintain consistency, and know exactly which version of your infrastructure code was used for any deployment. It supports best practices like binary promotion, simplifies troubleshooting, and helps teams coordinate updates and rollbacks with confidence.

  • Why apply the version as a tag to Azure Resources?

    Tagging Azure resources with the template version gives you direct visibility into what was deployed, right from the Azure portal or API. This makes audits, governance, and troubleshooting much easier, as you can instantly see which template version created or updated a resource. It also helps correlate infrastructure changes with application releases and supports compliance requirements for traceability.

ARM templates have a Json property right at the top of the file underneath $schema called contentVersion. By default and if never changed, this will always be 1.0.0.0. As part of our versioning practice we can make sure that the templates that we deploy make use of the same version that is applied to other built artifacts. This allows us to track deployments and their respective templates through versions.

How to version templates using Azure DevOps

As part of a YAML CI/CD pipeline I typically have a build stage, with a inner Test and Version ARM Templates Job. The focus of the job is to do two things:

  1. Using the az cli - Build the Bicep templates
    • I also include the Bicep linting file, this means that as the templates are being transpiled into ARM I am also getting any build errors and warnings. I output these as logissues to the Azure DevOps Pipeline for visibility.
  2. Version the built ARM JSON artifacts to support Binary promotion and align with versioning practices.

I use the following tasks to complete the tasks:

  1. AzureCLI@2 - used to build the Bicep templates (transpile into ARM) and log any warnings or errors.
  2. VersionJSONFile@3 - used to version stamp the ARM templates.
  3. CopyFiles@2 - Used to copy the versioned ARM artifacts to the staging directory.
  4. PublishPipelineArtifact@1 - used to publish the ARM artifacts to the pipeline so they can be downloaded in future deployment stages.

Here is a sample YAML template:

# Yaml

...

- task: AzureCLI@2
  displayName: 'Build Bicep Files'
  inputs:
    azureSubscription: 'xxyyzz'
    scriptType: 'ps'
    scriptLocation: 'inlineScript'
    useGlobalConfig: true
    powershellerrorActionPreference: 'continue'
    inlineScript: |
      # create folder if it doesn't exist
      if (!(Test-Path -Path $(Build.SourcesDirectory)\{YourPath}\IAC\ARMOutput)) {
        New-Item -ItemType Directory -Path $(Build.SourcesDirectory)\{YourPath}\IAC\ARMOutput
      }
      write-host "Build the Bicep file"
      $output = az bicep build --file $(Build.SourcesDirectory)\{YourPath}\IAC\template.azuredeploy.bicep --outdir $(Build.SourcesDirectory)\{YourPath}\IAC\ARMOutput 2>&1      
  
      write-host "Process the output"
      $output | foreach-object {
         if ($_ -match 'Error') {
            Write-Host "##vso[task.logissue type=error]$_"
         } 
         if ($_ -match 'Warning') {
             Write-Host "##vso[task.logissue type=warning]$_"
         }
      }

- task: VersionJSONFile@3
  displayName: 'Version stamp ARM templates'
  inputs:
    Path: '$(Build.SourcesDirectory)\{YourPath}\IAC\ARMOutput'
    recursion: True
    VersionNumber: $(Build.BuildNumber) # an example versioning option but can also be options like gitversion
    useBuildNumberDirectly: False
    VersionRegex: \d+\.\d+\.\d+\.\d+
    versionForJSONFileFormat: '{1}.{2}.{3}.{4}'
    FilenamePattern: '\w+.json'
    Field: 'contentVersion' # Versioning is applied to the templates contentVersion field
    OutputVersion: OutputedVersion

- task: CopyFiles@2
  displayName: 'Copy ARMOutput to Artifact Staging Directory'
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)\{YourPath}\IAC\ARMOutput'
    Contents: '**/*.json'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/templates'
    flattenFolders: true

- task: PublishPipelineArtifact@1
  displayName: 'Publish ARMOutput as template build artifact'
  inputs:
    targetPath: '$(Build.ArtifactStagingDirectory)/templates'
    publishLocation: 'pipeline'
    artifactName: 'ARMtemplates'

This setup ensures your ARM templates are built, versioned, and published as pipeline artifacts in a repeatable, traceable way. By stamping each template with a version number and making it available as an artifact, you enable binary promotion through environments and make it easy to track exactly which template version was used for each deployment. This improves auditability, supports rollback scenarios, and aligns with best practices for infrastructure as code in CI/CD pipelines.

How to apply the template version as a tag to Azure Resources

Versioning our ARM templates is one part of the transparency and traceability. But it would also be nice to be able to see which template version deployed the Azure Resources from the Azure Resources themselves. We can do this by applying a tag onto our resources and tying it back to our ARM contentVersion property field. We can do this by using the deployment() function as such deployment().properties.template.contentVersion.

On a resource it would look like the following:

// Bicep

resource AppService 'Microsoft.Web/sites@2024-11-01' = {
  name: appServiceName
  location: location
  tags: {
	...
    version: deployment().properties.template.contentVersion
  }
  ...
}

Applying template versioning and tagging resources with the deployed template’s version brings transparency and traceability to your Azure infrastructure. You’ll always know which version of your code and templates are running in each environment, making troubleshooting and governance much easier. These small practices help teams stay organised, reduce deployment risks, and build confidence in their automation. Happy Bicep-ing!

For the original version of this post see Andrew Wilson's personal blog at Bicep Tips and Tricks | #1 | Template Versioning and Applying to Azure Resource Tags