How to set up a CDN using Cloudflare R2 and Workers. Salik Khan • Mon Mar 04 2024
backend
cloudflare

Cloudflare, Inc., is an American company that provides content delivery network services, cloud cybersecurity, DDoS mitigation, and ICANN-accredited domain registration services. They provide multiple tiers to their services.

You can get most Cloudflare services on the free tier but you have to move to their pay-as-you-go model for services like workers.

I am going to use my own repository for this, you can find it here.

Creating an R2 Bucket

Let's start with creating a Cloudflare R2 bucket. Log in to the Cloudflare dashboard and click R2 in the left bar.

Create a new bucket in the R2 dashboard:

Set a name for your bucket and finish the installation. This creates an R2 bucket, which is S3 compatible so you can use it as a data repository as-is. However you have to place worker in front of it to make it cache data on the Cloudflare network of servers and actually use it as a content delivery network.

Installing Wrangler

Wrangler is a cli tool built by Cloudflare to interact with workers. We have to install it from the npm registry, you need node.js and npm installed on your pc to run this command.

$ npm install -g wrangler

You can check your installation with:

$ wrangler --version

Creating a worker

On the Cloudflare dashboard, click workers and pages, create a new application, and name it.

Next, you need to create a new environment variable for this worker with the name AUTH_KEY_SECRET in the settings tab of the newly created worker, note down the key since it will be encrypted and you won't be able to see it again. Make sure to secure it somewhere safe. This is the key you will use to upload stuff to your bucket directly using curl or an API tool like Postman.

The .workers.dev domain will be your main route to access the worker for now. Next, scroll down on the settings page and bind the bucket you just created to the worker. Give it a variable name you can remember since you are going to be using it in a moment.

Setting up the worker

Open your terminal and run the following commands:

# Clone the repository with the code to run the worker:

$ git clone https://github.com/thesalikkhan/R2-Worker-API.git cdn

# Change directory:

$ cd cdn

# Login to wrangler:

$ wrangler login

Open the wrangler.toml file in your preferred text editor:

name = "YOUR_WORKER_NAME"
main = "src/index.js"
compatibility_date = "2022-09-02"

account_id = "YOUR_ACCOUNT_ID"
workers_dev = true

[[r2_buckets]]
binding = 'bucket'
bucket_name = 'YOUR_BUCKET_NAME'

Change the variables to your own, you can find your account id by running:

$ wrangler whoami

Inside [[r2 buckets]], do not change the binding, or you will have to change the binding from bucket to your own binding in the index.js file as well:

async fetch(request, env, context) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);
    if (!authorizeRequest(request, env, key)) {
      return new Response("Unauthorized.", {
        status: 403,
      });
    }
    switch (request.method) {
      case "PUT":
        await env.cdn.put(key, request.body);
        return new Response(`Put ${key} successfully!`);
      case "GET":
        try {
        const headers = new Headers();
        const cacheKey = new Request(url.toString(), request);
        const cache = caches.default;
        let response = await cache.match(cacheKey);
        if (response) {
          console.log(`Cache hit for: ${request.url}.`);
          return response;
        }
        console.log(
          `Response for request url: ${request.url} not present in cache. Fetching and caching request.`
        );  
        const object = await env.cdn.get(key);
        if (object === null) {
          return new Response("Object Not Found", { status: 404 });
        }
        object.writeHttpMetadata(headers);
        headers.set("etag", object.httpEtag);
        headers.append('Cache-Control', 's-maxage=31536000');
        response = new Response(object.body, {
          headers
        });
        context.waitUntil(cache.put(cacheKey, response.clone()));
        return response;
      } catch (e) {
          return new Response('Error thrown ' + e.message);
        }
      case "DELETE":
        await env.cdn.delete(key);
        return new Response("Deleted!");
      default:
        return new Response("Method Not Allowed", {
          status: 405,
          headers: {
            Allow: "PUT, GET, DELETE"
          }
        });
    }
  }
};
var hasValidHeader = (request, env) => {
  return request.headers.get("X-Custom-Auth-Key") === env.AUTH_KEY_SECRET;
};
function authorizeRequest(request, env, key) {
  switch (request.method) {
    case "PUT":
    case "DELETE":
      return hasValidHeader(request, env);
    case "GET":
      return key;
    default:
      return false;
  }

Next, run

$ wrangler deploy

and your worker will be live. You can now use curl or any API tool to interact with your bucket. Your endpoint is the .workers.dev domain on the workers page, you can also bind a new route with one of your existing domains on cloudflare to your worker and use that instead.

Using the worker

Replace <auth-key> with your actual key (remove the angle brackets), and example.workers.dev with your own worker route.

# Uploading binary data using curl
$ curl https://example.workers.dev/<location> -X PUT --header 'X-Custom-Auth-Key: <auth-key>' --data-binary 'test'
# Uploading files using curl
$ curl https://example.workers.dev/file.ext -X PUT --header 'X-Custom-Auth-Key: <auth-key>' --upload-file ./file.ext
# Deleting a file using curl
$ curl https://example.workers.dev/file.ext -X DELETE --header 'X-Custom-Auth-Key: <auth-key>'

The files you upload can be accessed publically on your worker route.

HTTP Methods

You can use GET, PUT or DELETE on the route. PUT and DELETE require authentication using the X-Custom-Auth-Key header.

Pricing

Cloudflare workers aren't free, it's a pay-as-you-go model with a free limit. Check the worker pricing structure here before you commit to this.