Lightweight, configurable static web container

Posted on December 3, 2019

In this post I’ll cover the approach I’ve used to make an easily deployable, configurable front end for a single page app (SPA) in a quasi-microservices deployment. It represents the extract the UI step as outlined in Christian Posta’s post.

Monolith to Micro-services

I use the following spectrum of monolith to micro-services:

Stage Description Advantages Constraints
Fully monolithic The UI and backend services are packaged and deployed together, and the UI is rendered server-side. Easy to coordinate deployments Hard to work on in parallel
Bundled SPA The UI is an SPA, and talks to the backend by an API, but is packaged with the backend during the build process UX improvements due to application like interactions Duplicated code to validate at the frontend and backend. More complex interactions between the frontend and backend
Packaged SPA Same as Bundled SPA, except the frontend produces its own build artifact that is included in the application (think webjar, or NPM package) UI developers do not need to know how to build the backend server. Able to have a dev API server As per Bundled SPA
Independent Frontend The UI is packaged and deployed separately to the backend Easily have different release cadences for the frontend and backend. Able to have multiple different frontends for different customers, tests, etc More coordination needed between the frontend and backend deployment (e.g., API versions, connection holding, etc)

There is no right position on this spectrum. It is highly dependent on the business requirements (development cadence), existing technologies, staff capabilities, etc.

Situation

In this post I consider the Independent Frontend situation. The basic requirements are these:

  1. We want to be able to deliver UI assets to the user
  2. We want to be able to update these assets in a versioned way
  3. We need to pass API traffic to the backend
  4. We want everything to be as light weight as possible, both for restart performance, and also to allow small instance sizes
  5. We need to deploy in a containerised environment
  6. We need the frontend to be configurable so that the artifact can be reused in multiple different scenarios (dev, UAT, prod, etc)

Even in a non-containerised system there is not good way to version static assets, other than using symlinks on disk to version directories. However, even this approach lacks any form of package management, so we’d have to roll that ourselves.

However, with containers we get a form of versioning, but then we need a way of serving these assets, and routing traffic, all while keeping resource utilisation to a minimum. One of the consequences of a container based architecture is lots of potentially duplicate processes. Where as once upon a time you could have one webserver process for all the applications on a machine (including multiple users), in a micro-services architecture many of these are their own processes.

Deployment Architecture

User –> WAF (Cloudflare) –> API Gateway –> UI + Backend

The API Gateway may be a webserver, like Apache or nginx, or a hosted cloud API Gateway.

The backend could be any application server, Tomcat, NodeJS, Flask, etc.

The UI is a light-weight HTTP container that can serve up static assets.

API Gateway Configuration

The API Gateway needs to direct API calls to the backend application server, and all other traffic to the UI container. A simple nginx configuration can be used here, but this is beyond the scope of this article.

UI Container

The Dockerfile for this blog is as follows:

FROM pierrezemb/gostatic

COPY . /srv/http/

This is fine if there is nothing to be configured in the UI to make it work correctly in any deployment environment.

If there were an SPA, you would need to make sure the paths correctly fall back to a base HTML file. e.g., /someresource/1 should resolve to index.html which will then route within the SPA to the correct resource.

FROM pierrezemb/gostatic

COPY . /srv/http/
CMD ["--fallback", "index.html]

In order to make this more configurable, for instance, allowing API keys to be configured, or switching features on and off on a per deployment basis, we can preprocess the entry files using templates. To achieve this you can utilise 3 light weight Golang applications

  1. gostatic for serving the files
  2. go-init for starting another application before the main application
  3. go-env-template which does the job of applying the templates based on the containers environment variables
FROM golang:latest as builder
RUN go get github.com/nigelsim/go-env-template
RUN go get github.com/pablo-ruth/go-init

FROM pierrezemb/gostatic

COPY --from=builder /go/bin/go-env-template /
COPY --from=builder /go/bin/go-init /

COPY . /srv/http

ENTRYPOINT ["/go-init"]

CMD ["-pre", "/go-env-template /srv/http", "-main", "/goStatic --fallback index.html"]

This will scan for files ending in .tmpl and use the Mustache template language to generate a new file (without the .tmpl extension) using the containers environment variables.

Conclusion

This approach has been meant a great improvement in time-to-production, given the ability to easily experiment with preview UIs, and reduced deployment downtime. The next improvement would be to extend goStatic to better handle caching to improve the TTL of client assets, and work more correctly with intermediate caches.