Akri
- 9 minutes read - 1816 wordsFor the past couple of weeks, I’ve been playing around with Akri, a Microsoft (DeisLabs) project for building a connected edge with Kubernetes. Kubernetes, IoT, Rust (and Golang) make this all compelling to me.
Initially, I deployed an Akri End-to-End to MicroK8s on Google Compute Engine (link) and Digital Ocean (link). But I was interested to create me own example and so have proposed a very (!) simple HTTP-based protocol.
This blog summarizes my thoughts about Akri and an explanation of the HTTP protocol implementation in the hope that this helps others.
I suspect Akri attempts to homogenize resources (e.g. IoT devices) to simplify their use by application developers.
When I initially read the Akri documentation, I considered an MQTT protocol implementation. This is the approach used by KubeEdge and, it makes sense because MQTT and IoT are well-suited. However, I’m uncertain whether MQTT is a good fit with Akri, see below.
After using Nessie and the IP camera examples, I wondered what the simplest IoT device solution would look like, an exemplar. And, while HTTP is not well-suited to every IoT device scenario, HTTP is widely-used, is easy to test (using e.g. curl
) and provides a trivial way to simulate IoT devices.
So, for the purposes of what follows, the IoT devices being accessed by Akri are simply very basic HTTP servers that, generate a random float on their root endpoint:
curl http://device-X:8080
0.4091323204249145
It is probable that a solution employs a myriad such devices and so, in the example, 9 devices exist and are named device-X
where X
is 1:9
. Devices come and go and so, rather than a static list of devices, the solution uses a basic discovery service. This is also an HTTP server and, it enumerates known devices on its root endpoint:
curl discovery:9999
http://device-1:8080
http://device-2:8080
http://device-3:8080
http://device-4:8080
http://device-5:8080
http://device-6:8080
http://device-7:8080
http://device-8:8080
http://device-9:8080
The device and discovery are written in Golang, packaged as containers and run as Kubernetes Deployments exposed through Services. In the above, the services are deployed to the default
namespace (for convenience) and so have fully-qualified names, e.g. http://device-7.default.svc.cluster.local:8080
.
An immediate question is: why would a developer need Akri to access these devices?
We need to remember that this example is intentionally the simplest possible not necessarily the most likely. So, let’s extrapolate on what an IoT device would more realistically present.
I think there are many types of IoT device in which the device’s API is not presented as a simple HTTP API, may also require an unconventional authentication and authorization and would very likely not be addressable over a conventional TCP network.
This bring us to the first benefit of Akri. Akri’s discovery mechanism provides an abstraction over the mechanism by which IoT devices are discovered. Even on a home TCP-based WiFi network, finding devices, is non-trivial. But, finding proprietary devices running a proprietary API over a proprietary network protocol is even more challenging.
For the HTTP example, discovery is straightforward:
device_list
.lines()
.map(|endpoint| {
let mut props = HashMap::new();
props.insert(BROKER_NAME.to_string(), "http".to_string());
props.insert(DEVICE_ENDPOINT.to_string(), endpoint.to_string());
DiscoveryResult::new(endpoint, props, true)
})
.collect::<Vec<DR>>();
The result of the HTTP GET show above is a list of device URLs. The discovery handler iterates over the list and, for each line (corresponding to a device), it adds 2 items to a HashMap
that correponds to properties of Akri’s twin of the device: BROKER_NAME
and DEVICE_ENDPOINT
. The DEVICE_ENDPOINT
is the device’s URL and this value becomes an environment variable in the broker that Akri creates in response to discovering this device.
let device_url = env::var(DEVICE_ENDPOINT)?;
This is part of the extension of the Akri Agent to support this HTTP example. The Agent creates a Kubernetes Pod (and possibly the Akri Controller creates a corresponding Service) for each device. What’s the simplest way (and Twelve-Factor App recommended way) to pass state into a container? Through its environment. And so, as a result of discovery, a device “twin” is created and it is given metadata about the device that it represents by Akri through user-defined environment variables. This list would likely be more expansive in a realistic scenario.
The discovery functionality could entail using some unconventional transport, authentication etc. The result is simply a list of DiscoveryResult
with a protocol specific list of configuration data.
Both discovery and the Broker’s “twin” main loop require interaction with a device. This involves use of the device’s network protocol (Akri cites bluetooth, LoRaWAN as possibilities) and I think this becomes a good signal of when to use Akri: If there is a specific network protocol that’s needed and from which the developer would benefit from being insulated.
As mentioned above, at runtime, the Agent will create Pod brokers (“twins”) for each IoT device that it discovers. In the Akri examples, Rust is used to implement the twin but this Rust is not required. The HTTP example uses Rust and Golang. The Agent creates Kubernetes Pods for the twins and these Pods may (!) expose TCP sockets.
So, another benefit of Akri is that the Agent creates twins and the twins may expose some API that exposes the underlying device functionality to other services in the cluster.
In the HTTP example, the basic (standalone) Broker has a timed main
loop that GETs its device’s endpoint (/
) every 10 seconds. In the standalone case, the Broker does not expose device functionality beyond the Broker.
After deploying a configuration describing a standalone HTTP Broker to the Akri Agent:
kubectl apply --filename=./http.yaml
Because the Discovery endpoint that the Agent is given lists 9 devices, the HTTP-specific configuration given to the Akri Agent (that is defined in http.yaml
) results in the creation of 9 “twins” in Kubernetes using the HTTP Broker image:
kubectl get pods --selector=akri.sh/configuration=http
NAME READY STATUS RESTARTS AGE
akri-http-231bfa-pod 1/1 Running 0 2m32s
akri-http-38a57c-pod 1/1 Running 0 2m32s
akri-http-4a70c3-pod 1/1 Running 0 2m32s
akri-http-703d61-pod 1/1 Running 0 2m32s
akri-http-8bb408-pod 1/1 Running 0 2m32s
akri-http-8c07de-pod 1/1 Running 0 2m22s
akri-http-8f52a9-pod 1/1 Running 0 2m32s
akri-http-ccb080-pod 1/1 Running 0 2m31s
akri-http-d62266-pod 1/1 Running 0 2m32s
Picking one of these at random:
kubectl logs pod/akri-http-8bb408-pod
[http:main] Entered
[http:main] Device: http://device-4:8080
[http:main:loop] Sleep
[http:main:loop] read_sensor(http://device-4:8080)
[http:read_sensor] Entered
[main:read_sensor] Response status: 200
[main:read_sensor] Response body: Ok("0.21385530533042224")
[http:main:loop] Sleep
From the twin’s logs we can see that it corresponds to device-4:8080
and we can query the device as well (because in this case, it’s not an IoT device but a Kubernetes Service exposing a shared Pod):
kubectl logs service/device-4
2020/11/13 18:51:01 [main] Paths: 1
2020/11/13 18:51:01 [main] Creating handler: /
2020/11/13 18:51:01 [main] Starting Device: [:8080]
2020/11/13 19:43:27 [main:handler] Handler entered: /
This is somewhat interesting but less useful than we’d like. A goal with Akri is to make these arbitrary IoT devices accessible to application developers.
In the HTTP example, this is achieved by exposing the device’s twin (!) as a gRPC service. In this case, the Broker is extended and implements a simple protobuf that includes a (gRPC) service:
syntax = "proto3";
package http;
service DeviceService {
rpc ReadSensor (ReadSensorRequest) returns (ReadSensorResponse);
}
message ReadSensorRequest {
}
message ReadSensorResponse {
string value = 1;
}
Instead of querying the device’s URL and logging the result, the Broker now returns the value when a client that implements the service calls ReadSensor
.
Let’s deploy a configuration to the Akri Agent representing the gRPC-based HTTP Broker:
kubectl apply --filename=./grpc.broker
There are still 9 devices and so, once again, 9 “twins” are created:
kubectl get pods --selector=akri.sh/configuration=http
NAME READY STATUS RESTARTS AGE
akri-http-231bfa-pod 1/1 Running 0 35s
akri-http-38a57c-pod 1/1 Running 0 35s
akri-http-4a70c3-pod 1/1 Running 0 35s
akri-http-703d61-pod 1/1 Running 0 35s
akri-http-8bb408-pod 1/1 Running 0 35s
akri-http-8c07de-pod 1/1 Running 0 35s
akri-http-8f52a9-pod 1/1 Running 0 35s
akri-http-ccb080-pod 1/1 Running 0 35s
akri-http-d62266-pod 1/1 Running 0 35s
But, if we check a twin’s log this time:
kubectl logs pod/akri-http-8c07de-pod
[main] Entered
[main] gRPC service endpoint: 0.0.0.0:50051
[main] gRPC service proxying: http://device-7:8080
[main] gRPC service starting
We see that a gRPC service is created to proxy a device (device-7
).
And, because Pods that expose ports are more effectively consumed using Services, this configuration also instructs the Akri Controller to generate Kubernetes services:
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
http-231bfa-svc ClusterIP 10.152.183.82 <none> 50051/TCP,9456/TCP 2m40s
http-38a57c-svc ClusterIP 10.152.183.192 <none> 50051/TCP,9456/TCP 2m41s
http-4a70c3-svc ClusterIP 10.152.183.113 <none> 50051/TCP,9456/TCP 2m41s
http-703d61-svc ClusterIP 10.152.183.102 <none> 50051/TCP,9456/TCP 2m42s
http-8bb408-svc ClusterIP 10.152.183.54 <none> 50051/TCP,9456/TCP 2m42s
http-8c07de-svc ClusterIP 10.152.183.108 <none> 50051/TCP,9456/TCP 2m42s
http-8f52a9-svc ClusterIP 10.152.183.17 <none> 50051/TCP,9456/TCP 2m41s
http-ccb080-svc ClusterIP 10.152.183.206 <none> 50051/TCP,9456/TCP 2m41s
http-d62266-svc ClusterIP 10.152.183.210 <none> 50051/TCP,9456/TCP 2m41s
http-svc ClusterIP 10.152.183.168 <none> 50051/TCP,9456/TCP 2m42s
This list is edited for convenience. You’ll note there’s a service for each Pod (for each device) and another service called http-svc
. This is a generic Service over all the Pods. We’re going to use that instead of picking a specific service.
Remember that it represents the gRPC-based Broker. The service is running on 50051
and, for simplicity, this is the port that used by the Pod and the Service.
We can use grpcurl
and our protobuf to check the endpoint:
./grpcurl \
--plaintext \
-proto \
./http.proto $(kubectl get service/http-svc --output=jsonpath="{.spec.clusterIP}"):50051 \
http.DeviceService.ReadSensor
{
"value": "0.7323459962156816"
}
NOTE We must determine the ClusterIP of
http-svc
because we’re runninggrpcurl
from outside the cluster. When we run in-cluster, we can reference it ashttp-svc
.
So, what?
Well, we know have a platform upon which application developers can work.
They’re able to use a well-defined and hopefully familiar gRPC API to access Kubernetes Service endpoints. These Service endpoints are “twins” of some elusive IoT that’s been discovered automatically for us by Akri and proxied using a Broker that, in practice, may be provided by the IoT device manufacturer, or possibly by some internal IT department.
Regardless, whether we have one or a thousand devices and whether there are a myriad types of device that we must use, all using different network protocols and authentication etc., we now only require application developers to know e.g. gRPC to build applications with these devices.
Back to MQTT.
In the case of MQTT, there’s no fundamental need for Akri discovery. Akri may be used to discover MQTT brokers but, I think, generally, these would be well-known endpoints.
A purpose of MQTT is to disintermediate consumers (e.g. application developers) from producers (e.g. IoT devices). Using MQTT, we should assume that the IoT devices are already connected to MQTT (a lighter-weight protocol than HTTP that’s more commonly used in IoT applications particularly those involving simpler devices). These IoT devices would be streaming data to MQTT topics.
I think it’s possible that Akri discovery could be used to discover MQTT topics, perhaps. But then, these messages are standardized. The application developer consumer (probably running as an in-cluster Kubernetes application) implements the MQTT SDK and, acting as a client, can pop data from topics.
This leaves little complexity that needs to be addressed by an Akri implementation and so I am unclear whether one is needed for MQTT.