Photo by Phil Hearing on Unsplash
I want to launch my own serverless service running in my local network
Idea
As a huge fan of serverless computing, I thought it would be fun to run your own "serverless-like" environment locally and use it inside the home network.
A while ago, I started playing with the Spin framework. This is an awesome way to create applications in WASM and run them basically anywhere. Spin is an open-source, Cloud Native Foundation sandbox project.
WASM itself is an interesting technology, and it fits great into the serverless environment. The cool thing about it is that it runs in the sandbox and doesn't bring much execution overwhelm. There are a few languages that have some way to compile to WASM.
Part of the fun is that I am going to use my Raspberry Pi 3 as a server running Spin. I will "publish" my functions there. The expectation is that I will end up with my own energy-efficient serverless service running inside my home network.
Architecture
The code is available in the GitHub repository
Raspberry Pi is my server that runs Spin service and exposes the endpoint to call APIs. At this point, it is connected to my local network and can be reached only from it.
I develop an application on my local machine and deploy it by rsync over SSH.
The data for the app is persistent in the sqlite DB that runs on the Raspberry Pi
Goal
I create a book catalog that I can query for books I have. For this experiment, it will be a single endpoint that returns a book based on ID.
Application development
I start from installing Spin on my local machine
I install ready-to-use templates for Rust (there are also versions for Python, TypeScript, TinyGo) spin templates install --git https://github.com/spinframework/spin --update
The last thing would be to add WASM target to Rust toolchain: rustup target add wasm32-wasip1
Once it is done, let's create the app boilerplate. I run spin up and pick the http-rust template
Now I run spin build and spin up
All right! The app is running on the localhost and I can CURL it to see a dummy "Hello World"
Ok, let's write actual code. Spin abstracts away "server-ish" part, and working in it feels a bit like writing Lambda Functions. Components are defined in the spin.toml
#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json
spin_manifest_version = 2
[application]
name = "books-spin"
version = "0.1.0"
authors = ["szymon"]
description = ""
[[trigger.http]]
route = "/book"
component = "books-spin"
[component.books-spin]
source = "target/wasm32-wasip1/release/books_spin.wasm"
allowed_outbound_hosts = []
[component.books-spin.build]
command = "cargo build --target wasm32-wasip1 --release"
watch = ["src/**/*.rs", "Cargo.toml"]
I have an HTTP trigger defined along with the path. There is also a build configuration for the component.
The handler code looks familiar. I start by defining the data model
// lib.rs
#[derive(Serialize, Debug)]
pub struct Book {
pub title: String,
pub author: String,
pub year: u32,
}
Now the handler. Inside, I define a router and add handlers for specific methods.
// lib.rs
#[http_component]
fn handle_books_spin(req: Request) -> anyhow::Result<impl IntoResponse> {
let mut router = spin_sdk::http::Router::new();
router.add("/book", spin_sdk::http::Method::Get, api::get_book_by_id);
println!("Handling request to {:?}", req.header("spin-full-url"));
Ok(router.handle(req))
}
Finally, the function to get the given book. For now it returns some dummy data
// lib.rs
mod api {
use spin_sdk::http::Params;
use super::*;
pub(crate) fn get_book_by_id(
_req: Request,
_params: Params,
) -> anyhow::Result<impl IntoResponse> {
let dummy_book = Book {
title: String::from("Dummy Book"),
author: String::from("Dummy Author"),
year: 2023,
};
Ok(Response::builder()
.status(200)
.header("content-type", "application/json")
.body(serde_json::to_string(&dummy_book)?)
.build())
}
}
Let's test it with spin build && spin up
Add SQLite
One of the cool features of the Spin is that it wraps APIs for various features. One of them is an interface for the embedded SQLite Database.
I just need to add a list of all allowed SQLite databases to the component's definition.
#...
[component.books]
source = "books/target/wasm32-wasip1/release/books.wasm"
allowed_outbound_hosts = []
sqlite_databases = ["default"]
#...
I create a simple migration file
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
year INTEGER NOT NULL
);
INSERT INTO books (id, title, author, year) VALUES (1, 'Wieża Jaskółki', 'Andrzej Sapkowski', 2014)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, author=excluded.author, year=excluded.year;
Now, it is enough to run Spin with the sqlite flag. The db file is created in the .spin directory
spin up --sqlite @migration.sql
For now, I keep connection inside a handler function. I am not sure if in the WASM environment, there is the idea of warm and cold execution. I need to check it, but for my small POC it doesn't really matter.
The updated handler function
// lib.rs
// ...
pub(crate) fn get_book_by_id(
_req: Request,
params: Params,
) -> anyhow::Result<impl IntoResponse> {
println!("{:?}", ¶ms);
let id_param = params.get("id").ok_or(anyhow!("Missing id parameter"))?;
let id = id_param.parse::<i64>()?;
let query_params = [Value::Integer(id)];
let connection = Connection::open_default()?;
let rowset = connection.execute(
"SELECT title, author, year FROM books WHERE id = ?",
&query_params,
)?;
let books: Vec<Book> = rowset
.rows()
.map(|row| Book {
title: row.get::<&str>("title").unwrap().to_string(),
author: row.get::<&str>("author").unwrap().to_string(),
year: row.get::<u32>("year").unwrap(),
})
.collect();
let book = books.first().ok_or(anyhow!("Book not found"))?;
Ok(Response::builder()
.status(200)
.header("content-type", "application/json")
.body(serde_json::to_string(&book)?)
.build())
}
//...
Now I should be able to get a book by ID.
Nice.
Deploy to Raspberry Pi
I am using a Raspberry Pi 3 B+. After SSHing into it, I install the Spin. I don't need any tools for development, as the idea is to load compiled WASM files.
The flow I plan to use is the following: I create a systemd service to run my Spin server. Then I sync spin.toml, SQL migration, and built binaries using rsync. After each sync, I restart the service.
My service is defined in the spin-books.service
[Unit]
Description=Spin Books Service
After=network.target
[Service]
User=szymon
Group=szymon
WorkingDirectory=/home/szymon/projects/spin-books
ExecStart=/usr/local/bin/spin up --listen 0.0.0.0:3003 --sqlite @migration.sql
Restart=always
[Install]
WantedBy=multi-user.target
I sync this file to the /etc/systemd/system/ directory on the Raspberry Pi.
After running spin build on the local machine, I synced the config and created binaries:
rsync -avz --relative spin.toml migration.sql target/wasm32-wasip1/release/books_spin.wasm szymon@szymonpi3.local:~/projects/spin-books
Now I can run the service and make sure that my spin application is up and running on the Raspberry Pi
sudo systemctl daemon-reload
sudo systemctl start spin-books
sudo systemctl status spin-books
Looks good:
I can call the endpoint from my development machine:
Fantastic! I have my own WASM-based application on the Raspberry Pi running in my local network.
One more thing: I make sure it will start after rebooting by running sudo systemctl enable spin-books.service. Now every time I start the Raspberry Pi, my book service starts too.
Summary
When working on the blog post, I wanted to check how hard it would be to build a simple WASM-based backend application. It turned out that Spin makes it really easy. It can be easily run on the Raspberry Pi, which unlocks a fun experience of building your own serverless service.
The code is available in the GitHub repository






Top comments (0)