Posted on 7/5/2022
Some time ago I posted this guide on Bicep templates. As some time has passed I wanted to update and extend this in a full guide on Azure Bicep.
Bicep is a domain-specific language (DSL) developed by Microsoft to reduce the complexity of Azure ARM Templates. Bicep is transparent abstraction for Azure ARM Templates. It does not change the way Resources are deployed in Azure. It provides any functionality ARM templates have but aims to simplify the definition.
To use Azure Bicep, head over to GitHub and get the latest version of the Bicep CLI.
If you use winget package manager you can also use winget install -e --id Microsoft.Bicep
to install the CLI.
Alternatively, you can also use the Azure CLI to install bicep. However, this would change the commands used in this article. Please see the docs article for more information.
If you don't have VSCode already, I highly recommend using it as there is this amazing extension for Bicep.
You will start writing a .bicep
file. After you are finished, this will be converted using the Bicep CLI to Azure ARM Templates. Those templates will then be deployed using the same way, you would normally deploy ARM Templates.
The language uses similar modules as ARM (parameter, variables, resources, etc.). For someone who does know already ARM Templates, those might be quite familiar. However, certain details are different.
> Note: Different than json, bicep doesn't use commas to separate arrays and uses single quotes ' instead of double quotes " for strings. You will see some examples below
Similar to ARM we can provide parameters. Those values need to be specified when deploying.
Parameters will be declared using the param myParameterName TYPE
syntax. You can also define default values to a parameter as shown below.
param myString string
param myInt int
param myBool bool
param myArray array
param myObject object
param myDefaultString = 'default value'
param location = resourceGroup().location
Variables work also the same way as they do in ARM Templates. You specify the name and then an expression or value which will be determined before the deployment starts.
var myString = 'my string'
var location = ' resourceGroup().location
var myObject = {
first: 'some value'
}
Decorators provide a way to attach constraints and metadata to a parameter. They are specified above a parameter using the @expression()
syntax. Decorators can be used to specify a string as a password or define the length of a parameter
@secure()
@minLength(12)
@maxLength(26)
param myPassword string
@allowed([
'one'
'two'
])
The major part of the template is the resource part. Here you can define an Azure resource.
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
name: st
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
properties: {
supportsHttpsTrafficOnly: true
}
}
If you have used ARM or Terraform before, the SKU and Properties section and the options there will be quite familiar.
Something is different to ARM, that you will provide every resource a name. This name can be used to reference this resource. This name must be unique within one template file. It does not need to be unique in Azure, however. It is only used within Bicep.
Resources or properties can be referenced using the name we gave a resource.
resource myStorageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
name: 'stmyname'
location: location
kind: 'StorageV2'
}
The resource id of the storage account can now be referenced using ```myStorageAccount .Id``
In some cases a resource can also be specifying directly for example when referencing a parent resource or for example in a depends on.
Note: DependsOn
will automatically be set if you specify a resource. A ```DependsOn`` is only needed if the resource, which creation you want to await, is not directly referenced.
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2021-09-01' = {
name: 'default'
parent: myStorageAccount
}
Using the existing
keyword, a resource that is not defined in the template can be referenced. The referenced resource must be in the same deployment scope.
resource myKeyVault 'Microsoft.KeyVault/vaults@2021-10-01' existing = {
name: 'mykeyvaultname'
}
The KeyVault in this example, which was created can now be referenced. Which is quite easier than dealing with the resourceId()
function.
resource keyVaultAcl 'Microsoft.KeyVault/vaults/accessPolicies@2021-10-01' = {
parent: myKeyVault
name: 'replace'
properties: {
...
}
}
Loops can be used to iterate a resource definition multiple times
param storageAccounts array
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = [for st in storageAccounts : {
name: st
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}]
Conditions can be used to deploy a resource based on a value or expression.
param myBool bool
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = if(myBool) : {
name: 'stmyaccount001'
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}]
Outputs are working quite the same as they do in ARM.
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
name: 'stmyaccount001'
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
output myStorageAccountName string = storageAccount.name
Modules can be used to separate a template into multiple files. This can be useful to reduce the complexity of a single file. Within a main file, multiple modules can be defined and parameters could be passed between those using the Outputs option.
Each module will be one deployment
. So resources defined in multiple modules will be deployed separately.
This can cause challenges but could also resolve circle dependencies.
Note: Bicep supports sub-folders. You could use a subfolder to separate the module files.
A Module can be called using the following syntax
module serviceBus 'servicebus.bicep' = {
name: 'myDeploymentName'
params: {
myParam: 'myValue'
}
}
Note: A good practice is to use unique deployment names. Otherwise you will overwrite the deployment history of a resource group. This could be achieved by including the timestamp. E.g. using the following function utcNow('yyyMMdd-hhmmss')
Last but not least - Deployment Scopes
Scopes can be used to associate a scope for the deployment. Possible Scopes are Resource Group, Subscription or Management Group Deployments.
modules 'mgModule.bicep' = {
name: 'myDeploymentName'
scope: managementGroup('myManagementGroupId')
}
Bicep files can be compiled into ARM templates using the az cli with the bicep tools installed.
az bicep build --file myBicepFile.bicep
This will create a file called myBicepFile.json, which is the compiled bicep code as ARM Template. Afterwards, the ARM Template can be used for deployment.
az deployment group create --resource-group exampleRG --template-file myBicepFile.bicep --parameters storageName=uniquename
The same can be achieved in Azure DevOps Pipelines. The following task will compile the bicep file.
- task: AzureCLI@2
displayName: 'Build Bicep file'
inputs:
azureSubscription: $(azureServiceConnection)
scriptType: 'pscore'
scriptLocation: inlineScript
inlineScript: az bicep build --file $(Build.SourcesDirectory)/myBicepFile.bicep
This file can then be deployed using the AzureResourceGroupDeployment
task.
- task: AzureResourceGroupDeployment@2
displayName: 'Azure Deployment: Deploy ARM Template'
inputs:
azureSubscription: '$(azureServiceConnection)'
resourceGroupName: '$(resourceGroupName)'
location: '$(resourceGroupLocation)'
csmFile: '$(System.SourcesDirectory)/myBicepFile.json'
deploymentMode: Incremental