Deploying a blog with Hugo, TravisCI, and Google App Engine

October 31, 2017

Because I wanted to start blogging, I recently moved tech.taymor.io from a couple of static html and scss files* to a static site with a blog generated with Hugo, deployed with Google App Engine. I hit a few snags trying to automate this site with TravisCI and Google App Engine, so I thought I’d share my process.

Build a Hugo app, make it work locally

I’ll handwave over this a bit. I’ve found Hugo to have a bit of a learning curve, but that’s out of scope for this post. However, assuming you have a Hugo site that looks like you want it to when you serve with hugo server, then you’re ready to deploy.

Integrate with TravisCI

Make your Github repo public. (You have to pay for private repos on Travis. If you care about that, you can probably integrate with mostly the same steps. Look at TravisCI docs for how to set up for a private repo)

Sign in to travisci.org with your Github account and enable Travis for your repo.

Write a no-op Makefile in the root dir of your git repository.

run:
	echo "no-op makefile for travis"

Add a basic, empty golang .travis.yml file in your root dir like so:

language: go
go: 1.9

Commit and push your changes to Github. Travis should kick off a new build automatically and you should see your no-op makefile for travis print out in the Travis run, which should go green. You now have a basic test and deployment pipeline set up, even if it does nothing. Every time you push your commits to Github, your pipeline will run.

Go get Hugo and run Hugo in your Travis file

Add the following to your .travis.yml to get and build Hugo, following the Hugo installation docs for installing from source:

before_install:
- go get github.com/kardianos/govendor
- govendor get github.com/gohugoio/hugo
- go install github.com/gohugoio/hugo

Update your Makefile to run Hugo in verbose mode. For good measure, add a clean step too.

run:
	hugo --verbose
clean:
	rm -rf public

The clean step will allow you to run make clean and remove your local generated site.

Try running make from the root dir of your repo. You’ll see that hugo generates a public dir with all your static files.

Set up Google App Engine:

Follow these docs to make a google project.

Get the gcloud cli here for testing (Optional. You could just keep pushing to travis but that’s a slower feedback loop)

Write an app.yaml file in your root repo dir like so:

---
runtime: python27 # you don't care what the runtime is for static files. Python27 was easy
api_version: 1
threadsafe: true

handlers:
- url: /$
  static_files: public/index.html
  upload: public/index.html
- url: /posts # serve the list of posts
  static_files: public/posts/index.html
  upload: public/posts/index.html
- url: /posts/(.*)/ # serve each post
  static_files: public/posts/\1/index.html
  upload: public/posts/(.*)
- url: /(.*) # serve my css
  static_files: public/\1
  upload: public/(.*)

What is the app.yaml doing?

Primarily you’re setting up handlers to server your content. The url is the endpoint covered by that handler. The static_files say what static file to serve from the files uploaded on the server. The upload_files indicates what files to upload to the server.

The regex capture groups (.*) in the url are then inserted in the places you see \1 in the static_files section.

There’s probably a more efficient, general way to set up your handlers. However, I had a very hard time getting the posts themself (and not just the posts index page) to serve, and this does work.

Try deploying your app with gcloud app deploy. Check that all your pages deploy successfully, not just the index.

Deploy to Google App Engine with Travis

Travis’s docs on deploying to Google App Engine are pretty good. Be sure to only check in the encrypted key, not the unencrypted key!!

I did have some problems getting Travis to decrypt the key with the –add flag. This is what ultimately ended up working for me:

before_install:
- openssl aes-256-cbc -K $encrypted_6b8ed8e8b3dc_key -iv $encrypted_6b8ed8e8b3dc_iv
  -in travis-ci-account-key.json.enc -out travis-ci-service-account-key.json
  -d

The travis.yml deploy section looks like this:

deploy:
  provider: gae
  keyfile: travis-ci-service-account-key.json
  project: <project-name>
  skip_cleanup: true

Tada!

Your app should be now running on Google App Engine, and should be redeployed by TravisCI every time you push new commits to Github.

I highly recommend you set up https for your blog. To do that, you can either use lets encrypt or the Google App Engine SSL beta (which is what I chose).

Once you have an SSL certificate set up, go to your app.yaml file, and for each route, add secure: always. This will redirect http traffic to https.

This is what worked for my site. Let me know if you find it helpful.

Why did I choose this tooling stack?

I’m an engineer on Cloud Foundry. cf push my-app is what I build. Why isn’t this a tutorial on “publishing a Hugo blog with Pivotal Web Services (Pivotal’s hosted Cloud Foundry)”?

The answer is twofold. First, I was curious about Google App Engine, and how it compares, if it does, to pushing an app with cf push. The answer is, for a low-traffic static “app” like this blog, I don’t think it much matters. At Cloud Foundry, we’re focused on the experience of operators at large companies, and their developers. For a few static files, if you don’t care who operates your platform, I don’t see any relevant difference.

So then we get to the second and main answer: Price. I have an employee Pivotal Web Services account, but you probably don’t. And for a single instance of a tiny, static blog like this with moderate traffic, Google App Engine’s free tier can’t be beat. PWS’s strengths lie elsewhere.

Why did I use Google App Engine over hosting the static files in Google Cloud Storage? Simply put, https. You can’t use https to serve a Cloud Storage website, and I beleive the whole web should be moving towards https.

If this site ends up with enough traffic to need to move past the free tier on Google App Engine someday, maybe you’ll see another tutorial on how to port it elsewhere. For now, this stack is cheap (I pay only for my domain name) and fairly easy.

tl:dr I was curious and it was cheap.

*And if you’re really curious, my previous “stack” for tech.taymor.io was a single Google Cloud Platform instance into which I would ssh and git pull and restart the apache server when I had updates. I know, it’s horrifying. But it worked okay enough for a site I almost never updated. It’s much much better and easier now, and it lets me add a blog I might update more frequently.