If you have deployed a resource in Microsoft Azure as part of your CI/CD pipeline you have probably worked with ARM templates. These templates can be used to automate your resource deployment to Azure and help you to have consistent environments whether it’s for testing, development or production purposes. However, there are some shortcomings when it comes to complex environments especially when you have many resources and the dependency between them makes the templates to be either super busy, very complex, or unreadable.
For that Microsoft has introduced Bicep which is designed to overcome these issues and help you with your infrastructure as code setup.
Although there are some great features which make working with ARM templates a good experience such as functions, variables, nested templates etc, there is some room for improvements regarding below which has been raised by the community:
Bicep is a DSL (domain specific language) which can be used to write your Infrastructure as Code (IaC). Instead of writing ARM templates you write your code with Bicep and it will transpile it to ARM for you. It simplifies the authoring experience and addresses some of the issues we mentioned earlier. Compared to using JSON, Bicep can help you simplify the template definition a great deal.
Let’s see this using a simple example. Imagine you are trying to create a storage account in Azure, with ARM template this is the minimum you will need:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccountType": {
"type": "secureString"
}
},
"variables": {
"diagStorageAccountName": "[concat('diags', uniqueString(resourceGroup().id))]"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
"name": "[variables('diagStorageAccountName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "[parameters('storageAccountType')]"
},
"kind": "Storage"
}
]
}
Using Bicep it will be simplified to:
@secure()
param storageAccountType string
param location string = resourceGroup().location
var diagStorageAccountName = concat('diags', uniqueString(resourceGroup().id))
resource diagsAccount 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: diagStorageAccountName
location: location
sku: {
name: storageAccountType
}
kind: 'Storage'
}
You can see how much time and space you could be saving if you were to use Bicep.
When it comes to the benefits of using Bicep, there is a list published in our documentation:
Every Bicep resource will have the below syntax:
resource <symbolic-name> '<resource-type>@<api-version>` = {
//properties
name: 'bicepstorage2063'
location: 'northcentralus'
properties: {
//...sub properties
}
}
Where:
resource
: is a reserved keyword.symbolic name
: is an identifier within the Bicep file which can be used to reference this resource elsewhere.resource-type
: is the type of the resource you’re defining, e.g. Microsoft.Storage
.api-version
: each resource provider publishes its own API version which defines which version of the Azure Resource Manager REST API should be used to deploy this resource.properties
: these are the resource specific properties. For example every resource has a name
and location
. In addition some have sub properties which you can pass on.When we talk about infrastructure as a code and reusability of our templates, we definitely end up using parameters to customise our resources. Be its name, sku, username or password, we will need to change these per environment or application.
In a Bicep file you can define the parameters that need to be passed to it when deploying resources. You can put validation on the parameter value, provide default value, and limit it to allowed values. The format of a parameter will be such as below:
param <parameter-name> <parameter-type> = <parameter-value>
Where:
param
: is a reserved keyword.parameter-name
is the name of the parameter.parameter-type
: is the type of the parameter such as string
, object
, etc.parameter-value
: is the value of the parameter you’re passing in.Let’s review two examples to get a better understanding of the structure.
@minLength(3)
@maxLength(24)
param storageName string
In this example you’re limiting the storageName
parameter’s value length to be between 3 and 24 characters. Or:
@allowed([
'Standard_LRS'
'Standard_GRS'
'Standard_RAGRS'
'Standard_ZRS'
'Premium_LRS'
'Premium_ZRS'
'Standard_GZRS'
'Standard_RAGZRS'
])
param storageRedundancy string = 'Standard_LRS'
In this example you’re specifying the allowed values for the storageRedundancy
parameter and also provide the default value if nothing is provided during the deployment.
With ARM templates you had to use a separate file to pass the parameters during the deployments usually with a name ending in .parameters.json
. In Bicep you need to use the same JSON file to pass the parameters in:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageName": {
"value": "myuniquestoragename"
},
"storageRedundancy": {
"value": "Standard_GZRS"
}
}
}
Similar to parameters, variables play an important part in our templates, especially when it comes to naming conventions. These can store complex expressions to keep our templates clean and their maintenance simple. In Bicep variables are defined using the var
keyword:
var <variable-name> = <value>
Where variable-name
is the name of your variable. For example in our previous Bicep file we could have used a variable for our storage name:
var storageAccName = 'sa${uniqueString(resourceGroup().id)}'
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: storageAccountName
//...
}
Since we need a unique name for our storage account the uniqueString
function is used (Don’t worry about that for now). The point is that we can create variables and use them in our template with ease.
There are multiple variable types you can use:
Expressions are used in our templates for variety of reasons, from getting the current location of the resource group to subscription id or the current datetime.
The good thing is that ANY valid ARM template function is also a valid Bicep function.
param currentTime string = utcNow()
var location = resourceGroup().location
var makeCapital = toUpper('all lowercase')
To use conditions in your deployments you would use the if
function in ARM templates, however, that’s not supported in Bicep. Instead, you can leverage the ternary operator:
param globalRedundancy bool = true
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: storageAccountName
location: location
kind: 'Storage'
sku: {
name: globalRedundancy ? 'Standard_GRS' : 'Standard_LRS' // if true --> GRS, else --> LRS
}
}
ARM templates have an output section where you could send information out of your pipeline to be accessed within other deployments or subsequent tasks. In Bicep you have the same concept via the output
keyword.
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
//...
}
output storageId string = stg.id
This will return the storage id out to be used later.
In ARM templates if you wanted to deploy a resource multiple times you could leverage the copy
operator to add a resource n
times based on the loop count. In Bicep you have the for
operator at your disposal:
resource foo 'my.provider/type@2021-03-01' = [for <ITERATOR_NAME> in <ARRAY> = {...}]
Where ITERATOR_NAME
is a new symbol that’s only available inside your resource declaration.
param containerNames array = [
'images'
'videos'
'pdf'
]
resource blob 'Microsoft.Storage/storageAccounts/blobServices/containers@2019-06-01' = [for name in containerNames: {
name: '${stg.name}/default/${name}'
//...
}]
This snippet creates three containers within the storage account in a loop.
If you want to deploy a resource which is depending on an existing resource you can leverage the existing
keyword.
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' existing = {
name: storageAccountName
}
You won’t need the other properties since the resource already exists. You need enough information to be able to identify the resource. Now that you have this reference, you can use it in other parts of your deployment.
In ARM templates you had the concept of linked templates when it came to reuse a template in other deployments. In Bicep you have modules
. You can define a resource in a module and reuse that module in other Bicep files.
.
├── main.bicep
└── stg.bicep
In our stg
file you will define the resource, its parameters, variables, outputs, etc:
//stg.bicep
param storageAccountName
var storageSku = 'Standard_LRS'
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: storageAccountName
location: resourceGroup().location
kind: 'Storage'
sku: {
name: storageSku
}
}
And in the main
file you will reuse the storage account as a module using the module
keyword:
//main.bicep
module stg './storage.bicep' = {
name: 'storageDeploy'
params: {
storageAccountName: '<YOURUNIQUESTORAGENAME>'
}
}
output storageName array = stg.outputs.containerProps
You only need to pass the required properties which in case of our storage account is the name.
any
keywordThere might be some cases where Bicep throws a false positive when it comes to errors or warnings. This might happen based on different situations such as the API not having the correct type definition. You can use the any
keyword to get around these situations when defining resources which have incorrect types assigned. One of examples is the container instances CPU and Memory properties which expect an int
, but in fact they are number
since you can pass non-integer values such as 0.5
.
resource wpAci 'microsoft.containerInstance/containerGroups@2019-12-01' = {
name: 'wordpress-containerinstance'
location: location
properties: {
containers: [
{
name: 'wordpress'
properties: {
...
resources: {
requests: {
cpu: any('0.5')
memoryInGB: any('0.7')
}
}
}
}
]
}
}
By using any
and passing the value you can get around the possible errors which might be raised during the build or the validation stage.
In terms of tooling the support is the same if not better than the ARM templates.
VS Code comes with an official extension for Bicep. This extension gives you validation, intellisense, dot property access, snippets etc.
If you’re using GitHub Actions for your CI/CD pipeline, there is already a Bicep action created by our developer advocate Justin Yoo which you can use to build you bicep file and deploy it to Azure.
Bicep comes with a CLI that you can install locally on Windows, MacOS, and Linux. That gives you the ability to build and deploy your Bicep files with Azure CLI.
In short, I highly recommend using Bicep and improving your IaC and deployments. Of course if your ARM templates are too many, or very complex you might benefit from converting them more, but if you already have a streamlined pipeline with maintainable templates, you could keep them and create any new template using Bicep instead.