Intro

In this blog post, I want to get my Bicep modules under source control. They are already in this repository, but it would be nice to use them in other scenarios as well, without copying them each time. Suppose I copy and paste my modules between repositories. In that case, I also have to update them everywhere, so in this post, I will move my modules out of the repository and into an Azure Container Registry. When I move them there, I can update each module individually and then reference the version I need for each scenario.

Prerequisites

The only prerequisite for this post is that you can deploy resources to Azure.

Creating the Azure Container Registry

The first thing I need to do is create a new Azure Container Registry (if you already have one, you can use that). Since we are doing everything as code, I will deploy this new registry using Bicep as well.

I will deploy the container registry to an existing resource group. The Bicep code below is creating the container registry.

@minLength(3)
param name string = 'level6'
param project string = 'AVD'

resource cr 'Microsoft.ContainerRegistry/registries@2025-11-01' = {
  name: 'acr${name}'
  location: resourceGroup().location
  sku: {
    name: 'Standard'
  }
  properties: {
    adminUserEnabled: false
    policies: {
      quarantinePolicy: {
        status: 'disabled'
      }
      trustPolicy: {
        status: 'disabled'
      }
    }
  }
  tags: {
    environment: name
    project: project
  }
}

After deployment, I have the new Azure Container Registry available in Azure.

Moving Bicep modules into a new folder

I want to publish my Bicep modules into the new container registry, but before I do that, I will move them into a new folder under the “Infrastructure/Bicep” path. I am moving the files so I can edit them and create new versions. If this were a production environment, I would place the files in a separate repository and dedicate that repository to module maintenance.

The image below illustrates the new folder structure.

The Level6 folder I am working in for this blog post now contains these files.

Publishing Bicep modules to Azure Container Registry

Publishing modules to Azure Container Registry is straightforward. I built a small PowerShell script that loops through the files in the modules folder and then publishes them to Azure Container Registry.

I use dates for tagging my modules, but this can also be in other formats, like v1.0. I like the date because I can see the creation date and then figure out whether I need to update the module or check if I already created a newer version.

Below is the PowerShell script I use for publishing my Bicep modules.

$Timestamp = Get-Date -Format "yyyy-MM-dd"
$Modules = Get-ChildItem -Path . -Filter *.bicep -Recurse
$Registry = "acrcloudninjalevel6"

foreach ($Module in $Modules) {
    $FilePath = $Module.FullName
    $ModuleName = $Module.BaseName
    
    # Publish with timestamped tag
    Write-Host "Publishing $ModuleName with tag $Timestamp..."
    az bicep publish --file $FilePath --target "br:${Registry}.azurecr.io/${ModuleName}:${Timestamp}" 
}

Using Bicep modules from Azure Container Registry

Now that I have my modules inside the Container Registry, I can update my deployment to use these instead of the local files I had before.

The two code snippets below show that I have changed the module path from “Modules/avd_hostpool.bicep” to “br:acrcloudninjalevel6.azurecr.io/avd_hostpool:2025-12-06”, and that is all there is to it. One thing to keep in mind, though, is that you will need permissions to the Container Registry, so if you deploy using a pipeline, this identity will need ArcPull access to that registry.

Before the change.

module avd_hostpool 'Modules/avd_hostpool.bicep' = {
  name: 'avd_hostpool'
  scope: rg_hostpool
  params: {
    location: location
    name: name    
    tags: tags
    vm_login_principal_id: entra_group_id
  }
}

After the change.

module avd_hostpool 'br:acrcloudninjalevel6.azurecr.io/avd_hostpool:2025-12-06' = {
  name: 'avd_hostpool'
  scope: rg_hostpool
  params: {
    location: location
    name: name    
    tags: tags
    vm_login_principal_id: entra_group_id
  }
}

Updating the modules and switch version in the code

I noticed there are some new API versions I can use in the Bicep resources I have in my modules. These new versions make it a good time to update the modules and test for any breaking changes. I have switched the AVD resources to use API version 2025-04-01-preview instead of 2024-08-08-preview.

Next, I have run my script to publish the modules, so they have now changed from 2025-12-06 to 2025-12-07. The image below shows what versions I now have in the Container Registry.

Next, I can update my main.bicep file to use this version.

targetScope = 'subscription'

@allowed([
  'Production'
  'Test'
])
param environment string = 'Production'
param location string = 'WestEurope'
param name string = 'level6'
param tags object = {
  Owner: 'Martin'
  Environment: environment
}
param domain_type string = 'EntraID'
param domain_guid string = 'd86cfa45-fb62-496f-8955-1ae7bcb4e0d8'
param domain_name string = 'cloudninja.nu'

param entra_group_id string = '2e822894-7b05-4c0a-9698-1c04ea8ec1cc' // ACC_AVD_Users

resource rg_hostpool 'Microsoft.Resources/resourceGroups@2024-07-01' = {
  name: 'rg-${name}'
  location: location
  tags: tags
}

resource rg_shared_services 'Microsoft.Resources/resourceGroups@2024-07-01' = {
  name: 'rg-${name}-shared-services'
  location: location
  tags: tags
}

module avd_hostpool 'br:acrcloudninjalevel6.azurecr.io/avd_hostpool:2025-12-07' = {
  name: 'avd_hostpool'
  scope: rg_hostpool
  params: {
    location: location
    name: name    
    tags: tags
    vm_login_principal_id: entra_group_id
  }
}

module avd_shared_services 'br:acrcloudninjalevel6.azurecr.io/avd_shared_services:2025-12-07' = {
  name: 'avd_shared_services'
  scope: rg_shared_services
  params: {
    location: location
    name: name    
    tags: tags
    create_storage_account: true
    create_key_vault: true
    desktop_dag: avd_hostpool.outputs.desktop_dag
    remote_app_dag: avd_hostpool.outputs.remote_app_dag
    domain_guid: domain_guid
    domain_name: domain_name
    domain_type: domain_type
  }
}

As shown in the image below, everything is still working as expected.

GitHub repository

I have uploaded all the code for this blog series to my public GitHub repository. You can find it here:

AVD on GitHub

Summary

In this post, I have improved my module handling and ensured that I can add new features to my modules without breaking any existing code. Using the Azure Container Registry to store and version my modules ensures I can make changes and test them in a controlled workflow. After I have done my testing, I can consider whether any older deployments I have can benefit from using the new features, and if so, I can then change the version in the code and redeploy these resources. When looking at the code, it will seem like a minimal change, but in terms of the overall workflow and reliability, this is a huge step forward. Creating modules provides the luxury of combining the desired resources for easy deployment, but it also enables the reuse of these modules, which can save a lot of time in the future. Suppose we don’t use versioning for these modules. In that case, problems such as missing-parameter errors or inability to use new features will occur.

I am planning to write another blog post on module versioning, since not everyone will want to use Azure Container Registry in their environment.

As always, feedback is welcome, and if I missed some points, please let me know.