Refactoring Common Code with Azure Bicep User Defined Functions

In this article, we are going to learn about writing clean and reusable code in Azure Bicep.

 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.

Command Palette

It opens up a popup where you can choose Bicep: Create Bicep Configuration File as shown below.

Create Bicep Configuration

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.