Bicep: Create IoT Hub and Storage routing with security in mind
Tags: azure, bicep, iothub, routing
Published: 2022-11-16 07:34:00

In this article, I introduce bicep scripts which provision

- IoT Hub
  - with managed identity enabled
  - routing to storage account for all telemetry
  - disable fallback for routing
- Storage Account (v2)
  - disable public access
  - allow access only via the IoT Hub managed identity

As you can see, IoT Hub needs to know storage account for routing, and storage account needs to know IoT Hub managed identity, which makes circular reference. So I use bicep module to:

1. Create IoT Hub and Storage Account without routing configuration
1. Set IAM for storage account by using IoT Hub managed identity
1. Set routing feature for IoT Hub to point to the storage account

## bicep script

I have four bicep scripts for this.

### main.bicep

The main.bicep calls modules by passing parameter. I use ``dependsOn`` to make sure the executing order.

```shell
@description('Define the project name or prefix for all objects.')
param projectName string = 'myawesomeproject'

@description('The datacenter to use for the deployment.')
param location string = resourceGroup().location

module iotHubStorage './iotHubStorage.bicep' = {
  name: 'iotHubStorageDeployment'
  params: {
    projectName: projectName
    location: location
  }
}

module role './role.bicep' = {
  name: 'roleDeployment'
  params: {
    projectName: projectName
    IoTHubPrincipalId: iotHubStorage.outputs.iotPrincipalId
  }
  dependsOn: [iotHubStorage]
}

module iotHubRouting './iotHubRouting.bicep' = {
  name: 'iotHubRoutingDeployment'
  params: {
    projectName: projectName
    location: location
  }
  dependsOn: [role]
}
```

### iotHubStorage.bicep

This bicep create IoT Hub and Storage Account (V2) with minimum settings. As I need managed identity information for next module, use ``output`` to pass the id. The storage account is lock down to allow only IoT Hub. 

```shell
param projectName string
param location string

@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('Partitions used for the event stream.')
param d2cPartitions int = 4

var iotHubName = '${projectName}-IoTHub'
var storageAccountName = '${toLower(projectName)}st'
var storageContainerName = '${toLower(projectName)}results'

resource iotHub 'Microsoft.Devices/IotHubs@2021-07-02' = {
  name: iotHubName
  location: location
  sku: {
    name: skuName
    capacity: skuUnits
  }
  identity:{
    type: 'SystemAssigned'
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties:{
    allowBlobPublicAccess: false
    allowSharedKeyAccess: false
    networkAcls:{
       bypass: 'AzureServices'
       defaultAction: 'Deny'
       resourceAccessRules:[{
         resourceId: iotHub.id
         tenantId: tenant().tenantId
       }]
    }
  }
}

resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = {
  name: '${storageAccountName}/default/${storageContainerName}'
  properties: {
    publicAccess: 'None'
  }
  dependsOn: [
    storageAccount
  ]
}


output iotPrincipalId string = iotHub.identity.principalId
```

### role.bicep

I setup IAM for storage account by using generated managed identity of IoT Hub. I set ``Storage Blob Data Contributor`` role for now.

```shell
param projectName string
param IoTHubPrincipalId string

var storageAccountName = '${toLower(projectName)}st'

resource storageAccount 'Microsoft.Storage/storageAccounts@2019-06-01' existing = {
  name: storageAccountName 
}

//https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
  name:'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
  scope: storageAccount
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  scope: storageAccount
  name: guid(storageAccount.id, IoTHubPrincipalId,  roleDefinition.id)
  properties: {
    roleDefinitionId: roleDefinition.id
    principalId: IoTHubPrincipalId
    principalType: 'ServicePrincipal'
  }
}
```

### iotHubRouting.bicep

Finally, I setup routing and custom endpoint pointing to the storage account.

```shell
param projectName string
param location string

@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('Partitions used for the event stream.')
param d2cPartitions int = 4

var iotHubName = '${projectName}-IoTHub'
var storageAccountName = '${toLower(projectName)}st'
var storageEndpoint = '${projectName}StorageEndpont'
var storageContainerName = '${toLower(projectName)}results'
var storageRouteName = '${projectName}Route'

resource storageAccount 'Microsoft.Storage/storageAccounts@2019-06-01' existing = {
  name: storageAccountName 
}

resource iotHub 'Microsoft.Devices/IotHubs@2021-07-02' = {
  name: iotHubName
  location: location
  sku: {
    name: skuName
    capacity: skuUnits
  }
  identity:{
    type: 'SystemAssigned'
  }
  properties: {
    eventHubEndpoints: {
      events: {
        retentionTimeInDays: 1
        partitionCount: d2cPartitions
      }
    }
    routing: {
      endpoints: {
        storageContainers: [
          {
            authenticationType: 'identityBased'
            endpointUri: storageAccount.properties.primaryEndpoints.blob
            containerName: storageContainerName
            fileNameFormat: '{iotHub}/{partition}/{YYYY}/{MM}/{DD}/{HH}/{mm}'
            batchFrequencyInSeconds: 60
            maxChunkSizeInBytes: 104857600
            encoding: 'JSON'
            name: storageEndpoint
          }
        ]
      }
      routes: [
        {
          name: storageRouteName
          source: 'DeviceMessages'
          condition: 'true'
          endpointNames: [
            storageEndpoint
          ]
          isEnabled: true
        }
      ]
    }
    messagingEndpoints: {
      fileNotifications: {
        lockDurationAsIso8601: 'PT1M'
        ttlAsIso8601: 'PT1H'
        maxDeliveryCount: 10
      }
    }
    enableFileUploadNotifications: false
    cloudToDevice: {
      maxDeliveryCount: 10
      defaultTtlAsIso8601: 'PT1H'
      feedback: {
        lockDurationAsIso8601: 'PT1M'
        ttlAsIso8601: 'PT1H'
        maxDeliveryCount: 10
      }
    }
  }
}
```

## Run bicep

You can use ``az`` cli to run it.

```shell
az login
az deployment group create --resource-group <your resource group> --template-file .\main.bicep
```

Once deployment is done, confirm telemetry routed to storage account.

## Summary

It was a bit tricky to handle circular reference and I am not 100% sure this is correct way to solve it, but it works as expected. Please give me any feedback if you know better way to achieve the same!!

## Reference

Find out Role Definition Id: 
https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles