Making ARM templates easier with Bicep

Lots of folks use Azure Resource Manager (ARM) templates to define their Azure resources, and tooling like the Az CLI or Az PowerShell to deploy them.

ARM has a lot going for it, but Microsoft have long acknowledged that ARM has its weaknesses; it's verbose, constructs like loops and conditions and functions are awkward to use, and it can be difficult to know if you've got the dependsOn elements right.

Enter Bicep.

This post won't be another getting started guide to Bicep. You can follow the Bicep teams own getting started guide and tutorials at your own pace for that. Rather, in this post we'll deconstruct a real Bicep template that I built recently for an Azure Function app.

But first some introductions.

What is Bicep?

Bicep is a new Domain Specific Language (DSL) for writing declarative Azure deployments. It uses ARM templates as an intermediate language. That is, your Bicep files compile to ARM JSON templates which you can then deploy with existing ARM template deployment processes like New-AzResourceGroupDeployment, az deployment, the Azure Portal, Azure DevOps Pipeline tasks, Github Actions, etc.

The Bicep team have written a good introduction to the what and why of Bicep on the project readme.

The project readme also has a list of the goals and non-goals of the project which do a good job of setting expectations around what Bicep is and is not. You can see them on the readme, but to summarise;

  • Goals
    • To be a strict superset of ARM that's easier to work with. Anything you can do with ARM you should be able to do with Bicep, and do it more easily.
    • It should be easier to understand and have a shallower learning curve than ARM.
    • You should be able to use all the same concepts you use for building ARM templates, but apply them more easily.
    • Excellent tooling that provides resource and type discoverability and validation, built alongside the compiler and not bolted on at the end.
  • Non-Goals (quoting directly from the readme)
    • "Build a general purpose language to meet any need. This will not replace general purpose languages and you may still need to do pre or post Bicep tasks in a script or high-level programming language."
    • "Provide a first-class provider model for non-Azure related tasks. While we will likely introduce an extensibility model at some point, any extension points are intended to be focused on Azure infra or application deployment related tasks."

It's still early days

Before going any further, I should point out that as of early 2021, Bicep is still an experimental language. It's still very early days for the language, tooling, and resource provider support. The latest version as I write this post is 0.2.328.

This means a few things;

  • Bicep is not yet at feature parity with ARM. There's things you can do in ARM that you can't do in Bicep. The list of major known limitations is on the readme. That list is much shorter now than it was when Bicep was first announced at Build 2020 and demo'd at Ignite 2020, but it's still not empty.
  • The language is still evolving, you should expect breaking changes in future releases.
  • Along with things you can't do with Bicep, there's also a number of things that, while you can do them with Bicep, it's not as easy as the Bicep team would like it to be. Expect new language syntax to be introduced to cover those things.

In short, Bicep is neither officially supported nor production ready. It has a lot of potential and the team are hard at work making it better (just look at the issue log!), but the current advice is to;

Use Bicep at your own risk.

Installing Bicep

To get going, see the installation documentation to install the CLI and VS Code extension.

You can find the latest releases on the project's GitHub Releases page.

For Windows users, the CLI package is also available on Winget (winget install bicep) and Chocolatey (choco install bicep) if that's your thing.

Using Bicep

At this stage in Bicep's life, there isn't any special support for Bicep in the Az CLI or Az PowerShell. You have to use the Bicep compiler to generate the ARM json templates, which you then deploy.

Do this with bicep build

bicep build deploy.bicep

When you build a Bicep file, it'll generate an ARM template named {bicep filename}.json, in this case deploy.bicep generates deploy.json.

Let's see the code!

In this post we'll deconstruct an existing Bicep template I wrote for an Azure Function app. You can find the deploy.bicep template in context on GitHub.

Parameters

Bicep parameters have two forms, there's a short syntax that only lets you define the name, type, and default value, and a longer form that lets you do all the other things you can do with ARM parameters.

A parameter with no default value can look like this:

param AppInsightsApiKeySecretResource string

When compiled to ARM with bicep build, it looks like a simple bare-bones parameter declaration:

"parameters": {
   ...
  "AppInsightsApiKeySecretResource": {
      "type": "string"
    },
  ...
}

To add a default value, add = {default value}, like this:

param location string = resourceGroup().location

This turns into a basic parameter with a default value. When you bicep build, this parameter turns into the following:

"parameters": {
  ...
  "location": {
    "type": "string",
    "defaultValue": "[resourceGroup().location]"
  },
  ...
}

You'll notice that ARM functions are supported too, just like in ARM templates. All the regular functions are supported in the same way as ARM, and with all the same restrictions.

Bicep supports "allowedValues", although you need to use the long form I mentioned earlier. Here's a parameter with 2 allowed values.

param environmentSuffix string {
  default: 'dev'
  allowed: [
    'dev'
    'prod'
  ]
}

becomes

"parameters": {
  ...
  "environmentSuffix": {
    "type": "string",
    "defaultValue": "dev",
    "allowedValues": [
      "dev",
      "prod"
    ]
  },
  ...
}

You use similar syntax to define other constraints and properties too. For example:

param environmentSuffix string {
  default: 'dev'
  metadata:{
    description: 'The environment suffix'
  }
  minLength: 3
  maxLength: 20
}

becomes

"parameters": {
  ...
  "environmentSuffix": {
    "type": "string",
    "minLength": 3,
    "maxLength": 20,
    "metadata": {
      "description": "The environment suffix"
    },
    "defaultValue": "dev"
  },
  ...
}

Variables

Bicep variables have a similar syntax to the 'simple' form of the parameter, although without needing to specify a type. The next few examples show using existing ARM functions, but also introduce some of the new Bicep language syntax.

Here we see Bicep's support for string interpolation, and a couple of additional ARM functions.

var appInsightsName = '${appBaseName}-${environmentSuffix}-appinsights'
var storageName = toLower('${appBaseName}${environmentSuffix}${uniqueString(resourceGroup().id)}')

These two variables appInsightsName and storageName become

"variables": {
  ...
  "appInsightsName": "[format('{0}-{1}-appinsights', parameters('appBaseName'), parameters('environmentSuffix'))]",
  "storageName": "[toLower(format('{0}{1}{2}', parameters('appBaseName'), parameters('environmentSuffix'), uniqueString(resourceGroup().id)))]",
  ...
}

You'll see that Bicep uses the format() string function rather than concat().

Next up, we see Bicep's support for ternary if-then-else expressions.

var computedKeyVaultName = keyVaultName == 'default' ? '${appBaseName}-${environmentSuffix}-kv' : 'default'

becomes

"variables": {
  ...
"computedKeyVaultName": "[if(equals(parameters('keyVaultName'), 'default'), format('{0}-{1}-kv', parameters('appBaseName'), parameters('environmentSuffix')), 'default')]"
  ...
}

Resources

Resources in Bicep look a bit different to ARM, and is where we start to see Bicep really shine. Let's take a look at the Storage account.

resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
  name: storageName
  location: location
  kind: 'StorageV2'
  sku: {
      name: storageSku
  }
}

resource tableAka 'Microsoft.Storage/storageAccounts/tableServices/tables@2019-06-01' = {
  name: '${stg.name}/default/${tableNameAka}'
}

As you might expect from reading the Bicep for these two resources, they become:

"resources": [
  ...
  {
    "type": "Microsoft.Storage/storageAccounts",
    "apiVersion": "2019-06-01",
    "name": "[variables('storageName')]",
    "location": "[parameters('location')]",
    "kind": "StorageV2",
    "sku": {
      "name": "[parameters('storageSku')]"
    }
  },
  {
    "type": "Microsoft.Storage/storageAccounts/tableServices/tables",
    "apiVersion": "2019-06-01",
    "name": "[format('{0}/default/{1}', variables('storageName'), parameters('tableNameAka'))]",
    "dependsOn": [
      "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]"
    ]
  },
  ...
}

There's a lot going on here, so let's break it down a bit. A resource in Bicep is defined by the following syntax;

resource <identifier> '<resource provider namespace>@<version>'= {
  ...<properties>...
}

The <identifier> is a special Bicep construct, it doesn't appear in the final ARM template. It lets us refer to the resource elsewhere in the Bicep file. We see this used in the .../tableServices/tables resource that defines a storage table. It's what allows Bicep to know that when we say ${stg.name}, it needs to generate a "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]" ] property.

The properties that go inside the braces are the same properties you would define in an ARM template, so use the existing ARM resource provider documentation to know what they are.

We also see Bicep's automatic dependsOn support showing up in some of the other resources. For example:

resource functionApp 'Microsoft.Web/sites@2020-06-01' = {
  name: functionAppName
  kind: 'functionapp'
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appService.id      // <----- references another resource
    httpsOnly: true
  }
}

Notice the line that says serviceFarmId: appService.Id? That becomes a "[resourceId(...)]" function, and generates another "dependsOn" property:

{
  "type": "Microsoft.Web/sites",
  ...snip for brevity...
  "properties": {
    "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServiceName'))]",
    "httpsOnly": true
  },
  "dependsOn": [
    "[resourceId('Microsoft.Web/serverfarms', variables('appServiceName'))]"
  ]
}

Let's take a look at the Function App's app settings resource, there's some interesting syntax going in there:

resource functionAppAppSettings 'Microsoft.Web/sites/config@2020-06-01' = {
  name: '${functionApp.name}/appsettings'
  properties:{
    // snip for brevity
    // Also... yay comments!
    WEBSITE_RUN_FROM_PACKAGE: '1'
    'X-Authorization': '@Microsoft.KeyVault(SecretUri=${XAuthSecretResource})'
    AppInsightsApiKey: '@Microsoft.KeyVault(SecretUri=${AppInsightsApiKeySecretResource})'
    AppInsightsAppId: reference(resourceId('Microsoft.Insights/components', appInsightsName), '2020-02-02-preview').AppId
    'AzureWebJobs.StatsCollector.Disabled': 'true'
  }
  dependsOn:[
    keyVaultAccessPolicies
  ]
}

A couple of things to point out here:

  • You can add explicit dependsOn if either a) Bicep doesn't correctly generate the "dependsOn" entry, or b) it's an implicit dependency that Bicep (or even ARM) has no way to know about.
    • In this case, the appSettings resource includes some App Service Key Vault references, and we want to make sure the keyVaultAccessPolicies resource is deployed first, to make sure the right access policies are in place so the App Service can retrieve the secrets as soon as it starts up.
  • There's a few ARM resource types that allow arbitrary property names, App Service app settings are one of them. Anything that's a valid JSON string can be used as the property name. But Bicep has rules governing the naming of properties that are more strict that those of JSON and ARM, so Bicep supports single-quote-escaping property names that wouldn't otherwise be valid Bicep identifiers.
    • We see this here with 'X-Authorization' and 'AzureWebJobs.StatsCollector.Disabled'. Neither of those are valid Bicep property names, so we have to escape them with single quotes.
  • Bicep also allows // comments! Yay!

Like the ARM VS Code extension, both the Bicep VS Code extension and the Bicep CLI are type aware. This allows the VS Code extension to tell you what properties you can and must include, and the CLI will emit warning or error messages if you're missing required properties, or if the types don't match, etc.

But Bicep's type support doesn't always get things right. So here's one last resource example showing how to break free of Bicep's type system when it gets things wrong.

resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = {
  name: any('${keyVaultName}/add')
  properties: {
    accessPolicies: [
      {
        tenantId: functionApp.identity.tenantId
        objectId: functionApp.identity.principalId
        permissions:{
          secrets: [
            'get'
          ]
        }
      }
    ]
  }
}

In this case, Bicep's type support thinks the only valid values for the name of a Microsoft.KeyVault/vaults/accessPolicies is one of add, replace, or remove. In this case, because we're deploying a child resource we also need to include the name of the parent resource, following ARMs usual child resource naming rules.

The special Bicep any() function let's us tell Bicep to ignore its type constraints and allows us to put define any value, effectively suppressing Bicep's type safety.

If we remove the any() function here, Bicep would emit an error and refuse to compile the Bicep file.

Outputs

Naturally Bicep allows us to define output values. These are exactly the same as ARM outputs, and use syntax similar to variables.

output computedStatsTableName string = tableNameStats
output functionAppHostName string = functionApp.properties.defaultHostName

Notice that unlike variables we have to specify the type, in this case they're both string's.

These two output declarations become:

"outputs": {
  ...
  "computedStatsTableName": {
    "type": "string",
    "value": "[parameters('tableNameStats')]"
  },
  "functionAppHostName": {
    "type": "string",
    "value": "[reference(resourceId('Microsoft.Web/sites', variables('functionAppName'))).defaultHostName]"
  },
  ...
}

Type safety and errors

We've seen some of the type safety features the Bicep introduces, and introduced the any() operator that lets you break out of the type system.

As an example of how the type safety system works, let's look at what happens if I remove one of the any() operators.

Here's the experience in VS Code:

Type error in VS Code

And here's the equivalent error you get from the CLI when you try to build the file:

Type error in CLI

The CLI output also shows a couple of warnings, saying that it doesn't have any type information for the API version of the resource providers we're using. You'd see the same warnings in VS Code when you work with an ARM json file with the official ARM VS Code extension.

A couple of final things to say about Bicep's type system:

  • The type information is only as good as what the Bicep team can generate from the API specifications for each resource provider. If you've worked with ARM templates long enough, you'll know that occasionally even those API specifications are themselves wrong. You'll need the any() function to get around those problems.
  • Even when the API specifications are correct, Bicep doesn't always apply the rules quite correctly. We saw this above with the name property on the Key Vault access policy resource, where we had to use the any() function to be able to include the parent resource name.
  • The Bicep team are tracking known occurrences of type mismatches on GitHub issue #784 - Missing type validation / inaccuracies. If you come across any places where you need to use the any() function, check the issue and if you've found a new one, add it to the list.

Conclusion

That's a long post, congratulations on making it to the end!

In this post we've introduced Bicep and seen how it's a DSL that essentially uses ARM json as an intermediate language for defining and deploying Azure resources. We've dissected a real .bicep file and seen some of what makes Bicep such a compelling addition to the Azure deployment story.

We've seen that Bicep is type-safe based on the published Azure Resource Provider specifications, and has a bunch of really useful language constructs that make it easier than ever to define your Azure infrastructure.

If you have any investment in ARM templates, take Bicep for a spin. I think you'll like it!

Resources

You can find the original version of this post on Anthony's personal blog.