Cloudflare CDN

May 17, 2022

I wanted to expose an existing Azure Storage Account container via Cloudflare's CDN. Looking into this, I needed to write a Cloudflare worker in JavaScript to get this to work. I then needed to use Terraform to provision the worker for each environment.

Worker Script

The following was created as cdn-script.js.. Note that this script does some manipulation of the incoming URL to remove the first two directories in the url offset.

// receives request from user web browser
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event));
});

// details of where the files are stored
// STORAGE_ACCOUNT passed as a cloudflare variable
const STORAGE_URL = `https://${STORAGE_ACCOUNT}.blob.core.windows.net`;

 // return the file from cache if present, or disk if not present
// put in cache if not present
async function serveAsset(event) {
    const url = new URL(event.request.url);
    console.log("Requested: " + url);
    const cache = caches.default;
    let response = await cache.match(event.request);

    if (!response) {
        const path_segments = url.pathname.split("/");
        const offset_segments = path_segments.splice(3); // lose first two elements from request path
        const offset_url = offset_segments.join("/");
        const path = `${STORAGE_URL}/${offset_url}`;
        console.log("Origin: " + path);

        response = await fetch(path);
        const headers = { 'cache-control': 'public, max-age=14400' };
        response = new Response(response.body, { ...response, headers });
        event.waitUntil(cache.put(event.request, response.clone()));
    } else {
        console.log("Fetched from cache");
    }

    return response;
}

// Only support GET on CDN
async function handleRequest(event) {
    if (event.request.method === 'GET') {
        let response = await serveAsset(event);
        if (response.status > 399) {
            response = new Response(response.statusText, { status: response.status });
        }
        return response;
    } else {
        return new Response('Method not allowed', { status: 405 });
    }
}

Cloudflare Worker

We now need to have some terraform that installs this worker script.

resource "cloudflare_worker_script" "cdn_script" {
  name    = lower("cdn-${module.locals.short_prefix}")
  content = file("cdn-script.js")

  plain_text_binding {
    name = "STORAGE_ACCOUNT"
    text = azurerm_storage_account.cdn.name
  }
}

Note that this refers to the Azure storage account we are exposing, and that this worker is named after the short_prefix variable applicable to this environment. This short_prefix uniquely identifies this environment so that we can install multiple copies of this terraform, one for each environment.

Worker Route

Next we need to install the worker route, so that we define which url the CDN operates on. This refers to a particular zone_id, which is one of the zones already installed in our Cloudflare. This defines the hostname that the worker is installed on.

 :::text
resource "cloudflare_worker_route" "cdn_route" {
  zone_id     = var.cloudflare_cdn_zone_id
  pattern     = lower("${var.cloudflare_cdn_zone_url}/cdn/${module.locals.short_prefix}/*")
  script_name = cloudflare_worker_script.cdn_script.name
}

Cloudflare Terraform Provider

We must also define the cloudflare terraform provider, and we do this specifying an api token and an account id. These can both be obtained from the Cloudflare website.

provider "cloudflare" {
  api_token  = var.cloudflare_api_token
  account_id = var.cloudflare_account_id
}