====== 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: - ''zedcloud_application'' -- the bundle definition (what to run) - ''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: * ''images'' -- what image to use, format, drive type, encryption, size * ''interfaces'' -- network interfaces with ACL rules and port mappings * ''resources'' -- CPU, memory, storage allocation * ''configuration'' -- cloud-init / custom config template * ''vmmode'', ''app_type'', ''deployment_type'' -- workload and deployment behavior ===== 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 = "" } } * ''add = true'' enables custom configuration for this app bundle * ''template = ""'' means no default template is baked in -- the cloud-init config is provided at the app **instance** level, not the bundle level * ''override = true'' means the instance-level config overrides any bundle-level 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: * ''networks = 3'' -- must match the number of ''interfaces'' blocks in the manifest * ''ignorepurge = 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 disk * Port mapping ACLs: each portmap rule needs a matching block for protocol (tcp), the listening port on the edge node (''lport''), the destination inside the app (''app_port''), and an IP match for allowed sources * ''user_defined_version'' tracks your app version string -- visible in ZEDUI and useful for fleet management * ''origin_type = "ORIGIN_LOCAL"'' means this app was created in your enterprise (not imported from the marketplace) ==== 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: * ''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 GB * Interface name ''enp1s0'' -- 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''. ==== 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: * ''type = "IO_TYPE_ETH_VF"'' -- designates a SR-IOV Virtual Function interface * ''directattach = true'' -- the VF is passed directly to the guest via VFIO/IOMMU, bypassing the EVE-OS virtual switch entirely * ''directattach = false'' on enp1s0 -- this NIC goes through the EVE-OS network instance (virtual switch), providing management connectivity * The edge node hardware must support SR-IOV on that NIC, and the VF must be configured in EVE-OS before the app can use it * ACL rules on directattach interfaces are advisory only -- with direct passthrough, EVE-OS does not enforce ACLs on the data path (the VM has raw NIC access) ===== 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. - Navigate to **Marketplace** > **Edge Apps** - Click **+** in the upper right - Select **Virtual Machine** as the app type - Fill in Identity: Name, Title, Version, Category, Logo URL - Configure **Drives**: select image, set format, size, encryption, and purge behavior - Configure **Resources**: vCPU, RAM, storage - Configure **Interfaces**: add NICs, set direct attach or virtual, define ACL rules - Configure **Custom Config**: enable cloud-init if needed - 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 ===== - The controller delivers the app instance config to EVE-OS at next heartbeat - EVE-OS transitions the instance through: INIT > DOWNLOAD > INSTALL > RUNNING - **DOWNLOAD**: EVE-OS fetches the QCOW2 image from the datastore, verifies SHA256 - **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. - 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. - 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'' - **RUNNING**: QEMU starts the VM. EVE-OS reports the run state to the controller at each heartbeat. - 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 | ===== Related Resources ===== * [[03_images|Images]] * [[02_datastores|Datastores]] * [[06_persistent_volumes_and_content_trees|Persistent Volumes and Content Trees]] * [[05_patch_envelopes|Patch Envelopes]] * [[08_deploying_from_marketplace|Deploying from Marketplace]]