For a long time I have been a huge fan of Terraform by Hashicorp for deploying my Azure cloud services. This is mainly due to finding ARM templates to be too verbose and cumbersome to work with - Microsoft’s response to these complaints is Bicep.
This week I decided to take a look at what all the fuss is about & see if I might replace Terraform with Bicep.
What is Bicep?
Bicep is a declarative language that is classified as a domain-specific language (DSL) for deploying Azure resources.
The goal of this language is to make it easier to write Infrastructure as Code (IaC) targeting Azure Resource Manager (ARM) using a syntax that’s more friendly than the JSON syntax of Azure ARM Templates.
Bicep works as an abstraction layer built on top of ARM Templates. Anything that can be done with Azure ARM Templates can be done with Bicep as it provides a "transparent abstraction" over ARM (Azure Resource Manager). With this abstraction, all the types, apiVersions, and properties valid within ARM Templates are also valid with Bicep.
Bicep is a compiled / transpiled language. This means that the Bicep code is converted into ARM Template code. Then, the resulting ARM Template code is used to deploy the Azure resources. This transpiling enables Bicep to use it’s own syntax and compiler for authoring Bicep files that compile down to Azure Resource Manager (ARM) JSON as a sort of intermediate language (IL).
The way that Bicep is transpiled into ARM JSON is similar to how there are many different languages that can be written in, then transpiled into JavaScript that can be run within the web browser. One popular example of this type of transpiled language is TypeScript. A transpiled language offers benefits of adding an abstraction layer to make it easier and / or more feature full to write code that then gets compiled down to IL code that gets executed. This is also similar to how C# and VB.NET code compile down to MSIL in .NET code.
In the development world, it’s common to encounter the use of transpiled languages. It’s also common in the DevOps world where YAML and JSON are converted between one or the other. Bicep offers some similarity in how it’s transpiled into ARM JSON. This enables you to use an alternative syntax and feature set for writing declarative Infrastructure as Code than the often-cumbersome ARM JSON syntax.
Bicep Benefits
- Support for all resource types and API versions.
- Better authoring experience using editors such as VS Code (you will get validation, type-safety, intellisense).
- Modularity can be achieved using modules. You can have modules representing an entire environment or a set of shared resources and use them anywhere in a Bicep file.
- Integration with Azure services such as Azure Policy, Templates specs, and Blueprints.
- No need to store a state file or keep any state. You can even use the what-if operation to preview your changes before deploying them.
- Bicep is open source with a strong community supporting it. All the binaries for the different supported operating systems can be downloaded from the official releases page of the Bicep open source project.
Bicep pre-requisites
The tooling is pretty much the same as for ARM templates. That means that you need the following:
- Either Azure PowerShell or Azure CLI. (I’ll use Azure CLI in this post, as I find it much more logical)
- The Bicep CLI (more on this in a second)
- Some form of text editor. I suggest VS Code as it can provide some pretty awesome help when working with Bicep templates
- Optional: The VS Code Bicep extension. This will give you superpowers when working with Bicep in VS Code
Note: I’m going to assume that you have the Azure CLI installed.
Bicep CLI
To be able to work with Bicep files instead of ARM templates, you need the Bicep CLI. This is the part of the tool chain that is responsible for transpiling Bicep files to and from ARM templates. Yes…to AND from! More on that later!
The Bicep CLI is installed by running:
az bicep install
Or…if you are using Azure CLI version 2.20.0 or above, you can just ignore that step, as the Bicep CLI will be automatically installed when you run a command that needs it. So, in most cases, you don’t need to do anything to get Bicep file support on your machine.
Note: If you are on an earlier version of the Azure CLI, I would recommend updating that, instead of manually installing the Bicep CLI.
To verify your Azure CLI version, you can run:
az version
{
"azure-cli": "2.34.1",
...
}
And to verify the installed version of the Bicep CLI you can run:
az bicep version
Bicep CLI version 0.4.1272
If you try running this command without having the Bicep CLI installed, you get an error message that says
Bicep CLI not found. Install it now by running "az bicep install".
And, as the error message says, you fix that by running az bicep install, or any Bicep related command that will automatically install it.
If you have an outdated Bicep CLI version, and want to update it to the latest and greatest, you just need to run:
az bicep upgrade
Once you have the Bicep CLI installed (or just want to ignore it and have the Azure CLI install it when needed), you need a text editor of some kind to edit the Bicep files.
VS Code and the Bicep extension
I would highly recommend using VS Code when working with Bicep files. The reason for this, besides it being light-weight, cross platform, fast and generally quite awesome, is the ability to install the Bicep extension that gives you extra help when working with Bicep files.
The Bicep extension is available from the marketplace. Just search for bicep and you will find it.
That’s actually all there is to it from a tooling point of view.
Bicep Syntax
Every Bicep resource will have the below syntax:
resource <symbolic-name> '<resource-type>@<api-version>` = {
//properties
name: 'ghostinthewiresstorage'
location: 'westeurope'
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.
Parameters
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(6)
@maxLength(21)
param storageName string
In this example you're limiting the storageName parameter's value length to be between 6 and 21 characters. Or:
@allowed([
'Standard_LRS'
'Standard_GRS'
'Standard_RAGRS'
'Standard_ZRS'
'Premium_LRS'
'Premium_ZRS'
'Standard_GZRS'
'Standard_RAGZRS'
])
param storageRedundancy string = 'Standard_GRS'
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"
}
}
}
Variables
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:
- String
- Boolean
- Numeric
- Object
- Array
Expressions
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.
Functions
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')
Output
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
Loops
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.
Existing keyword
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.
Modules
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
└── storage.bicep
In our storage file you will define the resource, its parameters, variables, outputs, etc:
//storage.bicep
param storageAccountName
var storageSku = 'Standard_LRS'
resource storage '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 storage './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.
The any keyword
There 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.
How to Create Bicep Files
As previously mentioned, developers can use Microsoft-provided Visual Studio Code extensions for the Bicep language to enhance the functionality that Bicep brings to the table. These extensions, specifically, provide language support and resource autocompletion to assist with creating and validating Bicep files, reducing coding errors, and making the writing of code more efficient.
One of the nice things with Bicep, compared to ARM templates, is the fact that you don’t need to add any form of "base structure" to make it a valid Bicep file. ARM templates require us to create a JSON root element. In Bicep, as long as the file extension is .bicep, it is considered a Bicep file.
Take a look at the following template Bicep code. Notice the compact code structure; it is maybe half the size of the typical ARM template. Bicep is smart enough to figure out if resources are dependent on each other. Additionally, Bicep knows it first needs to deploy appServicePlan and automatically adds the dependsOn part when it gets converted from Bicep to an ARM template. Here is the code:
param name string = 'ghostinthewires-bicep-webapplication'
param location string = resourceGroup().location
param sample string = 'ghostinthewires'
param sampleCode string = 'G1'
resource webApp 'Microsoft.Web/sites@2022-01-01' = {
name: name
location: location
properties: {
name: name
siteConfig: {
metadata: [
{
name: 'MY_TECH_STACK'
value: 'dotnetcore'
}
]
}
serverFarmId: appServicePlan.id
}
}
resource appServicePlan 'Microsoft.Web/serverfarms@2022-01-01' = {
name: name
location: location
properties: {
name: name
}
sample: {
Tier: sample
Name: sampleCode
}
}
The following code snippet is used by Bicep for deployment of your resources array, with the link to the template and a link to the parameters file if available.
"resources": [
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2022-01-01",
"name": "linkedTemplate",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "https://mystorageaccount.blob.core.windows.net/AzureTemplates/newStorageAccount.json",
"contentVersion": "1.0.0.0"
},
"parametersLink": {
"uri": "https://mystorageaccount.blob.core.windows.net/AzureTemplates/newStorageAccount.parameters.json",
"contentVersion": "1.0.0.0"
}
}
}
]
Once you have your fully formed Bicep file, we can verify that it is syntactically correct by building it. Building a Bicep file transpiles it to an ARM template.
To build your .bicep file, we can execute the following command:
az bicep build --file iac.bicep
How to Convert ARM Templates to Bicep
Bicep can be easily used to convert an ARM template to Bicep code. The command for this is az bicep decompile. It takes a JSON file as input and attempts to make it into Bicep.
To decompile ARM template JSON files to Bicep, use Azure CLI:
az bicep decompile --file AzureARM.json
Developers can export the template for a resource group and then pass it directly to the decompile command. Refer to the following example:
az group export --name "my_resource_group_name" > AzureARM.json
az bicep decompile --file AzureARM.json
CI/CD
If, like me, you're using GitHub Actions for your CI/CD pipeline, there is already a Bicep action created by Microsoft Developer Advocate Justin Yoo which you can use to build you bicep file and deploy it to Azure.
If you are using Azure Pipelines you can use the Azure CLI task as you would do from your laptop.
Conclusion
I find Bicep much nicer to work with than ARM Templates and looking at it from a purely Microsoft native standpoint, it would also be my bet for the future. Sure, ARM templates need to support pretty much any feature that Bicep uses, in some way. But I think the main focus from Microsoft, when it comes to the end-user experience, will go into Bicep.
However, back to my initial question:
Would I replace Terraform with Bicep ?
In a word, No!
Terraform is a different beast when compared to ARM and Bicep, even if the syntax is actually quite similar to the one used by Bicep.
Under the hood, it works in a completely different way, using the Azure REST API instead of talking directly to the Azure Resource Manager. The downside to this is that any new features being added to Azure will first have to be released in the REST API, then in the Go SDK, and finally in the Terraform Azure provider. A chain of events that might take a while.
Having that said, this is only really an issue if you are using bleeding edge features. If not, pretty much all other features are supported. The flip side to this is obviously that by using a provider-based system, Terraform is able to target a lot of different clouds and systems. Which in turn allows you to use your IaC not only for your Azure resources, but potentially for a bunch of different systems and clouds. Something that ARM/Bicep will never be able to do.
So, if you need to go outside the realm of Azure, where ARM/Bicep is not going to cut it, I still think Terraform is my favoured option. On the other hand, if you are strictly Azure focused, and have no other clouds/systems you want to integrate with, I think Bicep might still be a great option.
But keep in mind, there are also other tools such as Pulumi. An IaC tool that also utilizes the benefit of the provider-based architecture, but also adds the ability to use a real programming language when creating your desired state.
So I would urge you to try out the various options, MVP-style, and see what works for you!
Bonus tip: If you want to play around a bit more with Bicep, I suggest having a look at the Bicep learning path at Microsoft Docs. This will give you a deeper introduction to Bicep in an easy to digest format.
Important 😸
I regularly post useful content related to Azure, DevOps and Engineering on Twitter. You should consider following me on Twitter
Top comments (1)
good article! Would you choose bicep or terraform for a new project? assuming you have same knowledge of both.