12 Factor Applications with GatsbyJS
This article was originally written by Scott Taylor on Medium. Reposted with permission.
GatsbyJS is great for generating static sites. If you are not familiar with the concept: a “static site” is just “a bunch of files.” You do not need to run a traditional application server to serve them. This means you can typically take a Gatsby build and just drop the files into an asset bucket on a CDN like Amazon S3 or Google Cloud Storage.
Most of the development I do for websites includes technologies like Node, React, and GraphQL. A lot of sites I interact with use a CMS like WordPress or Contentful. However, some sites just need to be designed, developed, and then deployed somewhere on the public internet. They get zero or infrequent updates, and do not need to interact with live data or updates to the content once deployed. For this particular scenario, Gatsby is fantastic.
Gatsby is not for everyone, and does not solve every use case you can imagine. I have had some success recently in deploying projects to the Cloud™ and have established some efficient workflows. In order to achieve this efficiency, I have enforced the 12FA (Twelve-Factor App) methodology to the projects that I build.
12FA
If you have never heard of 12FA, this is a great intro.
The Twelve-Factor App methodology is a methodology for building software as a service applications. These best practices are designed to enable applications to be built with portability and resilience when deployed to the web.
Gatsby exposes top-level configuration through a file in the root of your project called (wait for it…) gatsby-config.js
. The contents of this file look like this:
Gatsby automatically exposes this data through GraphQL. There is a lot of black magic going on behind the scenes. Anything you declare in this file becomes available to query from your page components. Assume we have a page component for the /hello
route:
This works great for a simple example, and for static content, but what happens when you need a dynamic value? Or a value that changes per environment — meaning: dev vs staging vs production vs CI vs testing. To illustrate this, imagine needing to set a 3rd-part API key for use within your application.
The above example needs an API key to use a weather API. So let’s add a key to gatsby-config.js
:
We have solved 1 problem = we can now query for our API key and pass it to our component:
We have created a number of new problems:
- We now have an application secret hard-coded into our source code
- All versions of the app in every environment will use the same API key
- To create builds that use a different key, we will have to change the value by hand before building
- We will have to remember what API key is stored in the file if we commit changes to GitHub
- If we want to keep the value secret, we will have to Git-ignore
gatsby-config.js
when committing our code
What if told you there is way to make this value dynamic? (Prepare to have your mind blown.)
Environment Variables
Instead of hard-coding the value, let’s replace it with an environment variable (env var):
Forget for a minute how we are going to inject the variables into our environment, just know that we have now made our app configurable from the outside, without changing any application code! I like to avoid hard-coding API keys into query strings as is, but this makes the case for why.
With an environment variable, the env var controls the value set in gatsby-config.js
which is exposed via GraphQL which our page queries and sends to our component as a prop.
This is the heart of why 12-Factor apps are important. Your application should be able to run anywhere, because you have removed the specific configuration values from your code, allowing you to control it from the outside. This is why applications are able to run in Docker containers and be controlled on the outside by Kubernetes and kube.yml
files.
A lot of projects have only 2 environments: development
and production
. In some scenarios, it may be ok to share credentials between environments. This changes when you need hard-coded configuration values like siteUrl
to be different for EVERY environment, including different flavors of dev.
For instance:
siteUrl
forgatsby dev
=http://localhost:8000
siteUrl
forgatsby serve
=http://localhost:9000
This subtle difference means you need a place to manage these different values. We are going to add another value to our gatsby-config.js
:
env-cmd
To manage my environment variables in Node, I like to use the env-cmd
package. env-cmd
allows you to specify a .env-cmdrc
file in your project root which maps configuration identifiers to a set of values using JSON syntax
To “inject” an environment’s variables, you simply need to prefix your command with env-cmd production
, substituting the proper environment. I like to create scripts that match the environment names. Add this to package.json
Now, to start up an environment, you can simply type: yarn [ENVIRONMENT]. So for “dev”: yarn dev
Case Study: Google Cloud
For production, we actually need more functionality. I do not just want to build, I want to build and deploy to a Google Cloud Storage bucket associated with a Google Cloud Platform project. First, we need more env vars:
And we need to elaborate on our NPM script for production:
This particular script requires that you have GCP’s utils installed on your command line. If you are interesting in using this solution, you need to download them here.
Run yarn auth
to log in to GCP and associate your terminal with your account. From there, yarn production
will do the following:
1. inject the production
env vars
2. build the Gatsby project
3. switch to the proper GCP project
4. upload the built files to a GCS bucket
If you need an additional environment for staging
, just add an entry to .env-cmdrc
and add a script to package.json
Death Cab for Cutie’s Spotify Playlist Generator
I recently built a Gatsby site for Death Cab for Cutie/Atlantic Records that uses the concepts described above. The site uses the Google Places API, the Open Weather Map API, and the Spotify Web API. Managing multiple sets of keys across multiple environments proved cumbersome.
I arrived at a solution that worked for the project. From the command line, I could deploy to any GCP environment that allowed me as a user. There are many other ways the project could be configured, but at the core should remain the concept of maintaining a 12-Factor App.