Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created August 14, 2024 16:26
Show Gist options
  • Save mizchi/47ba840722f593a32a0587df334ee2f2 to your computer and use it in GitHub Desktop.
Save mizchi/47ba840722f593a32a0587df334ee2f2 to your computer and use it in GitHub Desktop.
Trying out spin and wasmCloud as wasm platforms

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.


spin

https://www.fermyon.com/spin

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.

Install

$ 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/

Looking at the generated code

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())
}

Deploy

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.

spin on k8s

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/

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.


wasmCloud

https://wasmcloud.com/

This is also a CNCF series product, which can be deployed to docker, k8s, and edge.

Install

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:

https://nats.io/

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.


Summary

  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment