Azure Bicep Updates — May 2026 Edition

Azure Bicep Updates — May 2026 Edition

The months are passing by this year, and we're already in May 2026. If you expected one release to land, you might be disappointed. Because actually, it was two!

The last release (v0.43.8) reverted one of the GA functionality retryOn because the backend changes against ARM weren't ready yet. Regardless, the team was able to push in two changes during this release, which will be covered later.

Besides the new retryOn decorator, this release was also packed with other features:

  • Two new functions you can use on resources.
  • Two new linter rules added.
  • One breaking change for Azure Container Registries (ACRs).
  • Richer output on snapshot command and another parameter added.

Let's get into it.

Another operation is "in progress"

In 2020, a user named rshariy created the Add "wait" and "retry" deployment options issue on GitHub. A clear description was given, and a ton of responses came along.

Now, the team has finally released something for it: the retryOn decorator. This decorator is quite simple and accepts one required parameter and one optional parameter. The first argument is the specific error code you want to retry on, and the second one will be how many times:

@retryOn(['<errorCode>'], <retryTimes>)

There are probably multiple ways to get the specific error code, but the two most useful are the already available list or using the method described here.

I could have posted a rough example of how you could use it on several resources, but if you go to the link above on the GitHub issue, you'll find plenty of them (or you encountered one already yourself).

Easier array and string expressions

The next addition in line are not the kind of features that make you rewrite an entire template. Instead, they're the kind of functions you start using once you know they exist. Bicep v0.43.1 added like() and distinct().

The like() function is used for pattern matching on strings, and yes, it does support the * wildcard. This is useful when you have established a naming convention, for example, in Azure. If all your production workloads start with prod-, you can use that directly in your template instead of passing another boolean parameter. More on that later.

Then the distinct() function does what the name already says: it returns a new array with duplicate values removed. The nice part about the function is the fact that it keeps the first occurrence order. You can use this function to generate resource child objects from arrays. Take IP rules, alert receivers, tags, or role assignments as an example for this.

To put it more in perspective, let's take a real-world example. Imagine you deploy storage accounts from a shared module. You know that storage accounts in production should get zone-redundant storage, while non-production storage accounts should stay locally redundant. You also want to remove duplicates from an allowed IP list, as these might come from different teams:

param location string = resourceGroup().location
param workloadName string

param allowedIpAddresses array = [
	'203.0.113.10'
	'198.51.100.25'
	'203.0.113.10'
]

var isProduction = like(workloadName, 'prod-*')
var uniqueAllowedIpAddresses = distinct(allowedIpAddresses)

resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = {
	name: '${replace(workloadName, '-', '')}st001'
	location: location
	sku: {
		name: isProduction ? 'Standard_ZRS' : 'Standard_LRS'
	}
	kind: 'StorageV2'
	properties: {
		minimumTlsVersion: 'TLS1_2'
		allowBlobPublicAccess: false
		publicNetworkAccess: 'Enabled'
		networkAcls: {
			defaultAction: 'Deny'
			bypass: 'AzureServices'
			ipRules: [for ipAddress in uniqueAllowedIpAddresses: {
				action: 'Allow'
				value: ipAddress
			}]
		}
	}
}

output productionWorkload bool = isProduction
output configuredIpRules array = uniqueAllowedIpAddresses

If you established that Azure naming convention, you notice when workloadName containing prod-, will get the Standard_ZRS SKU. Standard_LRS is set when dev- is found. And even though you added 203.0.113.10 twice, the generated storage account only received it once in the ipRules collection.

Small functions, but practical in nature.

Better guardrails before you hit deploy

One thing I liked in this release is that it added not only new syntax. It added a couple of new guardrails around the authoring experience. Two new rules were introduced.

The first new rule is use-recognized-resource-type. It might not be an obvious one, but it focuses on the underlying reference() ARM function. With this rule, Bicep validates the resource type string you pass in against its built-in Azure type registry. If it's not found, the rule is fired upon.

An obvious example is a typo. Take the following example:

param storageAccountName string

output keys object = listKeys(resourceId('Microsoft.Storage/storageAcounts', storageAccountName), '2025-08-01')

For the sharp eyes amongst us, the storageAcounts is spelled wrong. The rule catches this early. But there's also a less obvious situation. That situation will be custom or private resource types.

Organizations can register extension resources or use types that are not part of the standard Azure resource provider set. Bicep will not know about them. The rule will warn on those as well, even though the usage is correct. Keep this in mind when you configure this rule.

The second rule is no-module-name and if you've followed the Bicep releases lately, you going to love this new linting rule. See, explicit module names aren't needed anymore, but many often still copy-paste them. For example, this pattern that might be copied can now be flagged by this rule:

module networking './modules/networking.bicep' = {
	name: 'networking'
	params: {
		location: resourceGroup().location
	}
}

The new preferred version is a little simpler:

module networking './modules/networking.bicep' = {
	params: {
		location: resourceGroup().location
	}
}

Now, before moving on to the next section, I have already covered the new command in the previous edition (snapshot). In this release, the command became richer.

First, as already mentioned in the introduction, the output from the snapshot becomes more predictable. Having a predictable output can help you not only in reviewing, but also in the values in later stages if you deploy through a DevOps pipeline.

What happens now is that Bicep resolves whatever it can at compile time and keeps the rest as ARM expressions. This template with four outputs will resolve:

output storageAccountType string = storageAccountType
output blobUri string = sa.properties.primaryEndpoints.blob
output objectWithExpressions object = {
  foo: 'bar'
  baz: 123
  qux: sa.properties.primaryEndpoints.blob
}
output objectLiteral object = {
  storageAccountType: storageAccountType
  baz: 123
}

Produces this snapshot as JSON:

"outputs": {
  "storageAccountType": "Standard_LRS",
  "blobUri": "[reference('...storageAccounts/storejs26ofeqkqeve', '2022-09-01').primaryEndpoints.blob]",
  "objectWithExpressions": "[createObject('foo', 'bar', 'baz', 123, 'qux', reference(...).primaryEndpoints.blob)]",
  "objectLiteral": {
    "storageAccountType": "Standard_LRS",
    "baz": 123
  }
}

This is, of course, what Bicep can predict.

Better yet, the second thing the snapshot command gets is a management group ID parameter. That's helpful for platform engineering teams managing management groups to subscription level. Take an example when assigning a policy:

targetScope = 'managementGroup'

param policyAssignmentName string
param policyDefinitionId string

resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
	name: policyAssignmentName
	properties: {
		policyDefinitionId: policyDefinitionId
	}
}

output policyAssignmentId string = policyAssignment.id

Then you can generate a snapshot with:

bicep snapshot main.bicepparam --mode overwrite --management-group-id platform --location westeurope

That will give you a much better reviewable artifact. Combining that with the predicted output gives you output values that can be handed off later.

Only know registries, please

This release also includes a breaking change. That breaking change is related to where Bicep modules can be restored from. Bicep now enforces a trusted registry list when restoring OCI modules. If it's not on that list, the restore is blocked, and you get a BCP446 error.

What are the trusted registries? Currently, these are the built-in ones:

  • *.azurecr.io
  • *.azurecr.cn and *.azurecr.us
  • mcr.microsoft.com and mcr.azure.cn
  • ghcr.io

Personally, I don't think many people are affected by this change. Only when you use a custom domain for Azure Container Registry endpoints, because those support mapping to your own domain name. So, imagine you have a module reference like br:moduleStore.myCompany.com/networking/hub:1.0.0, you're going to be impacted by the change. You've got to change up the part myCompany.com to .azurecr.io.

If you're not sure what the login server is for a registry, you can use Azure PowerShell for it:

# Requires Az.ContainerRegistry
$registry = Get-AzContainerRegistry -ResourceGroupName <resourceGroupName> -Name <registryName>

$registry.LoginServer

Conclusion

It's good to see that there are always steps taken to improve the security posture, even though it's a breaking change. Also, having those new guardrails in place and a better experience on the snapshot command makes the overall product just a little gentler to use.

To see the full release notice, I would recommend taking a look at the v0.43.1 release, which contains most of the new functionality, bug fixes, and improvements.