13 Feb 2022

How to build a bicep module library with proper versioning

In this post I’m going to show how to create a bicep module library with a proper versioning and CI integration.

Why create an own module library

Despite the fact that you find a lot of useful templates in bicep repository on Github, you may want to create your own reference library for your organization or customers. Using modules makes your bicep templates much more reusable and modular

Different Possibilities

So let’s talk about how to create a module library There are different ways to achieve this, each with it’s pros and cons.

  • Dedicated Git repository, with Git submodules
  • Azure Template Spec
  • Azure Container Registry

In this post I’m focusing on the ACR. Usually I’m using an Azure Container Registry to store and pull container images for AKS or ACI. But since bicep version 0.4.1008 or azure cli version 2.31.0, an ACR can be used to store bicep templates. This allows us to store bicep modules centrally and with a proper versioning using artifact tags.

Getting started

Bicep Version

First you need to make sure, you have the correct bicep version installed on your dev machine. I’m using the bicep extension of azure cli.

az bicep version
az bicep upgrade

Create Git Repository

Create a new Git Repo in Azure DevOps, Github or any other compatible platform

Create a proper folder structure in the Repo

Create subfolders, matching the resource provider name or the resource type. Although this is an optional step, it helps organizing your modules Such as:

  • Compute
  • Network
  • Security
  • Storage

..and so on.

Create a container registry

Create a container registry where you want to store the modules. As an example I’ve added a bicep template.

param name string
param location string

@allowed([
  'Basic'
  'Standard'
  'Premium'
])
param sku string
@allowed([
  'Enabled'
  'Disabled'
])
param publicNetworkAccess string


resource acr 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = {
  name: name
  location: location
  identity:{
    type:'SystemAssigned'    
  }
  sku:{
    name:sku
  }
  properties:{
    publicNetworkAccess:publicNetworkAccess
    zoneRedundancy: 'Disabled'    
  }
}

output registryId string = acr.id

Deploy the registry

az deployment group create --resource-group <your RG> --name bicepacr --template-file .\myAcr.bicep --parameters name=myacr001 --location=westeurope

Assign push permissions to your service principal

az role assignment create --assignee <object id of your app registration> --role "ACR Push" --scope <registryId>

Assign pull permissions to your devops engineers

az role assignment create --assignee <object id of your AAD group> --role "ACR Pull" --scope <registryId>

Create bicep config file

  • In the root directory of the repo create a file named: bicepconfig.json
  • Edit the file and add the following lines
{
  "moduleAliases": {
      "br": {
        "myModules": {
            "registry": "myacr001.azurecr.io",
            "modulePath": "bicep/modules"
        }
      }
  }
}

This creates an alias, which can be used in master template for referencing the modules in the registry.

Create CI pipeline

Create a CI pipeline for continous build and push of the modules.


trigger:
- "*"

variables:
  isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
  registryLoginSrv: myacr001.azurecr.io
  serviceConnection: bicepbuild-connection
  buildOnlychangedModules: 'true'

jobs:
  - job: Build
    displayName: Build and publish Bicep Modules
    pool:
      vmImage: 'ubuntu-latest'

    steps:
    - task: AzureCLI@2
      displayName: 'Get changed files'
      inputs:
        azureSubscription: $(serviceConnection)
        scriptType: 'pscore'
        scriptLocation: 'inlineScript'
        failOnStandardError: true
        inlineScript: |
          Write-Host "BuildOnlyChangedModules: $(buildOnlychangedModules)"
          $targetfolder = "$(Build.StagingDirectory)" + "/"
          function CopyFiles{
              param( [string]$source )

              $target = $targetfolder + $source

              New-Item -Force $target
              copy-item $source $target -Force
          }

          If ("$(buildOnlychangedModules)" -eq 'true') {
            $changes = git diff --name-only --relative --diff-filter AMR HEAD^ HEAD .
          }
          else {
            $changes = Get-ChildItem $(Build.SourcesDirectory) -Filter "*.bicep" -Recurse
          }
          $changes
          if ($changes -is [string]){ 
            CopyFiles $changes 
          }
          else {
              if ($changes -is [array])
              {       
                  foreach ($change in $changes)
                  { 
                    CopyFiles $change 
                  }
              }
          }


    - task: AzureCLI@2
      displayName: 'Build Bicep Files'
      inputs:
        azureSubscription: $(serviceConnection)
        scriptType: 'pscore'
        scriptLocation: 'inlineScript'
        failOnStandardError: true
        inlineScript: |
          az bicep install
          az bicep version
          $bicepFiles = Get-ChildItem $(Build.StagingDirectory) -Filter "*.bicep" -Recurse
          Foreach ($file in $bicepFiles) {
            write-host "Building: $($file.FullName)"
            az bicep build --file $file.FullName
          }

    - task: AzureCLI@2
      condition: and(succeeded(), eq(variables.isMain, 'true'))
      displayName: 'Push Bicep Files to Registry'
      inputs:
        azureSubscription: $(serviceConnection)
        scriptType: 'pscore'
        scriptLocation: 'inlineScript'
        failOnStandardError: true
        inlineScript: |
          az bicep install
          az bicep version
          $bicepFiles = Get-ChildItem $(Build.StagingDirectory) -Filter "*.bicep" -Recurse
          Foreach ($file in $bicepFiles) {
            $folderName = (($file.FullName -split '/')[-2]).tolower()
            $fileName = (Split-Path -Path $file.FullName -LeafBase).tolower() 
            $targetRepository = "/bicep/modules/$folderName/$fileName"
            write-host "pushing: $fileName with Version: $(Build.BuildNumber)"
            az bicep publish --file $file.FullName --target br:$(registryLoginSrv)$($targetRepository):$(Build.BuildNumber)
          }

This pipeline will…

  • Build the changed bicep files at each commit
  • Push the changed bicep files to the registry on each pull request to the main branch, using the build number as a version tag

Make sure you protect the main branch of your repo and add a build validation check. With this you can make sure, only successfully compiled modules land in your registry.

Use the modules in your main bicep files

targetScope = 'subscription'
var subscriptionId string = '9c8c3766-89ae-4acb-9c23-5f1deb8a70e7'

resource myRG 'Microsoft.Resources/resourceGroups@2021-04-01' existing = {
  name: 'myTestResourceGroup'
  scope:subscription(subscriptionId)
}


module uidAgic 'br/myModules:security/usermanagedidentity:20220125.3' = {
  scope: myRG
  name: 'UserManagedIdentityAGIC'
  params: {
    identityName: 'd-umi-aks-agic'
    location: location
  }
}

More Information: https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/private-module-registry?tabs=azure-powershell https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/template-specs?tabs=azure-powershell

As always, if you have comments, questions or you find bugs. Feel free to reach out via Github / Twitter or LinkedIn.