In this article, we are going to learn about writing clean and reusable code in Azure Bicep.
Overview
Learn how to use Bicep's new feature (in preview currently) called "User Defined Functions" to make deploying your cloud resources on Microsoft Azure easier. This blog will teach you a simple way to write code that you can reuse over and over again, making your projects more organized and easier to manage.
Whether you're new to Bicep or already familiar with it, this guide will show you how to use functions effectively for better Azure deployments.
Pre-requisites
As Bicep – User Defined Functions is in preview, please enable the same in the Bicep Config of your root folder. You can click on View Command Palette as shown below.
It opens up a popup where you can choose Bicep: Create Bicep Configuration File as shown below.
It creates a bicepconfig.json. You can delete the existing content and add the below JSON content.
{
"experimentalFeaturesEnabled": {
"userDefinedFunctions": true
}
}
Note. If you don’t want to delete the existing content, you can just add the lines from 2-4 of the above code to the bicepconfig.json.
Before we start creating the Bicep – User Defined Functions, let’s discuss a scenario where you could have duplicate code in multiple places.
Real-World Scenario
Let’s say you have to fulfill a requirement of creating a Storage account with different replication strategies based on the environment.
For non-production environments, you can opt for Locally Redundant Storage (LRS) for cost-effectiveness, while for production environments, you may prefer Geo-Redundant Storage (GRS) for higher resilience.
Please also note that we may have to create multiple storage accounts based on requirements
Author Bicep Code
Let’s author the below Bicep code which accepts the Storage Replication Type as a parameter.
storage. bicep
param pStorageAccountName string
param pLocation string = resourceGroup().location
param pStorageSKU string
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
name: pStorageAccountName
location: pLocation
sku: {
name: pStorageSKU
}
kind: 'StorageV2'
}
main.bicep
param pEnv string
param pLogicAppStorageAccountName string
param pFunctionAppStorageAccountName string
param pLocation string = resourceGroup().location
module fappStorage 'modules/storage.bicep' = {
name: 'fappStorage'
params: {
pStorageAccountName: pFunctionAppStorageAccountName
pLocation: pLocation
pStorageSKU: pEnv == 'prd' ? 'Standard_GRS' : 'Standard_LRS'
}
}
module lappStorage 'modules/storage.bicep' = {
name: 'lappStorage'
params: {
pStorageAccountName: pLogicAppStorageAccountName
pLocation: pLocation
pStorageSKU: pEnv == 'prd' ? 'Standard_GRS' : 'Standard_LRS'
}
}
In the provided code snippet, a logical condition has been implemented to determine if the environment is designated as 'PRD'. If the environment is indeed classified as 'prd', the parameter pStorageSKU is assigned the value 'Standard_GRS'; otherwise, it defaults to 'Standard_LRS'.
The logical condition code contains repetitive logic that can be streamlined. Let's refactor it using Bicep User Defined Functions to create cleaner and more concise code.
User Defined Function
Let’s create a new Bicep User Defined Function as shown below.
func GetStorageAccountKind(Env string) string => Env=='prd' ? 'Standard_GRS' : 'Standard_LRS'
module fappStorage 'modules/storage.bicep' = {
name: 'fappStorage'
params: {
pStorageAccountName: pFunctionAppStorageAccountName
pLocation: pLocation
pStorageSKU: GetStorageAccountKind(pEnv)
}
}
module lappStorage 'modules/storage.bicep' = {
name: 'lappStorage'
params: {
pStorageAccountName: pLogicAppStorageAccountName
pLocation: pLocation
pStorageSKU: GetStorageAccountKind(pEnv)
}
}
As you can see in the above code snippet, the logical condition is now moved to the Bicep User-Defined Function named GetStorageAccountKind and the same User-Defined Function is invoked in place of the logical condition.
In case if you want to change the logic, you just need to modify it in just one place. It’s clean and crisp.
Unfortunately, the function GetStorageAccountKind can be used only in the current bicep file. Fortunately, if you would like to re-use the User Defined Function across all other bicep files, then you can create a Bicep Module for the User Defined Function. Let’s create a new module called udf. bicep as shown below.
Udf. bicep
param pEnv string
func GetStorageAccountKind(Env string) string => Env=='prd' ? 'Standard_GRS' : 'Standard_LRS'
output StorageAccountKind string = GetStorageAccountKind(pEnv)
Please note that the StorageAccountKind is passed as an output parameter.
Now, we need to invoke the UDF. bicep module from the main. bicep.
main. bicep
param pEnv string
param pLogicAppStorageAccountName string
param pFunctionAppStorageAccountName string
param pLocation string = resourceGroup().location
module udf 'modules/udf.bicep' = {
name: 'udf'
params: {
pEnv: pEnv
}
}
module fappStorage 'modules/storage.bicep' = {
name: 'fappStorage'
params: {
pStorageAccountName: pFunctionAppStorageAccountName
pLocation: pLocation
pStorageSKU: udf.outputs.StorageAccountKind
}
}
module lappStorage 'modules/storage.bicep' = {
name: 'lappStorage'
params: {
pStorageAccountName: pLogicAppStorageAccountName
pLocation: pLocation
pStorageSKU: udf.outputs.StorageAccountKind
}
}
Summary
This article explores the concept of User Defined Functions (UDFs) in Bicep. It demonstrates how UDFs can enhance code reusability, readability, and maintainability in infrastructure deployment processes. Through practical examples and tips, readers will learn how to leverage UDFs to modularize their Bicep code and streamline Azure deployments.
Whether you're a beginner or an experienced developer, this guide equips you with the knowledge to maximize the potential of Bicep functions for smoother and more scalable Azure deployments.