Experimenting With .NET & WebAssembly - Running .NET Based Spin Application On WASI Node Pool in AKS
I'm quite an enthusiast of WebAssembly beyond the browser. It's already made its way into edge computing with WasmEdge, Cloudflare Workers, or EdgeWorkers. It's also made its way into cloud computing with dedicated clouds like wasmCloud or Fermyon Cloud. So it shouldn't be a surprise that large cloud vendors are starting to experiment with bringing WASM to their platforms as well. In the case of Azure (my cloud of choice), it's running WASM workloads on WASI node pools in Azure Kubernetes Service. This is great because since Steve Sanderson showed an experimental WASI SDK for .NET Core back in March, I was looking for a good context to play with it too.
I took my first look at WASM/WASI node pools for AKS a couple of months ago. Back then the feature was based on Krustlet but I've quickly learned that the team is moving away from this approach and the feature doesn't work with the current version of the AKS control plane (it's a preview, it has that right). I've decided to wait. Time has passed, Deis Labs has evolved its tooling for running WebAssembly in Kubernetes from Krustlet to ContainerD shims, and the WASM/WASI node pools for AKS feature has embraced it. I've decided to take a look at it again.
The current implementation of WASM/WASI node pools provides support for two ContainerD shims: Spin and SpiderLightning. Both, Spin and Slight (alternative name for SpiderLightning) provide structure and interfaces for building distributed event-driven applications built from WebAssembly components. After inspecting both of them, I've decided to go with Spin for two reasons:
- Spin is a framework for building applications in Fermyon Cloud. That meant a potentially stronger ecosystem and community. Also whatever I would learn, would have a broader application (not only WASM/WASI node pools for AKS).
- Spin has (an alpha but still) a .NET SDK.
Your Scientists Were So Preoccupied With Whether They Could, They Didn't Stop to Think if They Should
When Steve Sanderson revealed the experimental WASI SDK for .NET Core, he showed that you can use it to run an ASP.NET Core server in a browser. He also clearly stated you absolutely shouldn't do that. Thinking about compiling .NET to WebAssembly and running it in AKS can make one wonder if it is the same case. After all, we can just run .NET in a container. Well, I believe it makes sense. WebAssembly apps have several advantages over containers:
- WebAssembly apps are smaller than containers. In general, size is an Achilles' heel of .NET, but, even for the sample application, I've used here the WASM version is about ten times smaller than a container based on
dotnet/runtime:7.0
(18.81 MB vs 190 MB). - WebAssembly apps start faster and execute faster than containers. This is something I haven't measured myself yet, but this paper seems to make quite a strong case for it.
- WebAssembly apps are more secure than containers. This one is a killer aspect for me. Containers are not secure by default and significant effort has to be put to secure them. WebAssembly sandbox is secure by default.
This is why I believe exploring this truly makes sense.
But before we go further I want to highlight one thing - almost everything I'm using in this post is currently either an alpha or in preview. It's early and subject to change.
Configuring Azure CLI and Azure Subscription to Support WASI Node Pools
Working with preview features in Azure requires some preparation. The first step is registering the feature in your subscription
az feature register \
--namespace Microsoft.ContainerService \
--name WasmNodePoolPreview
The registration takes some time, you can query the features list to see if it has been completed.
az feature list \
--query "[?contains(name, 'Microsoft.ContainerService/WasmNodePoolPreview')].{Name:name,State:properties.state}" \
-o table
Once it's completed, the resource provider for AKS must be refreshed to pick it up.
az provider register \
--namespace Microsoft.ContainerService
The subscription part is now ready, but to be able to use the feature you also have to add the preview extension to Azure CLI (WASM/WASI node pools can't be created from the Azure Portal).
az extension add \
--name aks-preview \
--upgrade
This is everything we need to start having fun with WASM/WASI node pools.
Creating an AKS Cluster
A WASM/WASI node pool can't be used for a system node pool. This means that before we create one, we have to create a cluster with a system node pool. Something like on the diagram below should be enough.
If you are familiar with spinning up an AKS cluster you can jump directly to the next section.
If you are looking for something to copy and paste, the below commands will create a resource group, container registry, and cluster with a single node in the system node pool.
az group create \
-l ${LOCATION} \
-g ${RESOURCE_GROUP}
az acr create \
-n ${CONTAINER_REGISTRY} \
-g ${RESOURCE_GROUP} \
--sku Basic
az aks create \
-n ${AKS_CLUSTER} \
-g ${RESOURCE_GROUP} \
-c 1 \
--generate-ssh-keys \
--attach-acr ${CONTAINER_REGISTRY}
Adding a WASM/WASI Node Pool to the AKS Cluster
A WASM/WASI node pool can be added to the cluster as any other node pool, with az aks nodepool add
command. The part which makes it special is the workload-runtime
parameter which takes a value of WasmWasi
.
az aks nodepool add \
-n ${WASI_NODE_POLL} \
-g ${RESOURCE_GROUP} \
-c 1 \
--cluster-name ${AKS_CLUSTER} \
--workload-runtime WasmWasi
The updated diagram representing the deployment looks like this.
You can inspect the WASM/WASI node pool by running kubectl get nodes
and kubectl describe node
commands.
With the infrastructure in place, it's time to build a Spin application.
Building a Spin Application With .NET 7
A Spin application has a pretty straightforward structure:
- A Spin application manifest (
spin.toml
file). - One or more WebAssembly components.
The WebAssembly components are nothing else than event handlers, while the application manifest defines where there are located and maps them to triggers. The application mentions two triggers: HTTP and Redis. In the case of HTTP, you map components directly to routes.
So, first we need a component that will serve as a handler. In the introduction, I've written that one of the reasons why I have chosen Spin was the availability of .NET SDK. Sadly, when I tried to build an application using it, the application failed to start. The reason for that was that Spin SDK has too many features. Among other things it allows for making outbound HTTP requests which require wasi-outbound-http::request
module, which is not present in WASM/WASI node pool (which makes sense as it's experimental and predicted to die once WASI networking APIs are stable).
Luckily, a Spin application supports fallback to WAGI. WAGI stands for WebAssembly Gateway Interface and is an implementation of CGI (now that's a blast from the past). It enables writing the WASM component as a "command line" application that handles HTTP requests by reading its properties from environment variables and writing responses to the standard output. This means we should start by creating a new .NET console application.
dotnet new console -o Demo.Wasm.Spin
Next we need to add a reference to Wasi
.Sdk` package.
dotnet add package Wasi.Sdk --prerelease
It's time for the code. The bare required minimum for WAGI is outputting a Content-Type
header and an empty line that separates headers from body. If you want to include a body, it goes after that empty line.
using System.Runtime.InteropServices;
Console.WriteLine("Content-Type: text/plain");
Console.WriteLine();
Console.WriteLine("-- Demo.Wasm.Spin --");
With the component ready, it's time for the application manifest. The one below defines an application using the HTTP trigger and mapping the component to a top-level wildcard route (so it will catch all requests). The executor
is how the fallback to WAGI is specified.
spin_version = "1"
authors = ["Tomasz Peczek <[email protected]>"]
description = "Basic Spin application with .NET 7"
name = "spin-with-dotnet-7"
trigger = { type = "http", base = "/" }
version = "1.0.0"
[[component]]
id = "demo-wasm-spin"
source = "Demo.Wasm.Spin/bin/Release/net7.0/Demo.Wasm.Spin.wasm"
[component.trigger]
route = "/..."
executor = { type = "wagi" }
The last missing part is a Dockerfile
which will allow us to build an image for deployment.
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY . .
RUN dotnet build -c Release
FROM scratch
COPY --from=build /src/bin/Release/net7.0/Demo.Wasm.Spin.wasm ./bin/Release/net7.0/Demo.Wasm.Spin.wasm
COPY --from=build /src/spin.toml .
To run the image on WASM/WASI node pool it needs to be built and pushed to the container registry.
az acr login -n ${CONTAINER_REGISTRY}
docker build . -t ${CONTAINER_REGISTRY}.azurecr.io/spin-with-dotnet-7:latest
docker push ${CONTAINER_REGISTRY}.azurecr.io/spin-with-dotnet-7:latest
Running a Spin Application in WASM/WASI Node Pool
To run the Spin application we need to create proper resources in our AKS cluster. First is RuntimeClass
which serves as a selection mechanism, so the Pods run on the WASM/WASI node pool. There are two node selectors related to WASM/WASI node pool kubernetes.azure.com/wasmtime-spin-v1
and kubernetes.azure.com/wasmtime-slight-v1
, with spin
and slight
being their respective handlers. In our case, we only care about creating a RuntimeClass
for kubernetes.azure.com/wasmtime-spin-v1
.
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: "wasmtime-spin-v1"
handler: "spin"
scheduling:
nodeSelector:
"kubernetes.azure.com/wasmtime-spin-v1": "true"
With the RuntimeClass
in place, we can define a Deployment
.
apiVersion: apps/v1
kind: Deployment
metadata:
name: spin-with-dotnet-7
spec:
replicas: 1
selector:
matchLabels:
app: spin-with-dotnet-7
template:
metadata:
labels:
app: spin-with-dotnet-7
spec:
runtimeClassName: wasmtime-spin-v1
containers:
- name: spin-with-dotnet-7
image: crdotnetwasi.azurecr.io/spin-with-dotnet-7:latest
command: ["/"]
Last part is exposing our Spin application to the world. As this is just a demo I've decided to expose it directly as a Service
of type LoadBalancer
.
apiVersion: v1
kind: Service
metadata:
name: spin-with-dotnet-7
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
app: spin-with-dotnet-7
type: LoadBalancer
Now we can run kubectl apply
and after a moment kubectl get svc
to retrieve the IP address of the Service
. You can paste that address into a browser and voilà.
That Was Fun!
Yes, that was really fun. All the stuff used here is still early bits, but it already shows possibilities. I intend to observe this space closely and possibly revisit it whenever some updates happen.
If you want to play with a ready-to-use demo, it's available on GitHub with a workflow ready to deploy it to Azure.