Cloudflare Workers

December 12, 2024

Cover Image
CodaBool

CodaBool

Cloudflare

I was interested in Cloudflare since there are a lot of things about AWS I felt could be improved upon. Namely the developer experience. Without a DevOps team to support an application team, development will slow to a crawl.

I really like the Lambda service but it has rough patches surrounding its developer experience. I went into Cloudflare to hopefully find a better experience with their competing service. What I found was that using Cloudflare Workers (the Lambda alternative) was incredible. I ended up migrating all but one of my Lambdas over to Workers.

The main improvent I am seeing is that connecting services is exceptionally easy.

Let's look at some of the code

🔧 Glue

most programming is writing glue code. Connect one thing to another... and another... and another...

All platforms I've work with use an SDK which wraps the API in a familiar coding lanaguage. Cloudflare on the other hand is doing something, well, magical. You write a config file in .toml which is all the glue code you need!

# wrangler.toml
# DOCS = https://developers.cloudflare.com/workers/wrangler/configuration
name = "d3erver"
main = "main.js"
compatibility_date = "2024-09-23"

[[kv_namespaces]]
binding = "KV"
id = "81ef062090584ece9c048283e16749f3"

[[r2_buckets]]
binding = "R2"
bucket_name = "module"

[[d1_databases]]
binding = "D1"
database_name = "foundry"
database_id = "64163431-5f3e-4f6d-90e4-4a07d177374f"

This connects my serverless code to 3 services:

  • KV: Cloudflare's Key-Value store
  • R2: Cloudflare's version of AWS S3
  • D1: Cloudflare's sqlite DB

To access these services I will use the env object passed as an argument to my function. Similar to how in Lambda you get a context or an event argument you can handle in your function.

Here is an example of what using D1 looks like:

// `env` is passed as an argument to your serverless function

// running SQL on D1
await env.D1.prepare(`SELECT 1`).run()

// putting a value in the KV store
await env.KV.put("myKey", "myValue", { expirationTtl: 14400 })

// downloading a blob from R2
const file = await env.R2.get("someFile")

no SDK imports needed 😼

This removes a significant amount of code. But that's really not even the best part, the larger win is the reduced application overhead. That's right, the glue. In AWS that would be configuring permissions: IAM roles, security groups and ACL. Then fiddling with network services: VPC, route tables and subnets.

That is all abstracted away and you don't need to resolve any of those complexities. Just start a service, take the ID and place it into wrangler.toml. Boom, its connected.

🕸 Web

services connections are cool but how does it handle web requests?

Workers are best built with itty router. It's basically an express router. Here is what a simple POST authentication route might looks like

import { AutoRouter } from "itty-router"

const router = AutoRouter()

router.post('/', async (request, env) => {
  const url = new URL(request.url)
  const secret = url.searchParams.get("secret")

  if (secret !== env.SECRET) {
    return new Response("unauthorized", { status: 403 })
  }

  return new Response("authorized", { status: 200 })
})

export default { fetch: router.fetch }

🐱‍💻 what I've done so far

I've written the following projects out in Workers:

  • A cron job, which will use Itch.io API to give me analytics emails
  • A email worker (which I use inside Uptime Kuma) which will send an email when a self-host service goes down on my server
  • A self-host solution for FoundryVTT module distribution (more on what module I have made in a future post)

What I do not use Workers for

I have a Discord slash command bot. Unfortunately I cannot use SSH from inside a worker, at least not with the popular ssh2 package. This is because the worker is actually not using the popular node runtime but a custom one built on top of V8 and WebAssembly. This means some expected node features are not there, and thus, I cannot use required crypto libraries in ssh2.

I also have kept my web scraper in AWS Lambda. Because it is written in Golang and Cloudflare workers is only JavaScript at the moment.

Other thoughts

  • cloudflare has no egress costs, this makes it effectively free for me to distribute my FoundryVTT modules. Which is savings that I am loving
  • Logs, they were awful a few years ago but Cloudflare has vastly improved their logging system. I can now see logs in real time instead of needing to push them to R2 myself.
  • there is a powerful CLI you use to do dev work with Cloudflare it is called wrangler. e.g. npx wrangler deploy
  • I got to talk to the Cloudflare team at the 2024 AWS conference and they were really helpful. I asked them about miniflare and some of the issues I had with it. They told me the dev who created that had left. lol.
  • I got to finally write some tests. I usually don't see a personal project to the point of being "mission critical" but with my Foundry module. I would say I've done that (~200 requests / day), so, tests! I used chai and chai-http. The tests are automated to run after code is pushed to the repo.

Tests passing

image