Running Dapr on Azure IoT Edge

I've been working on a large-scale project that has some interesting requirements performance wise. While having around 6+ modules, I should get a performance of around 10ms roundtrip! One container does AI inferencing within ~2ms.

Problem Definition

The problem I am seeing now is that the Azure IoT Edge SDK has some performance issues. When publishing and receiving a message, the delay instantly spikes up towards ~50ms which is not really what I need.

I could enable the new MQTT feature of IoT Edge, but seeing that this is in preview and I would prefer something more stable I started looking into alternatives.

Which is how I ended up again with using my new favourite cross-platform and cross-language dev tool: Dapr!

If I remember correctly, Dapr runs performance metrics internally that ensure a strong performance while being able to communicate between microservices (which is what IoT Edge Modules are in the simplest form). So let's see how we can hook up Dapr on Azure IoT Edge!

Dapr on Azure IoT Edge - Hello World

Prerequisites

To keep this article to the core of adding Dapr to IoT Edge, I will start of with Prerequisites. In case you need help, feel free to check out the public repository with this example.

The prerequisite is to:

  1. Create an Azure IoT Edge Solution
  2. Add a module
  3. Name: ModuleHelloWorld
  4. Type: javascript
  5. Initialize a Typescript project in this container.

Adding Dapr

Now we need to add Dapr. What is important to remember here is that Dapr is a sidecar based architecture, which means that we have a process running Dapr code next to our main process. Running in standalone mode, this means having 2 processes while as in Kubernetes mode we have 2 containers in 1 pod, where the Dapr container is injected automatically.

For this we should thus include the Dapr environment in our Container. There are 2 ways to do this:

  1. Utilize the Dapr CLI
  2. Utilize daprd as a daemon process out of the box

From the 2 options above, I went for the first one as the CLI is the default way to work with Dapr, and it has a slim mode that allows us to work minimally, while still having all the easiness to install Dapr.

To install Dapr, we add the following lines to our Docker file:

# Install the Dapr CLI
RUN wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash

# Install the latest Dapr Runtime
RUN dapr init -s; dapr --version

Which will make our Dockerfile for a typescript based dapr docker container look like this:

FROM node:latest

WORKDIR /usr/src/app

# #####################################
# Setting up Container
# #####################################
# Install deps
RUN apt-get update

# Create Certificate
RUN apt-get install ca-certificates

# #####################################
# Setting up Dapr
# #####################################
# Install the Dapr CLI (note: we could install Daprd but this is easier for documentation seeing that dapr run works)
RUN wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash

# Install the latest Dapr Runtime
# -> we install in slim mode which excludes placement service, redis and zipkin containers
# Note: we can change the version with dapr init --runtime-version
RUN dapr init -s; dapr --version

# #####################################
# Setting up Node.js App
# #####################################
# Install Package.json dependendencies
COPY package.json .
RUN npm install

# Copy Source Code
ADD . /usr/src/app

RUN npm run build

CMD [ "npm", "run", "start:dapr" ]
EXPOSE 8000

Dapr in our Application

Finally what we should do is to add the npm run start:dapr command to our application. We could do this in a .sh script, but seeing we are utilizing Node.js we have a package.json that contains a script key where we add the boot command:

"scripts": {
  "start": "npm run build && node dist/index.js",
  "start:dapr": "DAPR_APP_PORT=4000 dapr run --app-id dapr-iot-edge-hello-world --app-port 4000 --dapr-http-port 3500 --components-path ./components/ npm run start"
}

Utilizing a simple typescript file that prints Hello World when we invoke the main method through Dapr its Invocation API:

import Dapr, { Req, Res } from "@roadwork/dapr-js-sdk";

async function main() {
  const client = new Dapr("127.0.0.1", 3500);

  await client.invoker.listen("main", async (req: Req, res: Res) => {
    console.log(req.body);
    return res.json({ message: "Hello World" });
  });
}

main()
.catch((e) => {
  console.error(e);
  process.exit(1);
});

Testing

We now start the IoT Edge module and test with our favourite REST tool to see if we can invoke the application.

Conclusion

In this article I went more in-depth on what Dapr can mean for an IoT Edge application. However, if you take a look at the performance metric you can see a round-trip from Dapr to the application code and back as a response of 4ms!!! Which is simply amazing. If I were to do this in IoT Edge on the Bus and await the request and response, this would be ~45ms in my tests, which is a 10 fold increase in performance when using Dapr!

If we now want to replicate IoT Edge its on-edge bus, we could take a look on enabling Redis with Dapr Pub/Sub to replace the core functionality of IoT Edge its bus.

Note: Even if we do the above, we can still utilize the IoT Edge SDK to send to $upstream on our main communication module 🤩

In my opinion I would even consider switching to Dapr entirely for the following reasons:

  1. Simplified Development Stack: 1 stack to learn across all your applications
  2. Open-Source and heavily maintained
  3. Cross-Cloud and can even run on Kubernetes
  4. It reduces the dependency on Azure IoT Edge

What are your thoughts? Let me know!