Ingress and Workloads
Cosmonic Control uses Envoy as its HTTP ingress proxy and a set of Kubernetes CRDs for declaring workloads. This page explains how to configure ingress and deploy WebAssembly workloads.
What are Cosmonic Control deployment manifests?
Deploying a workload to Cosmonic Control is the same as deploying any other resource to Kubernetes: declarative Custom Resource Definition (CRD) manifests written in YAML.
Workload manifests can be applied manually with kubectl and managed in GitOps pipelines with tools like Argo CD. Manifests are composed according to the runtime.wasmcloud.dev/v1alpha1 API.
How ingress works
Cosmonic Control routes external HTTP traffic to Wasm workloads in two stages:
- An edge proxy terminates external connections. Traefik is deployed by default; Istio is an alternative; operators can also bypass the edge proxy and expose Envoy directly.
- Envoy sits behind the edge proxy on a
ClusterIPKubernetes Service namedingress. Envoy matches the request'sHostheader against the XDS routing configuration and forwards the request to the Wasm workload.
Envoy's routing table is pushed over gRPC from the XDS cache whenever workloads change. If a workload runs on multiple hosts, requests are load balanced round robin. If a host crashes, it drops out of the rotation and the workload continues to serve from the remaining hosts.
Traefik (default)
Installing the chart with no overrides deploys Traefik as the edge proxy and creates a Traefik IngressClass marked as the cluster default. Traefik's Service is a NodePort on ports 30080 (http) and 30443 (https):
helm install cosmonic-control oci://ghcr.io/cosmonic/cosmonic-control \
--version 0.4.1 \
--namespace cosmonic-system \
--create-namespaceFor a local Kind cluster, forward host ports 80 and 443 to those NodePorts:
# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 80
protocol: TCP
- containerPort: 30443
hostPort: 443
protocol: TCPThe chart auto-creates an Ingress for the Perses dashboard at perses.localhost.cosmonic.sh. To auto-create Ingress resources for additional hosts at install time, list them under ingress.hosts:
# values.yaml
ingress:
hosts:
- host: "welcome-tour.localhost.cosmonic.sh"
- host: "hello.localhost.cosmonic.sh"For workloads deployed after the install, add a standard Kubernetes Ingress that points at the ingress Service (Envoy) on port 80. Envoy resolves the request to the right workload based on the Host header:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: welcome-tour
namespace: cosmonic-system
annotations:
# "web" for http, "websecure" for https. List both to accept both.
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.observability.accesslogs: "true"
traefik.ingress.kubernetes.io/router.observability.metrics: "true"
traefik.ingress.kubernetes.io/router.observability.tracing: "true"
spec:
ingressClassName: traefik
rules:
- host: welcome-tour.localhost.cosmonic.sh
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ingress
port:
number: 80Istio
To route through an existing Istio IngressGateway instead, set ingress.provider=istio. The chart creates a Gateway (cosmonic-gateway by default, configurable via ingress.istio.gatewayName) and a VirtualService for each entry under ingress.hosts, plus a VirtualService for the Perses dashboard:
helm install cosmonic-control oci://ghcr.io/cosmonic/cosmonic-control \
--version 0.4.1 \
--namespace cosmonic-system \
--create-namespace \
--set ingress.provider=istio \
--set 'ingress.hosts[0].host=*.localhost.cosmonic.sh'If the IngressGateway pods use labels other than istio: ingressgateway, override ingress.istio.ingressGatewaySelector. Istio itself (control plane and IngressGateway) must be installed before the Cosmonic Control chart; istioctl install --set profile=minimal plus a separate components.ingressGateways[0] install covers the baseline setup.
Direct Envoy exposure
To bypass the edge proxy altogether and expose Envoy directly, set ingress.enabled=false and set envoy.service.type to the appropriate service type. Traefik is not deployed in this mode, and the Perses Ingress is not auto-created.
On a cloud cluster, expose Envoy as a LoadBalancer:
helm install cosmonic-control oci://ghcr.io/cosmonic/cosmonic-control \
--version 0.4.1 \
--namespace cosmonic-system \
--create-namespace \
--set ingress.enabled=false \
--set envoy.service.type=LoadBalancerCloud-specific annotations go on envoy.service.annotations. For example, to pin AWS to an internet-facing NLB:
--set-json 'envoy.service.annotations={"service.beta.kubernetes.io/aws-load-balancer-type":"nlb","service.beta.kubernetes.io/aws-load-balancer-scheme":"internet-facing"}'On a bare-metal cluster, expose a NodePort and front it with your own load balancer or proxy:
--set ingress.enabled=false \
--set envoy.service.type=NodePort \
--set envoy.service.httpNodePort=30950TLS
Envoy handles plain HTTP. TLS terminates at the edge:
- Traefik: configure TLS on the Traefik router with annotations such as
traefik.ingress.kubernetes.io/router.tls: "true"and acertresolver, or supply Secrets viaspec.tls. See the Traefik TLS routing docs. - Istio: configure TLS on the Gateway resource.
- Direct Envoy exposure: terminate TLS upstream, for example via Cloudflare or a cloud load balancer, and forward plain HTTP to the
ingressService.
How to deploy WebAssembly workloads with ingress
HTTP-driven WebAssembly workloads are deployed using the HTTPTrigger CRD. The minimum required fields per component are:
name: Name of the componentimage: OCI address for the component image
Additional interfaces (beyond wasi:http) and the ingress host are typically supplied as a values file for the HTTPTrigger Helm chart:
components:
- name: blobby
image: ghcr.io/cosmonic-labs/components/blobby:0.2.0
ingress:
host: 'blobby.wasmworkloads.io'
hostInterfaces:
- namespace: wasi
package: blobstore
version: 0.2.0-draft
interfaces:
- blobstore
- namespace: wasi
package: logging
version: 0.1.0-draft
interfaces:
- loggingIn an Argo CD deployment:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo-hello
namespace: argocd
spec:
project: default
source:
path: .
repoURL: oci://ghcr.io/cosmonic-labs/charts/http-trigger
targetRevision: 0.1.2
helm:
values: |
components:
- name: http
image: ghcr.io/cosmonic-labs/control-demos/hello-world:0.1.2
ingress:
host: "hello.localhost.cosmonic.sh"
destination:
name: in-cluster
namespace: hello
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ServerSideApply=true
- CreateNamespace=true
- RespectIgnoreDifferences=true
retry:
limit: -1
backoff:
duration: 30s
factor: 2
maxDuration: 5mThis creates an HTTPTrigger resource:
apiVersion: control.cosmonic.io/v1alpha1
kind: HTTPTrigger
metadata:
name: example
namespace: default
spec:
replicas: 1
ingress:
host: 'hello.localhost.cosmonic.sh'
paths:
- path: /
pathType: Prefix
template:
spec:
components:
- name: http
image: ghcr.io/cosmonic-labs/control-demos/hello-world:0.1.2The component automatically receives wasi:http/incoming-handler from the trigger; declare any additional capability imports under spec.template.spec.hostInterfaces. Components that fetch over the network add wasi:http/outgoing-handler and scope outbound destinations with localResources.allowedHosts:
spec:
timeout: 300s # bound the upstream Envoy wait time per route (default 15s)
template:
spec:
hostInterfaces:
- namespace: wasi
package: http
interfaces:
- outgoing-handler
- namespace: wasi
package: logging
version: 0.1.0-draft
interfaces:
- logging
components:
- name: api
image: ghcr.io/example/api:0.1.0
localResources:
allowedHosts:
- 'https://upstream.example.com'spec.timeout (added in chart 0.4.1) accepts Go duration strings (300s, 5m) and bounds how long Envoy will wait for the backing host before returning upstream request timeout.
Using multiple HTTPTriggers with the same host but different paths, you can route different workloads to different paths on the same domain.
The HTTPTrigger creates and manages a WorkloadDeployment pre-populated with the interfaces for an HTTP incoming handler. The WorkloadDeployment manages Workload and WorkloadReplicaSet resources.
Path matching
Each entry in spec.ingress.paths requires a path and a pathType. Two path types are supported, following the Kubernetes HTTPIngressPath spec:
Exact — The request path must match exactly.
paths:
- path: /api/users
pathType: Exact/api/users matches only /api/users. /api/users/123 does not match.
Prefix — The request path must begin with the specified prefix.
paths:
- path: /api
pathType: Prefix/api matches /api, /api/users, and /api/v1/users.
PathTypeImplementationSpecific is not supported. Triggers using any path type other than Exact or Prefix will be skipped during route resolution.
Conflict detection
No two HTTPTriggers may register the same (host, path) combination. If a conflict is detected during route resolution, the conflicting trigger is skipped and an error is logged. To avoid conflicts, ensure each trigger uses a unique combination of host and path.
Non-HTTP workloads
Workloads that do not export wasi:http/incoming-handler are deployed directly as a WorkloadDeployment, without an HTTPTrigger. The most common shape is a NATS-driven worker that exports wasmcloud:messaging/handler@0.2.0 and replies on the reply_to subject.
apiVersion: runtime.wasmcloud.dev/v1alpha1
kind: WorkloadDeployment
metadata:
name: task-worker
namespace: default
spec:
replicas: 1
template:
spec:
hostInterfaces:
- namespace: wasi
package: logging
version: 0.1.0-draft
interfaces:
- logging
- namespace: wasmcloud
package: messaging
version: '0.2.0'
interfaces:
- handler
- consumer
- types
config:
subscriptions: tasks.worker
components:
- name: task-worker
image: ghcr.io/example/task-worker:0.1.0
poolSize: 1config.subscriptions on the messaging interface is the NATS subject the host subscribes to on the component's behalf. Inbound messages on that subject invoke the component's wasmcloud:messaging/handler.handle_message export.
A typical two-component application pairs an HTTPTrigger with one of these WorkloadDeployments: the HTTPTrigger publishes a request via wasmcloud:messaging/consumer.request, and the WorkloadDeployment receives it on the subscribed subject and publishes a reply.
Service-plus-component workloads
For workloads that need a long-running, in-memory state alongside per-request component invocations (a parsed corpus, a search index, a cache), use template.spec.service to colocate a service with the components on the same host. The service exports wasi:cli/run and stays resident; components make loopback TCP calls to it, typically on 127.0.0.1.
spec:
template:
spec:
volumes:
- name: data
ephemeral: {}
service:
image: ghcr.io/example/index-service:0.1.0
localResources:
volumeMounts:
- name: data
mountPath: /data
components:
- name: api
image: ghcr.io/example/api:0.1.0
localResources:
volumeMounts:
- name: data
mountPath: /dataThe shared ephemeral volume is the canonical way to hand snapshots from the service side to the component side without reaching out to external storage. volumeMounts defaults to read-write; the readOnly field accepts only true or omission.
hostInterfaces is a scheduling filter
spec.template.spec.hostInterfaces is matched against the set of interfaces the host advertises. If a workload declares an interface the host does not advertise, the workload fails placement with WORKLOAD_STATE_ERROR rather than failing at link time inside a host pod. This is true on both HTTPTrigger and WorkloadDeployment.
Two consequences worth knowing:
- A workload that imports
wasi:webgpu/webgpu@0.0.1requires--set gpu=trueon the hostgroup chart so the host advertises WebGPU. Without it, the workload sticks atWORKLOAD_STATE_ERRORand never schedules. - If a workload declares only the interfaces it actually imports — and the host advertises at least those — the workload will place. Declaring extra interfaces that the host does not provide will block placement, even if the component does not actually use them.
Further reading
- Browse the Template Catalog for more
HTTPTriggerandWorkloadDeploymentexamples. - See Custom Resources for the full CRD reference.
- See GitOps with Argo CD for a complete GitOps workflow.
Many existing wasmCloud applications include OAM-formatted YAML manifests designed for vanilla wasmCloud deployments. These OAM manifests are not Kubernetes-native and are not to be confused with the CRD manifests used by Cosmonic Control. (OAM manifests can be identified by the API version core.oam.dev/v1beta1.)