Photo by Paz Arando on Unsplash

Provision, setup, and secure a TinaCMS cloud editor on AWS

Sean Michael
Level Up Coding
Published in
13 min readJun 24, 2020

--

I recently built a website with TinaCMS and Gatsby for the first time and I found the development process to be relatively smooth. The one glaring developer experience gap that I found was in setting up a cloud editor, which is what I’d like to share in this tutorial. Since the cloud editor runs off of the development build, I won’t be discussing your TinaCMS production builds and deployments in this tutorial. If you are curious, I used a CD process with GitHub actions to build and deploy new static assets to S3 (with CloudFront and static web hosting) in production on every new commit to the master branch of my project.

Before we get started, I’d like to explain that in order to allow for real-time cloud editing of TinaCMS with Gatsby, you currently need to run two separate instances of your project; one as a private cloud editor and the other as your production website. I believe this may change once TinaCMS releases their “TinaCMS Teams” product, but the current recommendation seems to be Gatsby Cloud (https://tinacms.org/blog/using-tinacms-on-gatsby-cloud), which I found to be barely usable for my client due to their branch and “preview” restrictions.

This tutorial will guide you through the setup, provisioning and securing the TinaCMS cloud editor with Gatsby and AWS. Hopefully it will save you time and money :-).

I’ll link to commits periodically in this tutorial, but here is the final tinacms-cloud-editor-tutorial repo.

Setup a TinaCMS project

Before we get started, make sure you have node, npm, and yarn all installed on your computer.

Let’s get started by cloning the canonical gatsby-starter-tinacms, installing dependencies and running the project locally to make sure it works as expected. If you already have a TinaCMS/Gatsby project setup, then you can follow along from your own project.

git clone https://github.com/tinacms/gatsby-starter-tinacms.git
cd gatsby-starter-tinacms
yarn install
yarn start

If everything works as expected, go ahead and create a new repo on GitHub, set your origin remote to the newly created repo, and push up your code. Make sure to replace remote url with your own.

git remote set-url origin git@github.com:user/project.git
git push origin master

Configure Gatsby for TinaCMS cloud editing

If you navigate to your gatsby-config.js file you will see where TinaCMS is being enabled via a plugin.

{
resolve: "gatsby-plugin-tinacms",
options: {
plugins: ["gatsby-tinacms-git", "gatsby-tinacms-remark", "gatsby-tinacms-json"],
sidebar: {
hidden: process.env.NODE_ENV === "production",
position: "displace"
},
},
},

The sub-plugin of gatsby-tinacms-git is the one that will allow our editor to talk to our git repo when making updates, so if you’re following along from your own project make sure you install and add this plugin. Also, take note of the configuration that hides the editor “sidebar” when the app is built for production. This aligns with what we’d like to achieve.

In order to inform our cloud editor of where it needs to push content updates, let’s replace the declaration of the gatsby-tinacms-git plugin with more detailed configuration. Make sure to replace the gitRemote value with the SSH address of your git remote repo. Feel free to replace the commit info with whatever you’d like.

{
resolve: "gatsby-plugin-tinacms",
options: {
plugins: [{
resolve: "gatsby-tinacms-git",
options: {
gitRemote: "git@github.com:user/project.git",
defaultCommitMessage: 'Edited with TinaCMS',
defaultCommitName: 'Cloud Editor',
defaultCommitEmail: 'git@project.com',
},
}, "gatsby-tinacms-remark", "gatsby-tinacms-json"],
sidebar: {
hidden: process.env.NODE_ENV === "production",
position: "displace"
},
},
},

Your local TinaCMS editor should now be connected to your project on GitHub. Go ahead and navigate to the blog at http://localhost:8000 , click the blue pencil in the bottom left of your browser, make some text changes, and click “Save”.

You just created a commit that was pushed to GitHub from the TinaCMS editor! Navigate to your project in GitHub and confirm that the commit was pushed to your branch.

Corresponding commit: https://github.com/Integral-Stack/tinacms-cloud-editor-tutorial/commit/349d5093e55b38d7db6c92ab9e2aee48cc969e5d

Add authentication to Gatsby

We are about to expose the CMS to the internet and we need a way to restrict its access to only the trusted editors of the website.

We could try to restrict access via IP whitelisting or geolocation in AWS, but neither of these will give us the fine-grained and convenient control that we can get via credential authentication.

Basic Authentication is not the most secure method to restrict access to our editor, but it is by far the quickest and it will suffice for our situation.

In order to maximize your security with Basic Auth, make sure that your password has a high cardinality and length. We will also be enabling https, which is necessary since Basic Auth is only base64 encoded before being sent over the wire. And lastly, we will run our editor from a branch other than master. This way, if our cloud editor ever happens to get hacked, the most the attacker can do is add a bunch of new commits to a branch that isn’t even deploying to production.

Let’s get started by installing the express-basic-auth package.

yarn add express-basic-auth

Fortunately Gatsby has given us a hook into our Express app instance, which will make installing basic auth a piece of cake. Just add the following to your gatsyb-config.js file:

const basicAuth = require("express-basic-auth")

module.exports = {
siteMetadata: {
...
},
developMiddleware: app => {
app.use(basicAuth({
users: { test: 'test' },
challenge: true,
realm: 'your app name',
}))
},
...
}

This adds basic authentication to all incoming requests to the app. Obviously we will eventually be adding a username/password, but we will test with “test” as the username and password. You will need to set the challenge and realm properties so that users browsers know to prompt them for a username/password when they try to access the website.

Now restart the process and try accessing http://localhost:8000. You should get prompted with a username/password dialog box. If you correctly enter the credentials, you should be able to access the site.

Corresponding commit: https://github.com/Integral-Stack/tinacms-cloud-editor-tutorial/commit/5de19cd413e38e484610ba5623a83155c81af7fa

Secure secrets

Now that we are starting to explore adding sensitive data to our app, we need to make sure we don’t expose any of these secrets to the public. In fact “Sensitive Data Exposure” is the OWASP #3 application security risk. We are going to do this the simplest way possible, by integrating dotenv for Node and inject our sensitive data at run-time.

We won’t need to install dotenv because it is already packaged with Gatsby. We will however need to add a require statement to the top of our gatsby-config.js in order to load our file properly:

require("dotenv").config({ path: ".env.cloud" })
const basicAuth = require("express-basic-auth")

...
modules.exports = {

I’m purposefully naming the dotenv something that won’t clash with our regular dotenv setup. Don’t worry, if the file does not exist, dotenv will not throw any errors on its absence. Let’s now create our .env.cloud file at the root of our project.

We will now want to add it to our .gitignore file so it doesn’t get committed to our repo:

# Live Github updates
.env.cloud

In our .env.cloud file we will be adding several variables. Make sure to fill in the values appropriately, especially a strong username and password:

AUTH_USER=*********
AUTH_PW=***************
ENABLE_AUTH=true
TINA_GIT_DEBOUNCE_MS=3000

I’ve added the Tina Git Debounce to 3000 ms because I’ve noticed GraphQL issues on the cloud editor when leaving this at its default of 1000 ms.

The authentication related environment variables will allow us to toggle and safely use our Basic Auth. It will require the following updates to your middleware in gatsby-config.js:

developMiddleware: app => {
if (process.env.ENABLE_AUTH) {
app.use(
basicAuth({
users: { [process.env.AUTH_USER]: process.env.AUTH_PW },
challenge: true,
realm: "your app name",
})
)
}
},

Restart the app and make sure that your new username and password credentials work correctly. Once confirmed, make sure you commit and push all of the new code to GitHub.

Corresponding commit: https://github.com/Integral-Stack/tinacms-cloud-editor-tutorial/commit/d33eef357af4569f9e232699fca794604403a0dc

Setup AWS resources

Congratulations, you’ve made it to the DevOps portion of the tutorial. We are now going to move our editor into the cloud.

If you are new to AWS, you may want to do some other tutorials to get familiar with it and at the very least learn to secure your account. If you already have an AWS account, please login. We are going to start out by provisioning our EC2 instance.

Once you login to your AWS account, navigate to EC2 dashboard and “launch” an instance. We are going to select the default “Amazon Linux 2 AMI (HVM), SSD Volume Type” on a “t2.micro” instance type. If you are free tier eligible, this selection qualifies as free, and if not the on-demand pricing for this configuration is very cheap. If you want to save even more money, you could look into running spot or reserved instances. The cloud editor basically only has to handle traffic from a few editors.

EC2 Dashboard -> Choose AMI -> Configure Instance

On the “Configure” page, we can leave all of the defaults

On the “Add Storage” page, we will use the default EBS storage.

On the “Add Tags” page, we will add a key value pair of Name: YourAppName so that it is easier to track.

On the “Security Groups” page, we will want to create a new Security Group for the cloud editor to be accessed via SSH (port 22), as well as a custom TCP rule to expose the the editor on port 8000. Then, follow the prompts to launch the instance.

Before launching the instance, you will be prompted with a modal to choose or create a key pair for the new instance. Go ahead and create a new pair, name it appropriately, download it, and launch your instance.

Before we get started provisioning the instance, I suggest you setup an Elastic IP and attach it to your newly running instance. Elastic IP’s are free as long as they are attached to an instance. This will maintain a default DNS and IP, so that the IP and DNS aren’t released if we stop and start our instance.

Elastic IP

Provisioning EC2 Instance

Now that you’ve downloaded your PEM key, lets get prepared to connect to our instance via SSH. We will move our pem key from our downloads folder to our .ssh folder. Then we will secure our pem key with the chmod command, only allowing user, read access.

mv ~/Downloads/YourAppKP.pem ~/.ssh
chmod 400 ~/.ssh/YourAppKP.pem

In order to connect via SSH we will need our the IPv4 Public IP of our running instance. Go back to your browser, visit the Instances page, and click on your instance to see its details. The public IP should be available from the “Description” tab.

My public IP is 3.224.202.92 . Make sure to replace that with your own IP in the following command.

ssh -i ~/.aws/YourAppKP.pem  ec2-user@3.224.202.92

Once you run this command, you should be connected to your EC2 instance via SSH. Let’s get the instance ready for use by installing the necessary dependencies.

sudo yum update -y
sudo yum install git -y
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
. ~/.nvm/nvm.sh
nvm install v12
npm install -g yarn

If all of that went smoothly, you will now have git, node 12.*, npm, and yarn installed on your instance. The next thing we will need to do is allow our instance to communicate with GitHub via a key pair that we will generate on our instance. Replace the following command with your own email and follow the default prompts.

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

We will now need to move our newly created public key to our GitHub settings of the account that we used to setup our repo. Run the following command and copy the output of the public key.

cat ~/.ssh/id_rsa.pub

Log into GitHub and navigate to Settings -> SSH and GPG keys, and then click on the “New SSH Key” button. Give the key a title, paste in your public key, and save.

Your instance should now be able to push and pull from your GitHub repo. Go back to your EC2 command line and clone your project onto the instance. Replace the project name with your own.

git clone git@github.com:Integral-Stack/tinacms-cloud-editor-tutorial.git

Run your cloud editor

From our SSH command line, let’s cd into the project, and install our npm dependencies

cd tinacms-cloud-editor-tutorial
yarn install

Now go ahead and use vim or nano to create a file called .env.cloud and copy and paste your environment variables from your local .env.cloud file into this new file. When this is done, you can delete .env.cloud from the repo locally. We only need it for the cloud editor.

Before we run the cloud editor, we will also want to increase our security by running from a branch other than master. This means that changes made to our website via the cloud editor will be committed and pushed on this new branch to GitHub. If you make production builds from master, this will protect someone from doing any damage if your cloud editor credentials are compromised. Keep in mind that with this strategy you will need to periodically make a PR with the content updates into master and deploy them to production. I’m going to use a branch called cloud-editor , but you are welcome to use whatever you’d like.

git checkout -b cloud-editor

We are now ready to run our cloud editor. We are going to need to run the app with a host of 0.0.0.0 (INADDR_ANY) rather than localhost in order for it to be exposed properly to the public via our security group that we set up earlier. We will also need to run the process detached from our SSH session, so that it can live and run in the background.

For these two reasons, we simply can’t just run yarn start. Let’s test the server start command that we will be using before we move on.

yarn develop -H 0.0.0.0 -S

If you receive the following error, you will need to create a ca-certificates directory.

cp: cannot create regular file ‘/usr/local/share/ca-certificates/devcert.cer’: No such file or directory

Run the following command to create that directory:

# Run this command to fix the error
sudo mkdir /usr/local/share/ca-certificates

Once you’ve confirmed that your dev server can run in the cloud, we will want to run our command in detached mode. In order to do this, we will leverage nohup.

nohup yarn develop -H 0.0.0.0 -S &

We will not see the output of the command, but we can check that the Gatsby dev server is up and running by visiting the live site. To do this, navigate back to the AWS EC2 console and copy your “Public DNS (IPv4)” address from the “Description” tab, paste it into your browser. Make sure to add https:// at the start of the url and add the port :8000 to the end before hitting enter. My URL looks like this:

https://ec2–3–224–202–92.compute-1.amazonaws.com:8000

Since we are using a self-signed SSL cert, you should land on the following page, where you will need to click “Advanced” -> “Proceed to…” in order to access the site. You will only be prompted one time with this message.

If your cloud editor loads, congratulations! You should be prompted first for your username and password, after which you will be able to make edits to your webpage. The second thing you will want to check is the TinaCMS “Save” functionality. This action should generate and push a new commit from your EC2 instance to GitHub. After saving some edits, go to GitHub and make sure the commit shows up on the current branch you are running.

That concludes the tutorial. Please comment or ask questions below.

How can we improve?

Infrastructure as code.

Theoretically, the server should be provisioned via code. This means utilizing a Linux docker image with node and installing yarn and git via the Dockerfile. In addition, you’ll want to inject all of your secrets to your container at runtime.

If you’d like to go the AWS route, this would probably include utilizing ECS for your container management and injecting your secrets from either Parameter Store or Secrets Manager. These secrets would need to be defined in your ECS Task Definition, so that they would be injected at runtime. In this setup you would be able to do port mapping via Docker to the host target of 8000 and expose the app on port 80. The container(s) would sit behind an Application Load Balancer, which would allow you to setup your DNS in Route53 to point to the ALB and you could easily add https by setting up an SSL cert with Certificate Manager.

And, if you wanted to take this a step further, you could set this up via CloudFormation templates so that the AWS resources are provisioned via code.

That’s a lot! If anyone decides to do that, please let me know because I would like to use the code.

--

--

I'm an independent software engineer and nomad that enjoys many of the fine things on the madre tierra. https://seanmichael.me