After covering Loops and conditions in the last post, now let´s have a look at Function options.
Functions in Bicep are a handy tool and play a crucial role in dynamizing the code and reducing complexity. They can be thought of as building blocks that perform a specific task in a Bicep template.
There are two types of functions in Bicep – predefined functions by Microsoft and user-defined. The in-house functions are provided by Bicep to perform common tasks such as string manipulation, mathematical calculations or access to system data. Accordingly, they are very similar to their IaC counterparts such as Terraform.
Custom functions, on the other hand, are defined by the developer themselves and can be used to perform specific tasks beyond the built-in functions. For example, this can be a formula to find and share repeatedly needed and more complex individually created variable values. User defined functions are also available in ARM templates, but not in Terraform.
Testing the Functions
However, before we introduce the two options in more detail, let’s take a quick look at the possibilities of testing them for their results. Right now, an equivalent to Terraform like the terraform console exists in experimental state and looks promising, but is yet not stable and GA. But you can give it a try on the Bicep console with some details here:[1]
Fortunately, the alternatives are not complicated – a deployment must be initiated, but this is usually very fast without resource creation. The best way to do this is to use a separate bicep template, in which the user-defined functions in particular are first created and tested. Testing of the results is carried out, for example, with the help of output definitions. The “provisioning” of the template can then be done with one click, for example, in the deployment pane in VS code[2] . By the way, the extension can also accept a validation request. Otherwise, it will also work with the appropriate Azure CLI or Powershell command. Only the variant “Deploy Bicep File..” via the contex menu is of little use, as the outputs do not appear directly here.
If you work with arrays and objects as parameters or variables in the files, you will probably come across something unusual with Bicep: Within a line, the values are declared as you are used to from JSON notation, but if the array is displayed in multiple lines, the comma between the entries must be omitted:
var testArray2 = [
'abc'
'the cat'
'lies'
'in the snow'
]
Built-in features
Most of the functions brought along are usual suspects and therefore more or less self-explanatory. Many relate to the editing of texts or the processing of arrays, whereby lambda expressions are also supported, for example for filtering. Splits, index, search, max-min and others can be found as they are known from other languages. String interpolation as a kind of function for dynamic strings is also available in Bicep:
'sto${storagePrefix}08154711'
Among other things, the network functions can be practical. The cidrSubnet function, for example, can directly generate matching subnet CIDRs within the VNet for the automatic creation of these using a VNet specification, subnet size, and index:
output gensubnets array = [for i in range(0, 5): cidrSubnet('10.140.0.0/16', 24, i)]
[
'10.140.0.0/24'
'10.140.1.0/24'
'10.140.2.0/24'
'10.140.3.0/24'
'10.140.4.0/24'
]
parseCidr returns an object based on a network parameter, which contains, among other things, the first usable IP address of the VNet.
A very useful tool are also the scope functions. These provide objects with information about the respective queried context of the template, i.e. either the current tenant, a management group, the subscription or a resource group. With the exception of the tenant() function, all other parameters passed in can query not only the direct context related to the place of use, but also a specific one. To put it simply, a resource can be created in a different resource group than the one specified for the deployment itself. With the scope functions, the number of parameters for a Bicep template can be reduced and it can still remain dynamically in the application – when using resourceGroup().location , for example, the region parameter can be omitted, since the region of the group and resource is usually the same. In addition, they are widely used in the modularization described in the next article.
For example, with the help of scopes, a deployment can only be carried out in a specific subscription or in one that starts with “prod” – setups such as alerts or higher SKUs of the services can thus simply be activated only in the desired environments, the template remains universal.
output tenantOutput object = tenant()
output subscriptionOutput object = subscription()
output resourceGroupOutput object = resourceGroup()
output specificRG object = resourceGroup(subscriptionId, resourceGroupName)
output checkSubs bool = subscription().displayName == 'Visual Studio Enterprise Subscription'
output doDeploy bool = startsWith(subscription().displayName, 'prod')
Furthermore, it is worth taking a look at the resource-specific functions. This makes it possible, for example, to list the keys from a created storage account, because they may be needed directly for the configuration of another service:
output stoKeys object = storage.listKeys()
output stoKey string = storage.listKeys().keys[0].Value
The outputs here are only used for the simple representation of the syntax – this should of course be used in productive templates.
The resourceid() functions known from the ARM templatesare also available for direct use, but also packaged under a simple alias as an Id-property of the resources. So if the symbolic name is available, for example because the resource was defined in the same template, the property can simply be used:
output resourceId string = storage.id
Parameter Functions
Two basically very practical functions may cause some confusion, but they are not supported in the normal Bicep template, but only in a .bicepparam file. readEnvironmentVariable() can read an environment variable, and getSecret() can retrieve the value of an Azure KeyVault secret.
Where data is being worked with, utcNow() can be handy for time and date. However, the handling is also a bit special here, because this function is known in the Bicep template, but only works in the context of a stand value for a parameter – not for outputs or variables. But since, for example, when generating subscription budgets, a start date is not older than the current month, we can help ourselves with this approach for the 1st in the current month:
param startDate string = '${utcNow('yyyy-MM')}-01'
A complete list of functions can be found at[3].
User-defined functions and types
If the arsenal of in-house functions or data types is not enough for you, you can also define your own. The company’s own functions are particularly helpful if a certain individual logic for creating names or values for properties of the resources is to be made available in a reusable way. The definition is done with the keyword func, a name, any parameters and lambda syntax for the expression to be returned.
The following example uses a value for the environment to determine the storage SKU you need:
func getStoSKU(env string) string =>
'${env == 'dev' ? 'Standard_LRS' : env == 'test' ? 'Standard_GRS' : 'Standard_GZRS'}'
There are still a few limitations, among other things there are no default values for the parameters and variables cannot be accessed.
User defined types represent a kind of class definition. This can be created with the type keyword and there are very flexible options for the expression. Simple strings, integers, arrays, or objects with mixed-type properties, even within arrays, are possible. Specific value ranges can be determined and defined via the decorator description and other specifications for the attribute values. The charm then lies in the “instantiation” of the type, because the Intellisense in Visual Studio Code can help with this and there is a kind of type safety.
With the following definition of our type:
type storageProperties = {
@maxLength(24)
Name: String
sku: ('Standard_LRS' | 'Standard_GRS')
container: array?
}
We need a name no longer than 24 characters, two possible values for the SKU – which are proposed – and optionally (?) an array for the container names for the use in the parameter. If necessary properties were missing, a new one was added or the value ranges were violated, this would be visible directly in the VS Code.
param storageAccountConfig storageProperties = {
Name: 'test123456'
SKU: 'Standard_LRS'
container: ['container1','container2']
}
Import and export of types and functions
User-defined types or functions are usually not defined in each template itself. This can be done at a central location and for the application by a tandem of @export directly above the element to be published and an import statement at the point of use:
#Definition of the function
@export()
func getStoSKU(env string) string => ...
#Application template
import * as myFuncs from 'functions.bicep'
var stosku = myFuncs.getStoSKU('dev')
For more details on the custom applications, see[4] and [5].
Summary
The last parts of the series were all about loops, functions and types for dynamizing the code. These support us in working with Bicep to make the templates more readable and flexible and thus also to support automation. The code examples used in the article are stored in the public repository for the series[6]. In the further course of the series, we will then devote ourselves to the relationship between parent and child resources, imports and referencing. In addition, the various possibilities for creating modules, saving them and retrieving them from various sources as needed are highlighted.
[1] https://johnlokerse.dev/2025/12/01/experiment-prototype-and-validate-azure-bicep-with-the-bicep-console/
[2] https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deploy-visual-studio-code#deployment-pane
[3] https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions
[4] https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/user-defined-functions
[5] https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/user-defined-data-types#import-types-between-bicep-files-preview
[6] https://github.com/thomash0815/bicepseries/tree/main
