Managing and Deploying an IoT Edge device with Azure

Azure IoT Edge allows you to have a runtime to easily control what is running on your devices and remotely update them. As an example, I have an IoT Edge device running in production almost 100km from my house and can target it remotely in one simple command!

Through this article I wish to provide some insights on how 1) Azure IoT Edge Deployments work and 2) An easy script I use to target this specific device and deploy to it.

The Azure IoT Edge Deployment Process

Currently with Azure IoT Edge a couple of things happen when you deploy a module to an IoT Edge device:

1. The Deployment Manifest

In your solution, you define a deployment manifest (e.g., deployment.template.json) that explains - in an architecture agnostic way - how your solution looks like and is build up.

For example, you might have 2 modules (a temperature sensor and a temperature prediction module) which send data over shared memory (a module - container configuration through binds). These modules utilize a custom repository so also use custom credentials from an environment variable.

💡 More information: https://learn.microsoft.com/en-us/azure/iot-edge/how-to-deploy-modules-portal?view=iotedge-1.4#configure-a-deployment-manifest

These modules are pulled from a container registry which is also configured in this manifest file.

2. Building the Deployment Manifest

Once the deployment manifest has been created, it's time to build it (generate the architecture specific config). This step will take of a couple of things:

  • Expand variables to the respective environment variables (e.g., $ACR_REPO towards YOUR_REPO_NAME if the environment variable ACR_REPO exists)
  • Replace placeholders (e.g., the architecture name)

Which will publish the result in the config/ folder (e.g., config/deployment.arm68v8.json)

3. Pushing the containers

Now the deployment configuration has been generated, the last step before we can configure our device is to push the containers of the modules to a container registry.

4. Configuring our Device

Finally, we configure our device to utilize the generated deployment manifest that will tell the device where the containers are situated and will pull + manage them accordingly

Summary

Below you can find an illustration of the process explained above.

Development Process

Putting this into practice can be quite intimidating and complex. Personally, I found the following interesting to work with:

  1. Build and push the containers cross-architecture through buildx
  2. Generate a Deployment Manifest config for my architecture
  3. Set the modules on a provided device

All these steps, I have automated through a simple script which you can find below and that I call through:

./run.sh <HUB_NAME> <DEVICE_ID> [PLATFORM] [ACR_REPO]

Deployment Script

#!/bin/bash
PLATFORM=${4:-arm64v8} # Set PLATFORM to arg $2 but default to arm64v8
ACR_REPO=${5:-your_repo} # Set ACR_REPO to arg $3 but default to hardcoded
SOLUTION=your-solution
CONTAINER_VERSION=${3:-0.0.1}

HUB_NAME=$1
DEVICE_ID=$2

if [ -z "$HUB_NAME" -o -z "$DEVICE_ID" ]; then
    echo "Usage: $0 <HUB_NAME> <DEVICE_ID> [CONTAINER_VERSION] [PLATFORM] [ACR_REPO]"
    exit 1
fi

echo "==============================================================================================="
echo "Deploying '$SOLUTION' to '$DEVICE_ID' ($PLATFORM)"
echo "==============================================================================================="
echo "Logging in to ACR Repo"
echo "(note: if it asks for the name use az account set --subscription \"<YOUR_SUBSCRIPTION>\")"
az acr login --name $ACR_REPO

# Get the ACR credentials and save them into ACR_USERNAME and ACR_PASSWORD
ACR_USERNAME=$(az acr credential show --name $ACR_REPO --query "username" --output tsv)
ACR_PASSWORD=$(az acr credential show --name $ACR_REPO --query "passwords[0].value" --output tsv)
ACR_URL="$ACR_REPO.azurecr.io"

# Initialize qemu
echo "Initializing QEMU"
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes > /dev/null

# Build and push your modules for the target architecture
echo "Building and pushing modules"
for d in modules/*; do
    MODULE_DIR=$d
    MODULE_NAME=$(echo $d | cut -d "/" -f 2)
    MODULE_NAME_LOWER=$(echo $MODULE_NAME | tr '[:upper:]' '[:lower:]')
    CONTAINER_NAME="$SOLUTION-$MODULE_NAME_LOWER"

    echo "building $ACR_REPO.azurecr.io/$CONTAINER_NAME:$CONTAINER_VERSION-$PLATFORM"
    echo "docker buildx build --push --platform linux/$PLATFORM -t $ACR_REPO.azurecr.io/$CONTAINER_NAME -f $MODULE_DIR/Dockerfile.$PLATFORM $MODULE_DIR"
    docker buildx build --push --platform linux/$PLATFORM \
        -t $ACR_REPO.azurecr.io/$CONTAINER_NAME:$CONTAINER_VERSION-$PLATFORM \
        -t $ACR_REPO.azurecr.io/$CONTAINER_NAME:latest-$PLATFORM \
        -f $MODULE_DIR/Dockerfile.$PLATFORM \
        $MODULE_DIR
done

# Generate the config from the deployment template
# More info: https://learn.microsoft.com/en-us/azure/iot-edge/module-composition?view=iotedge-1.4
echo "Generating Solution Config from 'deployment.template.json' into 'config/deployment.$PLATFORM.json'"
CONTAINER_REGISTRY_USERNAME=$ACR_USERNAME \
CONTAINER_REGISTRY_PASSWORD=$ACR_PASSWORD \
CONTAINER_REGISTRY_URL=$ACR_URL \
CONTAINER_REGISTRY_NAME=$ACR_REPO \
CONTAINER_NAME=$CONTAINER_NAME \
CONTAINER_VERSION=$CONTAINER_VERSION \
CONTAINER_PLATFORM=$PLATFORM \
iotedgedev solution genconfig --file deployment.template.json \
    --platform $PLATFORM > /dev/null
    
# # Push to the device
# # similar to: az iot edge set-modules --hub-name my-iot-hub --device-id my-device --content ./deployment.debug.template.json --login "HostName=my-iot-hub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=<SharedAccessKey>"
# # iotedgedev solution deploy --file config/deployment.$PLATFORM.json
az iot edge set-modules --hub-name $HUB_NAME --device-id $DEVICE_ID --content config/deployment.$PLATFORM.json

Summary

Let me know what you think of it and how you are currently managing your IoT Edge deployments!