Opens in a new tab
vmblog logo 2024 wht (updated)

How to Build a WebAssembly-on-Kubernetes Development Environment with k0s and Spin

Share: 

David Marshall | Published: November 2, 2023

WebAssembly promises increased flexibility, efficiency, and speed – and a path to the dream of “write once, run anywhere.” The ecosystem has matured in leaps and bounds over the last couple of years, and teams are seeing real benefits in production – take Adobe’s use of WebAssembly on Kubernetes, which delivers more scheduling flexibility via finer-grained, lighter-weight workloads.

Still, in the CNCF’s 2022 WebAssembly microsurvey, over 45% of respondents cited a lack of tooling as their greatest barrier to using WebAssembly (aka Wasm). The landscape is undoubtedly a work-in-progress, and while there are many cases where excellent tooling exists (or has emerged since that survey), teams don’t always have a good picture of their options.

I’ll be talking about shrinking Kubernetes workloads in a Lightning Talk at KubeCon NA 2023, with approaches ranging from the tried-and-true to the emerging. But here I want to zoom in on WebAssembly. If we’re going to deploy Wasm workloads on Kubernetes, developers need a quick and easy way to spin up a Wasm-on-Kubernetes environment for learning, development, and testing.

In this tutorial, we’ll walk through the setup of such an environment from beginning to end, creating a single-node local development cluster to run WebAssembly workloads. After examining the architecture, we’ll take our new environment for a spin.

Open source tooling for Wasm on Kubernetes

In this walkthrough, we’ll be focusing on three open source tools:

  • k0s
  • k0s plugin for wasmtime
  • Spin

The open source, CNCF-certified k0s distribution provides a lightweight Kubernetes cluster well-suited to serve as a single-node development cluster. In addition to being easy to install, version 1.27 and later can use a “wasm-enabler” plugin to easily and dynamically configure containerd to integrate with the wasmtime WebAssembly runtime. This reduced friction can help developers explore the possibilities of Wasm on Kubernetes.

Install k0s

k0s is quick to install. Check the docs for specific system requirements, but installation should be straightforward in an up-to-date Ubuntu Server environment (the sort you can spin up quickly with multipass or distrobox). Just make sure you’ve got at least 2 vCPUs, 4 GB memory, and a good amount of disk space-15 GB should be more than enough for our purposes here.  

First, download the k0s installer script:

$ curl -sSLf https://get.k0s.sh | sudo sh

Once the k0s binary is installed in /usr/bin/k0s, you will be able to run k0s to install k0s as a system service, with the cluster configured in a single-node implementation.

$ sudo k0s install controller –single

Start the service with:

$ sudo k0s start

After a moment, k0s will be ready to use. The service begins automatically after restarting the machine.

Apply the wasm-enabler plugin

To enable k0s’ wasm-enabler plugin, use this manifest. (Find the most up-to-date version on the plugin’s GitHub page.)

apiVersion: apps/v1

kind: DaemonSet

metadata:

  name: wasm-enabler

  namespace: kube-system

spec:

  selector:

    matchLabels:

      k0s-app: wasm-enabler

  template:

    metadata:

      labels:

        k0s-app: wasm-enabler

    spec:

      affinity:

        nodeAffinity:

          requiredDuringSchedulingIgnoredDuringExecution:

            nodeSelectorTerms:

              – matchExpressions:

                  – key: plugin.k0sproject.io/wasm-enabled

                    operator: DoesNotExist

      initContainers:

        – name: wasm-enabler

          image: quay.io/k0sproject/k0s-wasm-plugin:main

          env:

            – name: NODE_NAME

              valueFrom:

                fieldRef:

                  fieldPath: spec.nodeName

          securityContext:

            privileged: true

          volumeMounts:

            – name: bin

              mountPath: /var/lib/k0s/bin

            – name: imports

              mountPath: /etc/k0s/containerd.d/

      containers:

        – name: dummy

          image: registry.k8s.io/pause:3.6

      volumes:

        – name: bin

          hostPath:

            path: /var/lib/k0s/bin

            type: Directory

        – name: imports

          hostPath:

            path: /etc/k0s/containerd.d/

            type: Directory

apiVersion: node.k8s.io/v1

kind: RuntimeClass

metadata:

  name: wasmtime-spin

handler: spin

You can name this manifest wasm-enabler.yaml (or whatever you wish) and deploy it with kubectl apply. Note that if you’re using the kubectl bundled with k0s, you’ll run it as follows:

$ sudo k0s kubectl apply -f wasm-enabler.yaml

Check on the wasm-enabler init pods by getting the status of pods in the kube-system namespace:

$ sudo k0s kubectl get pods -n kube-system

If you see wasm-enabler init pods getting hung up, run the following to label our host node as suitable for running Wasm workloads:

$ kubectl label nodes “YOUR_NODE_NAME_HERE” plugin.k0sproject.io/wasm-enabled=true –overwrite

You can test the Wasm integration with a sample workload. To do so, create a manifest file (you can call it wasm-test.yaml) with the contents below:

apiVersion: apps/v1

kind: Deployment

metadata:

  name: wasm-spin

spec:

  replicas: 1

  selector:

    matchLabels:

      app: wasm-spin

  template:

    metadata:

      labels:

        app: wasm-spin

    spec:

      runtimeClassName: wasmtime-spin

      containers:

        – name: spin-hello

          image: ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.5.1

          command: [“/”]

          resources: # limit the resources to 128Mi of memory and 100m of CPU

            limits:

              cpu: 100m

              memory: 128Mi

            requests:

              cpu: 100m

              memory: 128Mi

apiVersion: v1

kind: Service

metadata:

  name: wasm-spin

spec:

  type: NodePort

  ports:

    – protocol: TCP

      port: 80

      targetPort: 80

  selector:

    app: wasm-spin

Note the RuntimeClassName in the Deployment spec. This tells the cluster which runtime to use for this workload-in this case, wasmtime-spin. 

Deploy with kubectl apply:

$ sudo k0s kubectl apply -f wasm-test.yaml

Check your services to find the port for the wasm-spin sample service:

$ sudo k0s kubectl get services

NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE

kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        9m57s

wasm-spin    NodePort    10.109.224.190   <none>        80:30817/TCP   5m6s

In my case, the port is 30817. Now I can run a curl request against the NodePort exposed service:

$ curl localhost:30817/hello

Hello world from Spin!

Create a Wasm workload and deploy to Kubernetes

You’ve seen references to Spin a couple times now. This open source framework from Fermyon will help us create useful HTTP applications with WebAssembly-apps that can run both locally and on our Kubernetes cluster.

Installing Spin is usually a quick matter of running the install script-follow the instructions for your OS to get started.

Now we’re ready to create a simple WebAssembly workload and deploy it to Kubernetes. In this case, we’ll use the included base template for an HTTP handler in Go. (We’re using TinyGo, but this exercise won’t require TinyGo installed on our system.)

Begin a new Spin project with spin new and choose the http-go template. Choose a name for your app and set the HTTP base and HTTP path as below, simply pressing enter to accept the default values.

Pick a template to start your application with: http-go (HTTP request handler using (Tiny)Go)

Enter a name for your new application: gogo-wasm

Description: Go Wasm HTTP handler

HTTP base: /

HTTP path: /…

Spin will create a new templated project with the standard contents for a Go project-main.go, go.mod, and go.sum-along with a spin.toml file that contains Spin metadata.

The main.js file will look like this:

package main

import (

   “fmt”

   “net/http”

   spinhttp “github.com/fermyon/spin/sdk/go/http”

)

func init() {

   spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {

      w.Header().Set(“Content-Type”, “text/plain”)

      fmt.Fprintln(w, “Go go Wasm!”)

   })

}

func main() {}

Feel free to change the response body to something like “Go go Wasm!” as I’ve done here. 

We’ve created our simple HTTP application in Go. Before we proceed, make sure your spin.toml file looks exactly as below (except for the name and author):

spin_version = “1”

authors = [“Eric <[email protected]>”]

description = “Go Wasm HTTP handler”

name = “gogo-wasm”

trigger = { type = “http”, base = “/” }

version = “0.1.0”

[[component]]

id = “gogo-wasm”

source = “main.wasm”

environment = { VERSION = “v0.1.0” }

[component.trigger]

route = “/…”

[component.build]

command = “tinygo build -wasm-abi=generic -target=wasi -gc=leaking -no-debug -o main.wasm main.go”

Now it’s time to build to Wasm and deploy to Kubernetes. 

Build and publish

We’ll deploy our application via a minimal scratch container. (Very minimal-the compressed file size for our image will be around 84 KB.)

For our build, we need to make sure we’re using buildx. Working locally, the easiest way is to use Docker Desktop. Make sure it’s up-to-date and configured to enable Wasm images in the Beta settings menu.

In your project directory, create a Dockerfile that looks like this:

FROM –platform=${BUILDPLATFORM} tinygo/tinygo:0.25.0 AS build

WORKDIR /opt/build

COPY . .

RUN tinygo build -wasm-abi=generic -target=wasi -gc=leaking -no-debug -o main.wasm main.go

FROM scratch

COPY –from=build /opt/build/main.wasm .

COPY –from=build /opt/build/spin.toml .

When we build with this Dockerfile, we’ll first build the wasm in a TinyGo-based build container, and then copy the main.wasm output and the spin.toml file to a new scratch container.

To build, run this command from your project directory, using your own namespace and project name:

$ docker buildx build –provenance=false –platform=wasi/wasm -t ericgregory/gogo-wasm .

In this case, I’ll publish to Docker Hub:

% docker tag ericgregory/gogo-wasm ericgregory/gogo-wasm:0.1.1

% docker push ericgregory/gogo-wasm:0.1.1

Deploy to k0s development cluster

Now we can deploy our Wasm workload to our k0s cluster. We’ll use a manifest very similar to the one we used for a sample deployment:

apiVersion: apps/v1

kind: Deployment

metadata:

  name: gogo-wasm

spec:

  replicas: 1

  selector:

    matchLabels:

      app: gogo-wasm

  template:

    metadata:

      labels:

        app: gogo-wasm

    spec:

      runtimeClassName: wasmtime-spin

      containers:

        – name: gogo-wasm

          image: ericgregory/gogo-wasm:0.1.1

          imagePullPolicy: Always

          command: [“/”]

apiVersion: v1

kind: Service

metadata:

  name: gogo-wasm

spec:

  type: ClusterIP

  ports:

    – protocol: TCP

      port: 80

      targetPort: 80

  selector:

    app: gogo-wasm

Since we’re deploying a ClusterIP service this time, we’ll run a port-forward to access it locally:

$ kubectl port-forward svc/go-spin 80:80

In another terminal tab:

$ curl localhost:80/

Go go Wasm!

And that’s it. With these tools, you can start writing meaningful WebAssembly applications that run on Kubernetes using open source tooling – and take advantage of WebAssembly’s portability and efficiency. If you’d like to refer to the code in this guide, you can find it on GitHub. I hope you’ve found this useful!

++

Join us at KubeCon + CloudNativeCon North America this November 6 – 9 in Chicago for more on Kubernetes and the cloud native ecosystem. 

##

ABOUT THE AUTHOR

Eric Gregory, senior technical writer, Mirantis

Eric Gregory 

Eric Gregory is a senior technical writer at Mirantis who likes to share experiences with Kubernetes, containers, open source, and DevOps. His past experience includes teaching, as well as writing and curating content.