6 Simplifying ARM templates using the Bicep DSL

This chapter covers

By now you are familiar with ARM templates and the process of writing and deploying them. And hopefully you are impressed with the opportunities that IaC and ARM templates, in particular, give you.

Still, you may have noticed that describing more extensive infrastructure this way is not perfect. It is generally accepted that ARM templates have several drawbacks, mainly because they are written in JSON. These are some examples:

These drawbacks have fueled much debate in the community on the usefulness of JSON as a language for IaC. This discussion’s ultimate consequence has been that more and more users have moved away from ARM templates to alternatives like Terraform and Pulumi. But more recently, a new approach to dealing with the downsides of ARM templates has come up.

New languages have been developed that provide a better way of declaring Azure resources, along with tools that transpile these languages into ARM templates. A transpiler is a program that validates its input and transforms it into another format or language—the name is a variation on the term “compiler.” One open source example of a transpiler to ARM templates is called Farmer, available at https://compositionalit.github.io/farmer. Another alternative that has recently come up is Bicep. Bicep is a single-purpose DSL for declaring Azure infrastructure. Whereas Farmer is a community project, Bicep is maintained by Microsoft.

At the time of writing, Bicep is fully supported and has been declared ready for production use by Microsoft. The version we used to write this chapter is 0.4.1124. Bicep is still under development and is prone to changes in the future. To check the latest state of the language and the tools that come with it, you can check the Bicep GitHub page (https://github.com/Azure/bicep).

This chapter discusses Bicep in detail—a new syntax for things you have already learned. While this might be a bit dry, we believe that at some point in the future, Bicep templates will replace ARM templates as the de facto language for writing IaC for Azure. Everything you have learned up to now still has value for a couple of reasons: first, Bicep is just an abstraction on top of ARM templates, and second, deploying and debugging the templates doesn’t change, because Bicep is transpiled to an ARM template.

6.1 Bicep: A transpiler

The most straightforward way to show the benefits of Bicep over ARM templates is by exploring an example. First, look at the following definition for a storage account:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2020-08-01-preview",
            "name": "myStorageAccount",
            "location": "[resourceGroup().location]",
            "kind": "StorageV2",
            "sku": {
                "name": "Premium_LRS",
                "tier": "Premium"
            }
        }
    ]
}

Now compare it with the following declaration of the same storage account, this time using Bicep notation:

resource myStorageAccount 'Microsoft.Storage/storageAccounts    
     @2020-08-01-preview' = {    
    name: 'bicepstorageaccount'                                 
    location: resourceGroup().location
    kind: 'StorageV2'
    sku: {
        name: 'Premium_LRS'
        tier: 'Premium'
    }
}

The name of the deployment

The actual resource name

While the notation looks quite different initially, a closer look reveals that both notations describe precisely the same resource. The most notable difference in this example is that the Bicep notation combines the type and apiVersion for a resource into a single resource declaration. The full definition for a resource declaration is

resource <symbolic-name> <resourceType>@<apiVersion>

You already know about the resourceType and apiVersion properties, but the symbolic-name is new.

The symbolic name (myStorageAccount) is not the name of the resource in Azure, and the resource name and the symbolic name are separate properties for a reason. First, the name of the resource in Azure that you have worked with before is still specified using the name property. The symbolic name is new with Bicep, and it’s local to the template only. Symbolic names are intended for referencing the resource from within the same template and fetching its properties. You will see how to use this symbolic name later on.

Tip Like for ARM templates, there is an extension for VS Code, called Bicep, to help you edit Bicep files. The extension can be installed the same way you installed the ARM templates extension. If you require instructions on how to install extensions in VS Code, please refer back to chapter 2.

There are some other differences between Bicep templates and ARM templates that you might notice:

All these syntax changes, and more, are discussed in section 6.2. But first, let’s explore how to deploy a Bicep template to Azure and how to manually transpile a Bicep template into an ARM template.

6.1.1 Deploying

When you are working with the Azure CLI version 2.20.0 or later or Azure PowerShell version 5.6.0 or later, you can deploy Bicep templates without manually transpiling them first. Both have been updated to transpile Bicep templates on the fly if needed. To use this new capability, you can just provide a Bicep template instead of an ARM template.

When you are working with an older version of the Azure CLI or Azure PowerShell, or you deploy using other means, you first have to transpile the Bicep templates yourself, and then follow your existing deployment approach.

6.1.2 Transpiling

For transpiling a Bicep file into an ARM template, a CLI tool, also called Bicep, is provided by Microsoft. The latest version of this CLI tool is available at https://github.com/Azure/bicep/releases/latest. Builds are available for multiple operating systems and usage scenarios. After you download and install Bicep, the CLI tool should be available in your path and ready for use. As an alternative, you can install and invoke Bicep through the Azure CLI using version 2.22.0 or later. The syntax is then slightly different, as you will see when reading on.

Let’s continue with the Bicep example from section 6.1. Save the example code in a file called storage-account.bicep, and run the following command:

bicep build storage-account.bicep

Or use the following command if you are using the Azure CLI:

az bicep build --file storage-account.bicep

The first parameter (build) to the bicep command is the command you need to execute for transpiling. Then you provide the .bicep file to be transpiled. During transpilation, not only is the Bicep template transformed into an ARM template, but the transpiler also checks the validity of the template to the fullest extent possible without connecting to your Azure environment. Checks include resource names, API versions, property names, and property types.

After running this command, you should have another file called storage-account .json that contains a resource that is the same as the first ARM template example in this chapter. This resulting ARM template can be deployed as you learned before.

If you are working with the Azure CLI version 0.22.0 or later, you can also deploy Bicep templates directly as follows:

az deployment group create --resource-group <resource-group-name> 
     --template-file <arm-or-bicep-file>

When you specify a Bicep file, the file will automatically be transpiled, after which the resulting ARM template is submitted to the Azure Resource Manager.

The nice thing with a tool like Bicep is that it does not impose any constraints on your choice of template deployment tool. For example, in Azure DevOps, it is relatively straightforward to extend a pipeline with two more tasks: downloading the CLI and executing the transpilation.

Note When using Bicep from a pipeline, do not rely on the “latest” version of Bicep. Always retrieve a specific version to guarantee repeatable results. Also, consider caching that version locally to strengthen that guarantee and limit external dependencies.

6.1.3 Decompiling

ARM templates have already been around for years, so many companies have an existing collection of those. While it is possible to only use Bicep for new templates, you might want to convert some of your existing ARM templates into Bicep templates. For example, if you have to make significant changes to an existing template, it might be better first to convert the existing ARM template to a Bicep template. Conversion takes some time, but if you go through this process once, you can then leverage the benefits of Bicep templates. Another scenario where decompiling is helpful is after exporting an ARM template from the Azure portal.

To decompile the ARM template that you generated in the previous section, use the following command:

bicep decompile storage-account.json

Or use this command with the Azure CLI:

az bicep decompile --file storage-account.json

Instead of using the build command, you use the decompile command. And instead of providing a .bicep file, you provide a .json file. This command’s output is a storage-account.bicep file containing the Bicep code.

In this example, the decompile action results in precisely the same Bicep template that you started out with, but this will not always be the case. When you are decompiling more elaborate examples, differences might appear, especially around the use of variables.

In addition to better readability and the removal of ceremony like commas, Bicep also introduces other syntax differences from JSON, so let’s explore those next.

6.2 Bicep syntax differences

Bicep notation is partially inspired by the JSON notation for ARM templates, Terraform, and other declarative and non-declarative languages. Bicep has its own notation for all the constructs you learned about in chapters 3 and 4. In the following sections, we’ll revisit all these constructs and see how they work in Bicep.

6.2.1 Parameters

In Bicep, parameters are declared using the param keyword:

param myParameter string

This example declares a parameter with the name myParameter of type string. The same rules that you learned for JSON names and types apply. Bicep also supports allowed values and defaults. Here is an example using those:

@allowed([
    'defaultValue'
    'otherValue'
])
@secure
param myParameter string = 'defaultValue'

There are two things to note here: first, the use of annotations, which are keywords starting with the @ sign, and second, the inline assignment of a variable on the last line.

Annotations are used to specify one or more properties of a parameter, and they correspond to the parameter properties you know from ARM templates, like minValue, maxValue, minLength, maxLength, and allowed. In the preceding example, you’ll see the use of allowed to specify acceptable values for this parameter. In this case, the annotation @secure is used to convert the type from string to secureString. For parameters, all annotations are optional.

Second, you can specify default values for a parameter by appending the equals sign (=) and a value to the parameter declaration. If no value is specified for the parameter when you deploy the template, the default value will be used.

A final difference between Bicep and ARM templates is that Bicep parameters do not have to be grouped within a root object’s parameters property. Instead, they can be declared anywhere within the file.

Note At the time of writing, Bicep does not have an alternative syntax for writing parameter files. So if you want to work with parameter files, you can still write those in the ARM syntax you learned before. Since Bicep templates transpile to ARM templates, working with parameter files hasn’t changed.

6.2.2 Variables

The syntax for variables is similar to that of parameters and looks like this:

var myVariable = {
    property: 'stringValue'
    anotherProperty: true
}

In this example, a variable with the name myVariable is declared, and a value is assigned using the equals operator. While the assignment looks similar to that of a parameter, observe that there is no type declaration. That is because the type of a variable can always be derived from its value. This is called type inference, and you may recognize it from programming languages. In this case, the variable is of type object.

6.2.3 Outputs

Declaring an output in Bicep is very similar to declaring a variable with a default value. The syntax is the same, except for the keyword, which is output here:

resource saResource 'Microsoft.Storage/storageAccounts
         @2020-08-01-preview' = {
    name: 'myStorageAccount'
    location: resourceGroup().location
    kind: 'StorageV2'
    sku: {
        name: 'Premium_LRS'
        tier: 'Premium'
    }
}
 
output storageAccountId string = saResource.id           
output storageAccountName string = saResource.name

The symbolic name is used to refer a resource, a storage account in this case.

This example declares a storage account, as you have seen many times now. The last line is new, however. It returns an output of type string with the name storageAccountId. For the value, the declared storage account is referenced, and its id property is retrieved. As you can see in this example, you no longer need the reference(...) function. Instead, you use the symbolic name of the declared resource. This allows the use of dot notation to access properties of any resource in the same template directly. You can create as many outputs as you want.

6.2.4 Conditions

Just as in ARM templates, Bicep templates allow for the conditional deployment of resources. To conditionally deploy a resource in Bicep, the following syntax is used:

param deployStorage bool = true
 
resource myStorageAccount 'Microsoft.Storage/storageAccounts      
         @2020-08-01-preview' = if (deployStorage == true) { 
    name: 'myStorageAccount'
    location: resourceGroup().location
    kind: 'StorageV2'
    sku: {
        name: 'Premium_LRS'
        tier: 'Premium'
    }
}
 Start of the storage account resource declaration

In the preceding example, the resources’ properties come after the assignment operator and are preceded with an if clause. With this clause, the resource is only declared if the parentheses’ condition evaluates to true. When transpiling to an ARM template, the if clause will be replaced by a condition property on the resource, which you saw in chapter 5.

6.2.5 Loops

In ARM templates, loops were hard to write and even harder to read. In chapter 5, you may well have found this the most challenging part of ARM template syntax to understand. In Bicep, loops can still be a bit tricky, but they are definitely easier to understand.

Suppose you want to create three storage accounts from within the same template, and you have declared their names within an array like this:

var storageAccounts = [
    'myFirstStorageAccount'
    'mySecondStorageAccount'
    'myFinalStorageAccount'
]

In Bicep, you can use the following syntax to create these three storage accounts in one resource declaration:

resource myStorageAccounts 'Microsoft.Storage/storageAccounts
         @2020-08-01-preview' = [for item in storageAccounts: {      
    name: item                                                         
    location: resourceGroup().location
    kind: 'StorageV2'
    sku: {
        name: 'Premium_LRS',
        tier: 'Premium'
    }
}]

The for-item-in-list syntax is used for repeating a resource.

Item is used to retrieve the value for the current iteration.

In this example, the second line shows the [for item in list: { }] syntax that is used for declaring a loop. (If you are familiar with Python comprehensions, this syntax might seem familiar, as it looks and works similarly.) For list you specify the name of any list, either a parameter or a variable. The name for item is available within the loop to reference the current item in the list, such as to retrieve its value. Please note, you can use another iterator name than item.

You can loop over any valid list, so you can also build a more elaborate variable structure and then use that to create storage accounts with different SKUs, like this:

var storageAccounts = [
    {
        name: 'myFirstStorageAccount'
        sku: {
            name: 'Premium_LRS'
            tier: 'Premium'
        }
    }
    {
        name: 'mySecondStorageAccount'
        sku: {
            name: 'Standard_LRS'
            tier: 'Standard'
        }
    }
]
 
@batchSize(1)                                                  
resource myStorageAccounts 'Microsoft.Storage/storageAccounts
         @2020-08-01-preview' = [for item in storageAccounts: {
    name: item.name
    location: resourceGroup().location
    kind: 'StorageV2'
    sku: item.sku
}]

BatchSize specifies how many resources are created in parallel.

In this second example, the storageAccounts variable contains a list of objects, instead of a list of strings. Each object contains both a name and an object with the SKU properties for the storage account with that name. The result is that in the loop itself, item now holds the value of each complete object in the two iterations in this loop.

Also note that this example shows a new annotation, @batchSize, which can optionally be used to specify how many resources should at most be created in parallel. Choosing a value of 1, like here, effectively instructs ARM to create resources sequentially.

Loops can also be used in outputs. For example, to return the access tier for all created storage accounts, the following syntax can be used:

output storageAccountAccessTiers array = [for i in range(0, 
         length(storageAccounts)): myStorageAccounts[i].sku.tier]

Note that the use of storageAccounts within the call to the length(...) function is correct here. This is because it is not (yet) possible to iterate over a list of resources, like myStorageAccounts, directly. Here the code is “misusing” the fact that the number of items in the storageAccounts variable is the same as that in the myStorageAccounts resource list.

In this example, the for-item-in-list syntax is used again, now combined with a new function called range(...). The range(...) function generates a list of numbers, starting at the first parameter and going up to the second parameter. Combining this with the length(...) function is a way of generating a list of numbers—in this case, a list with two numbers: 0 and 1. This is the list over which the loop is executed. In the output value, this number is used to access every member of the array of resources in myStorageAccounts, allowing for the return of its access tier.

6.2.6 Targeting different scopes

With Bicep it is possible to target different scopes, just as with ARM templates. Instead of specifying the correct JSON Schema, as you do with ARM templates, you can use a targetScope declaration at the top of the file.

To target a resource group, you can use the following syntax:

targetScope = 'resourceGroup'

As a Bicep file targets a resource group by default, this can safely be omitted. Other valid values are subscription, managementGroup, and tenant.

6.2.7 Known limitations

At the time of writing, there is not yet support for all ARM template features in Bicep. The most notable limitation is that user-defined functions are not yet supported. Similarly, it is not yet possible to declare arrays or objects on a single line instead of specifying every entry or every property, respectively, on a new line.

Now that you are familiar with the Bicep syntax, let’s explore some of Bicep’s benefits over vanilla ARM templates.

6.3 Other improvements with Bicep

In addition to addressing the downsides of the existing ARM template syntax, Bicep templates also come with other improvements.

6.3.1 Referencing resources, parameters, and variables

One improvement is in referencing other resources and fetching their properties. To see the difference, let’s first revisit referencing a property in ARM template syntax, which you learned in chapter 3:

[listKeys(resourceId('Microsoft.Storage/storageAccounts',
         parameters('storageAccountName')), 2019-04-01').key1]

Remember how you first had to build the full resourceId for the function using its type and name? With Bicep, that’s not necessary for resources that are declared in the sample template. Now you can use the symbolic name for the resource, which you saw earlier, and directly access any of its properties, like this:

listKeys(myStorageAccount.id, '2019-04-01').key1

Here the listKeys(...) function is called, again using two parameters. But instead of using functions, you can directly reference the resource using its symbolic name and then fetch its id property. This yields the same result as building that id manually using the resourceId(...) function. Just comparing these two code examples shows the improvement to readability.

Referencing parameters and variables works the same way as referencing a resource. If you want to refer to a parameter for setting a value, you can use it directly like this:

parameter storageAccountName string
 
resource myStorageAccount 'Microsoft.Storage/storageAccounts
         @2020-08-01-preview' = {
    name: storageAccountName
    ...
}

This example declares a storage account. The storage account name is not specified as a literal but references the value of the storageAccountName parameter.

One thing to note is that parameters, variables, and resources share a single namespace. Sharing a namespace means that you cannot use the same name for a parameter, variable, or resource—all names have to be unique. The single exception is outputs. Outputs can have the same name as parameters, variables, or resources. This allows you to do something like this:

var storageAccountName = 'myStorageAccount'
 
resource myStorageAccount 'Microsoft.Storage/storageAccounts
         @2020-08-01-preview' = {
    name: storageAccountName
    location: resourceGroup().location
    kind: 'StorageV2'
    sku: {
        name: 'Premium_LRS'
        tier: 'Premium'
    }
}
 
output storageAccountName string = storageAccountName

In this example, you see a typical scenario where a resource’s name is generated using a variable value and is returned as an output. Reusing the same name for the variable and the output allows for better readability than using different names for the same thing.

6.3.2 Using references in variables and outputs

In ARM templates, it is impossible to use the reference(...) function in variables. That’s because ARM calculates variable values before deploying any resource, and in that stage it cannot evaluate the reference(...) function. In a Bicep template, this problem no longer exists. To see why, take a look at the following Bicep template:

resource myStorageAccount 'Microsoft.Storage/storageAccounts
         @2020-08-01-preview' = {
    name: 'myStorageAccount'
    location: resourceGroup().location
    kind: 'StorageV2'
    sku: {
        name: 'Premium_LRS'
        tier: 'Premium'
    }
}
 
var storageAccountName = myStorageAccount.name
var accessTier = myStorageAccount.properties.accessTier
 
output message string = '${storageAccountName} with tier ${accessTier}'

In both variables in the preceding example, a storage account is referenced. Here the Bicep transpiler does something clever. Instead of simply transpiling these Bicep variables into ARM variables and using the variable value in the output, it removes the variable and inlines the complete expression that yields the parameter’s result into the output. Here’s an example:

{
  "$schema": "https:/ /schema.management.azure.com/schemas/
           2019-04-01/deploymentTemplate.json#",
    "variables": {
        "storageAccountName": "myStorageAccount"
    },
    "resources": [
        {
          "type": "Microsoft.Storage/storageAccounts",
          ...
        }
    ],
    "outputs": {
        "message": {
        "type": "string",
        "value": "[format('{0} was deployed with accessTier {1}', variables
             ('storageAccountName'), reference(resourceId('Microsoft
             .Storage/storageAccounts', 'myStorageAccount')).accessTier)]"
      }
    }
}

In the preceding example, you can see how the output’s value is built up without using the accessTier variable. Instead, the reference(...) function moves into the output’s value calculation, where it works.

6.3.3 Referencing existing resources

When you want to reference an existing resource on multiple occasions, you can repeatedly use the reference(...) function. Alternatively, you can also make that resource available in your template under a symbolic name. To do this, you can use the existing keyword as follows:

resource existingStorageAccount 'Microsoft.Storage/storageAccounts
         @2020-08-01-preview' = existing {                          
    name: 'preexistingstorageaccountname'
}

The existing keyword indicates a pre-existing resource.

This example does not declare a new resource at all. Instead, it builds a reference to an existing resource with the provided name. Note that if you deploy this Bicep template and the resource does not exist yet, the deployment will result in an error.

6.3.4 Dependency management

A second improvement Bicep provides is that you no longer have to declare dependencies. If you recall from chapter 5, you are required to declare all dependencies between your resources in ARM templates, even if they are already implicitly declared through a parent-child relationship. With Bicep, this is not needed anymore.

When you transpile a Bicep file to an ARM template, all implicit dependencies are automatically detected, and the correct dependsOn declarations are automatically generated. Implicit dependencies are parent-child relations and resources that reference each other. For example, when your app settings reference a storage account to store its key as a setting, you no longer need to declare dependencies manually.

If you want to control the deployment order of resources, and there is no implicit dependency, you can still declare your own dependencies using the dependsOn property. The dependsOn property still works precisely the same way it did in ARM templates.

6.3.5 String interpolation

Another nuisance when building larger ARM templates is the lack of built-in support for string interpolation. String interpolation is a technique used for building text strings out of literals, parameters, variables, and expressions. For example, take a look at the following common approach for generating names for resources based on the environment name.

    "parameters": {
        "environmentName": {
            "type": "string",
            "allowed": [ "dev", "tst", "prd" ]
        }
    },
    "variables": {
        "mainStorageAccountName": "[concat('stor',
             parameters('environmentName'), 'main')]",
        "otherStorageAccountName": "[concat('stor',
             parameters('environmentName'), 'other')]",
        "somethingWebAppName": "[concat('webappp',
             parameters('environmentName'),'something')]"
    },

While the preceding approach works quite well, it takes a few seconds to calculate the actual resource name in your mind when reading the template, and that’s for this relatively simple example. Just imagine how that would work when listing tens of resources.

In Bicep templates, you no longer need to use the concat(...) function for concatenating values. Instead, you can use the ${...} syntax anywhere within a string literal, and the value between the curly braces is automatically interpreted as an expression. Here is an example:

@allowed([
    'dev'
    'tst'
    'prd'
])
param environmentName string
 
var mainStorageAccountName = 'stor${environmentName}main'
var otherStorageAccountName = 'stor${environmentName}other'
var somethingWebAppName = 'webapp${environmentName}something'

As you can see, the values for the three variables are now much easier to read because you no longer need to call the parameters(...) function in combination with the ${...} syntax. In this example, environmentName = 'dev', 'webapp${environmentName}something' will evaluate to 'webappdevsomething'. As a bonus, when you are using the Bicep extension for VS Code, syntax highlighting shows where string interpolation is used, to enhance readability even more.

6.3.6 No mandatory grouping

One of the consequences of using JSON as the basis of ARM templates is the object notation for parameters and variables. This means all parameters and variables have to be grouped in one location in a template—usually at the start. Take a look at the following example:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "environment": {                                         
            "type": "string"
        },
    },
    "variables": {
        "webAppPlanName": "[format('myAppPlan-{0}',
             parameters('environment'))]",
        "anotherVariable": "someValue"
    },
    "resources": [
                                                                 
        {
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2020-06-01",
            "name": "[variables('webAppPlanName')]",             
            "location": "westeurope"
        }
    ]
}

The declaration of a parameter

Imagine there are tens or hundreds of lines at this location.

The usage of a parameter

Especially in larger templates, this made for much scrolling up and down, as parameters were all defined at the top and used much further down in the file. JSON provides no way to work around this, and you cannot declare parameter or variable keys more than once.

With the new Bicep syntax, grouping parameters at the top is no longer necessary, and you can do something like this:

param environment string = 'tst'                                 
 
                                                                 
 
var webAppPlanName = 'myAppPlan-${environment}'
 
resource myWebApp 'Microsoft.Web/serverfarms@2020-06-01' = {
    name: webAppPlanName                                         
    location: 'westeurope'
}

The declaration of a parameter

Imagine there are tens or hundreds of lines at this location.

The usage of a parameter

In this example, parameters are declared just before they are used. Other variables can also be declared where they are used without problems. Strictly speaking, you can even use variables before they are defined, as the declarations are eventually grouped together when the Bicep file is transpiled to an ARM template. All of this greatly enhances both the readability and understandability of the templates.

6.3.7 Comments

A final improvement of the Bicep language is the introduction of comments. While ARM templates already support comments, they have one downside: it makes the templates no longer JSON-compliant. This leads to many text editors pointing out comments as errors. Luckily, VS Code is an exception to that rule, as it understands ARM templates instead of “just” JSON.

With Bicep, this is no longer an issue. You can use comments as follows:

var webAppPlanName = 'myAppPlan' // A double slash starts a comment
 
resource myWebAppPlan 'Microsoft.Web/serverfarms@2020-06-01' = {
    name: webAppPlanName
    location: 'westeurope'
    /*
        Using slash star you can start a multi-line comment
        Star slash closes the comment
    */
}

In this example, there are two types of comments. A single-line comment is added using a double forward slash (//). A single-line comment does not have to start at the beginning of a line, but can start at any position on the line. A multiline comment is created by enclosing it in /* and */. You don’t need to put the /* and */ on lines of their own, but it is the convention.

6.3.8 Using the contents of other files

In some cases, you’ll need to provide a value in your Bicep template that is very long, complex, JSON, or binary. It can be hard to deal with those values in a readable way. Fortunately, from Bicep 0.4.412 onwards, a new function is available: loadTextContent(filePath, [encoding]). You can use this function to include the contents of any file in your Bicep.

One example where this can be beneficial is when you are executing scripts as part of a template deployment. Let’s assume you have a rather lengthy PowerShell script, updateReleaseNotes.ps1, that should be invoked as part of your deployment. Instead of inlining that script into your Bicep template, you can do the following:

resource inlineScriptResource 'Microsoft.Resources/deploymentScripts
         @2020-10-01' = {
    name: 'inlinePowerShellScript'
    location: 'westeurope'
    kind:'AzurePowerShell'
    properties: {
        scriptContent: loadTextContent('updateReleaseNotes.ps1')    
        azPowerShellVersion: '5.0'
        retentionInterval: 'P1D'
  }
}

Invocation of loadTextContent to load the contents from another file

In this example, you can see how the use of loadTextcontent(...) helps to keep the template clean and to the point. This function also ensures that the contents of the loaded file are properly escaped. This way, both the Bicep template and the PowerShell script can be written using their native syntax, which is a great benefit.

You can find out more about loadTextcontent(...) and its cousin loadFileAsBase64(...), which automatically encodes the contents using Base64, in Microsoft’s “File functions for Bicep” article (http://mng.bz/v6x1).

Now that you know all about working with Bicep templates and the extra benefits they bring, let’s look at another powerful new feature that allows you to split larger templates into parts: modules.

6.4 Modules

In chapter 5 you learned about linked templates as an approach to splitting larger solutions into multiple files. The downside of that approach is that the linked template has to be staged on a publicly accessible URL before it can be used. While this is possible, it is a bit of a nuisance to stage your files before you can use them as a linked template.

Bicep provides a solution to this problem. Bicep permits the use of modules that allow you to split a template into multiple files without resorting to linked templates to deploy them all together. Modules are inlined in the template when transpiling to an ARM template, giving you the benefits of both worlds:

Let’s start our discussion of modules by writing a module:

param webAppPlanName string
 
resource myWebAppPlan 'Microsoft.Web/serverfarms@2020-06-01' = {
    name: webAppPlanName
    location: 'westeurope'
}
 
output myWebAppPlanResourceId string = myWebAppPlan.id

If you look at this module closely, you’ll see that it’s nothing but a regular Bicep template. This is intentional, as every Bicep template is reusable as a module in another template. Save this snippet to a file called webAppPlan.bicep, and you can use it as a module like this:

module deployWebAppPlan './webAppPlan.bicep' = {
    name: 'deployWebAppPlan'
    params: {
        webAppPlanName: 'nameForTheWebAppPlan'
    }
}
 
var demoOutputUse = deployWebAppPlan.outputs.myWebAppPlanResourceId

Several new concepts are introduced here. First, the module keyword signals the inclusion of a module. Instead of specifying a resource type and apiVersion, a local file is referenced for including, using a relative path. Between the curly braces, the details for the module inclusion are defined. First, you must specify a name for every module. This name is used as the name for the linked deployment that the module is transpiled into. One other optional property can be specified, the params, for specifying the parameters declared in the module. In this case, there is only one parameter, namely webAppPlanName. Finally, the example shows how to retrieve outputs from the linked template deployment. If you want to include more than one module, you have to repeat this whole block, but with a different name, filename, etc.

Compiling this example to an ARM template would yield a 47-line-long ARM template. Comparing that with the 13 lines in the two preceding snippets really illustrates the added value of Bicep over ARM templates.

6.4.1 Deploying to another scope

When referencing a module from a template, you can also deploy the module to another scope than the main template. This is done using the scope property:

module deployWebAppPlan './webAppPlan.bicep' = {
    name: 'deployWebAppPlan'
    scope: resourceGroup('theNameOfAnotherResourceGroup')
    params: {
        webAppPlanName: 'nameForTheWebAppPlan'
    }
}
 
var demoOutputUse = deployWebAppPlan.outputs.myWebAppPlanResourceId

In the preceding example, you’ll see the same module deployment as before, but with one more property: scope. The value for this property can be any of the allowed scopes: tenant, management group, subscription, or resource group. The value needs to be provided as a full resource id, like /subscriptions/{guid}/resourceGroups/ {resourceGroupName}. The easiest way to provide a scope is using the functions resourceGroup(...), subscription(...), managementGroup(...), and tenant(...).

6.4.2 Debugging Bicep deployments

When you deploy a Bicep template, you are really first transpiling to an ARM template and then deploying that ARM template. The consequence is that when there is an error in your deployment, you will have to debug the ARM template, not your Bicep template.

This means, first, that you will have to deal with line-number differences. If there is a line number in your deployment error, this will correspond to a line number in the intermediate ARM template, not to a line number in your Bicep template. Unfortunately, you will have to find the corresponding location in your Bicep file yourself.

Second, the resulting ARM template will likely be hard to read. That’s not just because ARM templates are more difficult to read than Bicep templates, but also because the use of modules can generate hard-to-follow, complex expressions. Also, the use of modules will result in the generation of nested templates. These two effects can make troubleshooting Bicep deployments a tough job.

But in all fairness, when you switch from ARM templates to Bicep, your chances of ending up in a debugging session are much, much lower. The Bicep extension and Bicep transpiler will catch many errors before starting the deployment. This means that 90% of your feedback loops will be much faster, compensating for the troubles of debugging obscure, generated ARM templates.

6.5 A larger Bicep example

Let’s now put all that you have learned together by rebuilding the infrastructure you created in chapters 3 and 5 using Bicep. Figure 6.1 illustrates this architecture to refresh your memory.

Figure 6.1 An Azure solution architecture

To recreate this architecture from scratch in Bicep, let’s use the modular approach introduced in chapter 5 and create as many resource templates as we need, and a single composing template, using the following structure:

+-- Composing
|   +-- Main.bicep
|   +-- Main.parameters.prod.json
|   +-- Main.parameters.prod.json
|   +-- Api.bicep
|   +-- Configuration.bicep
|   +-- Storage.bicep
+-- AppConfiguration
|   +-- AppConfiguration.bicep
+-- Insights
|   +-- ApplicationInsights.bicep
+-- KeyVault
|   +-- KeyVault.bicep
+-- Sql
|   +-- SqlServer.bicep
|   +-- SqlServerDatabase.bicep
+-- Storage
|   +-- StorageAccountV2.bicep
+-- WebApp
|   +-- Serverfarm.bicep
|   +-- WebApp.bicep

As you already know what to build, you can use a bottom-up approach, where you first build the resource templates and later the composing templates.

6.5.1 AppConfiguration.bicep

The first resource template to rebuild is the template with the AppConfiguration. As an ARM template, this template is 30 lines and looks like this:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "appConfigurationName": {
            "type": "string"
        },
        "skuName": {
            "type": "string",
            "defaultValue": "free"
        },
        "location": {
            "type": "string"
        }
    },
    "resources": [
        {
            "type": "Microsoft.AppConfiguration/configurationStores",
            "apiVersion": "2019-11-01-preview",
            "name": "[parameters('appConfigurationName')]",
            "location": "[parameters('location')]",
            "sku": {
                "name": "[parameters('skuName')]"
            },
            "properties": {
                "encryption": {}
            }
        }
    ]
}

Now we need to rewrite this as a Bicep module. First, you declare the three parameters, which are also in the ARM template:

param appConfigurationName string
param skuName string
param location string

After declaring the parameters, you can write the remainder of the template, which is a single resource:

resource configurationStore 'Microsoft.AppConfiguration/configurationStores
         @2019-11-01-preview' = {
    name: appConfigurationName
    location: location
    sku: {
        name: skuName
    }
    properties: {
        encryption: {}
    }
}

You can again see the ease of use that Bicep brings, as 30 lines of ARM template translated into only 13 lines of Bicep in total.

6.5.2 ApplicationInsights.bicep

The next ARM template is ApplicationInsights.json, which looks like this:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "applicationInsightsName": {
            "type": "string"
        },
        "logAnalyticsWorkspaceName": {
            "type": "string"
        },
        "location": {
            "type": "string"
        }
    },
    "resources": [
        {
            "apiVersion": "2020-02-02-preview",
            "name": "[parameters('applicationInsightsName')]",
            "type": "Microsoft.Insights/components",
            "kind": "web",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[parameters('logAnalyticsWorkspaceName')]"
            ],
            "properties": {
                "applicationId": "[parameters('applicationInsightsName')]",
                "WorkspaceResourceId": "[resourceId('Microsoft
                     .OperationalInsights/workspaces',
                     parameters('logAnalyticsWorkspaceName'))]"
            }
        },
        {
            "name": "[parameters('logAnalyticsWorkspaceName')]",
            "type": "Microsoft.OperationalInsights/workspaces",
            "apiVersion": "2020-08-01",
            "location": "[parameters('location')]",
            "properties": {
                "sku": {
                    "name": "pergb2018"
                },
                "retentionInDays": 30,
                "workspaceCapping": {
                    "dailyQuotaGb": -1
                }
            }
        }
    ]
}

Again, this is quite the template—a total of 46 lines. Let’s see how far that can be trimmed down by switching to Bicep. First, you can add the parameters:

param applicationInsightsName string
param logAnalyticsWorkspaceName string
param location string

Next, declare the first resource, the Application Insights component:

resource applicationInsightsResource 'Microsoft.Insights/components
         @2020-02-02-preview' = {
    name: applicationInsightsName
    kind: 'web'
    location: location
    dependsOn: [
        omsWorkspaceResource
    ]
 
    properties: {
        applicationId: applicationInsightsName
        WorkspaceResourceId: omsWorkspaceResource.id
    }
}

In the preceding code, you can make a small optimization. In the ARM template, the name of the Log Analytics workspace (called OMS workspace in Bicep and ARM templates for historical reasons) is used directly as the second parameter to the resourceId(...) function call. In the Bicep file, this is changed to directly reference the resource and get its id, which is much more straightforward.

Now let’s add that Log Analytics workspace to make this a valid reference:

resource omsWorkspaceResource 'Microsoft.OperationalInsights/workspaces
         @2020-08-01' = {
    name: logAnalyticsWorkspaceName
    location: location
    properties: {
        sku: {
            name: 'PerGB2018'
        }
        retentionInDays: 30
        workspaceCapping: {
            dailyQuotaGb: -1
        }
    }
}

The last three Bicep snippets total 28 lines (not counting empty lines and line continuations)—not much more than half of the 46 lines of ARM template you started out with. That’s not bad!

You can continue onward with the remaining six files and translate them from ARM templates into a Bicep templates. Of course, you can also experiment with bicep decompile, which you learned about in section 6.1.3. Or you can pick up the files from the book’s GitHub repository at http://mng.bz/BMj0. Once all the resource templates are in place, it is time to continue with the composing templates.

6.5.3 Configuration.bicep

The shortest composing ARM template is Configuration.json, so let’s transform that into Configuration.bicep together. These are the contents of the Configuration .json file:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "keyVaultName": {
            "type": "string"
        },
        "appConfigurationName": {
            "type": "string"
        },
        "templateSettings": {
            "type": "object"
        }
    },
    "variables": {
        "templateBasePath": "[concat(parameters('templateSettings')
             .storageAccountUrl, '/',
              parameters('templateSettings').storageContainer)]"
    },
    "resources": [
        {
            "apiVersion": "2020-10-01",
            "name": "KeyVault",
            "type": "Microsoft.Resources/deployments",
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri": "[concat(variables('templateBasePath'),
                         '/Resources/KeyVault/KeyVault.json')]",
                    "queryString": "[parameters('templateSettings')
                         .storageAccountKey]",
                    "contentVersion": "1.0.0.0"
                },
                "parameters": {
                    "keyVaultName": {
                        "value": "[parameters('keyVaultName')]"
                    },
                    "location": {
                        "value": "[parameters('templateSettings')
                             .location]"
                    }
                }
            }
        },
        {
            "apiVersion": "2020-10-01",
            "name": "AppConfiguration",
            "type": "Microsoft.Resources/deployments",
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri": "[concat(variables('templateBasePath'),
                         '/Resources/AppConfiguration
                         /AppConfiguration.json')]",
                    "queryString": "[parameters('templateSettings')
                         .storageAccountKey]",
                    "contentVersion": "1.0.0.0"
                },
                "parameters": {
                    "appConfigurationName": {
                        "value": "[parameters('appConfigurationName')]"
                    },
                    "location": {
                        "value": "[parameters('templateSettings')
                             .location]"
                    }
                }
            }
        }
    ]
}

To convert to this Bicep, again, start with the parameters:

param keyVaultName string
param appConfigurationName string
param templateSettings object

Next, there is a templateBasePath variable. When converting from an ARM template to Bicep, you would normally push the variable declarations as close to the place where they are used as possible. Since both resources use the variable in this template, it makes sense to just put it directly after the parameters section. Still, the template’s readability can be greatly improved by switching to string interpolation syntax from using the concat() function:

var templateBasePath = '${templateSettings.storageAccountUrl}/
     ${templateSettings.storageContainer}'

To complete the translation of the full template, the two linked deployments need to be translated next. Because linked deployments are only used here to split a larger template into smaller parts, we can abandon this approach and switch to Bicep modules. In this case, that leads to the following Bicep code:

module keyVaultModule '../Resources/Keyvault/KeyVault.bicep' = {
    name: 'keyVaultModule'
    params: {
        keyVaultName: keyVaultName
        location: templateSettings.location
    }
}
 
module appConfModule '../Resources/AppConfiguration
     /AppConfiguration.bicep' = {
    name: 'appConfigurationModule'
    params: {
        appConfigurationName: appConfigurationName
        location: templateSettings.location
    }
}

Once you’ve inserted the preceding Bicep modules, the templateBasePath variable and parts of the templateSettings parameter are obsolete. This is due to the fact that Bicep transpiles the main file and all modules into one large file, removing the need for linked files and thus these supporting variables. They can safely be removed.

The remaining composing templates, Api.json, Storage.json, and Main.json, can be translated exactly like Configuration.bicep. Again, you can practice with this yourself or copy the files from the GitHub repository.

Converting the composing templates completes the conversion from ARM templates to Bicep templates. If you want, you can now deploy the Main.bicep file using the following commands:

az group create --location westeurope --resource-group deploybicep
az deployment group create --resource-group deploybicep 
     --templatefile Main.bicep --parameters Main.parameters.test.json

Before you deploy, you may have to update the value for the env parameter in Main .parameters.test.json to prevent collisions in Azure resource names. Also, don’t forget to remove the resources once you are done to avoid incurring unnecessary costs.

Going through this exercise of converting from ARM templates to Bicep at least once is valuable, as many companies still have many ARM templates that you will probably want to convert to Bicep at some point.

Summary