Configure A Static Single Page Application At Runtime
Photo by Kai Dahms on UnsplashWhat Problem Are We Trying To Solve?
I used to be a frontend developer in the past. However, life and job made me switch to the backend side and now to the joy of deploying those applications in the cloud.
Nowadays, all frontend applications subscribed to the single page app paradigm, using various frameworks like React, Vue, Angular or TheLastVeryPopularJsToy. They all have in common that they are web applications: all code is HTML, Javascript, CSS, and static assets. As a result, to host this application you just need to serve the static resources. Web servers like NGinx or Apache can do that very well, but in the Cloud you also have other alternatives like storing those static resources on object storages like S3 that allows to serve them.
The only problem that we are facing is that those single page applications often rely on a backend API. Depending on the environment that you are serving (dev, uat, pre-production), you probably do not want to serve the same endpoint. Unfortunately, static resources are static and do not allow configuring such endpoint dynamically. What we would like to do is to package our HTML application in a Docker container that conforms to the 12 factors app pattern and takes environment variables to specify the endpoint URL.
One Solution: A Different Build For Each Environment
A common solution to that problem is to build one image for each environment. By using tools like WebPack and a dotenv
file, we can easily build a version of our web application with the encoded endpoint as part of the packaging.
The problem with this solution is that you need one separate build for each environment and if you want to qualify your application thru several review steps, this simply means that the image that your QA team reviews won’t be the one that will run in production. Just from an audit standpoint, this may be a problem.
Emulating The Dotenv File
In our application, we just need a variable or an object containing a dictionary of values that reflects the value of the environment variables. For instance, let’s use a global variable (attached to the window
object) that holds our values:
window._env_ = {
API_URL: "https://jsonplaceholder.typicode.com/users",
}
If we are able to generate an env-config.js
file at startup that evaluates our environment variables, then we can include that file in our index.html
file. Thanks to this hack from Krunoslav Banovac a simple shell script can do that for us.
I created a simple Docker image with an entrypoint that embeds that mechanism. In the end, the entrypoint take the resulting directory and moves it to a destination volume or synce it with a S3 bucket. A usable docker-compose file would then become:
version: '3'
services:
html:
image: dmetzler/static-html
command: vol /html_dir
environment:
API_URL: https://jsonplaceholder.typicode.com/users
volumes:
- html:/html_dir/
nginx:
image: nginx
ports:
- "8080:80"
volumes:
- html:/usr/share/nginx/html/:ro
volumes:
html:
The entrypoint will move the static files (with our evaluated env-config.js
) to a common volume between our image and the NGinx container.
What I like in this solution, is that the developer just takes care of the application and not how it is served. The DevOps can then choose its preferred way to deploy the application: NGinx, Apache or S3. For instance, to deploy our application on an S3 bucket, we just need to run:
docker run --rm \
-e API_URL=https://jsonplaceholder.typicode.com/users \
-e AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY \
-e AWS_DEFAULT_REGION \
-e AWS_SESSION_TOKEN \
-it dmetzler/static-html s3 s3://mysamplestaticapp.com
The remaining drawback of this solution is that the Docker image is quite big because it holds a distribution to be able to run the shell and the aws-cli
command.
The Distroless Solution
However, it is a bit sad to embed a complete distribution to distribute our application. We need that distribution for two things:
- a shell to be able to evaluate environment variables and copy files
- the
aws-cli
to sync files to S3
Hopefully, it’s quite easy to find in Golang a replacement for those two features. We can create a tool in Go that will embed this logic: it will be the only artifact that we will add to our application image.
I created a go-deploy
tool (Github) that does exactly that. It is also available as a base Docker image on DockerHub so that it’s super easy to package an application with a multi-step build:
FROM node:alpine as builder
WORKDIR /app
COPY package.json .
COPY yarn.lock .
RUN yarn
COPY . .
RUN yarn build
FROM dmetzler/go-deploy
ENV SRC_DIR=/src
COPY --from=builder /app/build $SRC_DIR
You can build then use this image using:
# docker build -t dmetzler/static-html .
...
# docker run dmetzler/static-html help
Deploy a web application stores in $SRC_DIR in various places.
Usage:
go-deploy [command]
Available Commands:
help Help about any command
s3 Deploys to a S3 bucket
serve Serve the web app (for development use only)
volume Deploys the application in a directory, usually a Docker volume.
Flags:
-h, --help help for go-deploy
Use "go-deploy [command] --help" for more information about a command.
Thanks to some go features we are also able to locally serve the content of our package for development purpose.
# docker run -e API_URL=https://myapi.com/ dmetzler/static-html serve
2019/07/25 20:31:42 [WARN] This is a development server, don't use for production
2019/07/25 20:31:42 [INFO] Listening for connection on port :8080
Conclusion
In this post, I adapted a solution to package an application in a distroless Docker image that allows the configuration on environment variables at runtime.
I coded the Go-Deploy tool very (very) quickly in order to replace all the external tooling (shell and aws-cli
), which allows the distroless image to be very small (21.4MB).
That tool is currently sufficient for my needs but it could definitely be improved, for instance by implementing other solutions like the one that modifies the index.html
file instead of adding a JS file.
Feel free to create Github issues or fork it and send some PR.
References
- Go Deploy
- A Sample app
- The origin of this idea: https://www.freecodecamp.org/news/how-to-implement-runtime-environment-variables-with-create-react-app-docker-and-nginx-7f9d42a91d70/
- An issue on Create React App that covers the problem and exposes some other solutions https://github.com/facebook/create-react-app/issues/2353