The longer and deeper you work with Azure Bicep, you’ll probably notice that you need more and more options for more granular customizations in order to automate end-to-end deployments. Let’s take a look at user-defined data types in Azure Bicep and what benefits they might bring.
User-defined data types
We already know the common data types for parameters and variables like string, array, object, etc. All those data types follow strict schemas. You may have already encountered the issue where you nicely parametrized your resources and modules and at some point want to hand over nested values in an array or object parameter, which has to be in an exact schema, i.e.:
param sku object = { 'name': 'Standard_RAGRS' 'tier': 'Standard' }
Soon you’ll figure that there is neither intellisense nor any hints of what the declaration might be as soon as you fill in the parameter values. Another issue I often encounter is the large section of repetitive and incrementing parameters when multiple resources of the same type get deployed. It might soon look something like this:
param storageAccount01Name string = 'demoudefdatatypes01' param storageAccount01AllowBlobPublicAccess bool = true param storageAccount01Kind string = 'BlobStorage' param storageAccount01SupportsHttpsTrafficOnly bool = true param storageAccount01Sku object = { name: 'Standard_RAGRS' tier: 'Standard' } param storageAccount02Name string = 'demoudefdatatypes02' param storageAccount02AllowBlobPublicAccess bool = false param storageAccount02Kind string = 'BlobStorage' param storageAccount02SupportsHttpsTrafficOnly bool = true param storageAccount02Sku object = { name: 'Standard_LRS' tier: 'Standard' } param storageAccount03Name string = 'demoudefdatatypes03' param storageAccount03AllowBlobPublicAccess bool = true param storageAccount03Kind string = 'BlobStorage' param storageAccount03SupportsHttpsTrafficOnly bool = false param storageAccount03Sku object = { name: 'Standard_ZRS' tier: 'Standard' }
Let’s take a look on how user-defined data types help us with those issues. As the name suggests, this feature allows us to create our very own data types which can help us in the following areas:
- Easier and faster development due to intellisense on parameters
- Reduce complexity by creating your own structures inside data types
- Reduce duplicated code by using data types across multiple Bicep files
- Increase readability due to clearer structures of parameters
- etc.
Let’s take the two examples from above and combine them into a new deployment using our own data type for the storage account definition. We create a module file for our storage account first (you don’t need to create modules for it to work, I just prefer working with modules).
Create a file modules/storageAccount.bicep
with the following code:
// PARAMETERS param location string param storageAccountName string param sku object @allowed([ 'BlobStorage' 'BlockBlobStorage' 'FileStorage' 'Storage' 'StorageV2' ]) param kind string param allowBlobPublicAccess bool param supportsHttpsTrafficOnly bool // RESOURCES resource sto 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: storageAccountName location: location sku: sku kind: kind properties: { accessTier: 'Hot' allowBlobPublicAccess: allowBlobPublicAccess supportsHttpsTrafficOnly: supportsHttpsTrafficOnly } }
Nice, we now have our module file. Next, create a new file main.bicep
. First we add our new data type definition, usually the type definitions stay at the end of the file:
// DATA TYPES type storageAccountDefinition = { @minLength(3) @maxLength(24) @description('Name of the storage account') storageAccountName: string @description('The SKU name. Required for account creation.') sku: { name: 'Premium_LRS' | 'Premium_ZRS' | 'Standard_GRS' | 'Standard_GZRS' | 'Standard_LRS' | 'Standard_RAGRS' | 'Standard_RAGZRS' | 'Standard_ZRS' tier: 'Standard' | 'Premium' } @description('Indicates the type of storage account.') kind: 'BlobStorage' | 'BlockBlobStorage' | 'FileStorage' | 'Storage' | 'StorageV2' @description('Allow or disallow public access to all blobs or containers in the storage account.') allowBlobPublicAccess: bool @description('Allows https traffic only to storage service if sets to true.') supportsHttpsTrafficOnly: bool }
We can now start with adding our parameters referencing our new data type:
targetScope = 'subscription' // PARAMETERS param location string = 'switzerlandnorth' param resourceGroupName string = 'demo-udef-datatypes' param storageAccount01 storageAccountDefinition = { sku: { name: 'Standard_RAGRS' tier: 'Standard' } kind: 'BlobStorage' allowBlobPublicAccess: true storageAccountName: 'demoudefdatatypes01' supportsHttpsTrafficOnly: true } param storageAccount02 storageAccountDefinition = { sku: { name: 'Standard_LRS' tier: 'Standard' } kind: 'BlobStorage' allowBlobPublicAccess: false storageAccountName: 'demoudefdatatypes02' supportsHttpsTrafficOnly: true } param storageAccount03 storageAccountDefinition = { sku: { name: 'Standard_LRS' tier: 'Standard' } kind: 'BlobStorage' allowBlobPublicAccess: false storageAccountName: 'demoudefdatatypes03' supportsHttpsTrafficOnly: true }
Pay attention when writing out the parameters, our new data type is now suggested next to the default data types:
All needed parameters and schemas are created automatically after selecting required-properties
:
Predefined values as described in the data type work aswell:
Let’s now add our modules including the parameters:
// RESOURCES resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { name: resourceGroupName location: location } // MODULES module sto01 'modules/storageAccount.bicep' = { scope: rg name: storageAccount01.storageAccountName params: { location: location storageAccountName: storageAccount01.storageAccountName kind: storageAccount01.kind sku: storageAccount01.sku allowBlobPublicAccess: storageAccount01.allowBlobPublicAccess supportsHttpsTrafficOnly: storageAccount01.supportsHttpsTrafficOnly } } module sto02 'modules/storageAccount.bicep' = { scope: rg name: storageAccount02.storageAccountName params: { location: location storageAccountName: storageAccount02.storageAccountName kind: storageAccount02.kind sku: storageAccount02.sku allowBlobPublicAccess: storageAccount02.allowBlobPublicAccess supportsHttpsTrafficOnly: storageAccount02.supportsHttpsTrafficOnly } } module sto03 'modules/storageAccount.bicep' = { scope: rg name: storageAccount03.storageAccountName params: { location: location storageAccountName: storageAccount03.storageAccountName kind: storageAccount03.kind sku: storageAccount03.sku allowBlobPublicAccess: storageAccount03.allowBlobPublicAccess supportsHttpsTrafficOnly: storageAccount03.supportsHttpsTrafficOnly } }
The entire main.bicep file should now look like this:
targetScope = 'subscription' // PARAMETERS param location string = 'switzerlandnorth' param resourceGroupName string = 'demo-udef-datatypes' param storageAccount01 storageAccountDefinition = { sku: { name: 'Standard_RAGRS' tier: 'Standard' } kind: 'BlobStorage' allowBlobPublicAccess: true storageAccountName: 'demoudefdatatypes01' supportsHttpsTrafficOnly: true } param storageAccount02 storageAccountDefinition = { sku: { name: 'Standard_LRS' tier: 'Standard' } kind: 'BlobStorage' allowBlobPublicAccess: false storageAccountName: 'demoudefdatatypes02' supportsHttpsTrafficOnly: true } param storageAccount03 storageAccountDefinition = { sku: { name: 'Standard_LRS' tier: 'Standard' } kind: 'BlobStorage' allowBlobPublicAccess: false storageAccountName: 'demoudefdatatypes03' supportsHttpsTrafficOnly: true } // RESOURCES resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { name: resourceGroupName location: location } // MODULES module sto01 'modules/storageAccount.bicep' = { scope: rg name: storageAccount01.storageAccountName params: { location: location storageAccountName: storageAccount01.storageAccountName kind: storageAccount01.kind sku: storageAccount01.sku allowBlobPublicAccess: storageAccount01.allowBlobPublicAccess supportsHttpsTrafficOnly: storageAccount01.supportsHttpsTrafficOnly } } module sto02 'modules/storageAccount.bicep' = { scope: rg name: storageAccount02.storageAccountName params: { location: location storageAccountName: storageAccount02.storageAccountName kind: storageAccount02.kind sku: storageAccount02.sku allowBlobPublicAccess: storageAccount02.allowBlobPublicAccess supportsHttpsTrafficOnly: storageAccount02.supportsHttpsTrafficOnly } } module sto03 'modules/storageAccount.bicep' = { scope: rg name: storageAccount03.storageAccountName params: { location: location storageAccountName: storageAccount03.storageAccountName kind: storageAccount03.kind sku: storageAccount03.sku allowBlobPublicAccess: storageAccount03.allowBlobPublicAccess supportsHttpsTrafficOnly: storageAccount03.supportsHttpsTrafficOnly } } // DATA TYPES type storageAccountDefinition = { @minLength(3) @maxLength(24) @description('Name of the storage account') storageAccountName: string @description('The SKU name. Required for account creation.') sku: { name: 'Premium_LRS' | 'Premium_ZRS' | 'Standard_GRS' | 'Standard_GZRS' | 'Standard_LRS' | 'Standard_RAGRS' | 'Standard_RAGZRS' | 'Standard_ZRS' tier: 'Standard' | 'Premium' } @description('Indicates the type of storage account.') kind: 'BlobStorage' | 'BlockBlobStorage' | 'FileStorage' | 'Storage' | 'StorageV2' @description('Allow or disallow public access to all blobs or containers in the storage account.') allowBlobPublicAccess: bool @description('Allows https traffic only to storage service if sets to true.') supportsHttpsTrafficOnly: bool }
This is just a small example of how to use user-defined data types. Use it in combination with conditions (if/else) and loops to further automate and optimize your Bicep deployments. Take a look at the documentation where you can find more information and examples for user-defined data types.
You can find all the examples in my GitHub repository. Feel free to download/clone it and utilize it for your own data types, cheers!