Upcoming book:

Modern .NET Development with Azure and DevOps

Azure DevOps: Bicep CI/CD Pipelines with bicepparam

Introduction

Notice that this post is a continuation of the base post, Azure DevOps: Bicep CI/CD Pipelines.

In this post, we'll be looking at improving the Developer Experience (DX) for our Bicep projects using the new Bicep parameters file format, and how we can easily use them from our Azure DevOps pipelines to avoid duplicating files.
Once again, all the tasks and configuration used in this post should work for both Azure DevOps Services and Azure DevOps Server.

Our Bicep project

To recap from the previous post, let's assume we have a main Bicep file as follows (main.bicep):

@description('Location for all resources.')
param location string = resourceGroup().location

@description('The name of the SQL Server resource.')
param sqlServerName string

@description('The administrator username for SQL Server.')
param sqlServerAdministratorLogin string

@description('The administrator password for SQL Server.')
@secure()
param sqlServerAdministratorLoginPassword string

resource sqlServer 'Microsoft.Sql/servers@2022-11-01-preview' = {
  name: sqlServerName
  location: location
  properties: {
    administratorLogin: sqlServerAdministratorLogin
    administratorLoginPassword: sqlServerAdministratorLoginPassword
  }
}

And this is accompanied by a traditional json parameters file (main.parameters.json):

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "sqlServerName": { 
      "value": "" 
    },
    "sqlServerAdministratorLogin": { 
      "value": "" 
    },
    "sqlServerAdministratorLoginPassword": { 
      "value": "" 
    }
  }
}

Moving to bicepparam

Bicep parameter files are an addition to the Bicep language that has recently been pushed to General Availability (GA). These files differ greatly from the original JSON files, as it allows for greater flexibility and a much more succinct syntax.
Among other things, Bicep parameter files allows you to succinctly write arrays and objects, and once the custom object types is pushed to GA, it will also support that. Unlike the JSON syntax, if you use the Bicep extension for Visual Studio Code, you get immediate feedback if you are missing parameters, have parameters that are not declared in the referenced Bicep file, and more.

The following is a very simple Bicep parameter file, with the parameters required for our project:

using './main.bicep'

param sqlServerName = ''
param sqlServerAdministratorLogin = ''
param sqlServerAdministratorLoginPassword = ''

I hope it's immediately clear how much shorter these files can be.

Updating the Build pipeline

In the original article, we ended up with a pipeline akin to this:

trigger:
- main

pool:
  vmImage: ubuntu-latest

steps:
- task: AzureCLI@2
  inputs:
    azureSubscription: 'BicepDeploymentConnection'
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: 'az bicep install'

- task: AzureCLI@2
  inputs:
    azureSubscription: 'BicepDeploymentConnection'
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: 'az deployment group validate --resource-group Blog --template-file main.bicep --parameters main.parameters.json'
    workingDirectory: ./Code

- task: CopyFiles@2
  inputs:
    Contents: 'Code/**'
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

Using the new file format in any of the Bicep commands is as simple as changing the --parameters switch from main.parameters.json to main.parameters.bicepparam. In our pipeline, for example, that would mean using:

    inlineScript: 'az deployment group validate --resource-group Blog --template-file Code/main.bicep --parameters Code/main.parameters.bicepparam'

The Microsoft article for these Bicep parameter files suggests having one file per environment, such as main.local.bicepparam/main.dev.bicepparam/etc. However, I think there's a better way of doing this for projects that use CI/CD pipelines. Instead of duplicating the files, which, at best, requires one file for local testing and another that reads from environment variables, I'll show how we can transform the bicepparam file into the standard JSON format, using the build-params command.

What we want to do, is ensure that our Build Artifact ends up with the standard JSON parameters file, so we don't have to update anything on the Release side of things. This can be achieved easily with a task like this:

- task: Bash@3
  inputs:
    script: bicep build-params main.parameters.bicepparam 
    workingDirectory: ./Code
    targetType: inline

And we want to run this after our validation task, as we only care about this once everything looks good. In other words, we want:

trigger:
- main

pool:
  vmImage: ubuntu-latest
  name: 'Azure Pipelines'

steps:
- task: AzureCLI@2
  inputs:
    azureSubscription: 'BicepDeploymentConnection'
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: 'az bicep install'

- task: AzureCLI@2
  inputs:
    azureSubscription: 'BicepDeploymentConnection'
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: 'az deployment group validate --resource-group Blog --template-file main.bicep --parameters main.parameters.bicepparam'
    workingDirectory: ./Code

- task: Bash@3
  inputs:
    script: bicep build-params main.parameters.bicepparam 
    workingDirectory: ./Code
    targetType: inline

- task: CopyFiles@2
  inputs:
    Contents: 'Code/**'
    TargetFolder: '$(Build.ArtifactStagingDirectory)'
    
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

The command creates a file with the same name as the input, so we will end up with a main.parameters.json file inside the Code folder. Our CopyFiles@2 task will copy the generated JSON file, which is then picked up by the Release we created in the original post and updated to use the proper values for each environment we want.

Conclusion

The new Bicep parameter files are an excellent addition to the language as they make it much easier to write and validate parameters. Let me know in the comments section (or via email) your thoughts and questions, and if you would like me to create a follow up post with more advanced content.