Creating a IoT hub within the Azure portal is a piece of cake, so why should we use Bicep and what is Bicep anyway. In this article, I will explain about Bicep and we will create a simple Bicep example to deploy an IoT hub.

Bicep

Bicep is a language, like ARM (but much easier to understand), to deploy resources to Azure. The definition of the infrastructure is stored in a Bicep file. Bicep files can be deployed manually or can be deployed by Azure DevOps pipelines. Because the definition of the infrastructure is stored in files, deployments are consistent and repeatable. The Bicep files can also be added to your repository, enabling version control. That is why many developers used it as a solution for “Infrastructure as code“.

As mentioned before, Bicep is stored in a file. Bicep files have the extension “.bicep”. The files contain a certain structure. In the image below, I highlighted some important parts of the infrastructure.

  • Parameter settings – Additional settings for the parameters. For example, you may restrict the length of the given parameter.
  • Parameters – Parameters are unknown variables which need to be provided when the Bicep is being executed
  • Variables – A variable is a predefined representation of a value
  • Resources – This part will represent the Azure resources. Each Azure resource has its own Bicep documentation page on the Microsoft site. Here you can find all the available properties and settings which you may apply. For example, the documentation page of the IoT hub can be found here: https://learn.microsoft.com/en-us/azure/templates/microsoft.devices/iothubs?pivots=deployment-language-bicep
  • Modules – A module can be compared with a template, it contains a subset of one or more Bicep resources. Modules are very useful when deploying the same Azure resource but with different settings.

Now we know some of the basics, we can start building.

Prerequisites

In this showcase, I use VS Code for development. Before we start creating the IoT hub with bicep, there are some prerequisites:

  • An Azure subscription
  • Create a new resource group in Azure to deploy the IoT hub to
  • Install Bicep tools, click this link for instruction
  • Create a new project in VS Code

Let’s get started

Now we can really get started. Create a file “IoThub.bicep” in a (new) folder, named “Modules”:

Edit the file and add the following code:

// --- Parameters
@description('Define the iotHub name.')
param iotHubName  string 

@description('The Azure region in which all resources should be deployed.')
param location string = resourceGroup().location

@description('The SKU to use for the IoT Hub.')
param skuName string = 'S1'

@description('The number of IoT Hub units.')
param skuUnits int = 1

// --- Resources
resource IoTHub 'Microsoft.Devices/IotHubs@2021-07-02' = {
  name: iotHubName
  location: location
  sku: {
    name: skuName
    capacity: skuUnits
  }  
}

This Bicep contains the bare minimum for creating an IoT hub. In a real life project, the file will contain much more properties to set everything.

Is it working?

To check if the Bicep file is working, we are going to deploy it manually to Azure. Open a terminal in VS Code via the menu (see the screenshot below) or by using the shortcut Ctrl+Shift+`.

Log in into Azure and select the subscription where the resources should be deployed to:

az login
az account set --subscription "<your subscription id>"

Now we can deploy the resources using the Bicep file:

az deployment group create --resource-group <your resource name> --template-file .\Modules\IoTHub.bicep 

Next, the script will ask you to provide values for all the parameters without a default value. In our example, this would only be the name of the IoT hub because the other parameters have a default value. After a couple of seconds, the resource is deployed and the result will look something like this:

Ok, now we know that our Bicep is working.

Take it to the next step

Our Bicep for creating the IoT hub was quite simple. In a real life situation, you have to deploy much more resources. Suppose we want to store the data from the IoT hub into a storage account. Let’s create some more logic to our sample project to do this.

Create a file “Storage.bicep” in the folder “Modules” and edit the file by adding the following code:

// --- Parameters
@description('''
Define the Storage Account name
restrictions:
- Storage account names must be between 3 and 24 characters in length and may contain numbers and lowercase letters only.
- Your storage account name must be unique within Azure. No two storage accounts can have the same name.
''')
@minLength(3)
@maxLength(24)
param storageAccountName string

@description('The Azure region in which all resources should be deployed.')
param location string = resourceGroup().location

// --- Variables
@description('The SKU to use for the Storage Account.')
param storageAccountSkuName string = 'Standard_LRS'

// --- Resources
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageAccountSkuName
  }
  kind: 'StorageV2' 
}

output id string = storageAccount.id

This Bicep also contains the bare minimum for creating a Storage Account. But it is enough for this sample project.

Did you also notice the extra restrictions I added to the parameter storageAccountName? The name of a storage account must be between 3 and 24 characters, by adding these extra restrictions the Bicep will enforce a string between 3 and 24 characters.

Next, we need to connect the IoT hub to the storage account. So, let’s open the file IoThub.bicep again and change it to:

// --- Parameters
@description('Define the iotHub name.')
param iotHubName  string 

@description('The Azure region in which all resources should be deployed.')
param location string = resourceGroup().location

@description('The SKU to use for the IoT Hub.')
param skuName string = 'S1'

@description('The number of IoT Hub units.')
param skuUnits int = 1

@description('Define the name of the storage account.')
param storageAccountName  string 

@description('Define the id of the storage account.')
param storageAccountID  string 

@description('Define the name of the container.')
param storageContainerName string

// --- Variables
var storageEndpoint = '${storageAccountName}StorageEndpont'
var storageAccountKey = listKeys(storageAccountID, '2022-09-01').keys[0].value

// --- Resources
resource IoTHub 'Microsoft.Devices/IotHubs@2021-07-02' = {
  name: iotHubName
  location: location
  sku: {
    name: skuName
    capacity: skuUnits
  }  
  properties: {
    eventHubEndpoints: {
      events: {
        retentionTimeInDays: 1
        partitionCount: 4
      }
    }
    routing: {
      endpoints: {
        storageContainers: [
          {
            connectionString: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${storageAccountKey}'
            containerName: storageContainerName
            fileNameFormat: '{iothub}/{partition}/{YYYY}/{MM}/{DD}/{HH}/{mm}'
            batchFrequencyInSeconds: 100
            maxChunkSizeInBytes: 104857600
            encoding: 'JSON'
            name: storageEndpoint
          }
        ]
      }
      routes: [
        {
          name: 'ContosoStorageRoute'
          source: 'DeviceMessages'
          condition: 'level="storage"'
          endpointNames: [
            storageEndpoint
          ]
          isEnabled: true
        }
      ]
      fallbackRoute: {
        name: '$fallback'
        source: 'DeviceMessages'
        condition: 'true'
        endpointNames: [
          'events'
        ]
        isEnabled: true
      }
    }    
  }
}

Combine the two Bicep files

Now, we have two separated Bicep files, and it is possible to deploy them each individually. But it is better to combine them, so they can be deployed at once. For this, we need to create an extra file “Main.bicep” in the root folder. Edit the file by adding the following code:

// --- Parameters
@description('The Azure region in which all resources should be deployed.')
param location string = resourceGroup().location

@description('Define the Storage Account name')
param storageAccountName string 

@description('Define the name of the container.')
param storageContainerName string 

@description('Define the iotHub name.')
param iotHubName string 

// --- Storage account
module storageAccount 'Modules/Storage.bicep' = {
  name: 'storageAccountAux'
  params: {
    storageAccountName: storageAccountName
    storageAccountSkuName: storageContainerName
    location: location
  }
}


// --- IoT hub
module IoTHub 'Modules/IoThub.bicep' = {
  name: 'IoTHub'
  params: {
    iotHubName: iotHubName
    location: location
    storageAccountName: storageAccountName
    storageAccountID: storageAccount.outputs.id
    storageContainerName: storageContainerName
  }
}

By deploying the main Bicep file, a storage account and IoT hub will be generated at once. Let’s give it a try, open the terminal window (if you already closed it) and execute:

az deployment group create --resource-group <your resource name> --template-file .\Main.bicep 

Next, the script will ask you to provide values for all the parameters without a default value. After a minute or so, the resources will be generated.

What’s next?

The next step is to integrate the Bicep deployments into your Azure DevOps pipeline. Actually, this is very easy by creating a “AzureCLI@2” job step in your pipeline. Execute the main.bicep file within the right context of your deployment. In the example below, I added a sample of a jobstep of a DevOps pipeline:

- task: AzureCLI@2
  displayName: 'Deploy infrastructure'
  inputs:
    azureSubscription: <Azure Resource Manager connection>
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: |
      az deployment group create \
        --resource-group <your resource name> \
        --template-file Main.bicep \
        --parameters \         
          storageAccountName="<My storage account>" \
          storageContainerName="<My containter>" \
          iotHubName="<My IoT Hub>"

Conclusion

In this article, we created an IoT hub using a simple Bicep example. Again, this is a simple example of using Bicep to create resources in Azure. But imagine this in a real life project where you have many more resources and different environments. How easy it is to deploy the same resources over and over again.