Getting started with Azure Bicep
Authoring Infrastructure as Code templates, like ARM, just got easier. With Bicep, we can more easily manage and build our templates with a typed and IntelliSense-powered approach, and easily convert them to ARM templates when we need to deploy them. Here's a first look.
In this article, I'm breaking down the Bicep topic into a digestible format. I took it for a spin, and I have mostly great things to say about it.
Here's what to expect from this post:
- What is Bicep?
- What about existing ARM templates?
- Installing the necessary tools.
- Working with Bicep.
- Building a Bicep file.
- Parameters, Variables, Modules, Outputs, and Scopes.
- Taking a look at the output ARM template file.
- Convert from ARM JSON to Bicep
What is Bicep?
Infrastructure as Code is a popular way to manage your infrastructure, and we've often used ARM templates for this. However, ARM templates can get pretty complex and hard to manage. Bicep's idea is to make this easier by providing a DSL (Domain Specific Language) to abstract away some of the complexity.
With Bicep, you can author .bicep files, making them more readable and generally easier to compose. We can get away from shoehorning everything into the json files the ARM templates where it lives.
When you later do a deployment, the Bicep file gets converted into an ARM template, and this is what gets deployed in the end. So you can see the Bicep language as a way to abstract away the complexity of your ARM templates during the time of authoring. However, the underlying infrastructure still deploys your ARM templates.
What about my experience and knowledge around ARM templates?
The knowledge you have from authoring your ARM templates can be applied when you write your Bicep files.
It took about 1-2 minutes to author the below file, and it was EASY!
resource storage1 'Microsoft.Storage/storageAccounts@2020-08-01-preview' = {
name: 'tobiasStorageDemoBicep123'
location: 'westeurope'
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
accessTier: 'Hot'
allowBlobPublicAccess: false
allowSharedKeyAccess: false
minimumTlsVersion: 'TLS1_2'
networkAcls: {
defaultAction: 'Deny'
ipRules:[
{
action: 'Allow'
value: '152.44.26.251'
}
]
}
}
}
Here's the equivalent in json in the output ARM template.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"functions": [],
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2020-08-01-preview",
"name": "tobiasStorageDemoBicep123",
"location": "westeurope",
"sku": {
"name": "Standard_LRS"
},
"kind": "StorageV2",
"properties": {
"supportsHttpsTrafficOnly": true,
"accessTier": "Hot",
"allowBlobPublicAccess": false,
"allowSharedKeyAccess": false,
"minimumTlsVersion": "TLS1_2",
"networkAcls": {
"defaultAction": "Deny",
"ipRules": [
{
"action": "Allow",
"value": "152.44.26.251"
}
]
}
}
}
],
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.3.1.62928",
"templateHash": "8612494210110786020"
}
}
}
We can see the similarities quite easily. I have worked with ARM templates for years, and it seems like a relatively low threshold for me to get into authoring Bicep files. So far, I like it - and the winning factor when using Visual Studio Code is the full IntelliSense I have. I'm genuinely impressed.
Is it ready for prime time?
- As of this writing, the date is 2021-03-22, and the current release is 0.3.126.
Before digging into exploring Bicep, you might be here to see if you can replace your templates and processes to use Bicep instead of pure ARM.
I have already tried to build some of our more complex templates, but the language still doesn't support the things we use, and the conversion between ARM and Bicep can't handle most of our real-world templates.
Bicep could handle some of the starter-templates you find on GitHub. To figure out whether it can support your templates, you can use Microsoft's side-by-side tool: Bicep playground.
My advice is to keep watching as Bicep evolves. It's the early days of the language, but the team is making notable progress.
From the Bicep FAQ:
Is this ready for production use?
Yes. As of v0.3, Bicep is now supported by Microsoft Support Plans and Bicep has 100% parity with what can be accomplished with ARM Templates. As of this writing, there are no breaking changes currently planned, but it is still possible they will need to be made in the future.
- See the current list of current issues.
- See known limitations.
With that said, I'm keeping my eyes on this project, and I have started to use it for some of my lighter templates - and I like it. I see myself using primarily Bicep moving forward with new templates, and slowly phase out the existing ARM-only templates.
Install the tools
To work with Bicep, we should get the tools we need. I am a big fan of the Azure CLI, which I will use. You can also use other ways to install Bicep.
Installing Bicep CLI
If you already have the Azure CLI, you can utilize that to install bicep.
az bicep install
To make sure it works, just run az bicep version
. If you at any point run this command, you'll also be prompted if there is an upgrade available. Convenient.
Great. We're ready to run Bicep commands now. Let's move on.
Visual Studio Code extension for Bicep
In the Visual Studio Code marketplace, there's an extension named Bicep. Get it.
You can, of course, install it from inside Visual Studio Code, too.
Working with Bicep
Earlier in this post, I mentioned that Bicep converts a bicep file into the equivalent ARM template file. We'll look at how to work with the Bicep files, convert an existing ARM template into a Bicep file, and build a Bicep file to get the ARM json file.
In the end, you decide whether you want to abstract the complexities away with Bicep, or stick to ARM. I thought this could at least shed some light on how these various things work at first glance.
Working with a Bicep file
We already installed the Bicep extension for Visual Studio Code, so now we can make use of the sweet intellisense that it provides.
To define a resource in Bicep, the basic format looks similar like this:
resource <friendlyName> '<type@apiversion>' =
{
property1: 'value'
}
Making an example out of that, let's provision an Azure Key Vault using Bicep. First, we need to define the resource in the Bicep file according to the above format.
We'll see that IntelliSense helps us along the way. If you are familiar with an Azure Key Vault's properties and configuration, you'll probably recognize the syntax in this file.
We can also define the output variables that we want to collect after the deployment, and again we have full IntelliSense for most things we do.
Here's my very basic Key Vault bicep file:
resource kv 'Microsoft.KeyVault/vaults@2020-04-01-preview' = {
location: 'westeurope'
name: 'myKeyVaultDemoFromBicep1'
tags: {
'demo-delete': 'true'
}
properties: {
tenantId: subscription().tenantId
sku: {
family: 'A'
name: 'standard'
}
accessPolicies: [
]
}
}
output keyVaultUri string = kv.properties.vaultUri
output keyVaultSkuName string = kv.properties.sku.name
To build the bicep file, we'll use the Bicep CLI, which I've installed from the Azure CLI. Hence, my CLI syntax will be something like this:
az bicep build --files .\keyvault.bicep
Conveniently, I'm running this from within Visual Studio Code, and when it completes, you'll have your ARM template file in JSON:
Here's the ARM template produced from the Bicep file build command. It becomes clear that the Bicep file's definition syntax is cleaner than the ARM template, but in the end, we still get our ARM templates. We can verify and validate them the same ways we usually do with existing tools and processes.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"functions": [],
"resources": [
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2020-04-01-preview",
"name": "myKeyVaultDemoFromBicep1",
"location": "westeurope",
"tags": {
"demo-delete": "true"
},
"properties": {
"tenantId": "[subscription().tenantId]",
"sku": {
"family": "A",
"name": "standard"
},
"accessPolicies": []
}
}
],
"outputs": {
"keyVaultUri": {
"type": "string",
"value": "[reference(resourceId('Microsoft.KeyVault/vaults', 'myKeyVaultDemoFromBicep1')).vaultUri]"
},
"keyVaultSkuName": {
"type": "string",
"value": "[reference(resourceId('Microsoft.KeyVault/vaults', 'myKeyVaultDemoFromBicep1')).sku.name]"
}
},
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.3.126.58533",
"templateHash": "1802761384726456213"
}
}
}
Variables, Parameters, Modules, Outputs, and Scopes.
With Bicep, we can make use of various helpful features. Let's take a quick look at the variables, parameters, modules, outputs, and scopes. When I explored Bicep, these were the things I wanted to understand, so I can better fit Bicep into my workflows and processes.
Below, I will outline a few examples of these language features and finally share a template that makes use of these features together.
Bicep parameters
By defining parameters, we can make our templates more generic and reuse them across multiple deployments of the same type, much like in ARM.
Example: I want to accept the resource name and an API key to use in my template deployment. I am also specifying the apiKey variable's additional properties to tell the template that this needs to be a secure parameter to avoid it being visible in the UI and the deployment input fields.
// A parameter
param projectFriendlyName string
// A parameter with constraints
@minLength(3)
@maxLength(18)
param resourceName string
// A secure parameter
param apiKey string {
secure: true
}
When deploying the template, we can specify these parameters. Here's an example of what it looks like in the Azure Portal when deploying a custom template. The same applies when calling template deployment from code, CLI, or other APIs.
- Read more about parameters in the language definition: Bicep Parameters
Bicep variables
A variable in Bicep is used inside a template file, similar to variables in ARM. I usually define variables after defining my parameters, as I can then incorporate any parameter values into my variables.
var locationShortName = 'weu'
var locationLongName = 'westeurope'
var resourcesMetadata = {
name: '{0}-${locationShortName}-${replace(resourceName, '-', '')}'
location: locationLongName
}
In this case, I'm defining the variables for location and name. The name is based on the input parameter we previously defined and adds the type and location into the mix. Our naming goes like this in many of our production workloads: type-location-resourcename
, for instance, kv-weu-mykeyvault
.
We can then use these variables from our resource creation. This example relies on the parameters I set up previously and on the recently created variables.
Please note the replace function, where I'm replacing any hyphens with empty string because storage accounts don't allow anything but alphanum. I'm replacing the placeholder variable {0} with the actual type "st" for a storage account to follow our naming conventions.
// Resource: Define an Azure Storage Account
resource st1 'Microsoft.Storage/storageAccounts@2019-06-01' = {
// replace hyphens (not allowed in storage account names), replace type with "st" to follow our naming practices.
name: replace(replace(resourcesMetadata.name, '{0}', 'st'), '-', '')
location: resourcesMetadata.location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
- Read more about variables in the language definition: Bicep Variables
Bicep modules
We can split our resources into separate files with modules and call them from a single deployment file. This split can help make the authoring of templates less complex and more easily maintained; you can also more easily collaborate across teams with fewer merge conflicts. If you've ever had json files with thousand and thousands of lines and want to collaborate with many colleagues, you know what I mean.
A few things to know:
- Every bicep file can be consumed as a module. There's no specific format!
- The name property must be specified when consuming modules.
- To support cross-platform, we need to use the
./file.bicep
format (forward slash).
I've created a new file, maindeployment.bicep
, and I'm calling my other files as a module and passing in any parameters as I need. These are the very same files I defined earlier. No additional logic has been added to modularize them.
module kv './keyvault.bicep' = {
name: 'myKeyVault'
}
module storage './storage.bicep' = {
name: 'myStorage'
}
When I compile this maindeployment.bicep
file, it will consolidate the content from the modules and build the final ARM template file with all the content from all referenced templates or modules.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"functions": [],
"resources": [
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2019-10-01",
"name": "myKeyVault",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"functions": [],
"resources": [
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2020-04-01-preview",
"name": "myKeyVaultDemoFromBicep1",
"location": "westeurope",
"tags": {
"demo-delete": "true"
},
"properties": {
"tenantId": "[subscription().tenantId]",
"sku": {
"family": "A",
"name": "standard"
},
"accessPolicies": []
}
}
],
"outputs": {
"keyVaultUri": {
"type": "string",
"value": "[reference(resourceId('Microsoft.KeyVault/vaults', 'myKeyVaultDemoFromBicep1')).vaultUri]"
},
"keyVaultSkuName": {
"type": "string",
"value": "[reference(resourceId('Microsoft.KeyVault/vaults', 'myKeyVaultDemoFromBicep1')).sku.name]"
}
}
}
}
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2019-10-01",
"name": "myStorage",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"functions": [],
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2020-08-01-preview",
"name": "tobiasStorageDemoBicep123",
"location": "westeurope",
"sku": {
"name": "Standard_LRS"
},
"kind": "StorageV2",
"properties": {
"supportsHttpsTrafficOnly": true,
"accessTier": "Hot",
"allowBlobPublicAccess": false,
"allowSharedKeyAccess": false,
"minimumTlsVersion": "TLS1_2",
"networkAcls": {
"defaultAction": "Deny",
"ipRules": [
{
"action": "Allow",
"value": "152.44.26.251"
}
]
}
}
}
]
}
}
}
],
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.3.126.58533",
"templateHash": "16590741462662706834"
}
}
}
- Read more about modules in the language definition: Bicep Modules
Bicep outputs
With output declarations, we define the values to return post-deployment. Similar to ARM, we can define output variables in Bicep.
I usually define these near the end of the file, collectively. It makes it easy to see the expected outputs from the template deployment.
Here's a basic example. I want to return the URI and SKU name of the Key Vault I deployed.
output keyVaultUri string = kv.properties.vaultUri
output keyVaultSkuName string = kv.properties.sku.name
Similar to ARM, we can create output objects and return them.
// output variables from created resources.
output keyVaultUri string = kv.properties.vaultUri
output keyVaultSkuName string = kv.properties.sku.name
output storageAccountName string = st1.name
output storageAccountSku string = st1.sku.name
// output objects.
output deploymentResourceGroup object = resourceGroup()
When reviewing the deployment output in the Azure Portal or from the returned deployment outputs, we see the output we defined.
- Read more about outputs in the language definition: Bicep Outputs
Bicep scopes
In Azure Resource Manager, we have Scopes. With these scopes, we get to choose where we want to target our deployment. There are four scopes in ARM:
- Management Group
- Subscription
- Resource Group
- Resource
To learn about the scopes and their constraints, check out the docs about scopes in ARM.
- Read more: Scopes in ARM.
With Bicep, the default scope is a resource group. If we don't specify the scope, we automatically target a Resource Group.
We get lots of help from IntelliSense if we author our files with the extension for Visual Studio Code, and we can see the available scopes when we start to define the targetScope
:
NOTE as of 2021-03-22: Multi-scope definitions have not yet been implemented in Bicep.
- Read more about scopes: Resource Scopes.
Bringing it all together
To illustrate how Bicep can use these features together, here's a template that defines variables, accepts parameters, uses modules to split the logic, and defines a deployment's scope.
It is a basic template but should cover the concepts we just touched on.
// Scope
targetScope = 'resourceGroup'
// Parameters
@minLength(3)
@maxLength(18)
param resourceName string
// Variables
var locationShortName = 'weu'
var locationLongName = 'westeurope'
var resourcesMetadata = {
name: '{0}-${locationShortName}-${replace(resourceName, '-', '')}'
location: locationLongName
}
// Resource: Key Vault
module kv './keyvault.bicep' = {
name: 'keyVaultModule'
params:{
location: resourcesMetadata.location
name: replace(resourcesMetadata.name, '{0}', 'kv')
}
}
// Resource: Storage Account
module storage './storage.bicep' = {
name: 'storageModule'
params: {
location: resourcesMetadata.location
name: replace(resourcesMetadata.name, '{0}', 'st')
}
}
// output objects.
output deploymentResourceGroup object = resourceGroup()
You will get the following output:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"resourceName": {
"type": "string",
"maxLength": 18,
"minLength": 3
}
},
"functions": [],
"variables": {
"locationShortName": "weu",
"locationLongName": "westeurope",
"resourcesMetadata": {
"name": "[format('{{0}}-{0}-{1}', variables('locationShortName'), replace(parameters('resourceName'), '-', ''))]",
"location": "[variables('locationLongName')]"
}
},
"resources": [
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2019-10-01",
"name": "keyVaultModule",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"parameters": {
"location": {
"value": "[variables('resourcesMetadata').location]"
},
"name": {
"value": "[replace(variables('resourcesMetadata').name, '{0}', 'kv')]"
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"type": "string"
},
"name": {
"type": "string"
}
},
"functions": [],
"resources": [
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2019-09-01",
"name": "[parameters('name')]",
"location": "[parameters('location')]",
"tags": {
"demo-delete": "true"
},
"properties": {
"tenantId": "[subscription().tenantId]",
"sku": {
"family": "A",
"name": "standard"
},
"accessPolicies": []
}
}
],
"outputs": {
"keyVaultUri": {
"type": "string",
"value": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('name'))).vaultUri]"
},
"keyVaultSkuName": {
"type": "string",
"value": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('name'))).sku.name]"
}
}
}
}
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2019-10-01",
"name": "storageModule",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"parameters": {
"location": {
"value": "[variables('resourcesMetadata').location]"
},
"name": {
"value": "[replace(variables('resourcesMetadata').name, '{0}', 'st')]"
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"type": "string"
},
"name": {
"type": "string"
}
},
"functions": [],
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
"name": "[replace(parameters('name'), '-', '')]",
"location": "[parameters('location')]",
"kind": "StorageV2",
"sku": {
"name": "Standard_LRS"
}
}
],
"outputs": {
"storageAccountName": {
"type": "string",
"value": "[replace(parameters('name'), '-', '')]"
},
"storageAccountSku": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Storage/storageAccounts', replace(parameters('name'), '-', '')), '2019-06-01', 'full').sku.name]"
}
}
}
}
}
],
"outputs": {
"deploymentResourceGroup": {
"type": "object",
"value": "[resourceGroup()]"
}
},
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.3.126.58533",
"templateHash": "13278404564319210682"
}
}
}
Convert / decompile an existing ARM JSON to Bicep
Sure enough, this question comes up a lot. We have all built many ARM templates over the years, and I wouldn't want to re-create them as Bicep files manually.
Never fear; the Bicep CLI is here. However, there are limitations. Read more about the ones I encountered below.
Run this command to decompile an ARM template into a Bicep file:
az bicep decompile -f myTemplate.json
There are known limitations. When I'm trying to de-compile one of my ARM templates, I'm presented with this warning:
- WARNING: Decompilation is a best-effort process, as there is no guaranteed mapping from ARM JSON to Bicep. You may need to fix warnings and errors in the generated bicep file(s), or decompilation may fail entirely if an accurate conversion is not possible.
Following the warning, I received plenty of errors when I tried to decompile one of my semi-complex templates.
- complexArmTemplate.json: Decompilation failed with fatal error "[218:7]: The 'copy' property is not supported"
- Error BCP034: The enclosing array expected an item of type "module[] | (resource | module) | resource[]", but the provided item was of type "string".
- Error BCP073: The property "location" is read-only. Expressions cannot be assigned to read-only properties.
- Error BCP035: The specified "resource" declaration is missing the following required properties: "kind".
- Warning BCP081: Resource type "Microsoft.Web/sites/config@2015-08-01" does not have types available.
- Warning BCP038: The property "metadata" is not allowed on objects of type "SiteConfig". Permissible properties include "acrUseManagedIdentityCreds", "acrUserManagedIdentityID", "apiDefinition", "apiManagementConfig", "appCommandLine", "appSettings", "autoHealEnabled", "autoHealRules", "autoSwapSlotName", "connectionStrings", "cors", "defaultDocuments", "documentRoot", "experiments", "handlerMappings", "healthCheckPath", "ipSecurityRestrictions", "javaContainer", "javaContainerVersion", "javaVersion", "limits", "linuxFxVersion", "loadBalancing", "localMySqlEnabled", "logsDirectorySizeLimit", "managedPipelineMode", "managedServiceIdentityId", "nodeVersion", "numberOfWorkers", "phpVersion", "powerShellVersion", "preWarmedInstanceCount", "publishingUsername", "push", "pythonVersion", "remoteDebuggingVersion", "requestTracingExpirationTime", "scmIpSecurityRestrictions", "scmIpSecurityRestrictionsUseMain", "scmType", "tracingOptions", "use32BitWorkerProcess", "virtualApplications", "vnetName", "windowsFxVersion", "xManagedServiceIdentityId".
- Warning BCP037: No other properties are allowed on objects of type "VirtualNetworkRule".
Post the full list of things here as it doesn't make sense. Given the cadence Bicep is being worked on, many of these things might be fixed when you're reading this already. I'll be back to update accordingly when new versions are released.
I could work around some of these by either changing my ARM templates significantly or removing portions of them altogether and re-building them, to the extent possible, in Bicep.
- Note: The test to decompile happened with the latest version as of 2021-03-23.
TLDR on decompiling.
About 20% of my current real-world production templates could be decompiled and fixed with small adjustments. The rest requires effort ranging from medium to a lot, depending on the template's complexity.
The result of decompiling a template will vary depending on your mileage and template complexity.
Summary
Bicep is supported in production.
When I am creating more templates ahead, I will make it a bicep-first approach. If I don't have to decompile the templates and instead start ground-up, it makes perfect sense.
I'm optimistic about the future of Bicep!
Getting started with Bicep is as easy as 1-2-3. I believe the value comes when you have large and complex templates you need to build. Crafting them using the Bicep definition can make the authoring of your templates easier and less complex.
As time goes and more releases see the light of day, I'm confident Bicep will solve many of the current blockers, and we can start making use of it for our existing project templates. When I've tried an update where the more complex templates work, I'll revisit this topic and update accordingly.
That said, there are great resources available that walk through the Bicep language's ins and outs, so I will refer to those sources to deepen your Bicep experiences.
Final words:
- Do I recommend checking out Bicep? Yes!
Read more:
- Bicep Repo (GitHub)
Recent comments