Fenil Sonani

Building and Deploying WASM Modules in Kubernetes: Complete Guide

1 min read

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

# 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

# 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

# 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

// 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": "[email protected]"},
        {"id": 2, "name": "Bob", "email": "[email protected]"}
    ]"#;
    
    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

# 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

// main.go - TinyGo WASM service
package main

import (
    "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: "[email protected]"},
        {ID: 2, Name: "Bob", Email: "[email protected]"},
    }
    
    body, _ := json.Marshal(users)
    
    return &httpserver.HttpResponse{
        StatusCode: 200,
        Header: map[string]httpserver.HeaderValue{
            "content-type": {Value: "application/json"},
        },
        Body: body,
    }, nil
}

Build Scripts

#!/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

# 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

# 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

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

// 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! 🚀

Share this content

Reading time: 1 minutes
Progress: 0%
#Kubernetes#Container Orchestration#DevOps#Rust#Systems Programming#Performance
Building and Deploying WASM Modules in Kubernetes: Complete Guide - Fenil Sonani