Skip to content

Building and Deploying WASM Modules in Kubernetes: Complete Guide

Building and Deploying WASM Modules in Kubernetes: Complete Guide

Kubernetes is evolving beyond traditional containers to embrace WebAssembly as a first-class citizen. This comprehensive guide covers everything you need to build, package, and deploy WASM modules in Kubernetes, from development to production at scale.

Table of Contents

  1. WASM on Kubernetes Architecture
  2. Setting Up WASM Runtime Support
  3. Building WASM Applications for Kubernetes
  4. Packaging and Distribution
  5. Deployment Strategies
  6. Service Discovery and Networking
  7. Configuration and Secrets Management
  8. Monitoring and Observability
  9. Scaling and Auto-scaling
  10. Production Best Practices

WASM on Kubernetes Architecture

Overview of WASM Integration

Kubernetes WASM Stack:
┌─────────────────────────────────────────────────┐
│               kubectl/K8s API                   │
├─────────────────────────────────────────────────┤
│              kube-scheduler                     │
├─────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │   kubelet   │ │  Krustlet   │ │  SpinKube   │ │
│ │ (containers)│ │ (WASM pods) │ │ (Spin apps) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │     runC    │ │  Wasmtime   │ │    Spin     │ │
│ │   (Linux)   │ │   (WASI)    │ │   (HTTP)    │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────┤
│              Linux Kernel                      │
└─────────────────────────────────────────────────┘

WASM Runtime Options

RuntimeDescriptionUse CasesMaturity
KrustletWASM kubelet implementationGeneral WASM workloadsBeta
SpinKubeSpin framework for K8sHTTP services, APIsStable
WasmCloudActor-based WASM platformDistributed appsAlpha
LunaticErlang-inspired WASM runtimeFault-tolerant systemsAlpha

Setting Up WASM Runtime Support

Installing Krustlet

# @filename: script.sh
# Install Krustlet
curl -fsSL https://krustlet.dev/install.sh | bash

# Create kubeconfig for Krustlet
kubectl create serviceaccount krustlet
kubectl create clusterrolebinding krustlet \
  --clusterrole=system:node \
  --serviceaccount=default:krustlet

# Generate bootstrap token
kubectl create token krustlet --duration=8760h > token.txt

# Start Krustlet
krustlet-wasi \
  --node-ip=192.168.1.100 \
  --node-name=krustlet-node \
  --bootstrap-file=token.txt \
  --cert-file=/etc/ssl/certs/krustlet.crt \
  --private-key-file=/etc/ssl/private/krustlet.key

Krustlet Configuration

# krustlet-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: krustlet-config
  namespace: kube-system
data:
  config.yaml: |
    node:
      name: krustlet-node
      ip: "192.168.1.100"
      labels:
        kubernetes.io/arch: wasm32
        kubernetes.io/os: wasi
        node.kubernetes.io/instance-type: wasm

    runtime:
      wasmtime:
        precompile: true
        cache_config_load_default: true
        cranelift_opt_level: "speed"
        
    logging:
      level: info
      format: json

Installing SpinKube

# @filename: models.py
# Install SpinKube operator
kubectl apply -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.yaml

# Install containerd shim
curl -fsSL https://github.com/spinkube/containerd-shim-spin/releases/latest/download/install.sh | bash

# Configure runtime class
kubectl apply -f - <<EOF
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: wasmtime-spin
handler: spin
EOF

Validating Installation

# @filename: manifest.yaml
# Check nodes
kubectl get nodes -o wide

# Verify WASM runtime class
kubectl get runtimeclass

# Test basic WASM pod
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: hello-wasm
spec:
  runtimeClassName: wasmtime-spin
  containers:
  - name: hello
    image: webassembly/hello-wasm
    ports:
    - containerPort: 8080
EOF

# Check pod status
kubectl get pod hello-wasm -o wide

Building WASM Applications for Kubernetes

Rust HTTP Service

// @filename: lib.rs
// src/lib.rs - Spin HTTP component
use spin_sdk::{
    http::{Request, Response},
    http_component,
    key_value::Store,
};

#[http_component]
fn handle_request(req: Request) -> Result<Response> {
    match req.uri().path() {
        "/" => handle_root(),
        "/health" => handle_health(),
        "/api/users" => handle_users(req),
        "/api/metrics" => handle_metrics(),
        _ => Ok(Response::builder()
            .status(404)
            .body(Some("Not Found".into()))?),
    }
}

fn handle_root() -> Result<Response> {
    Ok(Response::builder()
        .status(200)
        .header("content-type", "application/json")
        .body(Some(r#"{"service": "user-api", "version": "1.0.0"}"#.into()))?)
}

fn handle_health() -> Result<Response> {
    // Health check logic
    let store = Store::open_default()?;
    let healthy = store.exists("health_check").unwrap_or(false);

    if healthy {
        Ok(Response::builder()
            .status(200)
            .body(Some("OK".into()))?)
    } else {
        Ok(Response::builder()
            .status(503)
            .body(Some("Service Unavailable".into()))?)
    }
}

fn handle_users(req: Request) -> Result<Response> {
    match req.method() {
        &http::Method::GET => get_users(),
        &http::Method::POST => create_user(req),
        _ => Ok(Response::builder()
            .status(405)
            .body(Some("Method Not Allowed".into()))?),
    }
}

fn get_users() -> Result<Response> {
    let users = r#"[
        {"id": 1, "name": "Alice", "email": "alice@example.com"},
        {"id": 2, "name": "Bob", "email": "bob@example.com"}
    ]"#;

    Ok(Response::builder()
        .status(200)
        .header("content-type", "application/json")
        .body(Some(users.into()))?)
}

fn handle_metrics() -> Result<Response> {
    let metrics = format!(
        "# HELP http_requests_total Total HTTP requests\n\
         # TYPE http_requests_total counter\n\
         http_requests_total{{method=\"GET\",path=\"/\"}} {}\n\
         http_requests_total{{method=\"GET\",path=\"/health\"}} {}\n",
        get_counter("requests_root"),
        get_counter("requests_health")
    );

    Ok(Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body(Some(metrics.into()))?)
}

fn get_counter(key: &str) -> u64 {
    let store = Store::open_default().unwrap();
    store.get(key)
        .unwrap_or_default()
        .and_then(|v| String::from_utf8(v).ok())
        .and_then(|s| s.parse().ok())
        .unwrap_or(0)
}

Build Configuration

# @filename: config.toml
# spin.toml
spin_manifest_version = "1"
name = "user-api"
version = "1.0.0"
description = "User management API"

[[component]]
id = "user-api"
source = "target/wasm32-wasi/release/user_api.wasm"
allowed_http_hosts = ["https://api.external.com"]
key_value_stores = ["default"]

[component.trigger]
route = "/..."

[component.build]
command = "cargo build --target wasm32-wasi --release"

Go WASM Service

// @filename: main.go
// main.go - TinyGo WASM service
package main


    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "os"

    "github.com/wasmcloud/actor-tinygo"
    "github.com/wasmcloud/interfaces/httpserver/tinygo"
)

type Actor struct{}

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    actor.RegisterHandlers(httpserver.NewProviderHandler(&Actor{}))
}

func (a *Actor) HandleRequest(ctx context.Context, req httpserver.HttpRequest) (*httpserver.HttpResponse, error) {
    switch req.Path {
    case "/":
        return handleRoot()
    case "/health":
        return handleHealth()
    case "/users":
        return handleUsers(req)
    default:
        return &httpserver.HttpResponse{
            StatusCode: 404,
            Header:     make(map[string]httpserver.HeaderValue),
            Body:       []byte("Not Found"),
        }, nil
    }
}

func handleRoot() (*httpserver.HttpResponse, error) {
    response := map[string]interface{}{
        "service": "user-api-go",
        "version": "1.0.0",
        "runtime": "wasmcloud",
    }

    body, _ := json.Marshal(response)

    return &httpserver.HttpResponse{
        StatusCode: 200,
        Header: map[string]httpserver.HeaderValue{
            "content-type": {Value: "application/json"},
        },
        Body: body,
    }, nil
}

func handleHealth() (*httpserver.HttpResponse, error) {
    // Simple health check
    hostname := os.Getenv("HOSTNAME")
    if hostname == "" {
        hostname = "unknown"
    }

    return &httpserver.HttpResponse{
        StatusCode: 200,
        Header: map[string]httpserver.HeaderValue{
            "content-type": {Value: "text/plain"},
        },
        Body: []byte(fmt.Sprintf("OK - %s", hostname)),
    }, nil
}

func handleUsers(req httpserver.HttpRequest) (*httpserver.HttpResponse, error) {
    users := []User{
        {ID: 1, Name: "Alice", Email: "alice@example.com"},
        {ID: 2, Name: "Bob", Email: "bob@example.com"},
    }

    body, _ := json.Marshal(users)

    return &httpserver.HttpResponse{
        StatusCode: 200,
        Header: map[string]httpserver.HeaderValue{
            "content-type": {Value: "application/json"},
        },
        Body: body,
    }, nil
}

Build Scripts

# @filename: script.sh
#!/bin/bash
# build.sh

set -e

echo "Building Rust WASM module..."
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release

echo "Building Spin application..."
spin build

echo "Building Go WASM module..."
tinygo build -o user-api-go.wasm -target wasi main.go

echo "Optimizing WASM modules..."
wasm-opt -Os target/wasm32-wasi/release/user_api.wasm -o user_api_optimized.wasm
wasm-opt -Os user-api-go.wasm -o user_api_go_optimized.wasm

echo "Creating OCI artifacts..."
spin registry push ttl.sh/user-api:latest

Packaging and Distribution

OCI Artifact Format

# @filename: Dockerfile
# Dockerfile for WASM OCI artifact
FROM scratch
COPY user_api_optimized.wasm /
COPY spin.toml /
LABEL org.opencontainers.image.title="User API WASM"
LABEL org.opencontainers.image.description="User management service as WASM"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.artifact.mediatype="application/vnd.wasm.content.layer.v1+wasm"

Registry Operations

# @filename: script.sh
# Build and push to registry
docker buildx build --platform wasi/wasm32 -t myregistry.io/user-api:v1.0.0 .
docker push myregistry.io/user-api:v1.0.0

# Using ORAS for WASM artifacts
oras push myregistry.io/user-api:v1.0.0 \
  user_api_optimized.wasm:application/vnd.wasm.content.layer.v1+wasm \
  spin.toml:application/vnd.spin.config.v1+toml

# Pull and inspect
oras pull myregistry.io/user-api:v1.0.0
oras manifest fetch myregistry.io/user-api:v1.0.0

Multi-Architecture Support

# GitHub Actions for multi-arch build
name: Build and Push WASM
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          target: wasm32-wasi

      - name: Build WASM
        run: |
          cargo build --target wasm32-wasi --release
          wasm-opt -Os target/wasm32-wasi/release/app.wasm -o app.wasm

      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          platforms: wasi/wasm32
          push: true
          tags: |
            ${{ secrets.REGISTRY }}/user-api:latest
            ${{ secrets.REGISTRY }}/user-api:${{ github.sha }}

Deployment Strategies

Basic WASM Deployment

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wasm-user-api
  labels:
    app: user-api
    runtime: wasm
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-api
  template:
    metadata:
      labels:
        app: user-api
        runtime: wasm
    spec:
      runtimeClassName: wasmtime-spin
      containers:
        - name: user-api
          image: myregistry.io/user-api:v1.0.0
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: SERVICE_NAME
              value: 'user-api'
            - name: LOG_LEVEL
              value: 'info'
          resources:
            requests:
              memory: '10Mi'
              cpu: '50m'
            limits:
              memory: '50Mi'
              cpu: '200m'
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 2
            periodSeconds: 5
      nodeSelector:
        kubernetes.io/arch: wasm32

apiVersion: v1
kind: Service
metadata:
  name: wasm-user-api-service
spec:
  selector:
    app: user-api
  ports:
    - port: 80
      targetPort: 8080
      name: http
  type: ClusterIP

Spin Application Deployment

# spin-app.yaml
apiVersion: core.spinkube.dev/v1alpha1
kind: SpinApp
metadata:
  name: user-api-spin
spec:
  image: 'myregistry.io/user-api:v1.0.0'
  replicas: 5
  executor: containerd-shim-spin

  # Resource limits
  resources:
    limits:
      cpu: 200m
      memory: 50Mi
    requests:
      cpu: 50m
      memory: 10Mi

  # Environment variables
  variables:
    - name: 'SERVICE_NAME'
      value: 'user-api-spin'
    - name: 'LOG_LEVEL'
      value: 'info'

  # Scaling configuration
  scaling:
    minReplicas: 2
    maxReplicas: 20
    targetCPUUtilizationPercentage: 70

  # Health checks
  livenessProbe:
    httpGet:
      path: '/health'
      port: 80
    initialDelaySeconds: 10
    periodSeconds: 30

  readinessProbe:
    httpGet:
      path: '/health'
      port: 80
    initialDelaySeconds: 5
    periodSeconds: 10

WasmCloud Actor Deployment

# wasmcloud-actor.yaml
apiVersion: core.wasmcloud.dev/v1alpha1
kind: WasmCloudHostConfig
metadata:
  name: user-api-host
spec:
  hostId: 'user-api-host'
  latticePrefix: 'production'

  # Host configuration
  config:
    wasmcloud_log_level: 'info'
    wasmcloud_rpc_host: '0.0.0.0'
    wasmcloud_rpc_port: '4222'
    wasmcloud_ctl_host: '0.0.0.0'
    wasmcloud_ctl_port: '4223'

---
apiVersion: core.wasmcloud.dev/v1alpha1
kind: Actor
metadata:
  name: user-api-actor
spec:
  image: 'myregistry.io/user-api-wasmcloud:v1.0.0'

  # Actor configuration
  config:
    max_concurrent: 100

  # Link to HTTP server capability
  links:
    - name: 'httpserver'
      wit_namespace: 'wasi'
      wit_package: 'http'
      interfaces: ['incoming-handler']
      target:
        name: 'httpserver'
        config:
          address: '0.0.0.0:8080'

---
apiVersion: core.wasmcloud.dev/v1alpha1
kind: Provider
metadata:
  name: httpserver-provider
spec:
  image: 'wasmcloud.azurecr.io/httpserver:0.19.1'

  # Provider configuration
  config:
    default_port: '8080'
    readonly_mode: false

Service Discovery and Networking

Service Mesh Integration

# Istio VirtualService for WASM services
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: user-api-wasm
spec:
  hosts:
    - user-api.example.com
  http:
    - match:
        - uri:
            prefix: '/api/v1/'
      route:
        - destination:
            host: wasm-user-api-service
            port:
              number: 80
          weight: 90
        - destination:
            host: legacy-user-api-service
            port:
              number: 8080
          weight: 10
      fault:
        delay:
          percentage:
            value: 0.1
          fixedDelay: 5s
      retries:
        attempts: 3
        perTryTimeout: 2s

---
# DestinationRule for WASM services
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: user-api-wasm
spec:
  host: wasm-user-api-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1000
      http:
        http1MaxPendingRequests: 100
        maxRequestsPerConnection: 10
    loadBalancer:
      simple: LEAST_CONN
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

Network Policies

# Network policy for WASM workloads
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: wasm-user-api-netpol
spec:
  podSelector:
    matchLabels:
      app: user-api
      runtime: wasm
  policyTypes:
    - Ingress
    - Egress

  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
        - podSelector:
            matchLabels:
              app: gateway
      ports:
        - protocol: TCP
          port: 8080

  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432
    - to: [] # Allow DNS
      ports:
        - protocol: UDP
          port: 53

Configuration and Secrets Management

ConfigMap for WASM Apps

# wasm-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: wasm-user-api-config
data:
  app.toml: |
    [server]
    host = "0.0.0.0"
    port = 8080
    timeout = 30

    [database]
    host = "postgres.default.svc.cluster.local"
    port = 5432
    database = "users"
    max_connections = 20

    [logging]
    level = "info"
    format = "json"

    [cache]
    redis_url = "redis://redis.default.svc.cluster.local:6379"
    ttl = 300

  features.json: |
    {
      "features": {
        "user_registration": true,
        "email_verification": true,
        "password_reset": true,
        "admin_panel": false
      }
    }

---
# Updated deployment with config
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wasm-user-api
spec:
  template:
    spec:
      containers:
        - name: user-api
          image: myregistry.io/user-api:v1.0.0
          env:
            - name: CONFIG_PATH
              value: '/config/app.toml'
            - name: FEATURES_PATH
              value: '/config/features.json'
          volumeMounts:
            - name: config
              mountPath: /config
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: wasm-user-api-config

Secrets Management

# secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: wasm-user-api-secrets
type: Opaque
data:
  db-password: <base64-encoded-password>
  jwt-secret: <base64-encoded-jwt-secret>
  api-key: <base64-encoded-api-key>

---
# Using secrets in deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wasm-user-api
spec:
  template:
    spec:
      containers:
        - name: user-api
          image: myregistry.io/user-api:v1.0.0
          env:
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: wasm-user-api-secrets
                  key: db-password
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: wasm-user-api-secrets
                  key: jwt-secret
            - name: API_KEY
              valueFrom:
                secretKeyRef:
                  name: wasm-user-api-secrets
                  key: api-key

External Secrets Integration

# external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: wasm-user-api-external-secret
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: vault-secret-store
    kind: SecretStore
  target:
    name: wasm-user-api-secrets
    creationPolicy: Owner
  data:
    - secretKey: db-password
      remoteRef:
        key: secret/user-api
        property: database_password
    - secretKey: jwt-secret
      remoteRef:
        key: secret/user-api
        property: jwt_secret_key
    - secretKey: api-key
      remoteRef:
        key: secret/user-api
        property: external_api_key

Monitoring and Observability

Prometheus Metrics

// @filename: lib.rs
// Rust metrics implementation
use spin_sdk::key_value::Store;
use std::collections::HashMap;

#[derive(Debug)]
struct Metrics {
    requests_total: u64,
    requests_duration: f64,
    errors_total: u64,
}

impl Metrics {
    fn increment_requests(&mut self) {
        self.requests_total += 1;
        self.persist();
    }

    fn record_duration(&mut self, duration: f64) {
        self.requests_duration += duration;
        self.persist();
    }

    fn increment_errors(&mut self) {
        self.errors_total += 1;
        self.persist();
    }

    fn persist(&self) {
        let store = Store::open_default().unwrap();
        store.set("requests_total", &self.requests_total.to_string().into_bytes()).unwrap();
        store.set("requests_duration", &self.requests_duration.to_string().into_bytes()).unwrap();
        store.set("errors_total", &self.errors_total.to_string().into_bytes()).unwrap();
    }

    fn export_prometheus(&self) -> String {
        format!(
            "# HELP http_requests_total Total HTTP requests\n\
             # TYPE http_requests_total counter\n\
             http_requests_total {}\n\
             # HELP http_request_duration_seconds HTTP request duration\n\
             # TYPE http_request_duration_seconds summary\n\
             http_request_duration_seconds_sum {}\n\
             http_request_duration_seconds_count {}\n\
             # HELP http_errors_total Total HTTP errors\n\
             # TYPE http_errors_total counter\n\
             http_errors_total {}\n",
            self.requests_total,
            self.requests_duration,
            self.requests_total,
            self.errors_total
        )
    }
}

ServiceMonitor Configuration

# service-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: wasm-user-api-metrics
  labels:
    app: user-api
    runtime: wasm
spec:
  selector:
    matchLabels:
      app: user-api
  endpoints:
    - port: http
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s
  namespaceSelector:
    matchNames:
      - default

---
# Grafana Dashboard ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: wasm-user-api-dashboard
  labels:
    grafana_dashboard: '1'
data:
  dashboard.json: |
    {
      "dashboard": {
        "id": null,
        "title": "WASM User API Dashboard",
        "panels": [
          {
            "id": 1,
            "title": "Request Rate",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(http_requests_total{job=\"wasm-user-api\"}[5m])",
                "legendFormat": "Requests/sec"
              }
            ]
          },
          {
            "id": 2,
            "title": "Response Time",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(http_request_duration_seconds_sum{job=\"wasm-user-api\"}[5m]) / rate(http_request_duration_seconds_count{job=\"wasm-user-api\"}[5m])",
                "legendFormat": "Avg Response Time"
              }
            ]
          },
          {
            "id": 3,
            "title": "Error Rate",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(http_errors_total{job=\"wasm-user-api\"}[5m])",
                "legendFormat": "Errors/sec"
              }
            ]
          }
        ]
      }
    }

Distributed Tracing

// @filename: lib.rs
// OpenTelemetry integration
use opentelemetry::{global, trace::{Span, Tracer, TracerProvider}};
use opentelemetry_jaeger::JaegerPipeline;
use spin_sdk::http::{Request, Response};

static mut TRACER: Option<Box<dyn Tracer + Send + Sync>> = None;

fn init_tracing() {
    let tracer = JaegerPipeline::new()
        .with_service_name("wasm-user-api")
        .with_endpoint("http://jaeger-collector:14268/api/traces")
        .install_simple()
        .expect("Failed to initialize tracer");

    unsafe {
        TRACER = Some(Box::new(tracer));
    }
}

fn handle_request_with_tracing(req: Request) -> Result<Response> {
    let tracer = unsafe { TRACER.as_ref().unwrap() };
    let mut span = tracer.start("handle_request");

    // Add span attributes
    span.set_attribute("http.method", req.method().to_string());
    span.set_attribute("http.url", req.uri().to_string());

    let result = handle_request(req);

    // Record span status
    match &result {
        Ok(response) => {
            span.set_attribute("http.status_code", response.status().as_u16() as i64);
        }
        Err(e) => {
            span.record_error(e);
            span.set_status(opentelemetry::trace::Status::error(e.to_string()));
        }
    }

    span.end();
    result
}

Scaling and Auto-scaling

Horizontal Pod Autoscaler

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: wasm-user-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: wasm-user-api
  minReplicas: 5
  maxReplicas: 100
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 70
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: '1000'
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
        - type: Pods
          value: 10
          periodSeconds: 60
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60
      selectPolicy: Min

Vertical Pod Autoscaler

# vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: wasm-user-api-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: wasm-user-api
  updatePolicy:
    updateMode: 'Auto'
  resourcePolicy:
    containerPolicies:
      - containerName: user-api
        minAllowed:
          cpu: 10m
          memory: 5Mi
        maxAllowed:
          cpu: 500m
          memory: 100Mi
        controlledResources: ['cpu', 'memory']

KEDA Autoscaling

# keda-scaler.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: wasm-user-api-scaler
spec:
  scaleTargetRef:
    name: wasm-user-api
  minReplicaCount: 3
  maxReplicaCount: 200
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus:9090
        metricName: http_requests_per_second
        threshold: '1000'
        query: sum(rate(http_requests_total{job="wasm-user-api"}[1m]))
    - type: redis
      metadata:
        address: redis:6379
        listName: user_queue
        listLength: '10'
    - type: cron
      metadata:
        timezone: America/New_York
        start: '0 9 * * 1-5'
        end: '0 17 * * 1-5'
        desiredReplicas: '20'

Production Best Practices

Security Hardening

# security-policy.yaml
apiVersion: v1
kind: Pod
metadata:
  name: secure-wasm-pod
spec:
  runtimeClassName: wasmtime-spin
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: user-api
      image: myregistry.io/user-api:v1.0.0
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
      resources:
        limits:
          memory: '50Mi'
          cpu: '200m'
          ephemeral-storage: '100Mi'
        requests:
          memory: '10Mi'
          cpu: '50m'
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: var-run
          mountPath: /var/run
  volumes:
    - name: tmp
      emptyDir: {}
    - name: var-run
      emptyDir: {}

Resource Quotas

# resource-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: wasm-quota
  namespace: wasm-workloads
spec:
  hard:
    requests.cpu: '10'
    requests.memory: 20Gi
    limits.cpu: '20'
    limits.memory: 40Gi
    pods: 1000
    persistentvolumeclaims: '0'
    services: 20
    secrets: 50
    configmaps: 50

---
apiVersion: v1
kind: LimitRange
metadata:
  name: wasm-limits
  namespace: wasm-workloads
spec:
  limits:
    - default:
        cpu: '200m'
        memory: '50Mi'
      defaultRequest:
        cpu: '50m'
        memory: '10Mi'
      type: Container
    - max:
        cpu: '1'
        memory: '200Mi'
      min:
        cpu: '10m'
        memory: '5Mi'
      type: Container

Production Deployment Pipeline

# .github/workflows/deploy-wasm.yml
name: Deploy WASM to Kubernetes
on:
  push:
    branches: [main]
    paths: ['src/**', 'Cargo.toml', 'spin.toml']

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          target: wasm32-wasi

      - name: Cache Cargo dependencies
        uses: actions/cache@v3
        with:
          path: ~/.cargo
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Build WASM
        run: |
          spin build
          wasm-opt -Os target/wasm32-wasi/release/user_api.wasm -o user_api.wasm

      - name: Security scan
        run: |
          cargo audit
          # WASM-specific security checks

      - name: Build and push image
        uses: docker/build-push-action@v4
        with:
          context: .
          platforms: wasi/wasm32
          push: true
          tags: |
            ${{ secrets.REGISTRY }}/user-api:${{ github.sha }}
            ${{ secrets.REGISTRY }}/user-api:latest

      - name: Deploy to staging
        uses: azure/k8s-deploy@v3
        with:
          namespace: staging
          manifests: |
            k8s/deployment.yaml
            k8s/service.yaml
          images: |
            ${{ secrets.REGISTRY }}/user-api:${{ github.sha }}

      - name: Integration tests
        run: |
          kubectl wait --for=condition=ready pod -l app=user-api -n staging --timeout=300s
          npm run test:integration

      - name: Deploy to production
        if: success()
        uses: azure/k8s-deploy@v3
        with:
          namespace: production
          manifests: |
            k8s/deployment.yaml
            k8s/service.yaml
            k8s/hpa.yaml
          images: |
            ${{ secrets.REGISTRY }}/user-api:${{ github.sha }}

      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: failure
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Conclusion

WebAssembly in Kubernetes represents the future of lightweight, secure, and efficient container orchestration. By following this guide, you can:

  • Deploy WASM modules in Kubernetes with confidence
  • Leverage multiple runtimes (Krustlet, SpinKube, WasmCloud)
  • Implement proper scaling and resource management
  • Maintain security and operational best practices
  • Monitor and observe WASM workloads effectively

Key Takeaways

  1. Choose the right runtime based on your use case
  2. Start with Spin for HTTP services and APIs
  3. Use proper resource limits to maximize density
  4. Implement comprehensive monitoring from day one
  5. Follow security best practices for production

Next Steps

  • Explore advanced WASM capabilities
  • Implement edge computing scenarios
  • Contribute to the WASM ecosystem
  • Build custom WASM runtimes

The combination of WASM and Kubernetes is revolutionizing how we think about application deployment and resource utilization. Start experimenting today!

Resources

Ready for edge computing? Check out our next article on WebAssembly at the edge! 🚀

Kubernetes Container Orchestration DevOps Rust Systems Programming Performance
Share:

Continue Reading