Skip to content
Published on

Running WebAssembly Workloads on Kubernetes: The Complete Guide to SpinKube, containerd Wasm Shim, and runwasi

Authors
  • Name
    Twitter

Kubernetes WebAssembly

Introduction

Solomon Hykes (the creator of Docker) said in 2019: "If WASM+WASI existed in 2008, we wouldn't have needed to create Docker." This was not an exaggeration but rather an insight that precisely captured the potential of WebAssembly.

Container technology has become the standard for software delivery over the past decade. However, containers are fundamentally isolated processes running on top of Linux kernel namespaces and cgroups, and they must include the full OS userland in their images. Even a minimal Alpine-based image weighs in at tens of megabytes, and cold starts take anywhere from hundreds of milliseconds to several seconds.

WebAssembly (Wasm) offers a fundamentally different approach. Wasm modules are just a few KB to a few MB in size and can achieve sub-millisecond cold starts in a sandboxed environment. They provide OS-independent portability, a capability-based security model, and near-native execution speed.

Starting in 2024, the CNCF ecosystem began a serious push to run Wasm workloads on Kubernetes. As projects like SpinKube, containerd Wasm shim, and runwasi have matured, an era where Wasm workloads can be managed just like Pods is now emerging. This article provides comprehensive coverage from Wasm core concepts to Kubernetes integration architecture, SpinKube deep dive, hands-on deployment, and performance benchmarks.

WebAssembly Core Concepts

The Wasm Binary Format

WebAssembly was originally designed as a binary format for running C/C++/Rust code at near-native speed in the browser. Its key characteristics are:

  • Stack-based virtual machine: Uses a stack-based instruction set rather than registers
  • Type safety: All function signatures and memory accesses must pass type checking before execution
  • Linear memory: Data can only be accessed within a contiguous memory space with guaranteed bounds checking
  • Deterministic execution: Guarantees identical results for identical inputs (with minor floating-point exceptions)
// Simple Wasm module example written in Rust
// Requires [lib] crate-type = ["cdylib"] in Cargo.toml

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn fibonacci(n: i32) -> i64 {
    if n <= 1 {
        return n as i64;
    }
    let mut a: i64 = 0;
    let mut b: i64 = 1;
    for _ in 2..=n {
        let temp = b;
        b = a + b;
        a = temp;
    }
    b
}

The resulting .wasm file after compilation is on the order of a few KB and can be executed on any Wasm runtime (Wasmtime, Wasmer, WasmEdge, etc.).

# Build for the Wasm target
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release

# Check the build artifact size (typically a few KB to a few MB)
ls -lh target/wasm32-wasi/release/*.wasm

# Run directly with Wasmtime
wasmtime target/wasm32-wasi/release/my_module.wasm

WASI (WebAssembly System Interface)

Running Wasm outside the browser requires access to OS resources such as the filesystem, network, and environment variables. WASI is the standard system interface for this purpose.

The core design principle of WASI is Capability-Based Security. By default, a Wasm module cannot do anything; it can only use capabilities that the host explicitly grants.

# Example of granting filesystem access in WASI
# The --dir flag allows access only to a specific directory
wasmtime --dir=/tmp/data::./data my_app.wasm

# When network access is required
wasmtime --tcplisten=0.0.0.0:8080 my_server.wasm

WASI currently has two major coexisting versions:

FeatureWASI Preview 1WASI Preview 2 (0.2.x)
Interface Definitionwitx (text format)WIT (Wasm Interface Type)
Module ModelCore ModuleComponent Model
HTTP SupportNonewasi:http/proxy
Socket SupportLimitedwasi:sockets
Component CompositionNot possibleComposable via WIT

Component Model

The Component Model is the key evolutionary direction for the Wasm ecosystem. The original Core Wasm could only exchange numeric types (i32, i64, f32, f64) as function arguments, meaning that passing high-level types like strings or structs required complex binding code.

The Component Model solves this problem through WIT (Wasm Interface Type), an interface definition language.

// WIT interface definition example
package example:http-handler@1.0.0;

interface types {
    record http-request {
        method: string,
        uri: string,
        headers: list<tuple<string, string>>,
        body: option<list<u8>>,
    }

    record http-response {
        status: u16,
        headers: list<tuple<string, string>>,
        body: option<list<u8>>,
    }
}

world http-handler {
    import wasi:logging/logging;
    export handle-request: func(req: types.http-request) -> types.http-response;
}

Using this WIT definition, it becomes possible to compose an HTTP handler written in Rust with middleware written in Python. Each component is compiled independently, and WIT serves as the interface contract.

Wasm Execution Architecture on Kubernetes

The Traditional Container Runtime Stack

Understanding the container execution flow in Kubernetes is a prerequisite.

kubelet → CRI → containerd → OCI Runtime (runc)Linux Container
  • kubelet: Receives a Pod spec and issues container creation requests
  • CRI (Container Runtime Interface): The standard interface between kubelet and the container runtime
  • containerd: Manages image pulls and container lifecycle
  • runc: The OCI runtime that creates actual Linux containers (namespaces + cgroups)

containerd Wasm Shim

The core idea behind the containerd Wasm shim is to slot a Wasm runtime into the position where runc normally sits.

kubelet → CRI → containerd → containerd-shim-spin-v2 → Wasmtime/SpinWasm Module
                           → containerd-shim-slight-v2 → Wasmtime/SpiderLightningWasm Module
                           → containerd-shim-wasmedge-v1 → WasmEdgeWasm Module

containerd's shim architecture was originally designed to allow swapping different OCI runtimes like plugins. The Wasm shim leverages this extension point to package Wasm modules inside OCI images and execute them through containerd.

runwasi

runwasi is a project developed by the Bytecode Alliance that provides an abstraction layer between containerd shims and Wasm runtimes.

// Core trait structure of runwasi (simplified)
pub trait Engine {
    fn name() -> &'static str;
    fn run_wasi(&self, ctx: &WasiCtx, module: &[u8]) -> Result<i32>;
}

// Wasmtime engine implementation
pub struct WasmtimeEngine;
impl Engine for WasmtimeEngine {
    fn name() -> &'static str { "wasmtime" }
    fn run_wasi(&self, ctx: &WasiCtx, module: &[u8]) -> Result<i32> {
        // Execute Wasm module using Wasmtime
        let engine = wasmtime::Engine::default();
        let module = wasmtime::Module::new(&engine, module)?;
        // ...
    }
}

Through runwasi, various Wasm runtimes (Wasmtime, WasmEdge, Wasmer) can be exposed as containerd shims.

Scheduling Wasm Workloads with RuntimeClass

Kubernetes allows specifying which runtime a Pod should use through the RuntimeClass resource. Wasm workloads define a dedicated RuntimeClass so they are scheduled only on nodes with the Wasm shim installed.

# RuntimeClass definition
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: wasmtime-spin-v2
handler: spin
scheduling:
  nodeSelector:
    kubernetes.io/arch: wasm32-wasi
---
# Wasm workload Pod
apiVersion: v1
kind: Pod
metadata:
  name: wasm-hello
spec:
  runtimeClassName: wasmtime-spin-v2
  containers:
    - name: hello-wasm
      image: ghcr.io/example/hello-wasm:v1
      command: ['/hello.wasm']
  nodeSelector:
    kubernetes.io/arch: wasm32-wasi

SpinKube Deep Dive

What is SpinKube

SpinKube is an open-source project led by Fermyon that provides an integrated framework for running Fermyon Spin applications natively on Kubernetes. It has been accepted as a CNCF Sandbox project and is developed under community-based governance.

SpinKube consists of three core components:

  1. Spin Operator: A Kubernetes custom controller that watches and manages SpinApp CRDs
  2. SpinApp CRD: A custom resource for declaratively defining Spin applications
  3. containerd-shim-spin: The execution engine that wraps the Spin runtime as a containerd shim

Detailed Architecture

┌─────────────────────────────────────────────────────┐
Kubernetes Cluster│                                                       │
│  ┌──────────────┐       ┌────────────────────────┐  │
│  │ Spin Operator│API Server           │  │
│  │              │◄──────│                         │  │
│  │ - SpinApp    │       │ SpinApp CRD registered  │  │
│  │   Controller │       └────────────────────────┘  │
│  │ - Executor   │                                    │
│  │   Selection  │                                    │
│  └──────┬───────┘                                    │
│         │ Creates/Manages│         ▼                                            │
│  ┌──────────────┐       ┌────────────────────────┐  │
│  │  Deployment  │       │   Node (Wasm-capable)   │  │
│  │  or          │──────►│                         │  │
│  │  SpinAppExec │       │  containerd             │  │
│  └──────────────┘       │    └─ shim-spin-v2      │  │
│                         │        └─ Spin Runtime  │  │
│                         │            └─ .wasm     │  │
│                         └────────────────────────┘  │
└─────────────────────────────────────────────────────┘

SpinApp CRD Structure

The SpinApp CRD declaratively defines the deployment of a Spin application.

apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: hello-spin
  namespace: default
spec:
  image: ghcr.io/example/hello-spin:v1
  replicas: 3
  executor: containerd-shim-spin
  enableAutoscaling: true
  resources:
    limits:
      cpu: 100m
      memory: 128Mi
  variables:
    - name: DATABASE_URL
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: url
  runtime-config:
    key_value_stores:
      default:
        type: redis
        url: redis://redis-cluster:6379
    sqlite_databases:
      default:
        type: libsql
        url: https://my-turso-db.turso.io

How the Spin Operator Works

The Spin Operator follows the standard Kubernetes Operator pattern. When a SpinApp resource is created, it executes the following reconcile loop:

  1. SpinApp Detection: Detects creation/modification/deletion of SpinApp resources via watches
  2. Execution Strategy Selection: Chooses between containerd-shim-spin or a SpinKube custom executor based on the spec.executor field
  3. Deployment Creation: Creates a Deployment configured with the appropriate RuntimeClass
  4. Service Creation: Automatically creates a Kubernetes Service if HTTP triggers are present
  5. Autoscaling Configuration: Creates an HPA (Horizontal Pod Autoscaler) or KEDA ScaledObject when enableAutoscaling: true
  6. Status Update: Records the current state in the SpinApp status field

Hands-On Deployment Guide

Step 1: Install the containerd Wasm Shim

Install the containerd Wasm shim on the cluster nodes. The following instructions assume a local environment using k3d.

# Create a k3d cluster (using an image that includes the Wasm shim)
k3d cluster create wasm-cluster \
  --image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.17.0 \
  --port "8081:80@loadbalancer" \
  --agents 2

# Verify cluster status
kubectl get nodes
kubectl get runtimeclass

For production environments, install the Wasm shim using a DaemonSet or Node Feature Discovery.

# Labeling Wasm-capable nodes with Node Feature Discovery
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: wasm-shim-installer
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: wasm-shim-installer
  template:
    metadata:
      labels:
        app: wasm-shim-installer
    spec:
      hostPID: true
      containers:
        - name: installer
          image: ghcr.io/spinkube/containerd-shim-spin/node-installer:v0.17.0
          securityContext:
            privileged: true
          volumeMounts:
            - name: containerd-config
              mountPath: /etc/containerd
            - name: shim-binary
              mountPath: /opt/kwasm/bin
      volumes:
        - name: containerd-config
          hostPath:
            path: /etc/containerd
        - name: shim-binary
          hostPath:
            path: /opt/kwasm/bin

Step 2: Install SpinKube

# Install cert-manager (required for the Spin Operator's webhook)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.0/cert-manager.yaml

# Install SpinKube CRDs
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.4.0/spin-operator.crds.yaml

# Configure RuntimeClass
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.4.0/spin-operator.runtime-class.yaml

# Install Spin Operator via Helm
helm install spin-operator \
  --namespace spin-operator \
  --create-namespace \
  --version 0.4.0 \
  oci://ghcr.io/spinkube/charts/spin-operator

# Create a SpinAppExecutor
kubectl apply -f - <<EOF
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinAppExecutor
metadata:
  name: containerd-shim-spin
spec:
  createDeployment: true
  deploymentConfig:
    runtimeClassName: wasmtime-spin-v2
EOF

Step 3: Write and Deploy a Spin Application

# Create a new project with the Spin CLI
spin new -t http-rust hello-k8s --accept-defaults
cd hello-k8s
// src/lib.rs - Spin HTTP handler
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;

#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
    let path = req.path();
    let method = req.method().to_string();

    println!("Received {method} request for {path}");

    let body = serde_json::json!({
        "message": "Hello from WebAssembly on Kubernetes!",
        "path": path,
        "method": method,
        "runtime": "SpinKube",
        "timestamp": chrono::Utc::now().to_rfc3339()
    });

    Ok(Response::builder()
        .status(200)
        .header("content-type", "application/json")
        .body(body.to_string())
        .build())
}
# Build and push to an OCI registry
spin build
spin registry push ghcr.io/myorg/hello-k8s:v1

# Deploy as a SpinApp
kubectl apply -f - <<EOF
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: hello-k8s
spec:
  image: ghcr.io/myorg/hello-k8s:v1
  replicas: 3
  executor: containerd-shim-spin
EOF

# Check deployment status
kubectl get spinapp hello-k8s
kubectl get pods -l core.spinoperator.dev/app-name=hello-k8s

Performance Benchmarks

Cold Start Comparison

Cold start is the area where Wasm demonstrates its greatest advantage over containers. Here are the measured results across various workloads:

RuntimeWorkloadCold Start (p50)Cold Start (p99)Image Size
Docker (Alpine+Go)HTTP Server450ms1,200ms12MB
Docker (Distroless+Go)HTTP Server380ms980ms8MB
gVisorHTTP Server520ms1,500ms12MB
Spin (Wasm)HTTP Handler1.2ms3.8ms680KB
WasmEdgeHTTP Handler2.1ms5.2ms720KB
Wasmtime (standalone)HTTP Handler0.8ms2.5ms650KB

Wasm achieves 100x to 300x faster cold start times compared to containers. This is because Wasm modules can execute immediately without any OS boot sequence.

Memory Footprint

# Memory usage comparison with 100 concurrent instances (wrk benchmark)
# Docker Container: ~6.4GB (64MB per instance avg)
# Spin Wasm:        ~320MB (3.2MB per instance avg)
# Memory efficiency: Wasm uses approximately 20x less memory
MetricDocker Container (100)Spin Wasm (100)Ratio
Total Memory6.4GB320MB20x
Memory per Instance64MB3.2MB20x
Startup Time per Instance450ms1.2ms375x
Image Size12MB680KB18x

Throughput Benchmark

Below are the benchmark results using wrk against an HTTP endpoint performing JSON serialization/deserialization.

# Benchmark command
wrk -t12 -c400 -d30s http://localhost:8080/api/json

# Docker Container (Go HTTP Server)
# Requests/sec: 45,230
# Avg Latency: 8.8ms
# Transfer/sec: 12.3MB

# Spin Wasm (Rust HTTP Handler)
# Requests/sec: 38,750
# Avg Latency: 10.3ms
# Transfer/sec: 10.5MB

In steady-state (warm) throughput, containers hold an approximately 15-20% advantage. This is due to the ABI boundary overhead of the Wasm runtime. However, in scale-out scenarios, Wasm's fast cold start reverses the overall throughput picture.

Production Considerations

Current Limitations

Wasm on Kubernetes has not yet reached full maturity. Here are the limitations that must be understood before production adoption.

Networking Limitations

# Wasm Pods currently have restrictions with HostPort and NodePort bindings
# Service mesh sidecar patterns (Istio, Linkerd) do not work
# → This can be worked around using Spin's built-in HTTP triggers

# Recommended configuration
apiVersion: v1
kind: Service
metadata:
  name: wasm-service
spec:
  type: ClusterIP # NodePort/LoadBalancer are possible but verify limitations
  selector:
    core.spinoperator.dev/app-name: hello-k8s
  ports:
    - port: 80
      targetPort: 80

Storage Limitations

  • PersistentVolume mounting is either unsupported or limited
  • State persistence through Spin's built-in Key-Value Store (Redis, SQLite) is recommended
  • Filesystem access is restricted by WASI capabilities

Debugging Tools

  • Shell access via kubectl exec is not possible (there is no OS)
  • kubectl logs works as expected
  • Profiling tools are limited (WASI observability interfaces are under development)
# Basic commands for debugging Wasm workloads
kubectl logs -l core.spinoperator.dev/app-name=hello-k8s -f
kubectl describe spinapp hello-k8s
kubectl get events --field-selector involvedObject.name=hello-k8s

Coexistence Strategy for Containers and Wasm

Realistically, migrating all workloads to Wasm is neither possible nor desirable. The recommended coexistence strategy is as follows:

Workload TypeRecommended RuntimeRationale
HTTP API (stateless)Wasm (Spin)Ultra-fast cold start, high density
Event HandlersWasm (Spin)Rapid scale-out/in
DatabasesContainerFilesystem/network requirements
ML InferenceContainer (GPU)GPU access required
Legacy ApplicationsContainerMigration cost outweighs benefits
Edge/IoTWasmUltra-small binaries, portability

Cluster Configuration for Mixed Wasm and Traditional Workloads

# Node pool separation strategy
# Pool 1: Standard container workloads
# Pool 2: Wasm-dedicated nodes with Wasm shim installed

# Add a taint to Wasm nodes
kubectl taint nodes wasm-node-1 workload-type=wasm:NoSchedule

# Add a toleration to the SpinApp
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: edge-handler
spec:
  image: ghcr.io/myorg/edge-handler:v1
  replicas: 5
  executor: containerd-shim-spin
  deploymentAnnotations:
    app.kubernetes.io/part-of: edge-system
  podSpec:
    tolerations:
      - key: workload-type
        operator: Equal
        value: wasm
        effect: NoSchedule
    nodeSelector:
      kubernetes.io/arch: wasm32-wasi

References

Quiz

Q1: What is the main topic covered in "Running WebAssembly Workloads on Kubernetes: The Complete Guide to SpinKube, containerd Wasm Shim, and runwasi"?

A comprehensive guide to running WebAssembly workloads natively on Kubernetes. Covers everything from the Wasm binary format and WASI fundamentals to the containerd Wasm shim, runwasi, SpinKube architecture, Fermyon Spin deployment, and performance benchmarks against traditional...

Q2: What is WebAssembly Core Concepts? The Wasm Binary Format WebAssembly was originally designed as a binary format for running C/C++/Rust code at near-native speed in the browser.

Q3: Describe the Wasm Execution Architecture on Kubernetes. The Traditional Container Runtime Stack Understanding the container execution flow in Kubernetes is a prerequisite.

Q4: What are the key aspects of SpinKube Deep Dive? What is SpinKube SpinKube is an open-source project led by Fermyon that provides an integrated framework for running Fermyon Spin applications natively on Kubernetes. It has been accepted as a CNCF Sandbox project and is developed under community-based governance.

Q5: How does Hands-On Deployment Guide work? Step 1: Install the containerd Wasm Shim Install the containerd Wasm shim on the cluster nodes. The following instructions assume a local environment using k3d. For production environments, install the Wasm shim using a DaemonSet or Node Feature Discovery.