Deploy Azure IoT Edge deployment manifest programmatically

Azure IoT Edge is based on the concept of modules. A module is a container holding some logic executed on the edge device. These containers are actual Docker containers.

These can both be generic containers like a NodeJS that you have produced yourself, an open-source container, or a commercial container. In can also be a container supporting Azure IoT Edge module twins and the routing between modules using one of the Azure IoT Edge SDKs.

Anyway, the modules have to be deployed at one point in time.

By default, Azure IoT Edge devices are constructed with two basic modules registered, the edgeAgent (which is responsible for life-and-death of other modules) and the edgeHub (for enabling message routing between modules and the local gateway towards the cloud):

With life-or-death of other modules I mean the EdgeAgent is responsible for keeping the module configuration on the Azure IoT Edge device in sync with the registration and configuration in the IoT Hub device registration.

For this purpose, the Edge Agent is keen on receiving the so-called ‘deployment manifest‘.

Each time the configuration of an edge device registration in the IoT Hub changes, a new version of the deployment manifest is offered to the Edge Agent. It contains both the module descriptions and their configuration and a description of the message routing on the edge.

The Edge Agent then picks up the deployment manifest and checks for changes with the last manifest it received. If there are any configuration changes, or modules added or modules deleted, the edgeAgent will start the process of synchronizing the deployment.

If you check the documentation, three ways of altering the IoT Edge configuration (and thus deploying a new deployment manifest) are documented:

  1. Command Line Interface (CLI)
  2. The Azure portal
  3. Visual Studio Code

Notice these deployments are effectuated by hand.

For those seeking a CI/CD solution two other ways are offered:

  1. Azure DevOps
  2. Jenkins

These are advised if you want to automate the deployment in a CI/CD pipeline.

If you prefer to do everything by programming source code, you can deploy your manifest using REST calls.

Let’s see how that is done.

Microsoft offers Azure IoT SDKs for many languages. These SDKs cover either the devices or the IoTHub cloud gateway.

If we look at the IoTHub, under the covers, it is totally controlled using REST calls.

There is some documentation about the REST call capabilities although the details are pretty hard to understand.

One of the Rest calls is available for applying a configuration on IoT Edge: ‘applyConfigurationContent’.

Next to the minimal documentation, there is another catch. You need to provide a security token before you can call a REST call, eg. to submit a Deployment Manifest to a device.

IoT Hub uses security tokens to authenticate devices and services to avoid sending keys on the wire. Additionally, security tokens are limited in time validity and scope.

Luckily, Microsoft has a fine description of how to get your hands on a security token for an IoT Hub using some code.

The programming description on how to generate a security token comes with examples for multiple languages:

  • C#
  • Python
  • Javascript

Note: if you want to, you can actually rewrite this code in every programming language which is capable of doing basic HTTP calls.

Once the security token is constructed, the REST call for deploying an IoT Edge deployment manifest can be executed.

In the code example below, we see how to execute the REST call using a security token which is constructed first:

using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Net;

namespace ConsoleApp13
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var iotHubName = "[IoT Hub resource name].azure-devices.net";
            var policyName = "iothubowner";
            var key = "[primay key]";

            var token = ConstructToken(iotHubName, policyName, key);

            var deviceId = "rollout";

            var result = DeployManifest(iotHubName, deviceId, token);

            Console.WriteLine($"Press a key to exit {result}");
            Console.ReadKey();
        }

        private static string DeployManifest(string iotHubName, string deviceId, string token)
        {
            using var client = new HttpClient();

            client.DefaultRequestHeaders.Add("Authorization", token);

            var body = File.ReadAllText("deloyment.manifest.json");
            var stringContent = new StringContent(body, Encoding.UTF8, "application/json");

            var restUriPost = $"https://{iotHubName}/devices/{deviceId}/applyConfigurationContent?api-version=2020-03-13";

            using var resultPost = client.PostAsync(restUriPost, stringContent).Result;

            return resultPost.StatusCode.ToString();
        }

        public static string ConstructToken(string iotHubName, string policyName, string key, int expiryInSeconds = 3600)
        {
            var fromEpochStart = DateTime.UtcNow - new DateTime(1970, 1, 1);
            var expiry = Convert.ToString((int)fromEpochStart.TotalSeconds + expiryInSeconds);

            var stringToSign = $"{WebUtility.UrlEncode(iotHubName)}\n{expiry}";

            var hmac = new HMACSHA256(Convert.FromBase64String(key));
            var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));

            var token = $"SharedAccessSignature sr={WebUtility.UrlEncode(iotHubName)}&sig={WebUtility.UrlEncode(signature)}&se={expiry}";

            if (!string.IsNullOrEmpty(policyName))
            {
                token += "&skn=" + policyName;
            }

            return token;
        }
    }
}

Note: This code is intended to be executed within the cloud. It uses an IoT Hub Shared access policy key that must not be exposed in a device or in the programming environment. It’s like a skeleton key for the service.

As you can see, no extra Azure IoT NuGet package is needed, all you need is some basic HTTP and cryptography libraries.

The token generation looks a bit strange with eg. the ‘\n’ but this is how a token should be. Just copy the code and see what happens.

Note: For those seeking the original method from Microsoft to create the security token, please look twice. It’s a copy of the original code. I just modernized that method a bit.

If we look at the original REST call documentation, the potential content of the REST body is described. The description seems pretty useless:

But we are in luck, an IoT Edge Deployment manifest is just JSON and actually starts with a node called ‘modulesContent’!

That means we just have to send a complete deployment manifest like this:

{
  "modulesContent": {
    "$edgeAgent": {
      "properties.desired": {
        "modules": {
          "SimulatedTemperatureSensor": {
            "settings": {
              "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0.42",
              "createOptions": ""
            },
            "type": "docker",
            "version": "1.0",
            "status": "running",
            "restartPolicy": "always"
          }
        },
        "runtime": {
          "settings": {
            "minDockerVersion": "v1.25"
          },
          "type": "docker"
        },
        "schemaVersion": "1.0",
        "systemModules": {
          "edgeAgent": {
            "settings": {
              "image": "mcr.microsoft.com/azureiotedge-agent:1.0",
              "createOptions": ""
            },
            "type": "docker"
          },
          "edgeHub": {
            "settings": {
              "image": "mcr.microsoft.com/azureiotedge-hub:1.0",
              "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"443/tcp\":[{\"HostPort\":\"443\"}],\"5671/tcp\":[{\"HostPort\":\"5671\"}],\"8883/tcp\":[{\"HostPort\":\"8883\"}]}}}"
            },
            "type": "docker",
            "status": "running",
            "restartPolicy": "always"
          }
        }
      }
    },
    "$edgeHub": {
      "properties.desired": {
        "routes": {
          "route": "FROM /messages/* INTO $upstream",
          "SimulatedTemperatureSensorToIoTHub": "FROM /messages/modules/SimulatedTemperatureSensor/* INTO $upstream"
        },
        "schemaVersion": "1.0",
        "storeAndForwardConfiguration": {
          "timeToLiveSecs": 7200
        }
      }
    },
    "SimulatedTemperatureSensor": {
      "properties.desired": {
        "SendData": true,
        "SendInterval": 5
      }
    }
  }
}

In this deployment manifest example, the famous Microsoft temperature and humidity simulation module is deployed.

I put the deployment file next to the source code and after compilation, the application runs:

Once executed, we see the response of the REST call: NoContent. This looks a bit weird for a POST command but it is still better than a ‘Bad Request’ response (in case of an exception).

As we see when we check the Azure portal for the edge device configuration, it’s working. The simulation module is now part of the configuration:

The actual IoT Edge device will be sent a new deployment manifest now.

Just for jun, I tried to deploy a non-existing version of the sensor simulation module and it works great:

Of course, the Edge Agent will never be able to download this version but with the REST call, we are now in full control of deploying modules using custom code.

Conclusion

Now everybody should be able to program in every programming language the code for deploying an IoT Edge deployment manifest.