Table of Contents

Edge Apps

An Edge App in ZEDEDA Cloud is a bundle definition – the template that describes a workload: what image to run, how much compute to allocate, how many network interfaces to wire up, what ACL rules to apply, and how to pass initial configuration. The bundle is not deployed itself – you create App Instances from the bundle and target them at specific edge nodes.

Two objects, two steps:

  1. zedcloud_application – the bundle definition (what to run)
  2. zedcloud_application_instance – the deployment (where to run it)

App Types

app_type Description
APP_TYPE_VM Virtual machine running under KVM/QEMU on EVE-KVM. Full hardware virtualization.
APP_TYPE_CONTAINER OCI container running under containerd. Lightweight, faster startup.
APP_TYPE_COMPOSE Docker Compose runtime. Runs a Compose file defining multiple containers.

Deployment Types

deployment_type Description
DEPLOYMENT_TYPE_STAND_ALONE Single-node deployment. App instance runs on one specific edge node. Most common.
DEPLOYMENT_TYPE_CLUSTER Cluster-aware deployment. For EVE-k clusters – ZEDEDA schedules across cluster nodes.

VM Modes (vmmode)

vmmode Description
HV_HVM Hardware Virtual Machine. Guest OS runs unmodified using VT-x hardware virtualization. Standard KVM mode. Use this for Linux VMs, Windows, and appliances like FortiGate and MikroTik.
HV_HVM_LEGACY HVM with emulated legacy IO drivers. For older guest OSes that need legacy device emulation.
HV_PVH ParaVirtualization Hardware (beta). Guest uses paravirt drivers. Primarily Xen mode for Windows.
HV_FML Full Machine Virtualization. Used when attaching NVIDIA GPUs via passthrough. Requires UEFI boot.

Manifest Structure

Every VM app bundle in ZEDEDA is defined by a manifest. The manifest is the authoritative description of the workload. In Terraform, this is the manifest {} block inside zedcloud_application. The key sections are:

Key Manifest Fields

images block

Field Description
imagename Name of the registered image resource
imageid ID of the registered image resource
imageformat QCOW2, RAW, VMDK, CONTAINER
cleartext false = encrypt the volume at rest. Always false in production.
drvtype HDD = virtual hard disk. CDROM = read-only ISO.
ignorepurge true = this drive is NOT purged on Purge & Update. Use for the OS disk on appliances where you want to preserve config across image updates. false = drive is wiped on purge.
maxsize Maximum disk size in KB allocated for this drive.
target Disk = primary VM disk. Kernel = kernel image.

resources block

Resources are defined as name/value pairs:

name value Notes
resourceType custom Always custom for explicit resource definition
cpus integer Number of vCPUs assigned to the VM
memory integer (KB) RAM in kilobytes. 2097152 = 2 GB. 4097152 = ~4 GB.
storage integer (KB) Must match maxsize in the images block

Memory is in kilobytes: 1 GB = 1048576 KB, 2 GB = 2097152 KB, 4 GB = 4194304 KB.

configuration block

configuration {
  custom_config {
    add      = true
    name     = "cloud-config"
    override = true
    template = ""
  }
}

This pattern (empty template at bundle level, config at instance level) is the standard approach for reusable bundles deployed with per-node customization.

Terraform Examples

FortiGate Firewall VM (3 NICs, Port Mapping, ACLs)

A FortiGate 7.4.3 firewall appliance with 3 network interfaces. eth0 is the management/WAN interface with port mapping rules to expose SSH (10022 to 22) and HTTPS (10443 to 443) on the edge node. eth1 and eth2 are LAN/DMZ interfaces.

resource "zedcloud_application" "tf_atl_fw_app_1" {
  name     = "TF-FW-ATL-7.4.3"
  title    = "TF-FW-ATL-7.4.3"
  networks = 3
  manifest {
    ac_kind    = "VMManifest"
    ac_version = "1.2.0"
    name       = "TF-FW-ATL-7.4.3"
    owner {
      user    = "Your Name"
      company = "Zededa"
      website = "www.zededa.com"
      email   = "your@email.com"
    }
    desc {
      app_category = "APP_CATEGORY_UNSPECIFIED"
      category     = "APP_CATEGORY_SECURITY"
      logo = {
        url = "https://www.fortinet.com/content/dam/fortinet/images/general/fortinet-logo.svg"
      }
    }
    images {
      imagename   = zedcloud_image.tf_demo_fw_atl_image.name
      imageid     = zedcloud_image.tf_demo_fw_atl_image.id
      imageformat = "QCOW2"
      cleartext   = false
      drvtype     = "HDD"
      ignorepurge = true
      maxsize     = 40971520
      target      = "Disk"
    }
    interfaces {
      name         = "eth0"
      directattach = false
      privateip    = false
      acls {
        matches { ### Outbound - allow all
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
      acls {
        matches { ### Port map: external 10022 -> internal 22 (SSH)
          type  = "ip"
          value = "0.0.0.0/0"
        }
        actions {
          portmap = true
          portmapto {
            app_port = 22
          }
        }
        matches {
          type  = "protocol"
          value = "tcp"
        }
        matches {
          type  = "lport"
          value = 10022
        }
      }
      acls {
        matches { ### Port map: external 10443 -> internal 443 (HTTPS)
          type  = "ip"
          value = "0.0.0.0/0"
        }
        actions {
          portmap = true
          portmapto {
            app_port = 443
          }
        }
        matches {
          type  = "protocol"
          value = "tcp"
        }
        matches {
          type  = "lport"
          value = 10443
        }
      }
    }
    interfaces {
      name         = "eth1"
      directattach = false
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    interfaces {
      name         = "eth2"
      directattach = false
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    vmmode    = "HV_HVM"
    enablevnc = true
    resources {
      name  = "resourceType"
      value = "custom"
    }
    resources {
      name  = "cpus"
      value = 2
    }
    resources {
      name  = "memory"
      value = 4000000
    }
    resources {
      name  = "storage"
      value = 40971520
    }
    configuration {
      custom_config {
        add      = true
        name     = "cloud-config"
        override = true
        template = ""
      }
    }
    app_type            = "APP_TYPE_VM"
    deployment_type     = "DEPLOYMENT_TYPE_STAND_ALONE"
    cpu_pinning_enabled = false
  }
  user_defined_version = "7.4.3"
  origin_type          = "ORIGIN_LOCAL"
}

Key points for this firewall app:

Ubuntu 24.04 VM (Simple, 1 NIC)

A simple Ubuntu cloud VM with a single outbound-only interface and cloud-init support:

resource "zedcloud_application" "tf_atl_ub_app_1" {
  name     = "TF-ATL-CL-UB-1"
  title    = "TF-ATL-CL-UB-1"
  networks = 1
  manifest {
    ac_kind    = "VMManifest"
    ac_version = "1.2.0"
    name       = "TF-ATL-CL-UBUNTU-1"
    owner {
      user    = "Your Name"
      company = "Zededa"
      website = "www.zededa.com"
      email   = "your@email.com"
    }
    desc {
      app_category = "APP_CATEGORY_UNSPECIFIED"
      category     = "APP_CATEGORY_OPERATING_SYSTEM"
      logo = {
        url = "https://assets.ubuntu.com/v1/ff6a9a38-ubuntu-logo-2022.svg"
      }
    }
    images {
      imagename   = zedcloud_image.demo_atl_ub_image_1.name
      imageid     = zedcloud_image.demo_atl_ub_image_1.id
      imageformat = "QCOW2"
      cleartext   = false
      drvtype     = "HDD"
      ignorepurge = false
      maxsize     = 20971520
      target      = "Disk"
    }
    interfaces {
      name         = "enp1s0"
      directattach = false
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    vmmode    = "HV_HVM"
    enablevnc = true
    resources {
      name  = "resourceType"
      value = "custom"
    }
    resources {
      name  = "cpus"
      value = 2
    }
    resources {
      name  = "memory"
      value = 2097152
    }
    resources {
      name  = "storage"
      value = 20971520
    }
    configuration {
      custom_config {
        add      = true
        name     = "cloud-config"
        override = true
        template = ""
      }
    }
    app_type            = "APP_TYPE_VM"
    deployment_type     = "DEPLOYMENT_TYPE_STAND_ALONE"
    cpu_pinning_enabled = false
  }
  user_defined_version = "24"
  origin_type          = "ORIGIN_LOCAL"
}

Key differences from the firewall:

MikroTik Router VM (2 NICs)

A MikroTik CHR appliance with 2 NICs – WAN and LAN:

resource "zedcloud_application" "tf_demo_atl_mik_1" {
  name     = "TF-MIKROTIC-ATL-1"
  title    = "TF-MIKROTIC-ATL-1"
  networks = 2
  manifest {
    ac_kind    = "VMManifest"
    ac_version = "1.2.0"
    name       = "TF-MIKROTIC-ATL-1"
    desc {
      app_category = "APP_CATEGORY_UNSPECIFIED"
      category     = "APP_CATEGORY_OPERATING_SYSTEM"
      logo = {
        url = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/MikroTik_Logo_%282022%29.svg/250px-MikroTik_Logo_%282022%29.svg.png"
      }
    }
    images {
      imagename   = zedcloud_image.demo_mikrotick_1.name
      imageid     = zedcloud_image.demo_mikrotick_1.id
      imageformat = "QCOW2"
      cleartext   = false
      drvtype     = "HDD"
      ignorepurge = false
      maxsize     = 20971520
      target      = "Disk"
    }
    interfaces {
      name         = "eth0"
      directattach = false
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    interfaces {
      name         = "eth1"
      directattach = false
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    vmmode    = "HV_HVM"
    enablevnc = true
    resources {
      name  = "resourceType"
      value = "custom"
    }
    resources { name = "cpus"    value = 2 }
    resources { name = "memory"  value = 4097152 }
    resources { name = "storage" value = 20971520 }
    configuration {
      custom_config {
        add      = true
        name     = "cloud-config"
        override = true
        template = ""
      }
    }
    app_type            = "APP_TYPE_VM"
    deployment_type     = "DEPLOYMENT_TYPE_STAND_ALONE"
    cpu_pinning_enabled = false
  }
  user_defined_version = "24"
  origin_type          = "ORIGIN_LOCAL"
}

Ubuntu VM with SR-IOV Direct Attach NICs

Ubuntu VM with one virtual NIC (for management) and two SR-IOV Virtual Function NICs passed directly to the guest. Used for high-throughput data plane workloads where the VM needs direct hardware access to a NIC, bypassing the EVE-OS virtual switch.

resource "zedcloud_application" "tf_demo_atl_srio_ub_local_ni_1" {
  name     = "TF-DEMO-SRIO-ATL-UB-LOCAL-NI-1"
  title    = "TF-DEMO-SRIO-ATL-UB-LOCAL-NI-1"
  networks = 3
  manifest {
    ac_kind    = "VMManifest"
    ac_version = "1.2.0"
    name       = "TF-DEMO-SRIO-UBUNTU-APP"
    images {
      imagename   = zedcloud_image.demo_atl_ub_image_1.name
      imageid     = zedcloud_image.demo_atl_ub_image_1.id
      imageformat = "QCOW2"
      cleartext   = false
      drvtype     = "HDD"
      ignorepurge = false
      maxsize     = 20971520
      target      = "Disk"
    }
    interfaces {
      name         = "enp1s0"       ### Virtual NIC via EVE switch
      directattach = false
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    interfaces {
      name         = "enp6s0"       ### SR-IOV VF -- direct hardware passthrough
      type         = "IO_TYPE_ETH_VF"
      directattach = true
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    interfaces {
      name         = "enp7s0"       ### SR-IOV VF -- direct hardware passthrough
      type         = "IO_TYPE_ETH_VF"
      directattach = true
      privateip    = false
      acls {
        matches {
          type  = "ip"
          value = "0.0.0.0/0"
        }
      }
    }
    vmmode              = "HV_HVM"
    enablevnc           = true
    resources { name = "resourceType" value = "custom" }
    resources { name = "cpus"         value = 2 }
    resources { name = "memory"       value = 4097152 }
    resources { name = "storage"      value = 20971520 }
    configuration {
      custom_config {
        add      = true
        name     = "cloud-config"
        override = true
        template = ""
      }
    }
    app_type            = "APP_TYPE_VM"
    deployment_type     = "DEPLOYMENT_TYPE_STAND_ALONE"
    cpu_pinning_enabled = false
  }
  user_defined_version = "24"
  origin_type          = "ORIGIN_LOCAL"
}

SR-IOV key points:

ACL and Port Mapping Reference

ACL rules control inbound and outbound traffic through EVE-OS's distributed firewall for virtual NIC interfaces (directattach = false only).

Outbound Allow-All (most common)

acls {
  matches {
    type  = "ip"
    value = "0.0.0.0/0"
  }
}

Allows the app to initiate outbound connections to any destination.

Inbound Port Mapping

Maps a port on the edge node's external IP to a port inside the app. Pattern: one ACL block with an actions { portmap } and matching blocks for protocol, external port (lport), and source IP:

acls {
  actions {
    portmap = true
    portmapto {
      app_port = 22    # port inside the app
    }
  }
  matches {
    type  = "protocol"
    value = "tcp"
  }
  matches {
    type  = "lport"
    value = 10022      # port on the edge node
  }
  matches {
    type  = "ip"
    value = "0.0.0.0/0"
  }
}

After deployment, connecting to edge-node-ip:10022 forwards to port 22 inside the app.

ACL Match Types

type value example Description
ip 0.0.0.0/0 Source or destination IP/CIDR
protocol tcp, udp, icmp IP protocol
lport 10022 Listening port on the edge node (external port for inbound rules)
fport 443 Far/destination port for outbound rules

App Categories

category Description
APP_CATEGORY_SECURITY Firewalls, VPN gateways, IDS/IPS
APP_CATEGORY_OPERATING_SYSTEM General purpose OS (Ubuntu, RHEL, Windows)
APP_CATEGORY_NETWORK Routers, switches, SD-WAN (MikroTik, etc.)
APP_CATEGORY_UNSPECIFIED No specific category

ZEDUI: Creating an App Bundle

The ZEDUI can create app bundles from scratch for VMs. Terraform is recommended for repeatable deployments.

  1. Navigate to Marketplace > Edge Apps
  2. Click + in the upper right
  3. Select Virtual Machine as the app type
  4. Fill in Identity: Name, Title, Version, Category, Logo URL
  5. Configure Drives: select image, set format, size, encryption, and purge behavior
  6. Configure Resources: vCPU, RAM, storage
  7. Configure Interfaces: add NICs, set direct attach or virtual, define ACL rules
  8. Configure Custom Config: enable cloud-init if needed
  9. Click Save

Note: the only way to create a VM edge app from scratch is through the GUI. After creating it, you can export the manifest via ZCLI, modify it, and re-import it. Terraform can manage apps created this way by importing the resource.

App Instances

The app bundle defines what to run. An app instance defines where to run it. After creating the bundle, deploy instances to specific nodes:

resource "zedcloud_application_instance" "fw_instance_atl_01" {
  name       = "FW-ATL-NODE-01"
  title      = "FortiGate ATL Node 01"
  app_id     = zedcloud_application.tf_atl_fw_app_1.id
  device_id  = zedcloud_edgenode.demo_en_advantech_1.id
  project_id = zedcloud_project.demo_project.id
  activate   = true
  interfaces {
    name       = "eth0"
    netinstid  = zedcloud_network_instance.wan_ni.id
  }
  interfaces {
    name       = "eth1"
    netinstid  = zedcloud_network_instance.lan_ni.id
  }
  interfaces {
    name       = "eth2"
    netinstid  = zedcloud_network_instance.dmz_ni.id
  }
}

The instance-level interfaces block binds each interface name from the bundle to an actual network instance on the target node.

What Happens on the Edge Node

  1. The controller delivers the app instance config to EVE-OS at next heartbeat
  2. EVE-OS transitions the instance through: INIT > DOWNLOAD > INSTALL > RUNNING
  3. DOWNLOAD: EVE-OS fetches the QCOW2 image from the datastore, verifies SHA256
  4. INSTALL: EVE-OS creates the KVM domain definition – allocates vCPUs, RAM, creates the virtual disk from the image. For ignorepurge = true drives, an existing volume is reattached rather than recreated.
  5. Network wiring: EVE-OS creates TAP interfaces and connects them to the network instance bridges. For SR-IOV (directattach = true), EVE-OS binds the VF to VFIO and passes it to the KVM domain.
  6. Port mapping ACLs: EVE-OS programs iptables/nftables rules on the host to forward lport traffic to the app's internal IP and app_port
  7. RUNNING: QEMU starts the VM. EVE-OS reports the run state to the controller at each heartbeat.
  8. cloud-init: if a custom config was provided at the instance level, EVE-OS presents it as a cloud-init datasource (NoCloud) to the guest on first boot via a virtual CD-ROM drive

Memory Quick Reference

GB KB (for memory field)
1 GB 1048576
2 GB 2097152
4 GB 4194304
8 GB 8388608
16 GB 16777216