Skip to main content

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:

  1. 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.
  2. Envoy sits behind the edge proxy on a ClusterIP Kubernetes Service named ingress. Envoy matches the request's Host header 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.

Ingress architecture diagram

Ingress request flow diagram

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-namespace

For 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: TCP

The 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: 80

Istio

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=LoadBalancer

Cloud-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=30950

TLS

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 a certresolver, or supply Secrets via spec.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 ingress Service.

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 component
  • image: 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:
      - logging

In 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: 5m

This 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.2

The 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.

note

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: 1

config.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: /data

The 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.1 requires --set gpu=true on the hostgroup chart so the host advertises WebGPU. Without it, the workload sticks at WORKLOAD_STATE_ERROR and 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

note

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.)