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.