Simon's Blog

A real-time game from scratch - Continuous Deployment Part 1 [6]

March 20, 2021

Previous Entries

Introduction

Continuous Deployment is the DevOps discipline of having automated deployments that move our continuously integrated code out to development, staging or even production environments.

The reasoning is that by deploying quickly, often and in small chunks we can avoid the risks associated with large deployments, get feedback quickly and if we encounter issues we can rollback much faster.

Continuous Deployment Steps

Cabin Fever is a game that is played using a browser, so this continuous deployment would involve steps such as the following:

  1. Provision a VPS or some sort of cloud server
  2. Install all dependencies on that server to run the game server
  3. Transfer the game server binary onto the server
  4. Setup or update DNS to point to our new server

The above assumes that I am provisioning a new server every time. An alternative to this would be to provision the server ahead of time, followed by transferring the latest game server binary to the existing server and switching traffic from the old one to the new one.

One thing to deploy

Before I implement deployment, I need to ensure that I have something to deploy. In the previous article I explored the possibility of using Jenkins to build both the server and client, ending with archiving the client bundle and server binary for later use.

An alternative to this approach would be to build a single package which contains everything I need to place on the server to get started. Having worked extensively with Docker, it felt like the easiest way to proceed. It would also make the process simpler, as I could build the image on my CI machine and ship out the image only to the production machine.

Multiple Dockerfiles vs one Mutli-stage Dockerfile

In a project spanning different parts of a stack (in this case, backend and frontend), one will often find Dockerfiles for each part of the project, both for development and deployment purposes. This process helps keep the code both portable across machines (so as long as the docker daemon is available), more reproducible (since all the toolchain required is available in the docker image) and easier to get started with.

Since I am using a mono-repo for Cabin Fever (meaning all the code, regardless of the part of the stack, is in the same git repository), I chose to leverage a single multi-stage Dockerfile over having to wrangle separate ones.

Multi-stage Dockerfiles are considered a good practice even at a single part of the stack - it is a useful tool to avoid shipping larger docker images than are necessary. A smaller image ends up being both cheaper and faster to move around, cutting down on turnaround times.

A psuedo-code example of a multi-stage dockerfile could be the following:

  1. Stage 1: Start from the latest available golang image
  2. Stage 1: Compile the binary using the golang toolchain
  3. Stage 2: Start from the scratch image
  4. Stage 2: Copy the compiled binary from Stage 1
  5. Stage 2: Setup the image to run the binary

As a result, the final image will not be shipped with the golang toolchain (which is not required to be present in the image for our usecase), making it smaller, more secure and more efficient.

Mutli-stage for Cabin Fever

In the final resulting Dockerfile, I used the following pattern:

  1. Stage 1: Using the latest available golang image
  2. Stage 1: Compile the binary using the golang toolchain
  3. Stage 2: Using the latest nodejs image
  4. Stage 2: Download all required dependencies
  5. Stage 2: Build the production bundle
  6. Stage 3: Using the scratch image
  7. Stage 3: Copy the compiled binary from Stage 1
  8. Stage 3: Copy the production bundle from Stage 2
  9. Stage 3: Setup the image to run the server binary

This results in a 10.5MB image, consisting of both the server binary and the frontend bundle. The only two changes I had to do to the code to enable this process were:

  1. serve the index.html file at the root endpoint / and also to serve the static folder through the server so that JS and CSS files can be served to the end user.
  2. set the production bundler (in this case, parcel) to include /static/ as a path under the CSS & JS assets in the generated index.html using the following command line argument: --public-url ./static/. I included this addition in the package.json under the build script to avoid having to include it in the Dockerfile.

Conclusion

Whilst none of the above is technically deployment, it lays the groundwork for the packaging of all the code involved in delivering the full game experience to our end users. The next article will involve more of what I initially wrote about: the actual provision of a new server or usage of an existing one to serve the game experience.

Next Entry: Continuous Deployment Part 2 (Coming soon)


Written by Simon who lives in Malta. You can find out more about me on the about page, or get in contact.