Back to Blog
4 min read

Azure IoT Hub Device Provisioning Service Deep Dive

The Device Provisioning Service (DPS) automates the process of registering devices to IoT Hub without human intervention. It’s essential for zero-touch provisioning at scale.

Understanding DPS Architecture

DPS provides:

  • Zero-touch provisioning
  • Load balancing across multiple IoT Hubs
  • Multi-tenancy support
  • Geo-distribution of devices
  • Re-provisioning support

Setting Up DPS

# Create a DPS instance
az iot dps create \
    --name myDPS \
    --resource-group myResourceGroup \
    --location eastus

# Link to IoT Hub
az iot dps linked-hub create \
    --dps-name myDPS \
    --resource-group myResourceGroup \
    --hub-name myIoTHub \
    --connection-string "$(az iot hub connection-string show --hub-name myIoTHub --query connectionString -o tsv)"

Enrollment Types

Individual Enrollment with Symmetric Key

# Create individual enrollment
az iot dps enrollment create \
    --dps-name myDPS \
    --resource-group myResourceGroup \
    --enrollment-id device001 \
    --attestation-type symmetrickey \
    --primary-key "primary_key_here" \
    --secondary-key "secondary_key_here" \
    --initial-twin-properties '{"tags":{"location":"building1"},"properties":{"desired":{"telemetryInterval":60}}}'

Group Enrollment with X.509

# Upload root CA certificate
az iot dps certificate create \
    --dps-name myDPS \
    --resource-group myResourceGroup \
    --name rootCA \
    --path ./root-ca.cer

# Create enrollment group
az iot dps enrollment-group create \
    --dps-name myDPS \
    --resource-group myResourceGroup \
    --enrollment-id myEnrollmentGroup \
    --certificate-name rootCA \
    --allocation-policy hashed \
    --iot-hubs "myIoTHub1.azure-devices.net myIoTHub2.azure-devices.net"

Device-Side Provisioning Code

from azure.iot.device.aio import ProvisioningDeviceClient
from azure.iot.device.aio import IoTHubDeviceClient
import asyncio
import os

class DeviceProvisioner:
    def __init__(self, id_scope, registration_id, symmetric_key):
        self.id_scope = id_scope
        self.registration_id = registration_id
        self.symmetric_key = symmetric_key
        self.provisioning_host = "global.azure-devices-provisioning.net"

    async def provision_and_connect(self):
        """Provision device and return connected client"""

        # Create provisioning client
        provisioning_client = ProvisioningDeviceClient.create_from_symmetric_key(
            provisioning_host=self.provisioning_host,
            registration_id=self.registration_id,
            id_scope=self.id_scope,
            symmetric_key=self.symmetric_key
        )

        # Add custom payload for allocation policy
        provisioning_client.provisioning_payload = {
            "modelId": "dtmi:com:example:Thermostat;1",
            "region": "us-west"
        }

        print("Registering device...")
        registration_result = await provisioning_client.register()

        if registration_result.status == "assigned":
            print(f"Device registered to {registration_result.registration_state.assigned_hub}")

            # Create device client
            device_client = IoTHubDeviceClient.create_from_symmetric_key(
                symmetric_key=self.symmetric_key,
                hostname=registration_result.registration_state.assigned_hub,
                device_id=registration_result.registration_state.device_id
            )

            await device_client.connect()
            print("Connected to IoT Hub")
            return device_client
        else:
            raise Exception(f"Registration failed: {registration_result.status}")

# Usage
async def main():
    provisioner = DeviceProvisioner(
        id_scope=os.environ["DPS_ID_SCOPE"],
        registration_id=os.environ["DEVICE_REGISTRATION_ID"],
        symmetric_key=os.environ["DEVICE_SYMMETRIC_KEY"]
    )

    client = await provisioner.provision_and_connect()

    # Send telemetry
    await client.send_message('{"temperature": 25.5}')

asyncio.run(main())

X.509 Certificate Provisioning

from azure.iot.device.aio import ProvisioningDeviceClient
import ssl

class X509DeviceProvisioner:
    def __init__(self, id_scope, registration_id, cert_file, key_file):
        self.id_scope = id_scope
        self.registration_id = registration_id
        self.cert_file = cert_file
        self.key_file = key_file

    def create_ssl_context(self):
        """Create SSL context with device certificate"""
        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        ssl_context.load_cert_chain(
            certfile=self.cert_file,
            keyfile=self.key_file
        )
        ssl_context.load_verify_locations(cafile="./azure-iot-root-ca.pem")
        return ssl_context

    async def provision(self):
        """Provision using X.509 certificate"""
        provisioning_client = ProvisioningDeviceClient.create_from_x509_certificate(
            provisioning_host="global.azure-devices-provisioning.net",
            registration_id=self.registration_id,
            id_scope=self.id_scope,
            x509=self.create_x509()
        )

        return await provisioning_client.register()

    def create_x509(self):
        from azure.iot.device import X509
        return X509(
            cert_file=self.cert_file,
            key_file=self.key_file
        )

Custom Allocation Policy with Azure Functions

import azure.functions as func
import json
import logging

def main(req: func.HttpRequest) -> func.HttpResponse:
    """Custom allocation policy for DPS"""
    logging.info('Custom allocation policy triggered')

    try:
        req_body = req.get_json()
        logging.info(f"Request: {json.dumps(req_body)}")

        # Extract device information
        registration_id = req_body.get('deviceRuntimeContext', {}).get('registrationId')
        payload = req_body.get('deviceRuntimeContext', {}).get('payload', {})

        # Get available IoT Hubs
        iot_hubs = req_body.get('linkedHubs', [])

        # Custom allocation logic based on device payload
        region = payload.get('region', 'default')

        if region == 'us-west':
            selected_hub = next((h for h in iot_hubs if 'westus' in h['name']), iot_hubs[0])
        elif region == 'us-east':
            selected_hub = next((h for h in iot_hubs if 'eastus' in h['name']), iot_hubs[0])
        else:
            # Default: select hub with lowest weight
            selected_hub = min(iot_hubs, key=lambda h: h.get('allocationWeight', 1))

        response = {
            "iotHubHostName": selected_hub['name'],
            "initialTwin": {
                "tags": {
                    "region": region,
                    "provisionedBy": "custom-allocation"
                },
                "properties": {
                    "desired": {
                        "telemetryInterval": 60
                    }
                }
            }
        }

        return func.HttpResponse(
            json.dumps(response),
            status_code=200,
            mimetype="application/json"
        )

    except Exception as e:
        logging.error(f"Error: {str(e)}")
        return func.HttpResponse(
            json.dumps({"error": str(e)}),
            status_code=500
        )

Re-provisioning Scenarios

async def handle_reprovisioning(device_client, current_hub):
    """Handle device re-provisioning when hub changes"""

    async def connection_state_handler(new_state):
        if new_state == "disconnected":
            print("Disconnected from IoT Hub")
            # Check if re-provisioning is needed
            await check_and_reprovision()

    device_client.on_connection_state_change = connection_state_handler

async def check_and_reprovision():
    """Check if device needs to re-provision"""
    provisioner = DeviceProvisioner(...)
    result = await provisioner.provision_and_connect()

    if result.registration_state.assigned_hub != current_hub:
        print("Hub assignment changed, reconnecting to new hub")
        # Update connection to new hub

DPS is essential for deploying and managing IoT devices at scale across multiple regions and IoT Hubs.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.