Serverless Redis with Cloudflare Workers & Pulumi

K - Jun 16 '21 - - Dev Community

Cloudflare Workers are an exciting function as a service offering. They are built on the Service Worker web standard and are globally deployed on Cloudflare's edge network. This edge deployment makes them a low latency solution, so it comes naturally to use them with a low-latency database like Redis.

The problem is, Cloudflare Workers only allow requests to HTTP and HTTPS ports, so connecting to a Redis instance with a regular client isn't possible.

Luckily Upstash, a serverless Redis hosting provider, has now added a REST API to their product. This way, we can profit from the low latency of Redis inside a Cloudflare Worker.

Example Project

I created an example project with Pulumi, infrastructure as code tool like the AWS CDK, but it works with non-AWS cloud providers, like Cloudflare, too!

You can find the complete code on GitHub.

It creates a Worker script and links it up with a Worker route. This way, the script can be accessed from a custom domain.

Prerequisites

Setup

When you clone the project from GitHub, you have to login into your Pulumi account and provide the project with Cloudflare and Upstash credentials.

Cloudflare Credentials

First, we need to gather the Cloudflare credentials.

The API token can be created in the Cloudflare Dashboard.

Click on "Create Token" and then on "Edit Cloudflare Workers." This way, the token only has permissions to mess with Workers and not other Cloudflare resources.

If you click through that wizard, you will be presented with the token at the end, copy it, and use the following Pulumi CLI command inside the project directory:

$ pulumi set config cloudflare:apiToken <YOUR_API_TOKEN> --secret
Enter fullscreen mode Exit fullscreen mode

Next, you need to get your Cloudflare account ID. You can find it in the Cloudflare Dashboard when you click on your domain. Scroll down, and on the right side is the account ID together with the zone ID, which we will need next.

$ pulumi set config cloudflare:accountId <YOUR_ACCOUNT_ID>
Enter fullscreen mode Exit fullscreen mode

Then we need the zone ID from Cloudflare; otherwise, we can't use a custom domain for the Worker; it's right above the account ID we copied in the last step.

Open the index.ts file and replace <CLOUDFLARE_ZONE_ID> with your zone id.

new cloudflare.WorkerRoute("blog-route", {
  zoneId: "<CLOUDFLARE_ZONE_ID>",
  pattern: "example.com/posts",
  scriptName: blogScript.name,
});
Enter fullscreen mode Exit fullscreen mode

You also have to replace the domain in the pattern with your domain; example.com won't work.

Upstash Credentials

Next, we need the Upstash credentials; otherwise, our Cloudflare Workers can't talk to the database.

First, we have to create a new database in the Upstash console.

Click on "Create New Database," give it a name, and choose a region. Then you can click on the freshly created database to get the credentials for the REST API.

There is a REST API button that displays an example request with cURL.

curl <YOUR_DB_URL>/set/foo/bar \
  -H "Authorization: Bearer <YOUR_DB_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

We need two parts here. The API token and the URL of the database.

The URL is everything between curl and /set/foo/bar, and the token is everything after Bearer (without the last quotation mark).

These two strings need to be added to the index.ts; this time, we need to replace the config for the WorkerScript.

const blogScript = new cloudflare.WorkerScript("blog-script", {
  name: "blog-script",
  content: workerCode,
  plainTextBindings: [{ name: "DB_URL", text: "<UPSTASH_DB_URL>" }],
  secretTextBindings: [{ name: "DB_TOKEN", text: "<UPSTASH_DB_TOKEN>" }],
});
Enter fullscreen mode Exit fullscreen mode

The bindings we define make new global variables available inside the worker script. Namely DB_URL and DB_TOKEN.

Deployment

To deploy the whole thing, we have to run pulumi up. Pulumi will take care of provisioning the Cloudflare resources for us.

$ pulumi up
Enter fullscreen mode Exit fullscreen mode

Usage

If everything went correctly, you should have a /posts endpoint on your domain that accepts GET, POST, and DELETE requests.

http://example.com/posts
Enter fullscreen mode Exit fullscreen mode

How to Call Redis with REST?

Upstash has excellent documentation for their new REST API feature if you want to dive deeper, but let's look at some examples.

The Redis REST API is called like this:

GET https://us1-merry-cat-32748.upstash.io/redis-command/arg1/arg2/...?_token=...
Enter fullscreen mode Exit fullscreen mode

To call the API directly in JavaScript could look like this:

const response = await fetch("https://us1-merry-cat-32748.upstash.io/incr/post:id?_token=...");
const { result } = await response.json();
const newPostId = result;
Enter fullscreen mode Exit fullscreen mode

First, we send a request with fetch to the correct database endpoint. It includes the Redis command, the args, and the token.

Then we convert the response to a JavaScript object and extract the result property from it.

In the worker.js of the example project, I wrote a straightforward client for the REST API.

async function callRestApi(command, ...args) {
  args = Array.isArray(args[0]) ? args[0] : args;
  const response = await fetch(
    `${DB_URL}/${command}/${args.join("/")}?_token=${DB_TOKEN}`
  );
  const data = await response.json();
  return data.result;
}
Enter fullscreen mode Exit fullscreen mode

The client uses the two global variables, the DB_URL and the DB_TOKEN, which we defined as bindings for our Worker script and a Redis command plus its arguments.

This utility function simplifies the calls to the database a bit. The example call from above would now look like this:

const newPostId = await callRestApi("incr", "post:id");
Enter fullscreen mode Exit fullscreen mode

The API client can also be used with an array to execute a command with a dynamic amount of arguments.

A GET request to our Cloudflare Worker would execute the following code:

async function listPosts() {
  const postKeys = await callRestApi("smembers", "posts");
  const posts = await callRestApi("mget", postKeys);
  return new Response(
    JSON.stringify(posts.map((text, i) => ({ id: postKeys[i], text })))
  );
}
Enter fullscreen mode Exit fullscreen mode

First, it gets all postKeys from a key called posts. The command is smembers, because the posts key is a Redis Set.

Then it gets all the posts with mget, the multi-get command of Redis, that accepts multiple keys as arguments.

In this example, the posts are just strings, not a complex object, so in the end, we use map to relate every text to its key.

Conclusion

Cloudflare Workers is a low-latency FaaS product, so using a low-latency database is the obvious choice here.

The port restrictions for Cloudflare Workers rule out some database choices, but Upstash gets around with its new REST API. It's as simple as you expect a Redis API to be, so not much new is to learn.

If you want to learn what Upstash brings that Workers KV doesn't, there is also a new article explaining all this.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .