Azure configuration per environment in Bicep with types and functions

March 16, 2024

Azure configuration per environment in Bicep with types and functions Sneak peak of the final result using types and functions.

I'm using Bicep files for deploying Azure resources to multiple environments. Every environment has it's own demands and requirements. For example, the QA environment is allowed to scale down to zero instances, while the production environment should always have at least one instance running.

To accomplish that sort of configuration, I've been following the Configuration Set Pattern by Microsoft:

Rather than define lots of individual parameters, create predefined sets of values. During deployment, select the set of values to use.

A typical Bicep file using the pattern looks like this:

@allowed([ 'dev', 'qa', 'prod' ])
param environment string

var environments = {
  dev: {
    apiMinimumCapacity: 0
    blobStorageTier: 'Cool'
  },
  qa: {
    apiMinimumCapacity: 0
    blobStorageTier: 'Cool'
  },
  prod: {
    apiMinimumCapacity: 1
    blobStorageTier: 'Hot'
  }
}

resource appServicePlan 'Microsoft.Web/...' = {
  name: 'appServicePlan'
  sku: {
    minimumCapacity: environments[environment].apiMinimumCapacity
  }
}

(...)

This works and is easy to understand. This will prevents the excessive use of a lot of different parameters and makes it more clear what the configuration is for.

But there are some downsides to this approach...

  • There is no type checking for the configuration. Therefore it's easier to make mistakes and harder to maintain in the long run.
  • Duplicated values: if you have the same configuration for multiple environments, you have to duplicate it (e.g. apiMinimumCapacity).
  • If you want to add a new environment, you have to add every value to that environment.

A solution with types and functions

Recently the Bicep team has added support for user-defined functions and user-defined data types. This allows us to create our own custom types and custom functions. With this we could:

  • Create a type for the environment and utilities to retrieve the correct environment value.
  • Create a function which returns the correct typed configuration.

1. Defining the types and utility functions

We start with defining the types and utilities in the settings.bicep file:

@export()
type environmentType = 'prod' | 'qa' | 'dev'

func envValueString(env environmentType, value {
  default: string
  prod: string?
  qa: string?
  dev: string?
}) string  => contains(value, env) ? value[env] : value.default

func envValueInt(env environmentType, value {
  default: int
  prod: int?
  qa: int?
  dev: int?
}) int => contains(value, env) ? value[env] : value.default

The functions envValueString and envValueInt are used to retrieve the correct value for the environment. If the value is not defined for the environment, it will return the default value. This way we can define the default value and override it for specific environments which prevents duplication of values.

2. Function to retrieve the environment configuration

Now we can define the configuration type and the function to retrieve the configuration. We use the utility functions to retrieve the correct values for the given environment.

We add the following to the settings.bicep file:

type configurationType = {
  instanceCount: int
  blobStorageTier: string
}

@export()
func getConfiguration(env environmentType) configurationType => {
  instanceCount: envValueInt(env, {
    default: 0
    prod: 1
  })
  blobStorageTier: envValueString(env, {
    default: 'Cool'
    prod: 'Hot'
  })
}

3. Using the configuration in the main Bicep file

In the main Bicep we will be using the types and function defined in the settings.bicep file.

The environmentType type is used to define the environment parameter. And the getConfiguration function is used to retrieve the correct configuration for the environment.

Example of the main Bicep file:

import { environmentType, getConfiguration } from './settings.bicep'

param environment environmentType

var configuration = getConfiguration(environment)

resource appServicePlan 'Microsoft.Web/...' = {
  name: 'appServicePlan'
  sku: {
    minimumCapacity: configuration.apiMinimumCapacity
  }
}

(...)

The configuration is now type checked and we can easily add new environments without duplicating a lot of configuration. The settings.bicep file can be shared across multiple Bicep files and can be used to define the configuration for all environments in one place.