Multi-File Application Bundle Update Module

Applications rarely consist of a single file. A typical deployment might include binaries, configuration files, assets, and supporting scripts that must be deployed together to maintain consistency. While Mender provides several ready-made Update Modules for common scenarios, the need for a custom solution arises when deploying application bundles where files have specific destination paths and require atomic rollback.

This tutorial demonstrates how to create a streaming Update Module that accepts multiple payload files, deploys them according to a manifest, and provides automatic rollback on failure.

Prerequisites

  • A device running Mender client v4.x or v5.x (for v3.x, replace mender-update with mender in commands below)
  • mender-artifact tool installed on your development machine
  • jq installed on the target device

Understanding the Approach

Mender’s Update Module interface provides a stream-next mechanism for consuming payload files one at a time. Each call to cat stream-next returns the path to the next file in the artifact, or an empty string when no more files remain. Combined with artifact metadata (passed via -m during artifact creation), this allows the Update Module to know what files to expect and where to place them.

The deployment flow operates in distinct phases:

  1. Download: Stream all files to a staging area
  2. ArtifactInstall: Backup existing files, deploy staged files to final locations
  3. ArtifactCommit: Remove backup (update successful)
  4. ArtifactRollback: Restore backup if installation fails

The Update Module

Install this script as /usr/share/mender/modules/v3/app-bundle on your target device:

#!/bin/sh
set -ue

STATE="$1"
FILES="$2"

WORK_DIR="/tmp/mender-app-bundle"
STAGING_DIR="${WORK_DIR}/staging"
BACKUP_DIR="${WORK_DIR}/backup"
MANIFEST="${FILES}/header/meta-data"

case "$STATE" in
    NeedsArtifactReboot)
        echo "No"
        ;;

    SupportsRollback)
        echo "Yes"
        ;;

    Download)
        rm -rf "$STAGING_DIR"
        mkdir -p "$STAGING_DIR"

        jq -r '.files[].name' "$MANIFEST" | while read -r name; do
            file="$(cat stream-next)"
            [ -n "$file" ] || { echo "Stream ended early" >&2; exit 1; }
            cat "$file" > "${STAGING_DIR}/${name}"
        done

        [ -z "$(cat stream-next)" ] || { echo "Unexpected extra files" >&2; exit 1; }
        ;;

    ArtifactInstall)
        base_path=$(jq -r '.base_path' "$MANIFEST")

        # Backup existing files
        rm -rf "$BACKUP_DIR"
        mkdir -p "$BACKUP_DIR"
        jq -r '.files[].dest' "$MANIFEST" | while read -r dest; do
            [ -f "${base_path}/${dest}" ] || continue
            mkdir -p "$(dirname "${BACKUP_DIR}/${dest}")"
            cp -a "${base_path}/${dest}" "${BACKUP_DIR}/${dest}"
        done
        cp "$MANIFEST" "${BACKUP_DIR}/manifest.json"
        echo "$base_path" > "${BACKUP_DIR}/base_path"

        # Deploy new files
        mkdir -p "$base_path"
        jq -r '.files[] | "\(.name)\t\(.dest)\t\(.mode // "")"' "$MANIFEST" | \
        while IFS='	' read -r name dest mode; do
            mkdir -p "$(dirname "${base_path}/${dest}")"
            cp "${STAGING_DIR}/${name}" "${base_path}/${dest}"
            [ -z "$mode" ] || chmod "$mode" "${base_path}/${dest}"
        done
        ;;

    ArtifactCommit)
        rm -rf "$BACKUP_DIR"
        ;;

    ArtifactRollback)
        [ -d "$BACKUP_DIR" ] || exit 0
        base_path=$(cat "${BACKUP_DIR}/base_path")

        jq -r '.files[].dest' "${BACKUP_DIR}/manifest.json" | while read -r dest; do
            if [ -f "${BACKUP_DIR}/${dest}" ]; then
                mkdir -p "$(dirname "${base_path}/${dest}")"
                cp -a "${BACKUP_DIR}/${dest}" "${base_path}/${dest}"
            else
                rm -f "${base_path}/${dest}"
            fi
        done
        ;;

    Cleanup)
        rm -rf "$STAGING_DIR"
        ;;
esac

exit 0

The critical streaming pattern appears in the Download state. The manifest tells us how many files to expect, and stream-next provides them in order:

file="$(cat stream-next)"    # Get path to next payload file
cat "$file" > destination    # Stream content to staging

The Manifest Format

The manifest defines where files should be installed. Pass it to mender-artifact using the -m flag, which embeds it in the artifact’s metadata:

{
  "base_path": "/opt/myapp",
  "files": [
    {"name": "myapp", "dest": "myapp", "mode": "0755"},
    {"name": "myapp.conf", "dest": "myapp.conf", "mode": "0644"}
  ]
}

The name field identifies the source file in the bundle directory, while dest specifies the path relative to base_path. The optional mode field sets file permissions after deployment.

File order in the manifest must match the order of -f arguments when creating the artifact, as stream-next delivers files in artifact order.

Creating Artifacts

Structure your application bundle (you can use dummy text files as myapp and myapp.conf for testing):

my-bundle/
├── manifest.json
├── myapp
└── myapp.conf

Generate the artifact with mender-artifact:

mender-artifact write module-image \
    -T app-bundle \
    -n "myapp-v1.0" \
    -t <device-type> \
    -o myapp-v1.0.mender \
    -m my-bundle/manifest.json \
    -f my-bundle/myapp \
    -f my-bundle/myapp.conf

Deployment

Install the Update Module to /usr/share/mender/modules/v3/app-bundle on your device. From there, standard Mender deployment applies: upload the artifact to Hosted Mender or your self-hosted server and deploy to your device fleet, or use standalone mode with mender-update install.

Adapting for Your Use Case

The manifest-driven approach allows customization without modifying the Update Module itself - simply change base_path and dest fields in the manifest.

For service management, extend ArtifactInstall to stop services before deployment and restart them afterward:

ArtifactInstall)
    systemctl stop myapp || true
    # ... deployment code ...
    systemctl start myapp
    ;;

Conclusion

This Update Module demonstrates how Mender’s stream-next mechanism handles multi-file payloads while maintaining rollback guarantees. The manifest-driven design keeps the Update Module generic, with deployment specifics defined at artifact creation time.

1 Like