Special Offer: My C#/.NET Bootcamp Course is out now. Get 10% OFF using the code FRIENDS10.

Bicep is a transpiler that transforms domain-specific language (DSL) into ARM templates. It’s the next generation of Microsoft’s infrastructure as code (IaC) tooling.

It works similarly to writing TypeScript, which is transpiled into JavaScript for running in browsers or Node.

We can use the Azure CLI to transpile the Bicep files into ARM templates and deploy them to Microsoft Azure.

Bicep uses the same process and tooling we learned in the previous issue of this series when we manually created JSON-based ARM Templates.

Why Bicep?

Bicep was introduced in 2021 because Microsoft wanted to improve the developer experience for creating templates for the Azure cloud platform.

ARM Templates have been there for a long time, and it was time for something new. Also, ARM Templates have some flaws they wanted to fix with Bicep.

Bicep Visual Studio Code Extension

We will be using Visual Studio Code to write Bicep scripts. 

First, we need to install the Bicep Visual Studio Extension. It provides IntelliSense, validation, code snippets, and many more features.

Creating a Web App using Bicep

After installing the extension, we create a new file and name it web-app.bicep. For our first example, we want to create a definition for an Azure App Service using Bicep.

We can start the script with the resource keyword followed by the name of the resource and the API of the service we want to create. However, to do so, we must know the API names of the services we want to use. In this case, a web app is called “sites”.

To solve this issue, we can use code snippets provided by the Bicep Visual Studio Code extension instead. For example, we use the res-web-app snippet to create a web app definition, including a few select properties.

resource webApplication 'Microsoft.Web/sites@2021-01-15' = {
  name: 'name'
  location: location
  tags: {
    'hidden-related:${resourceGroup().id}/providers/Microsoft.Web/serverfarms/appServicePlan': 'Resource'
  }
  properties: {
    serverFarmId: 'webServerFarms.id'
  }
}

If we want to use the latest API version, we can quickly change it using code completion. We also want to remove the optional tags section. We also set the name to bicep-webapp-cb to make it globally unique.

Next, we want to create the missing location variable. We can define parameters using the param keyword followed by an identifier and a type. We set the default value to the location of the provided resource group.

param location string = resourceGroup().location

resource webApplication 'Microsoft.Web/sites@2022-09-01' = {
  name: 'bicep-webapp-cb'
  location: location
  properties: {
    serverFarmId: 'webServerFarms.id'
  }
}

Now, I can quickly show you another cool feature of the Bicep Visual Studio Code extension. We click the icon on the top right of the bicep file that looks like a graph with nodes. It opens the Bicep Visualizer, which provides a graphical representation of the Bicep script.

Visual Studio Code - Bicep Visualizer showing an Azure Web App

Creating an App Service Plan

An Azure Web App runs on an App Service Plan. The current definition contains a serverFarmId property that contains the identification of an app service plan. Let’s add a definition for the app service plan to the Bicep script.

We use another code snippet, res-app-plan, to create the App Service Plan. Again, we select the latest API version and set the name to bicep-app-plan-cb.

param location string = resourceGroup().location

resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = {
  name: 'bicep-app-plan-cb'
  location: location
  sku: {
    name: 'F1'
    capacity: 1
  }
}

resource webApplication 'Microsoft.Web/sites@2022-09-01' = {
  name: 'bicep-webapp-cb'
  location: location
  properties: {
    serverFarmId: appServicePlan.id
  }
}

As you can see in the Bicep Visualizer, we now have two independent resources. However, we want to add the web app to the App Service Plan. All we need to do is to set the serverFarmId to the id of the appServicePlan object.

Bicep Visualizer: App Service Plan and Web Application

As you can see, the Bicep Visualizer automatically updated and now shows a dependency from the web application to the App Service Plan.

Another feature of the Bicep Visual Studio Code extension is script validation. When we make a mistake, we see a red squiggle, and the error appears in the problems view – very handy.

How Bicep Modules Fix ARM Template Issues

One of the limitations of ARM Templates is that they are scoped to either a subscription or a resource group. It makes it complicated to create a resource group and then add resources within that resource group. A common solution is to create two templates and execute them sequentially.

Luckily, Bicep provides a simpler solution. We use the res-rg code snippet to add a resource group definition to the Bicep script. Again, we use the latest API version and set the name to bicep-rg-cb.

param location string = deployment().location
targetScope = 'subscription'

resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
  name: 'bicep-rg-cb'
  location: location
}

We now have a few errors in the problems view. There is a circle dependency because we use the resource group method in the location definition, and the location definition is used in the resource group.

We remove the default value of the location parameter. It means that we will have to provide this parameter when executing the script.

Next, we get the error that the resource group cannot be created for the current scope. Without any explicit definition, a Bicep script is scoped to a resource group. We change it by using the targetScope keyword and set it to subscription.

Now we have the same scoping issue with the app service plan and the web app. Bicep provides modules to solve this issue. We rename the current file to main.bicep and extract the app service plan into a app-service-plan.bicep file and the web app into a web-app.bicep file.

We need to add the location parameter again to the extracted definitions. The App Service Plan definition looks fine. However, we need to set the dependencies between the files and provide the App Service Plan id to the web-app.bicep file.

We fix the web-app.bicep file by defining a parameter named appServicePlanId of type string. We use the value of the parameter as the serverFarmId.

main.bicep:

param location string = deployment().location
targetScope = 'subscription'

resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
  name: 'bicep-rg-cb'
  location: location
}

app-service-plan.bicep:

param location string = resourceGroup().location

resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = {
  name: 'bicep-app-plan-cb'
  location: location
  sku: {
    name: 'F1'
    capacity: 1
  }
}

web-app.bicep:

param location string = resourceGroup().location
param appServicePlanId string

resource webApplication 'Microsoft.Web/sites@2022-09-01' = {
  name: 'bicep-webapp-cb'
  location: location
  properties: {
    serverFarmId: appServicePlanId
  }
}

Next, I insert the following code snippet into the main.bicep file.

module appServicePlan './app-service-plan.bicep' = {
  scope: resourceGroup
  name: 'bicep-app-plan-cb'
  params: {
    location: resourceGroup.location
  }
}

module appService './web-app.bicep' = {
  scope: resourceGroup
  name: 'bicep-webapp-cb'
  params: {
    location: resourceGroup.location
    appServicePlanId: appServicePlan.outputs.appServicePlanId
  }
}

We define a module each for the App Service Plan and the web app. The module keyword is followed by an identifier and the file path. We set the scope to the resource group we create within the main.bicep file a few lines above the module definitions.

We provide the required location parameter as well as the name of the resource. The app service requires the id of the app service plan. To make it available, we open the app-service-plan.bicep file and add the following line:

output appServicePlanId string = appServicePlan.id

It exposes the id of the appServicePlan object as the appServicePlanId variable of type string.

Now the error in the main.bicep file is gone. As we can see in the Bicep Visualizer, we now have a resource group with an app service plan and a web app. The web app also has a dependency on the app service plan.

Bicep Visualizer: Resource Group with App Service Plan and Web Application

Deploying Azure Resources using Bicep

Now that we have the definitions, we want to use the Bicep script to deploy the resources to Microsoft Azure.

As a first step, we need to make sure we have the Bicep CLI installed. We can check it using the az bicep version command. If the Bicep CLI isn’t installed, you can install it using the az bicep install command.

We can build a Bicep file using the following command:

az bicep build --file .\main.bicep

It will transpile the bicep file and generate an ARM Template. As you can see, the generated ARM Template is about 130 lines and much more complex compared to the Bicep definition.

{
  "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.19.5.34762",
      "templateHash": "13566891733243346758"
    }
  },
  "parameters": {
    "location": {
      "type": "string"
    }
  },
  "resources": [
    {
      "type": "Microsoft.Resources/resourceGroups",
      "apiVersion": "2022-09-01",
      "name": "bicep-rg-cb",
      "location": "[parameters('location')]"
    },
    {
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2022-09-01",
      "name": "bicep-app-plan-cb",
      "resourceGroup": "bicep-rg-cb",
      "properties": {
        "expressionEvaluationOptions": {
          "scope": "inner"
        },
        "mode": "Incremental",
        "parameters": {
          "location": {
            "value": "[reference(subscriptionResourceId('Microsoft.Resources/resourceGroups', 'bicep-rg-cb'), '2022-09-01', 'full').location]"
          }
        },
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "metadata": {
            "_generator": {
              "name": "bicep",
              "version": "0.19.5.34762",
              "templateHash": "2107576396377932717"
            }
          },
          "parameters": {
            "location": {
              "type": "string"
            }
          },
          "resources": [
            {
              "type": "Microsoft.Web/serverfarms",
              "apiVersion": "2022-09-01",
              "name": "bicep-app-plan-cb",
              "location": "[parameters('location')]",
              "sku": {
                "name": "F1",
                "capacity": 1
              }
            }
          ],
          "outputs": {
            "appServicePlanId": {
              "type": "string",
              "value": "[resourceId('Microsoft.Web/serverfarms', 'bicep-app-plan-cb')]"
            }
          }
        }
      },
      "dependsOn": [
        "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'bicep-rg-cb')]"
      ]
    },
    {
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2022-09-01",
      "name": "bicep-webapp-cb",
      "resourceGroup": "bicep-rg-cb",
      "properties": {
        "expressionEvaluationOptions": {
          "scope": "inner"
        },
        "mode": "Incremental",
        "parameters": {
          "location": {
            "value": "[reference(subscriptionResourceId('Microsoft.Resources/resourceGroups', 'bicep-rg-cb'), '2022-09-01', 'full').location]"
          },
          "appServicePlanId": {
            "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'bicep-rg-cb'), 'Microsoft.Resources/deployments', 'bicep-app-plan-cb'), '2022-09-01').outputs.appServicePlanId.value]"
          }
        },
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "metadata": {
            "_generator": {
              "name": "bicep",
              "version": "0.19.5.34762",
              "templateHash": "10615174901074759385"
            }
          },
          "parameters": {
            "location": {
              "type": "string"
            },
            "appServicePlanId": {
              "type": "string"
            }
          },
          "resources": [
            {
              "type": "Microsoft.Web/sites",
              "apiVersion": "2022-09-01",
              "name": "bicep-webapp-cb",
              "location": "[parameters('location')]",
              "properties": {
                "serverFarmId": "[parameters('appServicePlanId')]"
              }
            }
          ]
        }
      },
      "dependsOn": [
        "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'bicep-rg-cb'), 'Microsoft.Resources/deployments', 'bicep-app-plan-cb')]",
        "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'bicep-rg-cb')]"
      ]
    }
  ]
}

The build command allows us to generate the ARM Template that will be executed. However, for the deployment of the resources, all we need is the az deployment command like this:

az deployment sub create -f .\main.bicep -l westeurope

We start a deployment for the subscription scope and provide the name of the Bicep file. The -l parameter is the required location parameter.

Because we need to provide this parameter, we can use the value we provide here instead of asking for another parameter. We use the deployment() method and use its location property as the default value of the location parameter in the main.bicep script.

Let’s execute the deployment command.

After a few seconds, we see the resource group, including the app service plan and the web app in the Azure Portal.

Decompiling ARM Templates into Bicep Scripts

The Bicep Decompiler transforms existing ARM Templates into a Bicep file. For example, I wrote this ARM Template in the previous video of this Infrastructure as Code series. It contains an App Service Plan and a web app. The file is about 36 lines.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "functions": [],
    "variables": {},
    "resources": [
        {
            "name": "arm-web-app-plan",
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2020-12-01",
            "location": "[resourceGroup().location]",
            "sku": {
                "name": "F1",
                "capacity": 1
            },
            "properties": {
                "name": "arm-web-app-plan"
            }
        },
        {
            "name": "arm-webapp-demo",
            "type": "Microsoft.Web/sites",
            "apiVersion": "2020-12-01",
            "location": "[resourceGroup().location]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/serverfarms', 'arm-web-app-plan')]"
            ],
            "properties": {
                "name": "arm-webapp-demo",
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'arm-web-app-plan')]"
            }
        }
    ],
    "outputs": {}
}

Let’s use the following command to decompile it into a Bicep script.

az bicep decompile -f .\templates\arm-web-app.json

There is a warning telling us that the decompilation is a best-effort process and that we need to check if everything works as expected.

Let’s open the generated web-app.bicep file. As you can see, the definition is a lot shorter. There are a few warnings that we can quickly fix. 

resource arm_web_app_plan 'Microsoft.Web/serverfarms@2020-12-01' = {
  name: 'arm-web-app-plan'
  location: resourceGroup().location
  sku: {
    name: 'F1'
    capacity: 1
  }
  properties: {
    name: 'arm-web-app-plan'
  }
}

resource arm_webapp_demo 'Microsoft.Web/sites@2020-12-01' = {
  name: 'arm-webapp-demo'
  location: resourceGroup().location
  properties: {
    name: 'arm-webapp-demo'
    serverFarmId: arm_web_app_plan.id
  }
}

We extract the location definition into a parameter and use the generated value as its default value. Next, we remove the unnecessary name definitions. We end up with 18 lines of code, about half of the original ARM Template.

param location string
param appServicePlanId string

resource webApplication 'Microsoft.Web/sites@2022-09-01' = {
  name: 'bicep-webapp-cb'
  location: location
  properties: {
    serverFarmId: appServicePlanId
  }
}

Bicep vs. ARM Templates

ARM Templates use the JSON format, which was invented for machine communication.

It’s a great format for computers to exchange data, for example, between a server and a browser, but it’s not intended for human interaction. 

A lot of noise is going on with all the braces and symbols, and it lacks abstraction. Reusing parts of the definition is complicated, if possible at all.

Bicep uses a domain-specific language (DSL) on a higher abstraction level. It provides a syntax to define variables and reuse part of the template. It results in a more concise and simpler-to-read and write format.

Being a DSL, the text editor is able to provide IntelliSense, tab completion, and validation, which results in an improved developer experience.

Bicep scales better than ARM Templates. As your project grows, you can use more advanced features that help your infrastructure as code definition to grow with the needs of your application. 

You can deploy Azure Resource groups, including resources within a single script, and the syntax is much more human-friendly.

Bicep provides modules, allowing us to break up the template definition and reuse or repurpose parts of the deployment definition.

Both options use a declarative definition and the Azure CLI to deploy the resource to the Azure cloud. We can integrate the Azure CLI into an existing CI/CD toolchain.

Conclusion

After using both for a few days, I definitely prefer using Bicep over plain ARM Templates. It’s simpler to use, and, as a developer, it feels much better to use a programming language than writing JSON by hand.

For me, reusability and composability are the most significant advantages of Bicep over ARM Templates.

The Bicep Decompile command allows us to migrate existing ARM Templates to Bicep scripts, making the transition to Bicep simpler.

Keep in mind that Bicep (as well as ARM Templates) are Azure-only. If you want to use multi-cloud or deploy to another cloud platform, you need a different tool.

In the following issues of this series, we will look at Terraform. This completely different tool supports Azure and other cloud platforms such as the Google Cloud Platform or Amazon Web Services.

Consider subscribing to my YouTube channel so you won’t miss the next episode of this Infrastructure as Code series.

 

 

Claudio Bernasconi

I'm an enthusiastic Software Engineer with a passion for teaching .NET development on YouTube, writing articles about my journey on my blog, and making people smile.