November 02, 2023

Introducing Spin 2.0

Radu Matei Radu Matei

spin webassembly component model release wasmtime

Introducing Spin 2.0

/static/image/spin-v2-header.png

The Fermyon team is proud to introduce Spin 2.0 — a new major release of Spin, the open source developer tool for building, distributing, and running WebAssembly (Wasm) applications in the cloud.

Wasm is a technology that is making its way into more and more parts of modern computing — from browser applications, to plugin systems, IoT scenarios and more, and Spin makes it possible to build your serverless-style APIs, websites, full-stack or AI-capable applications as WebAssembly components, generating applications that are:

  • orders of magnitude smaller than container images
  • entirely portable across operating systems and CPU architectures
  • with incredibly low startup latency and capable of running tens of thousands of requests per second
  • capable of running anywhere, from tiny devices such as Raspberry Pis, in Docker Desktop, Kubernetes, Nomad, or Fermyon Cloud, and in even more places soon!

Since we first introduced Spin, our goal has been to create a foundation for a new kind of computing platform that could take advantage of WebAssembly’s runtime characteristics, and for Spin 2.0, we worked with the community on a few key scenarios:

  • enabling WebAssembly component composition
  • improving performance
  • laying the foundation for Wasm portability across runtimes and implementations
  • improving the developer experience, capability support, and SDK ergonomics

Let’s dive into what Spin 2.0 looks like!

Hello, Spin!

/static/image/spin-v2-intro.png

Spin is a developer tool and framework that guides users through creating, compiling, distributing, and running server-side applications with WebAssembly. You can take advantage of the spin new, spin build, and spin up set of commands to start your journey:

# Create a new application with one JavaScript component and enter it's directory
$ spin new hello-spin2 --template http-js
$ cd hello-spin2
# Add a new component written in Rust to our application
$ spin add hellorust --template http-rust

This creates all the configuration and source code required to run our application. Spin 2.0 comes with an updated spin.toml manifest that focuses on simplicity and on the capabilities a component is allowed to access. Let’s have a look at the manifest for a simple component that is allowed to access a Redis database:

# The HTTP route /hello-spin2 should be handled by the `hello-spin2` component
[[trigger.http]]
route = "/hello-spin2"
component = "hello-spin2"

[component.hello-spin2]
# The Wasm component to be instantiated and executed when receiving a request
source = "target/hello-spin2.wasm"

# Any capability this component can access (files, variables,
# outbound connections, KV stores, databases) must be declared
# in the component manifest.
# For example, this component can only make outbound connections
# to the following hosts:
allowed_outbound_hosts = ["redis://my-redis-server"]

[component.hello-spin2.build]
# The command to execute when building this component
command = "npm run build"

At this point, you can write a handler function using any of the supported languages, such as Rust, JavaScript, TypeScript, or Go (or any language that compiles to WASI).

Now let’s look at a simple component written in JavaScript — a single handler function that takes a request, then returns a response:

// Hello world in JavaScript.
export async function handler(req, res) {
    res.status(200).body("Hello, Spin 2.0 from JavaScript!")
}

When writing your applications, you can interact with built-in persistence, configuration, or data services from your Wasm components, or communicate with external systems. Here is a sample of what you can do today from a component running in Spin:

Next, spin build will execute the build commands for all components in the application, and spin up will start the application locally. If you’re iterating locally on your application, you might use spin watch to automatically rebuild and restart your app whenever you change your source code.

Finally, you can use spin cloud deploy to instantly deploy your application to Fermyon Cloud, push the application to a registry service such as Docker Hub or GitHub Container Registry using spin registry push, or deploy the application to Kubernetes using spin k8s deploy.

Let’s see how we can deploy our newly created Spin 2.0 application to Fermyon Cloud:

# Deploying our Spin 2.0 application to Fermyon Cloud
$ spin cloud deploy
Uploading hello-spin2 version 0.1.0 to Fermyon Cloud...
Deploying...
Waiting for application to become ready........ ready
Available Routes:
  hello-spin2: https://hello-spin2-0a8dkb8e.fermyon.app/hello-spin2
  hellorust: https://hello-spin2-0a8dkb8e.fermyon.app/hellorust

Bringing the Component Model and WASI Preview 2 to production

/static/image/spin-twcm-wasi.png

All the above was possible in Spin 1.x already. In fact, while our SDKs so far produced regular .wasm files, since Spin 1.5 they were turned into WebAssembly Component binaries before execution. So Spin and Fermyon Cloud have executed nothing but components for months now — but this was an implementation detail under the hood. With today’s release of Spin 2.0, we’re taking things a huge step further: we’re finally bringing some of the benefits of the WebAssembly Component Model and the upcoming WASI Preview 2 milestone to a production environment.

The Component Model introduces two key additions to WebAssembly: a simple way to bring efficient high-level interfaces to content running in WebAssembly, regardless of which language was used to create that content; and the ability to compose components using these interfaces, building powerful applications out of smaller pieces that are isolated from each other, but can efficiently communicate across language boundaries.

Through the rest of this post, we’ll talk about how we’re making use of these properties in Spin 2.0, and how we’re going about making a precursor of the upcoming WASI Preview 2 available for production use right now, while supporting the standardization process towards an ideal final version of the specification.

Enabling Polyglot Component Composition

More concretely, with the Component Model a component written in JavaScript can import a high-performance component written in Rust, or another written in Python, and they can communicate and exchange data in a portable way, without needing to be aware of the language, or any other implementation details, of each other.

Spin 2.0 can produce and run WebAssembly components natively, and today you can use tooling in Rust, JavaScript, TypeScript, and Python to build components that can run inside Spin applications.

Let’s explore a real example — one of the most used components in Spin applications is a static file server. It is a component that serves files from disk and sends them as HTTP responses, and it is used for every website powered by Spin (including the website you are reading this article on). We initially wrote it in Rust, which means until now, if we wanted to use its functionality from any other language, we had to either reimplement its functionality in a new language, or call it over the network.

With Spin 2.0, we can import the file server functionality and make use of it in a component written in another language. To achieve this for our example, we will use the following component tooling: cargo component, componentize-py, and wasm-tools compose, which generates a new component by linking the two according to their interface contract. In WIT, the Component Model’s language for describing interfaces for components to implement or consume, such a contract is called a “world”.

/static/image/spin-twcm-wasi.png

In other words — we implement our business logic in a high-performance, memory-safe language like Rust, targeting a known interface (in this case, the WASI HTTP proxy world), and we compile it using cargo component, which generates a standard WebAssembly component (spin_static_fs.wasm):

// A simplified Rust component that implements our business logic
// https://github.com/fermyon/spin-fileserver/blob/main/src/lib.rs
#[http_component]
async fn handle(req: IncomingRequest, res_out: ResponseOutparam) {
	println!("Hello from Rust!");
	// See the source file linked above for the actual implementation.
}

We can now use this interface in a new component, that we’ll write in Python and build with componentize-py. This component imports the same interfaces our Rust component implements, but instead of doing so itself, it handles our incoming HTTP request by calling into the Rust component we just built:

# A simplified Python component that imports the business logic
# https://github.com/fermyon/spin-fileserver/blob/main/examples/python/app.py
from proxy.imports import incoming_handler as file_server
from proxy.imports.types import IncomingRequest, ResponseOutparam
...
async def handler(req: IncomingRequest, res: ResponseOutparam):
  print("Hello from Python!")
	# Omitted: logic to further process the request, check authentication, etc.
	file_server.handle(req, res)

We now use componentize-py to create a new component, then use wasm-tools compose to link the two components, resulting in a new component that can be run in Spin 2.0 and other runtimes supporting the Component Model and the same precursor to WASI Preview 2:

# Build a component that imports the proxy world (http.wasm)
# This component will have to be linked with another component that
# implements the proxy world before it can be used.
$ componentize-py -d ../wit -w proxy componentize app -o http.wasm

# `spin_static_fs.wasm`, and generate a new component, `composed.wasm`
$ wasm-tools compose -d spin_static_fs.wasm http.wasm -o composed.wasm

/static/image/spin-twcm-wasi.png

And here’s this newly generated component running in Spin 2.0:

$ cat spin.toml
...
[component.fileserver]
source = "composed.wasm"
files = [{ source ="my-files/*", destination = "/" }]
$ spin up
Available Routes:
  fileserver: http://127.0.0.1:3000/static (wildcard)

Hello from Python!
Hello from Rust!
... stream the requested file over HTTP

This example walked through how to manually create components in different languages, link them, then execute the generated component with Spin.

As tooling for component composition matures, we are working within the language ecosystems to add more integrated support for components and to streamline the creation and composition of components.

You can find the example importing the file server component in JavaScript or Python, and an HTTP OAuth middleware component on GitHub. You can import them in your own Spin applications, or use them as a starting point for building new WebAssembly components.

Performance, Streaming, and WASI HTTP

The composition of components allows very fine-grained isolation of different parts of an application from each other, enabling developers to reason about which parts of their application have access to the most sensitive data, or are most critical to the application’s overall security, correctness, and stability.

But Spin adds another dimension to this fine-grained isolation: for every new request, it will create a fresh new instance of the Wasm component(s) handling the request, process it, then terminate the instance. That means that even in case an attacker can exploit a flaw in the application to corrupt its state, that corruption will only last for the current request, instead of affecting all future requests processed on the same machine.

Spin can do this because of the incredibly fast startup time for WebAssembly components, and the incredible work happening in the Wasmtime project.

For scenarios with many concurrent, short-lived instances (which is perfect for serverless-style workloads), Spin 2.0 has significantly improved performance compared to Spin 1.0, in large part due to using Wasmtime’s pooling memory allocator, which when combined with other performance work, can improve the throughput of Spin by up to an order of magnitude in real-world scenarios.

To showcase how fast Spin can create new isolated Wasm instances for every request, we can create a new “hello world” application using Spin 2.0 and run a simple load test locally:

# Creating a new Spin application
$ spin new perftest
$ spin build && spin up &
# Creating a load test for 10 seconds with 5 concurrent connections
$ bombardier localhost:3000 -c 5

Statistics        Avg      Stdev        Max
  Reqs/sec     28301.16    2875.66   32328.99
  Latency      175.56us    20.01us     4.57ms
  HTTP codes:
    1xx - 0, 2xx - 282999, 3xx - 0, 4xx - 0, 5xx - 0

On a macOS machine, in 10 seconds, Spin created and executed almost three hundred thousand WebAssembly instances, with about 28,000 requests per second and average latency that is below 200 microseconds.

Real-world workloads will most often be bottlenecked by external calls such as database access and outbound networking, but the time between sending the request until your application starts executing is crucial in a lot of scenarios, and Spin 2.0 ensures improved startup performance for such applications. And that goes for every request, not just those being processed after an initial startup and warmup phase.

Startup performance is just one aspect of performance — another crucial aspect is sending a response back as soon as it starts becoming available, even if the server hasn’t finished processing yet. This is known as “streaming” responses, and Spin 2.0 now has experimental support for streaming HTTP responses, built on top of WASI preview 2 and WASI HTTP.

We’re already making use of this in the file server example described above, but let’s see a focused example in action — we want to read parts of a file, then send the chunks back as soon as they are done processing (as opposed to waiting and sending the entire file back):

// https://github.com/fermyon/spin/blob/main/examples/wasi-http-streaming-file/src/lib.rs
async fn stream_file(_req: IncomingRequest, res: ResponseOutparam) -> Result<()> {
    // Create a response body
    let mut body = response.take_body();
    // Open a file for processing and start reading 1 MB chunks.
    let mut file = File::open("my-large-file.bin")?;
    const CHUNK_SIZE: usize = 1024 * 1024; // 1 MB

    // For every chunk read from the file, process it, then
    // immediately stream the processed part to the client.
    let mut buffer = vec![0; CHUNK_SIZE];
    loop {
        let bytes_read = file.read(&mut buffer[..])?;
        if bytes_read == 0 {
            break;
        }
        // Potentially further process the bytes read
        // and send the chunks back as they are available.
        let data = &buffer[..bytes_read]);
        body.send(data.to_vec()).await?;
        println!("sent {} bytes", data.len());
    }
    Ok(())
}

This is essentially the core of how the file server component is able to stream large files, and it is a pattern that can be used whenever performance and interactivity are crucial.

Bringing Production Use to the Standardization Process

To reiterate, all the above is made possible by the WebAssembly Component Model and the upcoming next version of WASI, Preview 2. Much of this work is driven upstream in the Bytecode Alliance, where we’ve long provided major contributions and helped realize the vision of the WebAssembly Component Model and WASI.

Neither the Component Model nor WASI Preview 2 are “done”, and while the former is by now very stable, the latter is still under heavy development. We’re actively involved in this development, but with today’s release, we’re doing something different: we’re bringing real-world production use to the standardization and implementation process.

A good standard that serves real-world use cases well can’t be created in isolation and without input from developers using it in the real world. Besides bringing exciting new features to Spin and Fermyon Cloud, that is a key motivation for what we’re releasing today: we want developers to have a place where they can take the Component Model and the current iteration of WASI Preview 2 for a spin, with real production use, not just experiments and test cases.

Adapting to a Changing World

This poses the question of how we’re doing this. If the specification is in flux and APIs are changing, how can we give developers a stable basis to build on top of?

Luckily, this is made fairly easy by the fact that Component Model interfaces are versioned. Spin 2.0 exposes the 2023-10-18 snapshot of WASI Preview 2, which is shipping with Wasmtime 14. Over the next few months, we’ll continue releasing new snapshots of WASI Preview 2, while keeping support for the previous ones. This enables existing content to continue working, while allowing new content to make use of the latest WASI Preview 2 improvements.

But that raises another question: how do we indefinitely support all these different snapshots?

The answer lies in the composability of WebAssembly Components described above. Whenever we introduce support for a new snapshot, we can decide which of the existing snapshots we want to continue supporting natively. When we decide to stop native support for a snapshot, we can move its implementation into a component itself, which functions as an adapter between different snapshots. Such a component will then import one snapshot of WASI Preview 2 and export another one, implemented in terms of the imported one.

Eventually the final version of WASI Preview 2 will be released. After that point, we’ll likely move support for all snapshot versions into adapters, and only have that single, stable version implemented natively. This way, we get the best of both worlds: we can give developers using Spin, and customers deploying their applications to Fermyon Cloud the stability they need for production use, while ensuring that we’re able to keep the Spin codebase maintainable and lean.

Towards an Interoperable Ecosystem

The Component Model and WASI are of course not just aiming to make it easy to support different languages, and to allow components built using different languages to interoperate. Another key goal is interoperability between hosts. Once the final version of WASI Preview 2 is released, we’ll see that happening for a wide range of platforms, the way it’s currently the case for WASI Preview 1.

For now, interoperability is more limited, because not everyone will have the ability to rapidly deploy support for WASI Preview 2 snapshot versions. But it’s not entirely absent either: Spin 2.0 uses the implementation of WASI APIs provided by Wasmtime 14, so content that only uses WASI APIs (as opposed to Spin’s non-WASI APIs) will work in Wasmtime 14 as well.

Additionally, we’ve worked with our friends at NGINX to implement experimental support for the Component Model and WASI HTTP in NGINX Unit. You can play around with this, and see the same content running in Spin, NGINX Unit, and Wasmtime by downloading the Docker image of their current demo of a pre-release version of the next version of NGINX Unit.

A Call to Action

I want to end this post with a call to action: please take Spin and the Component Model and WASI Preview 2 for a spin! And please let us know how it goes, and what does and doesn’t work for your projects!

And if you’re at KubeCon in Chicago next week, please come by our booth to chat about all this in person.


🔥 Recommended Posts


Quickstart Your Serveless Apps with Spin

Get Started