Serving our single page application using Cloudflare Workers
Here’s a new and efficient way to utilize Cloudflare Workers to serve your web application. Read more to see how Rewire managed to optimize its web application serving in no time.
For a long time now, our focus at Rewire has been on optimizing the build, deployment, and delivery of our single page web application. Our top priorities are to (1) ease the development process and (2) to serve our customers with maximal performance.
In this blog post, we will demonstrate how we utilize Cloudflare Workers to serve our web application.
History
The motivation
As a young startup, Rewire’s R&D team had to move fast and create an MVP as quickly as possible. It started with one service that served both the API and the web application, which is a set of static files served as a Single Page Application (SPA). When I joined rewire about two years ago, the R&D team was focused on breaking the monolith into API and SPA services. The SPA was built into a docker image and served via an Nginx server that runs on top of a Kubernetes cluster. While this structure did simplify our development process, it was not enough — we wanted to be resilient to outages in our Kubernetes cluster. We were sure that we can do better.
Moving to serverless
Going back to the drawing board, we had a thought: Why not go serverless? We were already utilizing Cloudflare Workers for a few small-scale projects and it seemed like a great match. Cloudflare Workers supports a set of really cool features like zero cold start, out of the box CDN, caching, a great development experience, and fast deployments, which were very important for this project.
Our design
The design is pretty simple: Use the same process that builds the web app and pack it into a docker image. But instead of publishing the docker image into the container registry, copy the files to a Google Storage bucket that will be accessed by the Cloudflare worker. Then we intercept the requests, fetch the static files from the bucket, and serve them to the client. Sounds simple, right?
The implementation we suggest is pretty common: We host our code on a GitLab instance. Each time a developer pushes a commit to the relevant branch, we run the following pipeline using GitLab CI:
- Fetch the code
- Build — fetch all relevant npm packages and run Webpack to build the web application
- Pack — store the files in a docker image
- Publish the docker image to the registry
- Deploy the image to our Kubernetes cluster using Argo rollout
In the new design, we replaced two major steps (deploy and publish) with one new step that copies the static files directly from the docker image and loads them into the relevant bucket. That’s it, now the Cloudflare worker can serve the files from the bucket.
Serving the index
The entry point to the application is an index HTML file. For every new release, this file loads a different bundle of javascript files, CSS files, images, and more.
Our application doesn’t directly request the index.html file. Instead, we have to serve it from the root /
. When we first see a request to the root path, we fetch the index file from the bucket and cache it in the worker key-value store to boost performance.
We make sure to clean the storage and fetch a new index file with each fresh deployment, in order to populate the index file when serving the next request.
This technique has amazingly cut the serving time of the index file by more than 80% — from 250ms to sub 50ms! But, it came with a price, which I’ll describe later on.
Utilizing worker cache
As you know, low latency is not the key feature of object store. In order to ensure our app’s fast loading times, we have decided to utilize the built-in cache of the Cloudflare worker. To do so, we had to push the resource to the cache for every request.
Deployment Gotcha
While testing the project in production, we encountered a deployment issue. Each index file refers to a list of bundles that are replaced on each deployment. The bundle file name contains a hash that is updated on each deployment. Hence, if the client fetches the index, and then the deployment replaces the folder, the worker will return 404 for the resources. To overcome this issue, we used the following technique:
- Utilize cache to return the files
- In case the cache is disabled or invalidated, we have to find another solution. So, we are now saving two live versions: The first one is the current, live version, and the second one is what we previously had for backup. During deployment, we copy the live version to the backup and replace it. In case we can’t find a resource in the live version, we search it on the backup, where it should exist
Summary
Using Cloudflare workers, we were able to build a high availability solution in record time. It required us to play with HTTP headers, cache directly from the code all while providing great flexibility to build the serving mechanism as we wished. Running on the edge serves in over 200 cities across the world, making it much more performant than our previous Kubernetes based solution. In other words, we basically got CDN for free using workers.
Being a customer-centric company, we are now able to provide our customers with a much better experience and a more stable app.
The next post in the series will demonstrate how this technique can be used to build an infinite number of staging environments for developers to sign-off their features with product managers, and how it can be used for quick end-to-end testing using the testim.io framework.