This is Part 3 of a three-part series on running a real camera fleet on Azure IoT Operations (AIO):

Part 2 put the fleet on the Azure IoT Operations MQTT broker. Now the broker is no longer just a message bus - it is an edge data plane, and two AIO features turn that into real leverage: data flows route telemetry to the cloud, and connectors bring in cameras that cannot speak our protocol at all. Both land without changing the control plane, which is the recurring theme of this series.

Forwarding telemetry to the cloud, for free

In Part 1, every producer publishes to one topic tree: cameras/<site>/<camera>/<channel>. That single fact makes cloud egress almost trivial. An AIO data flow has a source and a destination; point the source at the local broker on the cameras/# tree - the exact tree everything already uses - and point the destination at Azure. No producer changes, no new topics, nothing republished.

The destination here is Azure Event Hubs, addressed through its Kafka surface, authenticated with the AIO instance’s managed identity (no connection strings on disk):

apiVersion: connectivity.iotoperations.azure.com/v1
kind: DataflowEndpoint
metadata:
  name: cameranetwork-eventhub
  namespace: azure-iot-operations
spec:
  endpointType: Kafka
  kafkaSettings:
    host: "<EVENTHUB_NAMESPACE>.servicebus.windows.net:9093"
    authentication:
      method: SystemAssignedManagedIdentity
      systemAssignedManagedIdentitySettings: {}
    tls:
      mode: Enabled

The data flow itself wires the local broker to that endpoint. The source endpointRef: default is AIO’s built-in local MQTT broker (aio-broker), and one side of every data flow must be that local broker:

apiVersion: connectivity.iotoperations.azure.com/v1
kind: Dataflow
metadata:
  name: cameras-to-eventhub
  namespace: azure-iot-operations
spec:
  profileRef: default
  mode: Enabled
  operations:
    - operationType: Source
      sourceSettings:
        endpointRef: default          # the local broker (host aio-broker)
        dataSources: [ "cameras/#" ]   # the same tree from Part 1
    - operationType: Destination
      destinationSettings:
        endpointRef: cameranetwork-eventhub
        dataDestination: camera-telemetry   # the Event Hub / Kafka topic name

Before applying it, you create the Event Hubs namespace and hub and grant the AIO managed identity the Azure Event Hubs Data Sender role - that role grant is what the managed-identity auth above relies on. Telemetry is forwarded as JSON pass-through, so no schema registry is needed; the payloads landing in Event Hubs are the unchanged CameraNetwork.Contracts JSON from Part 1.

Cloud egress in one data flow: agents and gateways publish to the AIO MQTT broker at the Arc-enabled k3s edge, and a single data flow over the cameras topic tree forwards JSON pass-through to Azure Event Hubs via its Kafka surface with managed identity, fanning out to Fabric Real-Time Intelligence and Azure Data Explorer

As agents heartbeat, the Event Hubs “incoming messages” graph rises, carrying availability, inventory, status, and events for the whole fleet. From there, Fabric Real-Time Intelligence or Azure Data Explorer are the natural next stops for dashboards and historical analytics; those destinations add a schema-registry reference, but the edge side stays exactly as shown. The reason this is so cheap is structural: because Part 1 single-sourced the topic tree, cloud egress is one data flow over cameras/#, not an integration per producer.

Bringing in cameras that cannot speak the protocol

Data flows handle the outbound story. The inbound story is connectors. AIO ships an ONVIF connector and a media connector that can talk to standards-based cameras directly - discover them, model them as assets, and publish their telemetry into the broker. That is a different path from the Class A site gateway in Part 1, and it is interesting precisely because it lets AIO itself onboard a camera.

The connectors are preview and their custom resources are version- and install-specific, so they are deployed through the supported tooling rather than checked-in YAML. The flow is:

  1. Register the camera as an Azure Device Registry device (its ONVIF endpoint plus credentials).
  2. Let discovery enumerate the camera’s capabilities and profiles.
  3. Create assets for the streams and snapshots you care about.

At that point the connector is publishing asset telemetry into the AIO broker - but on its topic and in its shape, not on the cameras/<site>/<camera>/... contract the controller understands. Something has to translate. That something is a small bridge.

The AioBridge: a gateway whose probe is a connector

CameraNetwork.AioBridge is an in-cluster Worker service. Conceptually it is a site gateway whose probe is the AIO connector instead of a direct ONVIF query. It subscribes to the connector’s telemetry, maps each asset to a (site, camera) pair, and republishes onto cameras/<site>/<camera>/... using the same CameraJson serializer the rest of the fleet uses. To the controller, the result is indistinguishable from a Class A camera behind a gateway - which is the whole reason Part 1 insisted the controller never branch on camera class.

The bridge connects to AIO’s internal listener (aio-broker:18883, TLS plus a Kubernetes service-account token), so it never leaves the cluster. It gets its SAT through a projected volume and trusts the broker via the AIO CA bundle:

# 50-aiobridge-deployment.yaml (trimmed)
containers:
  - name: aio-bridge
    image: containerreg.qimata.net/cameranetwork-aiobridge:0.1.0
    env:
      - { name: AioBridge__Host,  value: "aio-broker" }
      - { name: AioBridge__Port,  value: "18883" }
      - { name: AioBridge__UseTls, value: "true" }
      - { name: AioBridge__CaFile, value: "/var/run/certs/ca.crt" }
      - { name: AioBridge__SatAuthFile, value: "/var/run/secrets/tokens/broker-sat" }
      - { name: AioBridge__ConnectorTelemetryTopic, value: "azure-iot-operations/data/#" }
      - { name: AioBridge__DefaultSiteId, value: "aio-edge" }
volumes:
  - name: broker-sat
    projected:
      sources:
        - serviceAccountToken: { path: broker-sat, audience: aio-internal, expirationSeconds: 86400 }

Asset-to-camera identity is explicit so cameras do not collide: the preferred path is custom attributes on each asset (cameraNetwork.site and cameraNetwork.camera), with a fallback of a default site id plus a slug of the asset name. Connectors are preview, so the two things most likely to differ between installs - the connector’s telemetry topic and its payload field names - are isolated in one class, ConfigurableAssetTelemetryMapper, and covered by tests. If your connector publishes elsewhere or names fields differently, you adjust that single mapper, not the controller.

The AioBridge: an ONVIF or RTSP camera is onboarded by the AIO ONVIF/media connector as an Azure Device Registry asset; CameraNetwork.AioBridge subscribes to the connector telemetry and republishes it on the cameras/{site}/{camera} tree via CameraJson, so the controller ingests it and it appears on the dashboard with no controller change

Commands back to connector-fronted cameras

A camera that only reports is half a control plane. The bridge also answers commands, behind a toggle (AioBridge:HandleCommands, on by default). It subscribes to cameras/+/+/cmd and answers only for cameras it has actually seen - it tracks the assets it has observed, so it never steals a command meant for a Class B agent that happens to share the topic space.

Command handling sits behind an IBridgeCommandExecutor seam, mirroring the site gateway’s policy. The default executor acks get_status as succeeded and acks everything else - including snapshot - as unsupported, until you plug in a real executor backed by media-connector capture or ONVIF control. The ack comes back on cameras/<site>/<camera>/cmd_ack, the same channel from Part 1. In the dashboard, you click get-status on an AIO camera and watch the cmd_ack arrive, exactly as you would for an agent-managed camera.

This is the closed command set from Part 1 doing its job again: the bridge cannot be told to do anything arbitrary, only to attempt actions from the fixed vocabulary, and it honestly reports unsupported for the ones it cannot yet perform.

The whole picture

Put the three posts together and the shape is a single contract with interchangeable parts underneath it:

The whole picture: three producer kinds - Class B izon-agent, Class A site-gateway, and the AIO connector plus AioBridge - all speak the cameras topic tree into the AIO MQTT broker; the broker feeds the controller's dashboard, API and metrics and an AIO data flow forwards to Event Hubs and on to Fabric RTI and ADX

Three different kinds of cameras, one wire contract, one broker, one data flow to the cloud - and the controller treats all of them identically. Azure IoT Operations did not reshape the application; it slotted in underneath the contract (the broker, Part 2) and alongside it (the connectors and data flows, Part 3). That is the argument for AIO in a camera control plane: it is an enterprise-grade edge MQTT broker and cloud data plane that you can adopt incrementally, behind a seam, without rewriting the system that already works.

Start at Part 1 if you came in here first - the contract and the network model are what make all of this hold together.

References