Bicep | User Defined Types

Problem Space

Over the years of developing Infrastructure as Code (IaC) with either ARM templates or Bicep (since it was released in 2020), I have made it my best practice where possible to use well-defined base type parameters (Strings | Integers | Booleans) so that the templates are usable and maintainable by collaborators apart from myself. This usually equated to where possible avoiding the use of Object and Array parameters, although in many cases the use of these types was inevitable given the complexity of the infrastructure and resources being deployed.

In the situations where I needed to make use of Object or Array parameters, I would heavily rely on defining attributes, comments, and defaults to allow the template to retain some kind of reusability and maintainability, such as:

Example: Array of Objects

@description('Array of API operations')
@minLength(1)
param apimAPIOperations array = [
  {
    name: '' // Name of API Operation
    displayName: '' // Friendly Name of API Operation
    method: '' // HTTP Method -  GET | POST | PUT | DELETE | PATCH
    lgWorkflowName: '' // Name of the Backing Logic App Workflow
    lgWorkflowTrigger: '' // Name of the Backing Logic App Workflow HTTP Trigger
  }
]

Although this approach provides nice documentation around what is expected of the parameter,

  • There is no enforcement on the object structure allowing users to pass in a variation of the object causing issues further on.
  • The parameter has a default value for documentation purposes, but if a user is unaware, they might use the default, again resulting in issues further on.
  • There is no intellisense when using the parameter. You will completely rely on the fact that you have referenced the properties and structuring within correctly.

I promise, its not all doom and gloom, the light at the end of the tunnel has arrived…

My New Best Practice

As of December 2023 Bicep Version 0.24.24 User Defined Types are no longer experimental! Bicep CLI version 0.12.X or higher is required to use this feature.

⚠️ NOTE:

When transpiled into ARM, the user-defined types uses the new Language Version 2.0.

  • The current release of the Azure Resource Manager Tools extension for Visual Studio Code does not recognize the enhancements made in languageVersion 2.0.
  • The Azure Portal Custom deployment does not currently recognize the enhancements made in languageVersion 2.0.

User Defined Types allow us to define our own custom types with ambient types Strings | Integers | Booleans, primitive literals for validation (allowed options) = 'bicep' | 'arm' | 'azure' and markings for optional properties ?. There is more configurations and abilities with this feature so for more information see User-defined data types in Bicep.

User Defined Types removes my problem space. I can now create a defined type that represents my complex object or array. This new Type can then be used on a parameter with full intellisense support and validation so incorrect usage is flagged early with a Fail Fast approach.

The problem case example shown earlier can now be represented as follows:

// ** User Defined Types **
// ************************

@description('Configuration properties for setting up a LG App stnd APIM API Operation')
@metadata({
  name: 'Name of the API Operation'
  displayName: 'User friendly name of the API Operation'
  method: 'The API Operations HTTP method'
  lgWorkflowName: 'Name of the Standard Logic App Workflow to use for the Operation Backend'
  lgWorkflowTrigger: 'Name of the Workflow HTTP Trigger'
})
@sealed()
type apimAPIOperation = {
  name: string
  displayName: string
  method: 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE'
  lgWorkflowName: string
  lgWorkflowTrigger: string
}

@description('One or more APIM API Operations to configure')
@minLength(1)
type apimAPIOperationArray = apimAPIOperation[]

// ** Parameters **
// ****************

@description('Array of API operations')
param apimAPIOperations apimAPIOperationArray

The example above shows the use of:

  • @description decorator to provide a high-level summary of the type

  • @metadata decorator being used to document the object properties.

  • @sealed decorator being used to mark the object parameter as only permitting properties specifically included in the type definition.

  • Primitive Literals for Validation, the value specified can only be one of the following: method: 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE'.

  • Union of types by creating an array type of a user-defined object type:

    type apimAPIOperationArray = apimAPIOperation[]

Have a go and see if this also becomes your new best practice. For more configurations and abilities with this feature see User-defined data types in Bicep.

For the original version of this post see Andrew Wilson's personal blog at Bicep | User Defined Types