November 30, 2023

Persisting Data in WebAssembly Applications Using Redis

Tim McCallum Tim McCallum

spin redis wasm webassembly cloud rust microservices

Persisting Data in WebAssembly Applications Using Redis

Discover the seamless integration of Redis with WebAssembly (Wasm) applications in Spin 2.0, where real-time data meets limitless scalability. This article offers a practical walkthrough, complete with a video guide, on harnessing Redis for robust data persistence in your Wasm projects on Fermyon Cloud. Step into the future of cloud applications with a hands-on exploration of Spin’s new Redis syntax and a concrete Fermyon Cloud deployment example. Oh, and did I mention there is a surprise at the end? Read on to find out.

As you may already know, we recently released a new major version of Spin – Spin 2.0. As one could expect, along with new features and functionality, there are subtle nuances in how application code and configuration are written (when comparing Spin 1 and Spin 2 applications). This article covers one of those nuances. Specifically, persisting data in Wasm applications using Redis.

Redis

As mentioned in our technical documentation on this subject, Redis is an open-source data store used by millions of developers as a database, cache, streaming engine, and message broker. I find Redis to be an extremely appealing option for persisting data in Wasm applications. Their tag lines like “Real-time data. Any scale. Anywhere.” and “Pay as you grow” describe the flexibility that Redis offers by design. Today we are going to write an application that essentially has two parts - business logic and storage. The business logic of the application will run solely on Fermyon Cloud. The application’s state will persist solely on Redis Cloud.

We will be using Redis Cloud for this example. Simply sign-up/log-in and create a new database to follow along.

Accompanying Video

The following video walks through the same steps as this article. Please feel free to follow along via YouTube as well if you like.

Installing Spin

If you haven’t already, please go ahead and install the latest version of Spin.

Upgrading Spin: If you have an older version of Spin, please see the Spin upgrade page of the developer documentation.

Using Spin Application Templates

We will start by scaffolding our application through the use of Spin templates. Please run the following command (to ensure that your system is synced with the latest templates and SDKs) in readiness for the creation of our application:

$ spin templates install --git https://github.com/fermyon/spin --update

Creating Our Application

Next, we use the spin new command and specify the template (using the -t option) from which to create our new application:

$ spin new -t http-rust
Enter a name for your new application: redisRustApplication
Description: A new Redis Spin Application.
HTTP path: /...

Configuring Our Application

The configuration is straightforward. We simply open the application’s manifest (the spin.toml file) and add an environment configuration value, in the [component.redis-rust-application] table (at the component level). For example:

[component.redis-rust-application]
//--snip--
environment = { REDIS_ADDRESS = "redis://username:password@redis-1234.redislabs.com:15730" }

We then add our Redis Cloud database endpoint (including the redis:// protocol) as one of the allowed_outbound_hosts for our application. This is also done inside the [component.redis-rust-application] table (at the component level):

[component.redis-rust-application]
//--snip--
allowed_outbound_hosts = ["redis://redis-1234.redislabs.com:15730"]

Writing the Application Source Code

The Rust code generated by the http-rust template will only return hello world, by default. In order to interact with Redis Cloud, let’s open our src/lib.rs file and paste in the following code:

use anyhow::{anyhow};
use spin_sdk::http::{IntoResponse, Request};
use spin_sdk::http_component;
use spin_sdk::redis;

const REDIS_ADDRESS_ENV: &str = "REDIS_ADDRESS";

#[http_component]
fn publish(_req: Request) -> anyhow::Result<impl IntoResponse> {

    let address = std::env::var(REDIS_ADDRESS_ENV)?;
    let conn = redis::Connection::open(&address)?;

    // Set the Redis key "spin-example" to value "Eureka Cloud!"
    conn.set("spin-example", &b"Eureka Cloud!"[..].to_vec())
        .map_err(|_| anyhow!("Error executing Redis set command"))?;

    // Get the value from the Redis key "spin-example"
    let payload =
        conn.get("spin-example").map_err(|_| anyhow!("Error querying Redis"))?;

    // Return the permanently stored value to the user's browser body
    Ok(http::Response::builder()
        .status(200)
        .header("foo", "bar")
        .body(payload)?)
}

Building the Application

To build and run our application on localhost we run the following command:

$ spin build --up

Building component redis-rust-application with `cargo build --target wasm32-wasi --release`
//--snip--
Finished building all Spin components
Logging component stdio to ".spin/logs/"

Serving http://127.0.0.1:3000
Available Routes:
  redis-rust-application: http://127.0.0.1:3000 (wildcard)

You can check the operation of the application on localhost by visiting http://127.0.0.1:3000 in your web browser.

Deploying the Application

The next command will deploy the application to Fermyon Cloud:

$ spin deploy

Uploading redisrustapplication version 0.1.0 to Fermyon Cloud...
Deploying...
Waiting for application to become ready....
Available Routes:
  redis-rust-application: https://redisrustapplication-1234.fermyon.app (wildcard)

You will note that the URL of our application is partly randomized. You can create a custom fermyon subdomain or bring your own domain name over to Fermyon Cloud. These, and other, Fermyon Cloud settings can be configured via your web browser by visiting https://cloud.fermyon.com/.

Further Business Logic Using Rust

As we can see from this Redis implementation document, there are many more Redis methods (other than set and get) that our application can use. If you are persisting data in a Rust/Wasm application using Redis and you need more information please head over to Discord say hi to the team, and chat about your requirements.

And Now for Something Completely Different!

I mentioned in the introduction that there would be a surprise at the end. This next section was not included in the accompanying video but is super fun. We are about to write and compile a Python application that persists data in Redis (in the fashion of a RESTFul API). Let’s go!

Python to Wasm

Python is a wildly popular language. It feels like every other person who visited our booth at KubeCon in Chicago, earlier this month, was interested in Python to Wasm. If we are being traditionally correct, we can correctly say that an interpreted language like Python does not compile, let alone to a Wasm target. But what if I told you that you and I could go ahead and write Python source code right now and then deploy that logic as Wasm?

Our py2wasm plugin takes a Python module and all its dependencies and produces a Spin-compatible WebAssembly module as output. What are we waiting for? Let’s try this out!

To compile Python programs to Spin components, we need to install a Spin plugin called py2wasm. The following commands will ensure that we the latest version of the plugin installed:

# Fetch all of the latest Spin plugins from the spin-plugins repository
$ spin plugin update
# Install py2wasm plugin
$ spin plugin install py2wasm

A RESTful Cheese Shop API: Using Python

The following command will create oour new Python application:

spin new -t http-py
Enter a name for your new application: cheese-shop
Description: The Cheese Shop
HTTP path: /cheeses/...

Once we have our application scaffolded out, thanks to the http-py template, we once again write a couple of lines of config (essentially the same as what we did above for our Rust application):

[component.cheese-shop]
source = "app.wasm"
environment = { redis_address = "redis://username:password@redis-1234.redislabs.com:15730" }
allowed_outbound_hosts = ["redis://redis-1234.redislabs.com:15730"]

Next, we replace the contents of our app.py file with the following source code:

import os
import json
from spin_http import Response
from spin_redis import redis_set, redis_get, redis_del

def handle_request(request):
    # Obtain the Redis address
    redis_address = os.environ['redis_address']
    # Process incoming POST request
    if request.method == 'POST':
        # Read the body of the request
        json_str = request.body.decode('utf-8')
        json_object = json.loads(json_str)
        # Store the Cheese in Redis
        redis_set(redis_address, json_object['name'], request.body)
        # Create return confirmation that the Cheese was stored
        return_value = bytes(f"Stored {json_object['name']}", "utf-8")
    # Process incoming GET request
    if request.method == 'GET':
        # Create return the Cheese object as specified in the URI
        return_value = redis_get(redis_address, request.uri.lstrip('/cheeses/'))
    # Process incoming DELETE request
    if request.method == 'DELETE':
        # Delete the Cheese object as specified in the URI
        redis_del(redis_address, [request.uri.lstrip('/cheeses/')])
        # Create return confirmation that the Cheese was deleted
        return_value = bytes(f"Deleted {request.uri.lstrip('/cheeses/')}", "utf-8")
    # Send the response with the return value (as plain text)
    return Response(200,
                {"content-type": "text/plain"},
                return_value)

We can then use Spin to compile this Python source code to Wasm:

$ spin build

And finally, we can deploy the application to Fermyon Cloud using the following command:

$ spin deploy 
Uploading cheese-shop version 0.1.0 to Fermyon Cloud...
Deploying...
Waiting for application to become ready.....
Available Routes:
  cheese-shop: https://cheese-shop-1234.fermyon.app/cheeses

Client Cheese Requests

POST

To add a new cheese to the shop via the RESTFul API we perform the following request (using the POST verb):

$ curl -X POST "https://cheese-shop-1234.fermyon.app/cheeses" -H "Content-Type: application/json" -d '{"name":"red-leicester", "texture":"crumbly", "quantity": 1}'                       

The above request will return the response red-leicester as a confirmation that the cheese was stored.

GET

Retrieving the red-leicester cheese information is done via the following request (using the GET verb):

curl -X GET "https://cheese-shop-1234.fermyon.app/cheeses/red-leicester" -H "accept: application/json"

The above request will return the Red Leicester JSON object:

{"name":"red-leicester", "texture":"crumbly", "quantity": 1}

DELETE

Deleting the red-leicester is done via a request that uses the DELETE verb:

curl -X DELETE "https://cheese-shop-1234.fermyon.app/cheeses/red-leicester"

If you are interested, there are a few more Redis methods available in this Python SDK. Also, from a design perspective, we can do much more with just this example above. Such as creating functionality to return a list of all cheeses and managing client authorization as part of the requests, i.e. -H "Authorization: Bearer YOUR_ACCESS_TOKEN". Speaking of authentication, did you know that thanks to Spin 2.x and the component model, we can now pass the incoming HTTP request to a separate authentication middleware component? The separate middleware authentication component can authorize the user to make the request (via a GitHub OAuth application). If successful, the authorization component will forward the HTTP request to our separate business logic component. Each separate component runs in its own sandboxed linear memory. See our Composing Components with Spin 2.0 blog post to learn more about this approach.

That was probably a bit to take in, but hopefully, this gives you enough information to inspire you to build new applications. I have left a list of resources below. Thanks for reading. KubeCon was a hoot, and I look forward to seeing you all again soon!

Resources


🔥 Recommended Posts


Quickstart Your Serveless Apps with Spin

Get Started