Developers & Practitioners

The serverless gambit: Building ChessMsgs.com on Cloud Run

ChessMsgs Hero Image

While watching The Queen’s Gambit on Netflix just recently, I was reminded about how much I used to enjoy playing chess. I was eager to play a game, so I started to tweet, “D2-D4” knowing that someone would recognize this as an opening move and likely respond with their move, giving me the fix I needed. I paused before hitting the tweet button because I realized that I’d need to set up a board (physical or virtual) to keep track of the game. If I received multiple responses, I’d need multiple boards. I decided not to send the tweet.

ChessMsgs.com screenshot

Later in the day, I had the idea to create a simple service that addresses my use case. Instead of designing a full chess site, I decided to create a chess board logger/visualizer to make it practical to play via Twitter or any other messaging/social platform.

Instead of tweeting moves back and forth, players tweet links back and forth, and those links go to a site that renders the current chessboard, allows a new move, and creates a new link to paste back to the opponent. I wanted this to be 100% serverless, meaning that it will scale to zero and have zero maintenance requirements. Excited about this idea, I put together a shopping list:

My MVP requirements:

  • Represent the board position—ideally completely in the URL to keep it stateless from a server-side perspective

  • Display a chessboard and let the player make their next move.

Stretch goals:

  • Enforce chess rules (allow only legal moves).

  • Dynamically create a png/jpg of the chessboard that I can use as an Open Graph and Twitter card image so that when a player sends the link, the image of the board will automatically display.

Putting it all together

Representing the board position

There is a standard notation for describing a particular board position of a chess game called Forsyth–Edwards Notation (FEN) that was exactly what I needed. A FEN is a sequence of ASCII characters. For example, the starting position for any chess game can be represented by the following string:

rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

Each letter is a piece: pawn = "P", knight = "N", bishop = "B", rook = "R", queen = "Q" and king = "K". Uppercase letters represent white pieces and lowercase letters represent black. The last part of the string is specific to certain rules in chess (read more about FEN).

I knew I could use this in the URL, so my first requirement was complete and I was able to represent the board state in the URL eliminating the need for a backend data store.

Displaying the chessboard and allowing drag-and-drop moves

Numerous chess libraries are available. One in particular that caught my eye was chessboard.js—described as “a JavaScript chessboard component with a flexible ‘just a board’ API”. I quickly discovered that this library can display chess boards from a FEN, allow pieces to be moved, and update the FEN. Perfect!

In only two hours, I had the basic functionality implemented.

Enforcing chess rules

I originally thought that making this service aware of chess rules would be difficult, but then I saw the example in the chessboard.js docs showing how to integrate it with another library called chess.js—“a JavaScript chess library that is used for chess move generation/validation, piece placement/movement, and check/checkmate/stalemate detection—basically everything but the AI”. A short time later, I had it working! Stretch goal #1 completed.

Here's what a couple of game moves look like:

Moving the pawn from D2 to D4 in a new game—https://chessmsgs.com/?fen=rnbqkbnr%2Fpppppppp%2F8%2F8%2F3P4%2F8%2FPPP1PPPP%2FRNBQKBNR+b+KQkq+d3+0+1&to=d4&from=d2&gid=mOhlhRlMboYsHLqBF1f7I

Black countering with a similar move of pawn from D7 to D5—https://chessmsgs.com/?fen=rnbqkbnr%2Fppp1pppp%2F8%2F3p4%2F3P4%2F8%2FPPP1PPPP%2FRNBQKBNR+w+KQkq+d6+0+2&to=d5&from=d7&gid=mOhlhRlMboYsHLqBF1f7I

The URL has the following data:

  • fen—the new board position

  • from and to—indicating what move occurred (I use this to highlight the squares)

  • gid—a unique game ID (I used nanoid)—I’ll use this to connect moves to a single game in the future. For example, I could add a feature that lets the user request the entire game transcript). 

Done! Except...

At this point, there were no server requirements other than simple HTML static hosting. But after playing it with some friends and family, I decided that I really wanted to accomplish the other stretch goal—dynamically create a png/jpg of the chessboard that I can use as an Open Graph and Twitter card image.  With this capability, an image of the board will automatically display when a player sends the link. Without it, the game is a series of ugly URLs.

Dynamically creating the Open Graph image

This requirement introduced some server-side requirements. I needed two things to happen on the server.

First, I needed to dynamically generate a board image from a FEN. Once again, open source to the rescue (almost). I found chess-image-generator, a JavaScript library that creates a png from a FEN. I wrapped this in a bit of Node.js/Express code so that I could access the image as if it were static. For example, here’s a demo of the real endpoint: https://chessmsgs.com/fenimg/v1/rnbqkb1r/ppp1pppp/5n2/3p4/3P4/2N5/PPP1PPPP/R1BQKBNR w KQkq - 2 3.png. This link results in this image:

Dynamically created chessboard

Second, I needed to dynamically inject this FEN-embedded URL into the content attribute of the meta tag in the main HTML. Like me, you might be thinking that you could just do some DOM manipulation in JavaScript and avoid having to dynamically change HTML on the server. But, the Open Graph image is retrieved by a bot from whatever service you use for messaging. These bots don’t execute any client-side JavaScript and expect all values to be static. So, that led to additional server-side work.

I needed to dynamically convert this:

  <meta property="og:url" content="{{url}}" />
<meta property="og:image" content="{{imgUrl}}" />

Into something like this:

  <meta property="og:url" content="https://chessmsgs.com/?fen=rnbqkb1r/ppp1pppp/5n2/3p4/3P4/2N5/PPP1PPPP/R1BQKBNR+w+KQkq+-+2+3&to=f6&from=g8&gid=ziL3VfMEoIT9iNwp6csBh" />
<meta property="og:image" content="https://chessmsgs.com/fenimg/v1/rnbqkb1r/ppp1pppp/5n2/3p4/3P4/2N5/PPP1PPPP/R1BQKBNR w KQkq - 2 3.png" />

I could have used one of many Node templating engines to do this, but they all seemed like overkill for this simple substitution requirement, so I just wrote a few lines of code for some string.replace() calls in my Node server. 

With this functionality added, a game on Twitter (and other services) now looks much better:

ChessMsgs game on Twitter

Check out the code

The source for chessmsgs.com is available on GitHub at https://github.com/gregsramblings/chessmsgs

Deciding where to host it

The hosting requirements are simple. I needed support for Node.js/Express, domain mapping, and SSL. There are several options on Google Cloud including Compute Engine (VMs), App Engine, and Kubernetes Engine. For this app, however, I wanted to go completely serverless, which quickly led me to Cloud Run. Cloud Run is a managed platform that enables you to run stateless containers that are invocable via web requests or Pub/Sub events. 

Cloud Run is also basically free for this type of project because the always-free-tier includes 180,000 vCPU-seconds, 360,000 GiB-seconds, and 2 million requests per month (as of this writing—see the Cloud Run pricing page for the latest details). Even beyond the free tier, it’s very inexpensive for this type of app because you only pay while a request is being handled on your container instance, and my code is simple and fast.

Lastly, deploying this on Cloud Run brings a lot of added benefits such as continuous deployment via Cloud Build, and log management and analysis via Cloud Logging, both of which are super easy to set up.

What’s next?

If this suddenly becomes the most popular site of the day, I’m actually in good shape from a scalability point of view because of my decision to use Cloud Run. If I really wanted to engineer this for extreme loads, I could easily deploy it to multiple regions throughout the globe and set up a load balancer and possibly a CDN. I also could separate the web hosting functionality from the image generation functionality to allow each to scale as needed.

When I first started thinking about the image generation, I naturally thought about caching the images in Google Cloud Storage. This would be easy to do and storage is crazy cheap. But, then I did a bit of research and learned the following fun facts. After two moves (one move for each player), there are 400 different distinct board positions. After each player moves again (two moves each), this number is now 71,782 distinct positions. After each player moves again (three moves each), the number is now 9,132,484 distinct positions! I could gain a bit of performance by caching the most popular openings, but each game would quickly go beyond the cached images so it didn’t seem worth it. By the way, to cache every possible board position would be about 1046 positions, which is a massive number that doesn’t even have a name.

Conclusion

This was a fun project – almost therapeutic for me since my “day job” doesn’t allow much time for writing code. If this becomes popular, I’m sure others will have ideas on how to improve it. 

This was my first hands-on with Cloud Run beyond the excellent Quick Starts (examples for Go, Node.js, Python, Java, C#, C++, PHP, Ruby, Shell, etc.). Because of my role in developer advocacy at Google, I was aware of most Cloud Run capabilities and features but after using it for something real, I now understand why developers love it!

Where to learn more