A 6 minute read written by
Jørn Ola Birkeland
You’ve got your SPA, consisting of static CSS, HTML and JS, and high-performing microservices to provide the dynamic behavior of your app. The only thing remaining is hosting the static content somewhere, point a DNS to it, and call it a day.
However, it turns out that your app needs a top menu, which is shared between all the apps in the organization, to provide a common look-and-feel and navigation. All of a sudden you’re pushed into the world of content transclusion, now loosely referred to as micro frontends
Logically, this is how your index.html should look:
<html> <head> <link href=“your_app_style.css” rel=“stylesheet”/> <!-- menu css and js references go here -—> </head> <body> <!-- menu html goes here --> <div class=“your_app_container/> <script src="your_app_behaviour.js"></script> </body> </html>
The menu content (html, css, and js) is provided as a service maintained by some other team, and editors are adjusting the content every now and then. In other words, it is almost static, but not quite. The menu editors want changes to propagate quickly across all apps, as and when it happens. So what to do?
Applying Client or Server Side Includes
A few options are available to you as a developer, typically variations of server-side includes (SSI) and client-side includes (CSI) techniques:
- Treat the menu content as static and deploy a snapshot embedded within your app. A menu update requires a redeploy, which could be relatively easy if downloading and embedding the snapshot was a part of your CD pipeline. Ensuring a fairly updated menu could be achieved by automatically redeploying often enough, or - slightly more sophisticated - triggering a redeploy when the content of the menu service changes. In both cases the pipeline must handle an unavailable menu service, or other issues, increasing the complexity. And with lots of apps in production, there is a governance issue in ensuring working CD pipelines across all of them.
- Embed the menu server side, when the content is served to the client. Of course, this would require running a web server for the static content, adding the hooks to intercept the request for index.html, and calling the functionality to download the menu content and decorating the html on the way out. Again, performance and failures would be an issue, having the user wait for the inclusion of content from a service you don’t have control over. And running a webserver to serve static content really was what we were hoping to avoid in the first place.
Edge-side Includes with AWS Lambda@Edge
AWS offers a CDN service called CloudFront, which caches static content closer to the clients, whereby offloading the origin web server and improving response times. July 2017 it became possible to run a piece of code when CloudFront receives a request or returns a response. More precisely, CloudFront offers four events that can trigger your code:
- When receiving a request from a client, regardless of the requested content is cached or not
- Before forwarding the request to the origin web server, in case of a cache miss
- After receiving the response from the origin web server, but before it is cached
- Before returning the response to the client, after it has been cached
The menu problem is a good use case for Lambda@Edge. At first glance, it seems it would be a good choice to latch on to the response from the origin web server, and decorate the html before it is cached and returned to the client. However, response triggers aren't able to access the response body or provide a generated body of any kind. The next best thing is to trigger your lambda function before the request is forwarded to the origin web server because of a cache miss. The steps you need to implement, are:
- Check if the client requests “index.html” (or “/“). If not, just forward the request and return the response
- Fetch index.html yourself, and get the content from the menu service
- Replace specific tags in index.html with the appropriate menu-content
- Set the cache headers to some fairly short expiry time (balancing the cost of regenerating index.html versus the risk of serving stale content)
- Return the decorated response
The final piece in the puzzle is to configure CloudFront to access your static content and then trigger your code with the right event. Using the web console of AWS Lambda, it should look something like this:
and then click “+ Add trigger”, select “CloudFront”, and click “Deploy to Lambda@Edge”:
You need to select your CloudFront distribution and event, before clicking “Deploy”.
So what have we gained? First, our static content can be hosted as such, for example in an AWS S3 bucket (set up as a static website), and deployed by a simple directory synch operation. No complex CD pipelines or dedicated web servers are required. Second, in case of a failure during transclusion, CloudFront will continue to serve the previously cached index.html. So a temporarily unavailable menu service will not impact the end-user. Third, the performance penalty is only paid occasionally by a single user (within a CDN region), and a small lambda execution timeout value will cause a relatively quick failure, resulting in the cached index.html being returned. Fourth, the lambda function decorating the index.html can be shared by all apps (provided they are authorised to do so), maintained in one place, all the while scaling is handled for you.
Finally, leveraging a CDN for static content is A Good Thing, and should reduce the cost of serving your files, while simultaneously improving the end-user experience. As an additional benefit, if you are building immutable web apps (which you should) - and therefore getting new file names with each build - serving them through a CDN will keep the files of previous versions available for a while. The slim chance that a client with an old index.html will request unavailable files, is avoided.
There are a couple of drawbacks, of course: Getting your lambda function to work correctly can be a bit of trial and error with somewhat limited debugging possibilities. Also, index.html is cached for a certain time period (decided by you). A new deployment of your app will not be immediately available to new clients. A cache invalidation request to CloudFront may take a few minutes, so in the odd case you need to be able to rapidly make a new version of your index.html available, using a CDN in front of it may be the wrong thing to do. For everybody else, it is definitely a tool to have in your toolbox.