DEV Community

Cover image for How to run WebAssembly/WASI application (Spin) with Dapr on Kubernetes
Thang Chung
Thang Chung

Posted on • Edited on

How to run WebAssembly/WASI application (Spin) with Dapr on Kubernetes

Introduction

As far as we know, WebAssembly (WASM) / WebAssembly System Interface (WASI) is gaining traction these days. It comes with a lot of benefits such as faster startup times (no cold-start like serverless, start-up around milliseconds), near-native performance (WASM format), lightweight (very small sizing), convenience and versatility (build once run everywhere promise), security (sandbox by default).

But WASM/WASI itself is only strong for the computing workload (e.g. int + int => int) just like serverless v2 in the cloud computing world. It is appropriate for heavy and ephemeral calculation/transformation tasks or some kind of trigger tasks by external components such as cloud services. And it might delegate persistent or message communication tasks to its old brothers (containers). That's the reason Containers and WebAssembly should come and complement together, not suited for stand-alone tasks when we intend to use it to build cloud applications.

Having said that, we can use many of the existing containers built by famous companies like Redis, Kafka, and RabbitMQ... Then we can run WebAssembly (WASI) components on Kubernetes with containerd-wasm-shims. But with the very limitation of components for persistence data, message broker, networking management, etc. we might think about how can we leverage some kind of OSS from CNCF like Dapr or KEDA to amplify its power. Imagine that we can use Dapr for service invocation, data binding, pub/sub, distributed workflow, you name it, so we can build out some battery-included applications on the cloud.

This article is the first step in addressing the described above. And it works because of the release of containerd-wasm-shims v0.9.0 just yesterday by DeisLabs team. In this release, there is a very cool feature called Linux Container Side-by-Side in a Pod: You can now run Linux containers alongside WebAssembly containers within the same Kubernetes Pod, and we can use it to run the Dapr sidecar model which runs side-by-side with Spin component. We will deep dive into how to make it work in a minute.

Sample application (Spin)

In this post, we would like to introduce a very simple application written on Spin which returns a list of products (drinks or foods) as below.

GET {{host}}/v1-get-item-types HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode
[
  {
    "image": "img/CAPPUCCINO.png",
    "itemType": 0,
    "name": "CAPPUCCINO",
    "price": 4.5
  },
  // ...
  {
    "image": "img/CROISSANT_CHOCOLATE.png",
    "itemType": 9,
    "name": "CROISSANT_CHOCOLATE",
    "price": 3.5
  }
]
Enter fullscreen mode Exit fullscreen mode

The code in Rust language:

use anyhow::Result;
use bytes::Bytes;
use serde::{Serialize, Deserialize};
use serde_json::json;
use spin_sdk::{
    http::{Params, Request, Response},
    http_component,
};

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct ItemType {
    name: String,
    item_type: i8,
    price: f32,
    image: String,
}

#[derive(Debug, Deserialize)]
struct GetItemByTypeModel{
    types: String,
}

impl TryFrom<&Option<Bytes>> for GetItemByTypeModel {
    type Error = anyhow::Error;

    fn try_from(value: &Option<Bytes>) -> std::result::Result<Self, Self::Error> {
        match value {
            Some(b) => Ok(serde_json::from_slice::<GetItemByTypeModel>(b)?),
            None => Err(anyhow::anyhow!("No body")),
        }
    }
}

#[http_component]
fn handle_product_api(req: Request) -> Result<Response> {
    println!("{:?}", req.headers());

    let mut router = spin_sdk::http::Router::default();
    router.get("/", health_handler);
    router.get("/v1-get-item-types", get_item_types_handler);
    router.get("/v1-get-items-by-types", get_item_by_types_handler);
    router.handle(req)
}

fn health_handler(_req: Request, _params: Params) -> Result<Response> {
    Ok(http::Response::builder()
        .status(200)
        .body(Some("".into()))?)
}

fn get_item_types_handler(_req: Request, _params: Params) -> Result<Response> {
    let items = json!(get_item_types());
    let result = bytes::Bytes::from(items.to_string());
    Ok(http::Response::builder()
        .header("Content-Type", "application/json")
        .status(200)
        .body(Some(result))?)
}

fn get_item_by_types_handler(req: Request, _params: Params) -> Result<Response> {
    let Ok(model) = GetItemByTypeModel::try_from(&req.body().clone()) else {
        return Ok(http::Response::builder()
        .status(http::StatusCode::BAD_REQUEST)
        .body(None)?);
    };

    let mut temp: Vec<ItemType> = Vec::new();

    for i in get_item_types() {
        let parts = model.types.split(',');

        let ii = i.clone();
        for j in parts {
            if ii.item_type.to_string().as_str() == j {
                temp.push(ii.clone())
            }
        }
    } 

    let result = bytes::Bytes::from(json!(temp).to_string());
    Ok(http::Response::builder()
        .header("Content-Type", "application/json")
        .status(200)
        .body(Some(result))?)
}

fn get_item_types() -> Vec<ItemType> {
    vec![
        ItemType {
            name: "CAPPUCCINO".to_string(),
            item_type: 0,
            price: 4.5,
            image: "img/CAPPUCCINO.png".to_string(),
        },
        // ... 
        // ...
        ItemType {
            name: "CROISSANT_CHOCOLATE".to_string(),
            item_type: 9,
            price: 3.5,
            image: "img/CROISSANT_CHOCOLATE.png".to_string(),
        },
    ]
}
Enter fullscreen mode Exit fullscreen mode

The whole source code for product-api can be found at https://github.com/thangchung/dapr-labs/blob/feat/spin-refactor/polyglot/product-api/src/lib.rs

Package the Spin application into Container

To make it work on Kubernetes, we need to package the application with docker buildx and the target should be wasi/wasm. Let's do it below

Login into GitHub artifact hub:

docker login ghcr.io -u <your username>
Enter fullscreen mode Exit fullscreen mode

It asks you to provide the password (PAT), please go to your developer profile to generate it.

Then we are ready to build the wasm/wasi container:

FROM --platform=${BUILDPLATFORM} rust:1.67 AS build
RUN rustup target add wasm32-wasi
COPY . /product
WORKDIR /product
RUN cargo build --target wasm32-wasi --release

FROM scratch
COPY --from=build /product/target/wasm32-wasi/release/product_api.wasm /target/wasm32-wasi/release/product_api.wasm
COPY ./spin.toml /spin.toml
Enter fullscreen mode Exit fullscreen mode

Let builds the image above:

cd product-api
docker buildx build -f Dockerfile --platform wasi/wasm,linux/amd64,linux/arm64 -t ghcr.io/thangchung/dapr-labs/product-api-spin:1.0.1 . --push
Enter fullscreen mode Exit fullscreen mode

This action will build everything that is needed for the Spin app can run on a container, and then push it into the container hub. After that, you can see it at https://github.com/thangchung/dapr-labs/pkgs/container/dapr-labs%2Fproduct-api-spin/124547580?tag=1.0.1

Prepare your Kubernetes (K3d) cluster

Install k3d into your Ubuntu:

wget -q -O - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
Enter fullscreen mode Exit fullscreen mode

Make it shim to be able to run WASM/WASI workloads (slight, spin, wws, lunatic):

k3d cluster create wasm-cluster --image ghcr.io/deislabs/containerd-wasm-shims/examples/k3d:v0.9.0 -p "8081:80@loadbalancer" --agents 2
kubectl apply -f https://github.com/deislabs/containerd-wasm-shims/raw/main/deployments/workloads/runtime.yaml
kubectl apply -f https://github.com/deislabs/containerd-wasm-shims/raw/main/deployments/workloads/workload.yaml
echo "waiting 5 seconds for workload to be ready"
sleep 15
curl -v http://127.0.0.1:8081/spin/hello
curl -v http://127.0.0.1:8081/slight/hello
curl -v http://127.0.0.1:8081/wws/hello
curl -v http://127.0.0.1:8081/lunatic/hello
Enter fullscreen mode Exit fullscreen mode

If you can curl it with 200 statuses, then everything is okay. Move on!

Refs: https://github.com/thangchung/containerd-wasm-shims/blob/main/deployments/k3d/README.md

Install Dapr 1.11.2 into K3d

In this post, we use Dapr v1.11.2, and you should see as:

dapr --version
CLI version: 1.11.0
Runtime version: 1.11.2
Enter fullscreen mode Exit fullscreen mode

For a demo purpose only, so we install it via Dapr CLI to k3d cluster (on production, we might need to use Dapr Helm chart with HA mode):

dapr init -k --runtime-version 1.11.2
Enter fullscreen mode Exit fullscreen mode

Wait a second for Dapr installed on your cluster, then we continue to install Redis component into K3d cluster as well (not need redis for now, but might need in the next posts).

helm install my-redis oci://registry-1.docker.io/bitnamicharts/redis --set architecture=standalone --set global.redis.password=P@ssw0rd
Enter fullscreen mode Exit fullscreen mode

Now we create some Dapr components which bind with Redis above:

kubectl apply -f components-k8s/
Enter fullscreen mode Exit fullscreen mode

Query it:

kubectl get components
Enter fullscreen mode Exit fullscreen mode

Should return:

NAME            AGE
baristapubsub   16h
kitchenpubsub   16h
statestore      16h
Enter fullscreen mode Exit fullscreen mode

Okay, let's move on to the final step.

Run Spin app with Dapr on Kubernetes (k3d)

We create the yaml file as below:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: product-api
  template:
    metadata:
      labels:
        app: product-api
      annotations:
(1)        dapr.io/enabled: "true"
(1)        dapr.io/app-id: "product-api"
(1)        dapr.io/app-port: "80"
(1)        dapr.io/enable-api-logging: "true"
    spec:
(2)      runtimeClassName: wasmtime-spin
      containers:
        - name: product-api
          image: ghcr.io/thangchung/dapr-labs/product-api-spin:1.0.1
          command: ["/"]
          env:
          - name: RUST_BACKTRACE
            value: "1"
          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: product-api
spec:
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 5001
      targetPort: 80
  selector:
    app: product-api

Enter fullscreen mode Exit fullscreen mode

(1): daprized the application so that Dapr control plane will inject the sidecar automatically for us.
(2): Wasmtime with Spin is chosen for this application.

Then we apply it to create the product-api:

kubectl apply -f iac/kind-spin/product-api-deploy.yaml
Enter fullscreen mode Exit fullscreen mode

Check it work:

kubectl get po
Enter fullscreen mode Exit fullscreen mode

You should see:

NAME                         READY   STATUS    RESTARTS       AGE
my-redis-master-0            1/1     Running   8 (134m ago)   16h
product-api-8ccbc56b-gvlc2   2/2     Running   0              78m
Enter fullscreen mode Exit fullscreen mode

If you notice, now we can have a sidecar (2/2) work very well with Spin app (WASM/WASI). Thanks, Mossaka and the Deislabs team for working very hard on it.

If you tail the logs of product-api-daprd, you should see:

kubectl logs pod/product-api-8ccbc56b-dxfmj --namespace=default --container=daprd --since=0
Enter fullscreen mode Exit fullscreen mode
...
time="2023-09-04T08:47:10.311158673Z" level=info msg="Dapr trace sampler initialized: DaprTraceSampler(P=0.000100)" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.311260921Z" level=info msg="Initialized name resolution to kubernetes" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.311556754Z" level=info msg="Loading components…" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.313604323Z" level=info msg="Component loaded: kubernetes (secretstores.kubernetes/v1)" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.318570002Z" level=info msg="Component loaded: baristapubsub (pubsub.redis/v1)" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.321000955Z" level=info msg="Component loaded: kitchenpubsub (pubsub.redis/v1)" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.3210958Z" level=info msg="Waiting for all outstanding components to be processed" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.325038524Z" level=info msg="Using 'statestore' as actor state store" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:10.325088749Z" level=info msg="Component loaded: statestore (state.redis/v1)" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
...
time="2023-09-04T08:47:10.352600037Z" level=info msg="application protocol: http. waiting on port 80.  This will block until the app is listening on that port." app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
...
time="2023-09-04T08:47:16.169135624Z" level=info msg="actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime.actor type=log ver=1.11.2
time="2023-09-04T08:47:16.169271184Z" level=info msg="Configuring workflow engine with actors backend" app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime.wfengine type=log ver=1.11.2
time="2023-09-04T08:47:16.169288176Z" level=info msg="Registering component for dapr workflow engine..." app_id=product-api instance=k3d-wasm-cluster-agent-1 scope=dapr.runtime type=log ver=1.11.2
time="2023-09-04T08:47:16.169440257Z" level=info msg="initializing Dapr workflow component" app_id=product-api component="dapr (workflow.dapr/v1)" instance=k3d-wasm-cluster-agent-1 scope=dapr.contrib type=log ver=1.11.2
time="2023-09-04T08:47:16.17126385Z" level=info msg="dapr initialized. Status: Running. Init Elapsed 5864ms" 
Enter fullscreen mode Exit fullscreen mode

Amazing, it could daprized and run product-api successfully. I struggled to make it work for a month (before containerd-wasm-shims v0.9.0), and now it is like a dream 🙌.

Now we get the services:

kubectl get svc
Enter fullscreen mode Exit fullscreen mode

You should get:

NAME                TYPE           CLUSTER-IP      EXTERNAL-IP                        PORT(S)                               AGE
kubernetes          ClusterIP      10.43.0.1       <none>                             443/TCP                               16h
my-redis-headless   ClusterIP      None            <none>                             6379/TCP                              16h
my-redis-master     ClusterIP      10.43.109.123   <none>                             6379/TCP                              16h
product-api-dapr    ClusterIP      None            <none>                             80/TCP,50001/TCP,50002/TCP,9090/TCP   81m
product-api         LoadBalancer   10.43.4.58      172.19.0.2,172.19.0.3,172.19.0.4   5001:32134/TCP                        81m
Enter fullscreen mode Exit fullscreen mode

For demo purpose, we do a port forward our product-api: 10.43.4.58 on 5001 port to host machine.

And using curl, we can achive our purpose like a breeze:

###
GET http://localhost:5001/v1-get-item-types HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

Return

HTTP/1.1 200 OK
content-type: application/json
content-length: 776
date: Mon, 04 Sep 2023 08:27:42 GMT

[
  {
    "image": "img/CAPPUCCINO.png",
    "itemType": 0,
    "name": "CAPPUCCINO",
    "price": 4.5
  },
  ...
  {
    "image": "img/CROISSANT_CHOCOLATE.png",
    "itemType": 9,
    "name": "CROISSANT_CHOCOLATE",
    "price": 3.5
  }
]
Enter fullscreen mode Exit fullscreen mode

Or,

###
GET http://localhost:5001/v1-get-items-by-types HTTP/1.1
content-type: application/json

{
  "types": "1,2,3"
}
Enter fullscreen mode Exit fullscreen mode

Return:

HTTP/1.1 200 OK
content-type: application/json
content-length: 241
date: Mon, 04 Sep 2023 08:29:19 GMT

[
  {
    "image": "img/COFFEE_BLACK.png",
    "itemType": 1,
    "name": "COFFEE_BLACK",
    "price": 3.0
  },
  {
    "image": "img/COFFEE_WITH_ROOM.png",
    "itemType": 2,
    "name": "COFFEE_WITH_ROOM",
    "price": 3.0
  },
  {
    "image": "img/ESPRESSO.png",
    "itemType": 3,
    "name": "ESPRESSO",
    "price": 3.5
  }
]
Enter fullscreen mode Exit fullscreen mode

Ta-da!!! It worked ❤️

This is just the beginning of a series of posts about how can we run WASM/WASI with Dapr on Kubernetes. More fun to come soon.

The source code of this sample can be found at https://github.com/thangchung/dapr-labs/tree/main/polyglot

WebAssembly, Docker container, Dapr, and Kubernetes better together series: part 1, part 2, part 3, part 4.

Top comments (1)

Collapse
 
sebastian_wessel profile image
Sebastian Wessel

Thanks for your article!