Infrastructure as Code - Getting started with Bicep

Apr 15, 2022 8:36 AM

Personal Blog
Microsoft
Azure
IaC
Bicep
ARM

What is Bicep?

Bicep, also known as project Bicep, is a declarative language to define your Azure services as code (also known as Infrastructure as Code). Defining your Azure services this way enables you to have more consistent deployments as well as it being repeatable and reusable. Bicep makes direct use of the Azure REST API which enables you to always have up-to-date features which are ready for implementation. This means that when something new is being added, it will also be available in the Bicep language.

You can see Bicep as the evolution of ARM templates, so let's look at the differences and how we can write our services in the Bicep language!

Bicep vs ARM

Azure Resource Manager templates (ARM) are based on the JSON format, making it statically typed and more cumbersome and complex to write due to the Domain-specific language (DSL) being embedded within the JSON itself. With Bicep the DSL moved to its own format, hence the .Bicep file format.

Let's compare both and see the difference in code yourself.

ARM template

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.4.1008.15138",
      "templateHash": "9634475065903215417"
    }
  },
  "parameters": {
    "serverName": {
      "type": "string",
      "defaultValue": "[uniqueString('sql', resourceGroup().id)]"
    },
    "sqlDBName": {
      "type": "string",
      "defaultValue": "SampleDB"
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    },
    "administratorLogin": {
      "type": "string"
    },
    "administratorLoginPassword": {
      "type": "secureString"
    }
  },
  "functions": [],
  "resources": [
    {
      "type": "Microsoft.Sql/servers",
      "apiVersion": "2019-06-01-preview",
      "name": "[parameters('serverName')]",
      "location": "[parameters('location')]",
      "properties": {
        "administratorLogin": "[parameters('administratorLogin')]",
        "administratorLoginPassword": "[parameters('administratorLoginPassword')]"
      }
    },
    {
      "type": "Microsoft.Sql/servers/databases",
      "apiVersion": "2020-08-01-preview",
      "name": "[format('{0}/{1}', parameters('serverName'), parameters('sqlDBName'))]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "Standard",
        "tier": "Standard"
      },
      "dependsOn": [
        "[resourceId('Microsoft.Sql/servers', parameters('serverName'))]"
      ]
    }
  ]
}

Bicep

param serverName string = uniqueString('sql', resourceGroup().id)
param sqlDBName string = 'SampleDB'
param location string = resourceGroup().location
param administratorLogin string

@secure()
param administratorLoginPassword string

resource server 'Microsoft.Sql/servers@2019-06-01-preview' = {
  name: serverName
  location: location
  properties: {
    administratorLogin: administratorLogin
    administratorLoginPassword: administratorLoginPassword
  }
}

resource sqlDB 'Microsoft.Sql/servers/databases@2020-08-01-preview' = {
  name: '${server.name}/${sqlDBName}'
  location: location
  sku: {
    name: 'Standard'
    tier: 'Standard'
  }
}

As you can see, the Bicep code is smaller, less complex and overall easier to read. If you want to see this for yourself, take a look at the Bicep playground.

While the ease of Bicep is very nice, it might not make everyone who had already invested a lot in creating ARM templates for their deployments very happy. Luckily Microsoft thought of that as well!

Convert ARM to Bicep

If you already have (multiple) environments up and running in ARM templates, it can be a very costly effort to write everything to Bicep. While rewriting, it will make it easier and more future-proof in the end, you might want to have the intermediate solution of converting your existing ARM templates to Bicep files.

To be able to do this, you will need to have the Bicep CLI installed. I recommend to use the extension within Visual Studio Code (VS code) for this.

When you've got the CLI installed, you can run the following command:

az bicep decompile --file main.json

Note: You might need to change the pathing of your ARM file to get it to work properly.

After your conversion, you can expect possible warnings and/or errors from the generated Bicep file, since the conversion is not a guaranteed success. You might need to do minor tweaks or clean it up, to make it more readable.

Even with these minor edits, it's a good intermediate solution to make the migration to Bicep.

Parameters

As you might have noticed in the comparison between ARM and Bicep, the declaration of parameters requires way less code in Bicep itself and always starts as param, but that is not the only thing. Bicep allows for 3 ways of defining parameters to your deployment.

  1. Default values within your Bicep itself;
  2. A Parameter file, the same as you might have used with your ARM templates;
  3. From the command line in your release pipeline (YAML).

Via each of these ways, the parameters can be Strings (text), Integers (numbers), Booleans (True or False), Objects and Arrays (structured data and/or lists), for these are the data types allowed within a Bicep parameter.

Personally I recommend a combination of options 1 and 3, which gives you all the flexibility you need and this way, you won't have multiple parameter files to manage.

To give an example:

A perfect default value would be: param location string = resourceGroup().location, which allows the Bicep to automatically determine the resourceGroup location to which you are deploying.

A good example for a command line fed parameter would be: -dev Which could easily be used for the determination of the environment to deploy to as well as to add for the naming convention of your services.

While parameters are great to use, they can also be a security risk when not used correctly and, for example, login and/or passwords are stored within them in plain text. To do this in a secure manner you can use the `@securebefore specific parameter to let Azure know it should not store any of these parameters within its deployment logs.

Now this is nice for deployments, but we still need to get the secrets in a secure way, which can be done by calling for your secrets from your Azure KeyVault within your Bicep module (more on this later).

resource keyVault 'Microsoft.KeyVault/vaults@2021-04-01-preview' existing = {
  name: keyVaultName
}

module applicationModule 'application.bicep' = {
  name: 'application-module'
  params: {
    apiKey: keyVault.getSecret('ApiKey')
  }
}

The keyVault.getSecret('ApiKey') allows you get the secret which is linked to the ApiKey and the ApiKey can be supplied to a @secure parameter via your command line deployment, as well as deployment variables.

Conditions and loops

While parameters already help a lot in making your deployments more dynamic, you can also use conditions within your Bicep files. Bicep allows you to use If-statements to check if something is True or False.

An example of use a Condition would be:

param environment string

resource auditingSettings 'Microsoft.Sql/servers/auditingSettings@2020-11-01-preview' = if (environment == 'prd') {
  parent: server
  name: 'default'
  properties: {
  }
}

Conditions help a lot with deployments, especially when you only want to deploy when resource configurations match or when wanting to deploy to a specific environment as in the example above.

Loops on the other hand allow you to integrate through an Array or a range of numbers if you need to deploy a resource multiple times but with different naming or configurations.

An example of a Loop would be:

param storageAccountNames array = [
  'saauditus'
  'saauditeurope'

]

resource storageAccountResources 'Microsoft.Storage/storageAccounts@2021-01-01' = [for storageAccountName in storageAccountNames: {
  name: storageAccountName
  location: resourceGroup().location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
}]

As you can see, the loop is called with a For [for storageAccountName in storageAccountNames:. This can also be replaced by [for i in range(1,4): to iterate over a range of numbers if need be.

Now you might also have some different, or even a multitude of configurations you need to deploy, such as Subnets, for example. For this, you can create a Variable containing the configuration which will be fed by a parameter. To make this more clear, let me show this in an example:

param addressPrefix string = '10.10.0.0/16'
param subnets array = [
  {
    name: 'frontend'
    ipAddressRange: '10.10.0.0/24'
  }
  {
    name: 'backend'
    ipAddressRange: '10.10.1.0/24'
  }
]

var subnetsProperty = [for subnet in subnets: {
  name: subnet.name
  properties: {
    addressPrefix: subnet.ipAddressRange
  }
}]



resource virtualNetwork 'Microsoft.Network/virtualNetworks@2020-11-01' = {
  name: 'VNet-workload-x'
  location: resourceGroup().location
  properties:{
    addressSpace:{
      addressPrefixes:[
        addressPrefix
      ]
    }
    subnets: subnetsProperty
  }
}

The Variable SubnetsProperty will iterate through all the items specified with an Array parameter and will add it as such to the configuration of the resource.

Modules and Outputs

Last but not least, Modules and Outputs. As we have already seen how to make our Bicep files more dynamic, condition and loopable they can also become reusable for different deployments and/or solutions. This can be done by creating one main.bicep file in which you call separate Bicep files to contain all the configurations needed for that particular resource.

An example of a Module would be:

module WebAppModule 'modules/WebApp.bicep' = {
  name: WebAppName 
  params: {
    location: location
    appServiceAppName: appServiceAppName
    environmentType: environmentType
  }
}

It calls the WebApp.bicep file from the Modules folder in your GIT and will deploy it with the specified Parameters. These separate bicep files can also have specific outputs, which you might need later on in your deployment.

A good example of this would be a ResourceId you need for adding a Managed Service Identity to another resource. You can do so by adding an output to the Bicep, like:

output WebAppResourceId string = appService.id

These Outputs become very handy since it doesn't require you to run custom CLI scripts to get the required data for later in your deployment.

Now we looked at all the fundamentals you need to know to make some amazing Bicep files! If you want to get templates for all services you can look here.

What's next?

Currently I'm wrapped up in multiple projects at the same time and managing between them all. Expect some new content about Logic apps, Bicep, CosmosDB and more in the upcoming weeks!