There are many kinds of serverless functions available. Cloudflare Workers are one popular form of function designed to run on the edge. But in many cases, you can compile, load, and execute Cloudflare Workers within Spin apps.
In this post, we’ll see one strategy that lets you embed an entire Cloudflare Worker inside of a Spin app, giving you the option to deploy the Worker to Cloudflare or deploy the entire app into any Spin-compatible runtime including SpinKube (that is, Kubernetes), Fermyon Cloud, or Akamai via Wasm Functions.
Getting Started
Before we dive into the procedural bits, its important to understand the primary difference between Cloudflare Workers and Spin apps. Cloudflare provides a JavaScript interpreter to run JS files. Specifically, it uses the V8 engine that powers the Chrome family of browsers. When Cloudflare runs a Worker, it loads the worker JS files into the interpreter and executes them.
In contrast, Spin executes WebAssembly binaries. Each time we run spin build
, we are compiling a binary file that will be executed. With a JavaScript app, what we’re actually doing is embedding the SpiderMonkey (Mozilla) JS runtime, loading the scripts into the interpreter, and then compiling that whole thing to WebAssembly. So there are no .js
files in the package that we run. It’s all just WebAssembly byte codes.
The technique we use in this blog post calls out to the Workers as libraries. And that means that at spin build
time, the worker will be compiled (alongside the rest of the Spin code) into that WebAssembly binary.
With the conceptual details out of the way, it’s time to dive into the code. We are assusme the installation of both Spin and NPM. That should be all you need to get going.
We’ll first create a Spin JavaScript app. Then, inside that app, we’ll create a Cloudflare Worker. We’ll verify that we can run that Worker locally, and then we’ll wire up Spin to route traffic to that worker. When we build the Spin app, we’ll have a single Wasm artifact that contains the Spin router and the Worker all in one.
Creating a Spin App
There is nothing special about creating a Spin app that will call out to a Cloudflare worker. We can follow the standard process:
$ spin new -t http-js --accept-defaults spin-worker
The command above will silently create a new spin-worker/
directory and add all of the necessary Spin files.
At this point, we can cd
into that directory and run spin build
to validate that the project can build.
$ cd spin-worker && spin build
Building component spin-worker with `npm install`
...
Component successfully written.
Finished building all Spin components
By starting a server and using curl
to send a request, we can verify the output of our new app. In one terminal, run spin up
to start the server. In another, access the server with curl
:
$ curl localhost:3000
hello universe
Now we’re ready to create a new Worker inside of our Spin app.
Create a Worker
The official Cloudflare guide describes how to get started with workers. We’ll follow the same process.
Still working inside of the spin-worker
directory, let’s create a new Cloudflare Worker called cf-worker
.
$ npm create cloudflare@latest -- cf-worker
This will kick of a series of prompts. You should answer:
- Category:
Hello World example
- Type:
Worker only
- Language:
JavaScript
- Git:
No
- Deploy your application:
No
At this point, you should have a new subdirectory inside of spin-worker
called cf-worker
.
Just to validate that this is a real worker and can be executed, we can run Wrangler:
$ cd cf-worker && npx wrangler dev
The above will start a server on localhost:8787
(convenient, since it does not clash with Spin’s use of port 3000). Using curl
in another terminal, we can see the result:
$ curl localhost:8787
Hello World!
Let’s do a quick modification of the Worker so that we’ll be able to easily tell later that the worker is indeed being executed. In spin-worker/cf-worker/src/index.js
, let’s edit the code to look like this:
export default {
async fetch(request, env, ctx) {
return new Response('Hello from a worker');
},
};
We’ve updated the response to say Hello from a worker
instead of Hello World!
. Let’s rebuild and test by running that Wrangler command again:
$ npx wrangler dev
And now we get the following output from curl
:
$ curl localhost:8787
Hello from a worker
We’re ready to edit our Spin app now to call this worker.
Calling the Worker from Spin
Let’s work on the Spin JavaScript code in spin-worker/src/index.js
. First off, let’s do the simple thing and change the default Hello universe
message to something clearer:
import { AutoRouter } from 'itty-router';
let router = AutoRouter();
router
.get("/", () => new Response("Hello from Spin"))
addEventListener('fetch', async (event) => {
event.respondWith(router.fetch(event.request));
});
We can run our Spin app with spin build --up
, and then use curl
to see the output:
$ curl localhost:3000
Hello from Spin
Note that we’re using a different URL than we were when testing the Worker. We’re on port 3000
instead of 8787
. When we hit the default route (/
), we get the Hello from Spin
message.
With a couple of modifications to our code, we can create a second route, called /worker
that executes the worker and returns the result. Once more, editing the spin-worker/src/index.js
file, we will add a few lines.
import { AutoRouter } from 'itty-router';
import Worker from '../cf-worker/src/index'
let router = AutoRouter();
router
.get("/", () => new Response("Hello from Spin"))
// When a client requests /worker, run the cf-worker function
.get("/worker", Worker.fetch)
addEventListener('fetch', async (event) => {
event.respondWith(router.fetch(event.request));
});
There are two important bits above.
First, we imported Worker
from ../cf-workers/src/index
. This line imports everything exported out of the worker. In our Worker code, we declared only one function, fetch
. So by importing this way, we can now execute that function using Worker.fetch
.
Then, in the router
, we add a new route, /worker
that, when called, will run Worker.fetch
.
Now we can rebuild our Spin app and test it again using spin build --up
. This time, we can run curl
twice to verify that we can hit the main route and get Hello from Spin
and then hit /worker
and get Hello from a worker
:
$ curl localhost:3000
Hello from Spin
$ curl localhost:3000/worker
Hello from a worker
And that’s the basics for how to call a Worker from within a Spin app.
Deploying a Worker to a Spin runtime
So far, we have run everything locally. It is easy to run this Spin-wrapped Worker on SpinKube (Kubernetes), Fermyon Cloud, Azure AKS, Rancher Desktop, and other places. For this example, we’re going to deploy the Spin app to Akamai using Fermyon Wasm Functions.
To do this, I will use the Spin Akamai plugin.
From inside of the spin-worker
directory, we just use the spin aka deploy
command:
$ spin aka deploy
App 'spin-worker' initialized successfully.
Waiting for application to be ready... ready
View application:
https://f1f1b106-edea-4b45-85c1-38de0c06aa2b.aka.fermyon.tech/
And with that, I can now access my application once more using curl
:
$ curl https://f1f1b106-edea-4b45-85c1-38de0c06aa2b.aka.fermyon.tech/
Hello from Spin
$ curl https://f1f1b106-edea-4b45-85c1-38de0c06aa2b.aka.fermyon.tech/worker
Hello from a worker
For directions on deploying to other environments, see the Spin Deployment Options documentation.
Next Steps
While many Cloudflare Workers can be run as easily as we showed above, there are a number of APIs that do not match between Workers and Spin apps. In a later post, we’ll cover how to write a more sophisticated app that can select resources like key value storage based on the runtime. With some careful code building, you can continue to manage one code base that combines both Spin and Workers.