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
- WASM on Kubernetes Architecture
- Setting Up WASM Runtime Support
- Building WASM Applications for Kubernetes
- Packaging and Distribution
- Deployment Strategies
- Service Discovery and Networking
- Configuration and Secrets Management
- Monitoring and Observability
- Scaling and Auto-scaling
- 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
Runtime | Description | Use Cases | Maturity |
---|---|---|---|
Krustlet | WASM kubelet implementation | General WASM workloads | Beta |
SpinKube | Spin framework for K8s | HTTP services, APIs | Stable |
WasmCloud | Actor-based WASM platform | Distributed apps | Alpha |
Lunatic | Erlang-inspired WASM runtime | Fault-tolerant systems | Alpha |
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
- Choose the right runtime based on your use case
- Start with Spin for HTTP services and APIs
- Use proper resource limits to maximize density
- Implement comprehensive monitoring from day one
- 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! 🚀