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)
nexus.tls / dataNats.tlsoff (plaintext)TLS for the control-plane and data-plane NATS connections; opt in for an external or managed NATS reached over a tls:// URL (enabled, existingSecret, caKey, certKey/keyKey, first)
connectionQuotas.maxConnectionsderived from RLIMIT_NOFILEHost-wide ceiling on concurrent workload connections; unset tracks the pod's descriptor budget. See Egress policy and connection quotas
egressPolicysocketEgress: count (preview)Raw-socket egress policy: allowlist enforcement plus special/private-range denials. See Egress policy and connection quotas
ociRegistryhost defaultsHow the host pulls components (pullTimeout, cacheDir, allowInsecure). See Host OCI-registry settings
Turning on NATS TLS

nexus.tls.enabled: true takes effect only when nexus.url is a tls:// URL. With a nats:// URL the certificates read as configured while the connection stays cleartext. A dataNats.url that names a separate server inherits nothing from nexus.tls, so set dataNats.tls whenever dataNats.url is set, or workload messaging runs unauthenticated. Leave caKey empty for a publicly-signed server: setting it replaces the trust store, and a kubernetes.io/tls secret carries no CA key and wedges the pod in ContainerCreating. See the worked example in the upgrade guide.

Install

helm install hostgroup oci://ghcr.io/cosmonic/cosmonic-control-hostgroup \
  --version 0.12.1 \
  --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.

The cosmonic:kafka host plugin

cosmonic:kafka (v0.3.0, serving producer, consumer, and a dispatched handler) is built into the host as of Cosmonic Control 0.11.0, the same way wasmcloud:nats is: a hostPlugins entry for it names no image or file, and there is no separate provider to deploy. It differs from wasmcloud:nats in where configuration lives. The workload names its own broker, credentials, and topics in its own hostInterfaces binding, and the plugin serves one unnamed binding per host. The host claims only the librdkafka properties that would hand the host process a capability (filesystem paths such as ssl.ca.location and sasl.kerberos.keytab, external commands, and the switches that turn off the TLS checks the host makes), so a manifest can never use config to escalate. The connection (bootstrap.servers, security.protocol, sasl.mechanism), the credentials (sasl.password, ssl.key.pem, and the rest), and the topics stay the workload's.

A workload reaches Kafka with a hostInterfaces binding:

spec:
  template:
    spec:
      hostInterfaces:
        - namespace: cosmonic
          package: kafka
          version: "0.3.0"
          interfaces: [producer, consumer, types]
          config:
            bootstrap.servers: broker.corp.internal:9092
            topics: orders,orders-processed   # grant: topics the component's own producer/consumer may name
            handler.topics: orders            # what the host consumes and dispatches to the component's exported handler
            handler.group.id: orders-prod     # required; no default
            dead-letter.topic: orders-dlq     # required when handler.topics is set
          secretFrom:
            - kafka-broker-auth               # sasl.* / ssl.*.pem credentials, never file paths

A few rules the plugin enforces:

  • interfaces lists imports, not the handler. producer, consumer, and the shared types are what the component imports; the handler the host dispatches records to is the component's own export, so it does not appear in the list.
  • topics and handler.topics are different. topics bounds what the component's own producer/consumer may name; handler.topics is the subscription the host reads and dispatches to the component's exported handler. A topic the handler only writes belongs in topics, not handler.topics; otherwise the handler consumes its own output.
  • handler.group.id is required and has no default, so two installations on a shared broker do not silently split a subscription. An operator can pin group.id on the host; a workload that asks for a different one is refused at bind.
  • Credentials go in secretFrom, not config. TLS and SASL material (ssl.ca.pem, sasl.password) is passed to librdkafka from the Secret. A credential librdkafka reads as a file path (ssl.ca.location, sasl.kerberos.keytab) is a host-owned key and is refused to a workload.

An operator can also configure the built-in plugin from the HostGroup with a source-less hostPlugins entry (id: kafka, no image or file), the same shape as wasmcloud:nats. This is where to pin a value a workload may not override, most usefully the consumer group:

hostPlugins:
  - id: kafka                 # source-less: configures the built-in plugin
    config:
      group.id: orders-prod   # pin the group; a workload asking for a different one is refused at bind

Kafka serves one unnamed binding per host: there is no per-binding bindings: map like wasmcloud:nats, and the workload still supplies broker, credentials, and topics in its own hostInterfaces binding above. The same workloadConfig governance (deny / warn / allow) described for wasmcloud:nats applies to the keys the host owns.

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.

Egress policy and connection quotas

Two chart values, both new in 0.11.0, bound what workloads on a host may do at the network layer. Both are host-wide.

egressPolicy gates raw-socket (wasi:sockets) egress. It ships in count mode, so enabling the upgrade changes nothing: the host evaluates the policy, logs each connection it would refuse (socket egress policy would refuse this connection, at debug level), and allows it anyway. Move to enforce only after running at RUST_LOG=debug for a representative period and confirming the would-refuse log is clean.

egressPolicy:
  socketEgress: "count"     # count (preview, the default) | enforce
  denySpecialRanges: true   # default on: refuses loopback, link-local (incl. the 169.254.169.254 metadata address), unspecified, multicast, and documentation ranges
  denyPrivateRanges: false  # default off: RFC1918/ULA/CGNAT stay reachable, which is the ordinary in-cluster case
  allowHostLoopback: false  # default off: reach the pod's own loopback via host.wasmcloud.internal (also needs the workload's own allowedHostLoopbackPorts)

Under enforce, each workload's allowedHosts allowlist and the range denials both apply. denySpecialRanges (on by default) then blocks the cloud metadata endpoint, so set it false if a workload legitimately needs metadata-based credentials; the count-period log shows those 169.254.169.254 hits so you can decide before flipping. Two independent planes are worth separating: wasi:http egress is always gated by each workload's allowedHosts, regardless of socketEgress; the raw-socket enforce mode additionally denies a hostPlugins entry that declares no allowedHosts (an empty allowlist), so check each plugin's grants before enforcing.

connectionQuotas.maxConnections caps concurrent workload connections host-wide: pooled outbound HTTP, raw wasi:sockets, and inbound published ports together. Left unset it derives from the host's RLIMIT_NOFILE (half, clamped to 64–32768), which tracks the container's real descriptor budget better than a fixed number, so most installs leave it empty. A host that runs out of inbound slots reports http_ingress_saturated on /readyz rather than pushing the failure onto image pulls.