It is English version of https://zenn.dev/mizchi/articles/wasm-platform (Japanese)
With wasi-http, wasm can now set up a web server standalone without relying on the host language.
Among recent developments, spin and wasmcloud are wasm hosting services. We'll try out these two while comparing them.
A wasm serverless service developed by fermyon.
There are patterns of hosting on spin cloud provided by spin itself, and hosting on k8s with SpinKube.
$ curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
$ cp spin ~/bin
$ spin new
$ cd spin-rust
$ spin build
$ spin up
# open http://localhost:3000/
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;
/// A simple Spin HTTP component.
#[http_component]
fn handle_spin_rust(req: Request) -> anyhow::Result<impl IntoResponse> {
println!("Handling request to {:?}", req.header("spin-full-url"));
Ok(Response::builder()
.status(200)
.header("content-type", "text/plain")
.body("Hello, Fermyon")
.build())
}
Let's try deploying to spin cloud...
$ spin login
prompts for login, so complete by connecting with GitHub and entering the one-time code.
$ spin cloud deploy
displays the deployment status on https://cloud.fermyon.com/.
This time it was deployed to https://spin-rust-tjttf312.fermyon.app/.
Changed one line and spin build && spin cloud deploy
.
It was reflected in 33 seconds. Seems not very fast for wasm. Let's check the build size.
$ ls -al target/wasm32-wasi/release/spin_rust.wasm
.rwxr-xr-x 1.9M kotaro.chikuba 10 Aug 17:32 target/wasm32-wasi/release/spin_rust.wasm
Maybe it's not optimized. Let's look inside with twiggy.
# cargo install twiggy
$ twiggy top target/wasm32-wasi/release/spin_rust.wasm -n 10
Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼──────────────────────────────────────────────────
699497 ┊ 36.02% ┊ custom section '.debug_str'
447542 ┊ 23.05% ┊ custom section '.debug_info'
276454 ┊ 14.24% ┊ custom section '.debug_line'
191232 ┊ 9.85% ┊ custom section '.debug_ranges'
52979 ┊ 2.73% ┊ custom section 'component-type:platform'
46720 ┊ 2.41% ┊ "function names" subsection
36418 ┊ 1.88% ┊ data[0]
14009 ┊ 0.72% ┊ custom section 'component-type:imports'
7913 ┊ 0.41% ┊ custom section 'component-type:wasi-http-trigger'
6538 ┊ 0.34% ┊ dlmalloc
162651 ┊ 8.38% ┊ ... and 668 more.
1941953 ┊ 100.00% ┊ Σ [678 Total Rows]
Debug info is the main part, so we should do a release build.
Add to Cargo.toml:
[profile.release]
lto = true
opt-level = "z"
This brings it down to 798k.
Remove debug information with wasm-snip:
# cargo install wasm-snip
$ wasm-snip target/wasm32-wasi/release/spin_rust.wasm -o target/wasm32-wasi/release/spin_rust.wasm
277kb
Let's look inside with twiggy in this state.
$ twiggy top target/wasm32-wasi/release/spin_rust.wasm -n 10
Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
52979 ┊ 19.15% ┊ custom section 'component-type:platform'
39908 ┊ 14.43% ┊ "function names" subsection
33620 ┊ 12.15% ┊ data[0]
14009 ┊ 5.06% ┊ custom section 'component-type:imports'
7913 ┊ 2.86% ┊ custom section 'component-type:wasi-http-trigger'
6670 ┊ 2.41% ┊ anyhow::fmt::<impl anyhow::error::ErrorImpl>::debug::he4ba69e13e10de28
6207 ┊ 2.24% ┊ dlmalloc
4577 ┊ 1.65% ┊ wasi:http/incoming-handler@0.2.0#handle
3447 ┊ 1.25% ┊ <&T as core::fmt::Display>::fmt::h22c4c1857bc12b26
3359 ┊ 1.21% ┊ <spin_sdk::http::Request as spin_sdk::http::conversions::TryFromIncomingRequest>::try_from_incoming_request::{{closure}}::h3e1fb0a378bb429d
103944 ┊ 37.57% ┊ ... and 553 more.
276633 ┊ 100.00% ┊ Σ [563 Total Rows]
It's related to platform and wasi interface, so this is roughly as small as we can make it casually.
Let's see if deployment speeds up in this state.
$ spin cloud deploy
Uploading spin-rust version 0.1.0 to Fermyon Cloud...
Deploying...
Waiting for application to become ready............ ready
View application: https://spin-rust-tjttf312.fermyon.app/
Manage application: https://cloud.fermyon.com/app/spin-rust
33 seconds. This seems unchanged. Probably the scale speed would change, but I don't want to benchmark on the free tier.
We'll need to consult about the ease of debugging and delete this kind of information.
Setting up k8s is troublesome, so I'll just introduce it, but there's a way to host spin on k8s and deploy spin to it.
https://developer.fermyon.com/spin/v2/kubernetes
https://www.spinkube.dev/docs/topics/packaging/
Looking at the Quickstart, it seems like you deploy spin cloud itself to k8s, and then deploy wasm to it.
$ k3d cluster create wasm-cluster \
--image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.15.1 \
--port "8081:80@loadbalancer" \
--agents 2
# Package and Distribute the hello-spin app
$ spin registry push --build ttl.sh/hello-spin:24h
$ spin kube deploy --from ttl.sh/hello-spin:24h
spinapp.core.spinoperator.dev/hello-spin created
I'm looking for lightness and ease in wasm, so I don't intend to use k8s deliberately, but conversely, it can be used if you want to experimentally try the wasm ecosystem in an environment using k8s. Even if you need to set up a VPC for security requirements, it seems you can handle it with k8s settings. The wider the entrance, the better.
https://www.fermyon.com/pricing
Starter plan is free. 100,000 requests/month, sqlite 1GB.
Growth plan $19.38/month can handle up to 1,000,000 requests. 50GB/month egress. More than that is negotiable. 50GB sqlite is included.
This is also a CNCF series product, which can be deployed to docker, k8s, and edge.
Install a CLI tool called wash. Sounds like laundry.
https://wasmcloud.com/docs/installation
# mac
$ brew install wasmcloud/wasmcloud/wash
$ wash up
🛁 wash up completed successfully, already running
🕸 NATS is running in the background at http://127.0.0.1:4222
📜 Logs for the host are being written to /Users/kotaro.chikuba/.wash/downloads/wasmcloud.log
nats is this:
NATS is a simple, secure and high performance open source data layer for cloud native applications, IoT messaging, and microservices architectures. We feel that it should be the backbone of your communication between services. It doesn't matter what language, protocol, or platform you are using; NATS is the best way to connect your services.
Is it something like a simplified version of k8s? It seems like you set up a platform and deploy to it.
https://wasmcloud.com/docs/ecosystem/wadm/
wasmCloud Application Deployment Manager (wadm) manages declarative application deployments and reconciles the current state of an application with its desired state. In the declarative deployment pattern, developers define the components, configuration, and scaling properties of their application using static configuration files that can be versioned, shared, edited, and used as a source of truth. In wasmCloud, these application manifests conform to the Open Application Model (OAM) and can be written in YAML or JSON. Once a deployment is declared, wadm issues low-level commands to realize that declaration.
It looks like nats-server + wadm is similar to k8s + deployment manifest.
$ wash new component hello --template-name hello-world-rust
$ cd hello
$ wash build
$ wash app deploy wadm.yaml
# Check processes
$ wash app list
Name Latest Version Deployed Version Deploy Status Description
rust-hello-world 01J4Y17MGBGT73PS74H9WYA81Z 01J4Y17MGBGT73PS74H9WYA81Z Deployed HTTP hello world demo in Rust, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)
$ curl localhost:8080
Hello from Rust
It worked.
I wonder what wadm.yaml looks like:
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: rust-hello-world
annotations:
description: 'HTTP hello world demo in Rust, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)'
wasmcloud.dev/authors: wasmCloud team
wasmcloud.dev/source-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/rust/components/http-hello-world/wadm.yaml
wasmcloud.dev/readme-md-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/rust/components/http-hello-world/README.md
wasmcloud.dev/homepage: https://github.com/wasmCloud/wasmCloud/tree/main/examples/rust/components/http-hello-world
wasmcloud.dev/categories: |
http,http-server,rust,hello-world,example
spec:
components:
- name: http-component
type: component
properties:
image: file://./build/http_hello_world_s.wasm
# To use the a precompiled version of this component, use the line below instead:
# image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0
traits:
# Govern the spread/scheduling of the component
- type: spreadscaler
properties:
instances: 1
# Add a capability provider that enables HTTP access
- name: httpserver
type: capability
properties:
image: ghcr.io/wasmcloud/http-server:0.22.0
traits:
# Establish a unidirectional link from this http server provider (the "source")
# to the `http-component` component (the "target") so the component can handle incoming HTTP requests,
#
# The source (this provider) is configured such that the HTTP server listens on 127.0.0.1:8080
- type: link
properties:
target: http-component
namespace: wasi
package: http
interfaces: [incoming-handler]
source_config:
- name: default-http
properties:
address: 127.0.0.1:8080
At a glance, it looks like a k8s manifest. Can it be applied directly to k8s?
https://wasmcloud.com/docs/kubernetes
kubectl apply -f wadm.yaml
application/echo created
It seems to be defined as a k8s custom resource.
https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
Let's look at Rust's lib.rs:
wit_bindgen::generate!({
generate_all
});
use exports::wasi::http::incoming_handler::Guest;
use wasi::http::types::*;
struct HttpServer;
impl Guest for HttpServer {
fn handle(_request: IncomingRequest, response_out: ResponseOutparam) {
let response = OutgoingResponse::new(Fields::new());
response.set_status_code(200).unwrap();
let response_body = response.body().unwrap();
ResponseOutparam::set(response_out, Ok(response));
response_body
.write()
.unwrap()
.blocking_write_and_flush(b"Hello from Rust!\n")
.unwrap();
OutgoingBody::finish(response_body, None).expect("failed to finish response body");
}
}
export!(HttpServer);
The difference is that spin uses derive while wasmcloud uses impl Guest for HttpServer, but there doesn't seem to be any essential difference.
By the way, I saw exactly the same code in moonbit's http-wasi:
response_body
.write()
.unwrap()
.blocking_write_and_flush(b"Hello from Rust!\n")
.unwrap();
It seems that using http-wasi results in the same interface. While spin probably hides it with a dedicated wrapper, wasmCloud exposes the wit.
It's using wit-deps. Let's look at wit/deps.toml:
http = "https://github.com/WebAssembly/wasi-http/archive/v0.2.0.tar.gz"
keyvalue = "https://github.com/WebAssembly/wasi-keyvalue/archive/main.tar.gz"
logging = "https://github.com/WebAssembly/wasi-logging/archive/main.tar.gz"
wit-deps update imports under wit/deps. These codes are expanded as code by the following macro:
wit_bindgen::generate!({
generate_all
});
The build size is 1.7M. Skipping optimization as it would be the same even if optimized.
As a vague impression, now that the foundation is unstable, it seems safer to expose wit rather than wrapping it poorly, and in that respect, wasmcloud seems to be better than spin.
I looked into deployment methods, but while wasmcloud itself seems to originate from https://cosmonic.com/, this itself hasn't been released to general users? Currently, to run wasmCloud, you need to prepare a k8s cluster.
- spin has a strong proprietary platform feel with SDK wrapping. It comes with a hosting service. There's little information about use cases, and reliability is unknown.
- wasmCloud is hosted by CNCF and has a strong standards-compliant feel. To run it, you need to prepare your own k8s cluster.