March 5, 2024 Marco

Azure Bicep features you didn’t know about – Pt. 1

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!

, , ,
Marco Gerber

Marco

Senior Cloud Engineer, keen on Azure and cloud technologies.

Leave a Reply

Your email address will not be published. Required fields are marked *