DEV Community

Kenichiro Nakamura
Kenichiro Nakamura

Posted on • Edited on

Bicep: Create IoT Hub and Storage routing with security in mind

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
  2. Set IAM for storage account by using IoT Hub managed identity
  3. 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.

@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]
}
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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'
  }
}
Enter fullscreen mode Exit fullscreen mode

iotHubRouting.bicep

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

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Run bicep

You can use az cli to run it.

az login
az deployment group create --resource-group <your resource group> --template-file .\main.bicep
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)