Skip to main content

Host Groups

A host group is a set of one or more wasmCloud host pods that run your Wasm workloads. Every Cosmonic Control installation needs at least one host group. Each pod runs a wasmCloud host that connects to Control's nexus NATS service and registers itself as available for workload scheduling.

Host groups are deployed with the cosmonic-control-hostgroup Helm chart — not as a Kubernetes custom resource. There is no HostGroup CRD to kubectl apply; you install, scale, and configure a host group entirely through Helm values. (An earlier k8s.cosmonic.io HostGroup CRD was removed from Cosmonic Control in favor of this chart.)

Host groups and hosts

Each pod a host group creates runs one wasmCloud host, represented in the cluster by the Host resource (runtime.wasmcloud.dev/v1alpha1). A host group is therefore a horizontally scalable Deployment of hosts that share the same configuration and serve the same kind of workloads. Cosmonic Control load-balances workloads across all available hosts in a group (round-robin) and redistributes them if a host crashes.

Components scheduled onto the same host pod remain sandbox-isolated from one another — there is no shared memory or direct call path between components in different namespaces unless an explicit link is declared.

Scheduling and labels

A host group carries a name and an optional environment and labels that the scheduler uses to place workloads:

  • hostgroup — the group's name, reported by every host in the group (as the cosmonic.io/hostgroup label) and used in scheduling decisions. Use a distinct name for each different type of host.
  • environment — hosts only run workloads from HTTPTriggers (and other resources) whose spec.environment matches. When empty, the host reads its environment from the pod (defaulting to the pod's namespace). A trigger with no environment is left unpinned and may land on any host, including a shared group.
  • hostLabels — arbitrary key: value labels every host in the group advertises. The operator copies them onto each Host object (rendered as --host-label=key=value), and a WorkloadDeployment's hostSelector matches against them to place workloads. Use them to describe a group by any dimension you schedule on, such as a zone, a hardware tier, or a compliance boundary.

To confine a tenant's workloads to a specific group, set HTTPTrigger.spec.environment — or a WorkloadDeployment hostSelector matching the group's hostgroup name — accordingly. See Multitenancy for the full isolation model.

Scheduling by label

hostLabels let you place workloads by any attribute, not just the group name. Advertise the labels on a group through Helm values:

# values.yaml for a GPU group in the us-east zone
hostLabels:
  zone: us-east
  tier: gpu

Then pin a workload to hosts that carry them with a matching hostSelector:

apiVersion: runtime.wasmcloud.dev/v1alpha1
kind: WorkloadDeployment
spec:
  template:
    spec:
      hostSelector:
        zone: us-east
        tier: gpu
      # …workload spec

The match is exact, and every entry in the selector must be present on the host (a host advertising only zone: us-east would not satisfy the selector above). A workload with no hostSelector is unpinned and may land on any host. Each label must be a valid Kubernetes label key and value, since the operator copies it onto the Host object.

Key chart values

ValueDefaultPurpose
hostgroupdefaultGroup name used in scheduling decisions
environment"" (pod namespace)Restricts which triggers schedule onto this group
hostLabels{}Extra key: value labels every host advertises, matched by a WorkloadDeployment hostSelector for placement
hostPlugins[]Host component plugins loaded before the host starts
replicaCount1Number of host pods; scale horizontally
controlNamespacecosmonic-systemNamespace where Cosmonic Control is installed
nexus.url"" (templated)NATS control-plane connection to Nexus
gpufalsePasses --wasi-webgpu so workloads can call wasi:webgpu
resources250m CPU request, 768Mi memory request, 6Gi memory limitPer-pod requests and limits; host guest memory scales with these
hostMemoryderived from resources.limits.memoryGuest memory budget and per-guest ceiling (maxGuestMemory / defaultHeapMemory / coreInstances)

Install

helm install hostgroup oci://ghcr.io/cosmonic/cosmonic-control-hostgroup \
  --version 0.10.0 \
  --namespace cosmonic-system

Wait for it to be ready:

kubectl rollout status deploy -l app.kubernetes.io/instance=hostgroup -n cosmonic-system

For the full value reference and operational procedures — scaling, running multiple groups, and GPU groups — see Installing HostGroups in the Operator Manual.

Host component plugins

A host component plugin is a Wasm component that serves a host capability from its own long-lived, supervised store. It loads before the host starts and is available to every workload on the group that imports its interface. Where a workload is sandboxed and reaches only what its own manifest grants, a host component plugin extends the host itself: it is operator-installed and more privileged than a workload, and nothing in a workload request can register one. Declaring plugins in the chart is the only way a host-global capability provider is added.

Declare them under hostPlugins, one entry per plugin:

hostPlugins:
  - id: acme-kv                        # required, unique on the host
    image: ghcr.io/acme/kv-host:1.0.0  # OCI ref, or `file:` a path in the container
    pull: ifNotPresent                 # always | ifNotPresent | never
    digest: sha256:…                   # optional; pins the image for supply-chain integrity
    maxRestarts: 3                     # supervised restarts before the plugin is declared dead

Each entry needs an id that is unique on the host. An entry that loads a plugin names exactly one source: image (an OCI reference) or file (a path inside the container); image pulls use the ambient credential chain (a mounted image.pullSecrets entry, then anonymous); private registries that need their own plugin credentials are not supported yet. An entry that names no source instead configures a plugin the host already has built in, such as wasmcloud:nats; pull, digest, and maxRestarts apply only to a loaded component and are rejected on a source-less entry.

Plugin configuration and egress

A plugin can take bind-time configuration and declare its own outbound access. Because it is more privileged than a workload, it is fail-closed by default: with no allowedHosts it can reach no outbound host, and with no allowedIpNameLookups it can resolve no name.

FieldWhat it does
configLiteral key: value bind-time config, delivered to the native capabilities the plugin imports (wasmcloud:secrets, wasi:config, and so on).
configFromNames of ConfigMaps in the release namespace to merge in, last source winning over config.
secretFromNames of Secrets in the release namespace, merged in last, winning over config and configFrom.
allowedHostsThe plugin's own wasi:http/outgoing-handler allowlist. Empty denies every outbound host.
allowedIpNameLookupsThe names the plugin's wasi:sockets/ip-name-lookup may resolve. Empty denies every lookup.
bindingsNamed bindings a built-in plugin serves, keyed by the (implements <name>) label a workload asks for. Each takes its own config/configFrom/secretFrom, layered over the entry's. See wasmcloud:nats.
workloadConfigWho may supply a binding's config: deny (default), warn, or allow. Under deny a workload that sets a host-owned key or widens a declared grant fails to deploy.
hostOwnedKeysExtra config keys the host owns even where nothing sets them, so an unwritten grant does not fall through to whatever the workload wrote.

Setting any of these renders the whole plugin entry into a host config file mounted into the pod, rather than a --host-plugin command-line argument, so a secret never appears in kubectl describe pod. The host reads that file once at startup. (bindings, workloadConfig, and hostOwnedKeys configure a built-in, binding-based plugin; see below.)

The wasmcloud:nats host plugin

wasmcloud:nats (NATS core pub/sub, JetStream, and KV) is built into the host. It is always registered, so a hostPlugins entry for it names no image or file and only configures it: where its bindings connect, as whom, and what they may reach. Because nothing in a workload request can register a provider or widen a grant, this entry is the only place that access is set.

hostPlugins:
  - id: wasmcloud-nats
    # Deny-by-default ceilings. A workload that imports the interface reaches
    # nothing until a grant here allows it, and may then ask only for a subset.
    config:
      subject-allow: "orders.>"
      stream-allow: ORDERS
    # Credentials: a Secret whose keys are token / nkey-seed / jwt / password.
    secretFrom:
      - orders-nats-auth
    bindings:
      # A workload selects this with `(implements orders)` and gets the
      # narrower grant; under the default workloadConfig it cannot widen it.
      orders:
        config:
          subject-allow: orders.received

Connection. A binding that sets no servers dials this host group's data plane (dataNats.url, falling back to nexus.url). Point a binding at a different cluster by giving it its own servers, with credentials and grants there.

Grants are deny-by-default ceilings. subject-allow, stream-allow, and bucket-allow cap what any workload on the binding may reach; a workload may request a subset and is refused only for asking for more. stream-allow on its own reads nothing: every stored message is also checked against subject-allow, so grant the subjects those streams store (the host warns at bind when they are missing). inbox-prefix is rejected on the entry's own config (two workloads sharing an inbox consume each other's replies), so set it per named binding, or leave it unset.

Who supplies a binding's config is governed by workloadConfig: deny (the default) fails any deploy where a workload sets a host-owned key or widens a declared grant; warn logs the same and refuses nothing; allow permits it. hostOwnedKeys marks additional keys the host owns even where nothing here sets them, so an unwritten grant does not fall through to whatever the workload wrote.

Credentials come from secretFrom (or configFrom): a Secret whose keys are token, nkey-seed, jwt, or password becomes plugin config. A credential the plugin reads as a file (creds, tls-key) does not belong in a Secret value; mount it through volumes/volumeMounts and put its path under config. A key the plugin does not read fails host startup rather than being ignored.

The workload keeps jetstream-subscriptions, core-subscriptions, kv-watches, component, and ack-mode in its own manifest; this plugin governs only connection, identity, and the grant ceiling.

Rotating plugin config

Editing a hostPlugins entry rolls the pod automatically, because the Deployment carries a checksum over the rendered config file. Rotating the contents of a ConfigMap or Secret named by configFrom or secretFrom behaves differently: the kubelet updates the projected volume in place, but the running host keeps serving what it read at boot. The Deployment carries configmap.reloader.stakater.com/reload and secret.reloader.stakater.com/reload annotations naming exactly those objects, so installing Stakater Reloader makes a rotation roll the host on its own. Without that controller the annotations are inert, and a rotation takes effect only after kubectl rollout restart.