With complex features and multiple collaborators, webapps have become increasingly harder to build and manage. As deployments increase in complexity and technology is more widespread throughout every industry, this efficiency is critical to organizations’ business objectives. Harness has managed this complexity by implementing a content delivery network (CDN) for our user interface (UI) Services. With a CDN, we ensure speed and stability for our platform.
In this blog, we’ll walk through what a CDN is and how Harness implemented it into our environment. We’ll discuss some of the challenges we ran into and how we addressed them.
So what is a CDN? A CDN is simply a set of servers distributed across the globe. According to Gartner:
Content delivery networks (CDNs) are a type of distributed computing infrastructure, where devices (servers or appliances) reside in multiple points of presence on multihop packet-routing networks, such as the Internet, or on private WANs. A CDN can be used to distribute rich media downloads or streams, deliver software packages and updates, and provide services such as global load balancing, Secure Sockets Layer acceleration and dynamic application acceleration via WAN optimization techniques.
The use cases are simple:
For UI Services at Harness, without CDN, all the bundled files (including index.html, and other static files such as Javascript, styles, and images) come from the app server. But with CDN, only index.html comes from the app server. All the remaining static files come from the CDN server (here, static.harness.io).
For starters, you’ll want to list your requirements and constraints. At Harness, ours were:
With this information documented, we started the implementation and ran into some challenges.
We used the same build for different environments and deploymentTypes (SaaS and on premise). We used Webpack for bundling and creating our build.
Public Path helps us set the base path for all the assets within the application. In simpler terms, Public Path defines the host or directory that your assets should be served from.
Earlier, we used a build-time public path. Webpack has built-in support for defining a run-time on-the-fly public-path, so we changed our code a bit. We added the hostname of our CDN server as the public path.
Here static.harness.io is your CDN hostname.
Our CDN is hosted on the Google Cloud Platform. We store our static assets in a Google Cloud Storage (GCS) bucket. We use Harness CD to deploy Harness.
Harness has built-in support for “Uploading Artifacts to GCS” — a basic, “quick-start” convenience step.
Our use case also included the need to:
To ensure that the latest bundled files are uploaded to GCS with the above use cases, instead of using the “Upload Artifacts to GCS” step, we use the “Run” step to add a custom script. In that script, we use gsutil.
gsutil is a Python application created by Google that lets you access Cloud Storage from the command line. We used it to compress (g-zip) the bundled assets and set their permissions. Here’s a snippet for reference:
We use Monaco Editor heavily for code-editing capabilities. If you have used Harness, chances are you have seen the Visual-vs-YAML view. The YAML view needs MonacoEditor.
Monaco Editor uses Web Workers to run in a separate thread from the main JS thread.
However, web browsers don’t allow cross-domain web workers.
This restriction means that if your website address starts with “app.harness.io,” but your Javascript files for MonacoEditor languages try to load from “static.harness.io” (CDN), web browsers will block them. Some MonacoEditor features, such as auto-complete, won’t work.
We tried the following to fix this: monaco-editor-webpack-plugin and worker-loader.
This monaco-editor-webpack-plugin plugin provides a publicPath similar to that of webpack, which tells which host to load the worker scripts from.
But…it didn’t work for us. It didn’t “win over” Webpack’s run-time public path.
Next, we tried worker-loader. It was the same story here. It didn’t “win over” Webpack’s run-time public path either.
We also thought of updating the version of monaco-editor-webpack-plugin, but we found that the new version was strongly tied to the version of MonacoEditor—we would have to update the version of both monaco-editor and monaco-editor-webpack-plugin. It would become a longer task.
Even though customLanguages support made it feel easy, we didn’t go down this path.
We then thought of configuring CDN to be behind “app.harness.io”—in other words, instead of the red path, take the green path. (Diagram below.)
This won't work for vanity urls such as “customer.harness.io”, because the browser will still block worker calls since we configured CDN behind “app.harness.io”
We came up with the solution in two steps.
worker-loader: workers loaded as Blobs.
But worker-loader is deprecated/archived in favor of webpack v5, so we didn’t use this.
We asked ourselves: What if we build worker files separately and manually try to change base-path? This required custom webpack configuration. A new "entry" in webpack, but it meant no lazy-loading of the monaco file.
But we took this solution further—with a twist. We built workers, but with a separate webpack config.
When referring to built files from within the component, we had to use current window.location. We got rid of the worker-loader and used recommended webpack v5 way for workers (using getWorker() and new Worker).
There are potential issues, such as no hash in the bundled file for workers, so we added version “2” manually in filenames (filenames are editorWorker2 and yamlWorker2). If we ever need to update the worker files, we will need to manually change the version 2—which acts as our hash We are cognizant of this limitation, and we accepted it because automating this can be complex.
Solving complex challenges and ensuring uptime for thousands of users is no easy task. By implementing CDN, Harness has been able to increase speed and ensure uptime through this distributed network model.
Keep an eye out for part 2 of our CDN implementation where we’ll share how we implemented CDN for our Micro-frontend child apps.