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:
zedcloud_application – the bundle definition (what to run)zedcloud_application_instance – the deployment (where to run it)| 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_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. |
| 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. |
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:
images – what image to use, format, drive type, encryption, sizeinterfaces – network interfaces with ACL rules and port mappingsresources – CPU, memory, storage allocationconfiguration – cloud-init / custom config templatevmmode, app_type, deployment_type – workload and deployment behavior| 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 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 {
custom_config {
add = true
name = "cloud-config"
override = true
template = ""
}
}
add = true enables custom configuration for this app bundletemplate = “” means no default template is baked in – the cloud-init config is provided at the app instance level, not the bundle leveloverride = true means the instance-level config overrides any bundle-level templateThis pattern (empty template at bundle level, config at instance level) is the standard approach for reusable bundles deployed with per-node customization.
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:
networks = 3 – must match the number of interfaces blocks in the manifestignorepurge = true on the OS drive – FortiGate stores its config inside the QCOW2 image. Setting ignorepurge preserves the drive (and its config) across image version updates. If ignorepurge were false, Purge & Update would wipe the firewall config.maxsize = 40971520 KB = ~40 GB virtual disklport), the destination inside the app (app_port), and an IP match for allowed sourcesuser_defined_version tracks your app version string – visible in ZEDUI and useful for fleet managementorigin_type = “ORIGIN_LOCAL” means this app was created in your enterprise (not imported from the marketplace)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:
ignorepurge = false – Ubuntu VM OS drive is wiped on Purge & Update. This is correct for a generic VM where you deliver per-instance config via cloud-init at deploy time, not stored inside the image.memory = 2097152 KB = exactly 2 GBenp1s0 – the PCI bus enumeration name inside the Ubuntu guest. This must match what the guest OS sees. For VirtIO-based images this is typically enp1s0, enp2s0, etc. For legacy emulation it may be eth0.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 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:
type = “IO_TYPE_ETH_VF” – designates a SR-IOV Virtual Function interfacedirectattach = true – the VF is passed directly to the guest via VFIO/IOMMU, bypassing the EVE-OS virtual switch entirelydirectattach = false on enp1s0 – this NIC goes through the EVE-OS network instance (virtual switch), providing management connectivity
ACL rules control inbound and outbound traffic through EVE-OS's distributed firewall for virtual NIC interfaces (directattach = false only).
acls {
matches {
type = "ip"
value = "0.0.0.0/0"
}
}
Allows the app to initiate outbound connections to any destination.
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.
| 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 |
| 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 |
The ZEDUI can create app bundles from scratch for VMs. Terraform is recommended for repeatable deployments.
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.
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.
ignorepurge = true drives, an existing volume is reattached rather than recreated.directattach = true), EVE-OS binds the VF to VFIO and passes it to the KVM domain.lport traffic to the app's internal IP and app_port| GB | KB (for memory field) |
|---|---|
| 1 GB | 1048576 |
| 2 GB | 2097152 |
| 4 GB | 4194304 |
| 8 GB | 8388608 |
| 16 GB | 16777216 |