Table of Contents

Sharing a Volume Between Multiple Edge Applications (ZEDEDA / EVE-OS)

Can a volume be shared by two apps?

Yes — but the rules differ for read-only vs read-write:

This is a documented ZEDEDA feature (“Attach Volume Instances to Multiple Edge Applications”, EVE-OS >= 12.7.0). When done correctly, EVE shares the volume to the guests over plan 9 (9P), which arbitrates concurrent access — so all attached apps can read AND write the same data.

The common error (and its real cause)

If you attach a writable, file-based volume (plain block-storage/QCOW2) to two apps, EVE rejects the second one with:

//Multiple app instances (N) are trying to use the same file-based volume <name>//

This is a deliberate safeguard, verified in EVE source (pkg/pillar/cmd/volumemgr/handlevolumeref.go). The guard fires when a volume is shared by ≥2 apps and it is not container-based and it is not read-only. It is not related to the node's storage backend, ZFS, zvols, disk format, or the multiattach flag — none of those bypass it. The fix is to use a container content-tree volume (below), or make the shared volume read-only.

How to set it up (writable shared volume)

  1. Content-tree volume — create a volume instance of type Content Tree, based on a container image. The image is never executed; its rootfs simply seeds the shared volume's initial contents, so use a small/empty image (e.g. busybox, or a FROM scratch image if you want the share to start empty). Do not use something like nginx — you'd just get its filesystem as clutter.
  2. Block-storage volume — create a Block Storage volume instance connected to that content-tree (content_tree_id), with an access mode and a label. Size it larger than the container.
  3. Attach to each app — on every edge app (container or VM), add a drive that references the block-storage volume by its label, and set a mount path.
  4. Deploy all the app instances on the same edge node.

Access modes

The accessmode field (each value prefixed with VOLUME_INSTANCE_ACCESS_MODE_):

Value (suffix) Meaning
READ / READONLY Read-only — any volume type can be shared this way
READWRITE Read + write
MULTIREAD_SINGLEWRITE Treated the same as Read,Write by EVE

For a writable shared content-tree volume, READWRITE (or MULTIREAD_SINGLEWRITE) plus multiattach = true.

Terraform (zedcloud provider)

# A tiny CONTAINER image — only used to seed the content-tree (never executed)
resource "zedcloud_image" "shared_ct_image" {
  datastore_id  = zedcloud_datastore.docker_hub.id
  image_type    = "IMAGE_TYPE_APPLICATION"
  image_arch    = "AMD64"
  image_format  = "CONTAINER"
  image_size_bytes = 0
  name          = "SHARED-CT-BUSYBOX"
  title         = "SHARED-CT-BUSYBOX"
  image_rel_url = "busybox:latest"
}
 
# 1) Content-tree volume seeded from that container image
resource "zedcloud_volume_instance" "shared_ct" {
  device_id = zedcloud_edgenode.my_node.id
  type      = "VOLUME_INSTANCE_TYPE_CONTENT_TREE"
  image     = zedcloud_image.shared_ct_image.name
  label     = "shared-ct"
  name      = "shared-ct"
  title     = "shared-ct"
}
 
# 2) Block-storage volume connected to the content-tree (this is what apps attach)
resource "zedcloud_volume_instance" "shared_blk" {
  device_id       = zedcloud_edgenode.my_node.id
  type            = "VOLUME_INSTANCE_TYPE_BLOCKSTORAGE"
  content_tree_id = zedcloud_volume_instance.shared_ct.id
  accessmode      = "VOLUME_INSTANCE_ACCESS_MODE_READWRITE"
  multiattach     = true
  size_bytes      = 2147483648
  label           = "shared-blk"
  name            = "shared-blk"
  title           = "shared-blk"
}

Then, on each app instance, add a drive referencing the block-storage volume by label:

  drives {
    imagename   = ""
    volumelabel = zedcloud_volume_instance.shared_blk.label
    mountpath   = "/data"
    target      = "Disk"
    drvtype     = "HDD"
    preserve    = true
    readonly    = false   # both apps may read AND write (9P arbitrates)
  }

Mounting inside the guest

modprobe 9pnet_virtio 2>/dev/null || true
mkdir -p /data
# fstab (nofail so a missing share never blocks boot):
echo "share_dir  /data  9p  trans=virtio,version=9p2000.L,rw,nofail  0  0" >> /etc/fstab
mount /data

Do this via cloud-init so it happens on every boot. No mkfs, no block device — it is a 9P filesystem, not a raw disk.

Requirements & gotchas

Doing it via zcli / REST / WebUI

Same objects as Terraform:

Summary

Worked example: a container writer + VM readers

The practical shape of “one app generates data, the others consume it” when the consumers are VMs (which cannot share a writable volume among themselves):

Wiring:

  1. Shared content-tree + block-storage volume (as above), attached to the writer container AND the reader VMs.
  2. The writer needs a LOCAL network instance — 169.254.169.254 is only reachable via a Local NI (a Switch NI is pure L2 and cannot reach it).
  3. Create a patch envelope (zedcloud_patch_envelope) with the artifact inline (base64), and bind it to the writer instance (zedcloud_patch_reference_update). action = PATCH_ENVELOPE_ACTION_ACTIVATE presents it to the app.
  4. Optionally expose a web UI from the writer with a portmap ACL (actions { portmap = true; portmapto { app_port = 8080 } }).

Notes proven on-device: description.json returns a list of envelopes, each with PatchID (capital ID) and BinaryBlobs[] entries carrying fileName, fileSha, and a full url. The content-tree's base image rootfs (e.g. busybox: bin/, etc/, …) appears under /data as the seed — use a FROM scratch base if you want /data to start empty.

Deployment gotchas (learned on-device)

Updating the config (runbook)

Push a new config version without redeploying the image, app, VMs, or volumes — you only recreate the patch envelope. The writer auto-fetches it on its next poll and drops a new versioned file into /data.

# 1. Edit the config artifact
$EDITOR Demo-Hummingbird/c-init/patch-config.json
 
# 2. Bump the version on the envelope in 6-Instances-Deploy-hum.tf
#    user_defined_version = "2.0"  ->  "3.0"
 
# 3. Recreate the envelope + its binding (in-place update is rejected -> -replace)
terraform apply \
  -replace='zedcloud_patch_envelope.tf_demo_config_pe' \
  -replace='zedcloud_patch_reference_update.tf_demo_config_pe_bind'
 
# 4. Wait ~30-60s (controller -> device propagation + the writer's poll interval)
 
# 5. Verify (on the writer container or any reader VM)
cat /data/config.latest.json      # new content
cat /data/config.history          # <ts>  <sha> per version
#    writer log line: [pcw] new config.json (v=3.0) -> /data/config-<ts>.json (sha=...)

First-time deploy (for reference)

# build + push the writer image (clean manifest — no attestation entries)
docker buildx build --platform linux/amd64,linux/arm64 --provenance=false --sbom=false \
  -t zedmanny/patch-config-writer:1.2.0 --push .
 
# bring everything up
terraform apply
 
# if the image was already created and you changed its tag, the image is immutable AND
# held by the app -> recreate the whole chain in one apply:
terraform apply \
  -replace='zedcloud_application_instance.tf_patch_config_writer_1' \
  -replace='zedcloud_application.tf_patch_config_writer_app' \
  -replace='zedcloud_image.demo_patch_config_writer'

Sources