Replacing Wordpress with Hugo - Part Two: Travis CI

After having moved this blog from WordPress to Hugo, my next objective is to have it built and deployed automatically to my web hosting server whenever I push a commit to GitHub. Travis CI is a hosted continuous integration service used to build and test software projects hosted at GitHub.

Instead of running Hugo local and upload the new website with an FTP client, I wanted to get past the issue of “runs on my machine” by making a portable, entirely reproducible environment. To achieve this goal, the code for this personal website is published to GitHub and continuously deployed using Travis-CI to my hosting provider.

The tools

  • Hugo – Generates our static website
  • GitHub – Hosts the source code and raw contents of the website
  • Travis CI – CI service which builds and deploy the website
  • Rsync – Transfers the Hugo generated static website to the web hosting server
  • Virtual Maschine - Helps us setup openSSL

Client Computer

Windows or macOS are updated throughout the year while we route your builds to Ubuntu Xenial 16.04. To get no problems with the compatibility of OpenSSL or Travis CI Client, we execute all commands on a virtual machine. To do this, we install the image ubuntu-16.04.6-server-i386 on Virtual Box to have a Linux environment identical to our build environment. We want to ssh into the virtual machine, thereforce we need to enable bridged networking instead of NAT. To enable bridged networking, open the Settings dialog of a virtual machine, go to the Network page and select Bridged Network in the drop-down list for the ‘Attached To’ field.

We just install with defaults. We use the command ‘ip a’ to find out the IP address of the virtual machine. From now on, we use ssh to control the machine:

ssh username@ip-address

Initial Setup

We update the virtual Machine and reboot:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get upgrade

Install travis

We need to install the Travis CI Client to do the encryption process. Getting ruby and rails to run on Ubuntu 16.04 LTS is not easy. We cannot use apt to install ruby, as we need at least Ruby version >= 2.4.0 to for the latest version of Travis.

I followed this guide [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-ubuntu-16-04 which boiled down to the following commands:

sudo apt-get install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm3 libgdbm-dev
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
type rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
rbenv install 2.5.0
rbenv global 2.5.0
ruby -v

Output: ruby 2.5.0p0 (2017-12-25 revision 61468) [i686-linux]

echo "gem: --no-document" > ~/.gemrc
gem install bundler
gem env home
gem install travis
travis version

Output: Shell completion not installed. Would you like to install it now? y 1.9.0

We create a local copy of the repository:

git clone https://github.com/kulturfolger/vhit.de.git

Travis CI Build Configuration and Settings

We have to instruct Travis to download Hugo in its virtual machine to build our website. Builds on Travis CI are configured mostly through the build configuration stored in the ‘.travis.yml’. Placing ‘.travis.yml’ in our repository allows our configuration to be version controlled and flexible.

cd vhit.de
vi .travis.yml

Press ‘i’ for insert mode and escape to end insert mode. Type ‘:wq’ to write and quit.

dist: xenial

language: node_js
node_js:
  - lts/*

install:
  - wget "https://github.com/gohugoio/hugo/releases/download/v${HUGO_RELEASE}/hugo_extended_${HUGO_RELEASE}_Linux-64bit.deb"
  - sudo dpkg -i hugo*.deb

script:
  - hugo

env:

  global:
  - PRODUCTION=true
  - HUGO_RELEASE=0.69.2

The deployment

We want Travis to deploy the public directory generated by Hugo to the web server through a secure SSH connection. Instead of adding a username and password to the .travis.yml, we generate a pair of RSA keys. The public key is added to the trusted Keystore of the web server, and the private key is given to Travis so that it can copy the files. This way, the private key is not revealed to outsiders.

Generate a keypair using OpenSSL

We can now generate a dedicated RSA keypair, the private key: deploy_rsa, and the public key: deploy_rsa.pub.

ssh-keygen -t rsa -b 4096 -C 'build@travis-ci.org' -f ./deploy_rsa

Don’t commit anything yet to your git repository; we must encrypt the private key first.

Authorize Travis CI for GitHub

We need to Authorize Travis CI for GitHub, which can be easily done by logging into https://travis-ci.org/signin with our GitHub account. It’s important not to confuse travis-ci.org with travis-ci.com. We want to use travis-ci.org.

Activate our GitHub repositories

We need to click on the Button ‘ACTIVATE ALL REPOSITORIES USING GITHUB APPS’ to install the GitHub App integration.

Log into travis on commandline

travis login --org
travis endpoint

The API endpoint has to be ‘https://api.travis-ci.org/'.

Encrypt the private key

Travis CI uses asymmetric cryptography. For each registered repository, Travis CI generates an RSA keypair. Travis CI keeps the private key private but makes the repository’s public key available to those who have access to the repository. The Travis client encrypted the deploy_rsa private key using the repository’s public key into the file deploy_rsa.enc. It added a decryption key, stored as an environment variable, to the .travis.yml file of the project, with some other lines which decrypt it during the build.

We can now encrypt the private key using the Travis CI Client, after having first logged into Travis:

travis encrypt-file deploy_rsa --add

Check environment variables

We can check the existence of the environment variable with the following command:

travis env list

This command should give us the output of the secure environment variables for decryption. The content is hidden with ‘[secure]'.

encrypted_<…>_key=[secure] encrypted_<…>_iv=[secure]

Copy Public key to hosting web server

We install our public key in the hosting web servers’s authorized_keys ('/.ssh/authorized_keys’) and delete it from the local computer, as well as the unencrypted version of the private key:

ssh-copy-id -i deploy_rsa.pub <deploy-user>@<web-server-host>
rm deploy_rsa deploy_rsa.pub

Commit the encrypted private key deploy_rsa.enc to the git repository

git add deploy_rsa.enc
git commit -m "Add private key"
git push

Modify OpenSSL order of execution

We edit .travis.yml, moving the decryption line from the before_install: section to the before_deploy: section. With the added decrypting commands, we ensure that the private key is decrypted and loaded into memory before the deploy process starts.

before_deploy:
- openssl aes-256-cbc -K $encrypted_<...>_key -iv $encrypted_<...>_iv -in deploy_rsa.enc -out deploy_rsa -d
  - eval "$(ssh-agent -s)"
  - chmod 600 deploy_rsa
  - ssh-add deploy_rsa 
  - echo -e "Host ${DEPLOY_HOST}\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config

Deployment configuration

We’re now ready to set up the deployment, which is done with rsync.

As a security measure, we’d like to keep secret:

  • the web hosting server
  • the user used to do the deployment, and
  • the directory where the website resides.

We encrypt these as environment variables using the Travis client, just like we did before with the private key.

travis encrypt DEPLOY_HOST=<web-server-host> --add
travis encrypt DEPLOY_USER=<deploy-user> --add
travis encrypt DEPLOY_DIRECTORY=<deploy-directory> --add

Each command adds a new line to the env.global: section of your .travis.yml file, beginning with - secure: followed by a long string of random-looking ASCII characters:

env:
  global:
    - PRODUCTION=true
    - HUGO_RELEASE=0.54.0
    - secure: <generated by travis gem>
    - secure: <generated by travis gem>
    - secure: <generated by travis gem>

We’re now ready to append the commands to transfer the website data to the web hosting server:

deploy:
  provider: script
  script: rsync -r --quiet --delete ${TRAVIS_BUILD_DIR}/public/ ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_DIRECTORY}
  on:
    branch: master

Final Check

If you followed these instructions, you should have a .travis.yml looking like that:

dist: xenial
sudo: required

language: node_js
node_js:
  - "lts/*"

install:
  - wget "https://github.com/gohugoio/hugo/releases/download/v${HUGO_RELEASE}/hugo_extended_${HUGO_RELEASE}_Linux-64bit.deb"
  - sudo dpkg -i hugo*.deb

script:
  - hugo # build the website

before_deploy:
  - openssl aes-256-cbc -K $encrypted_<...>_key -iv $encrypted_<...>_iv -in deploy_rsa.enc -out deploy_rsa -d
  - eval "$(ssh-agent -s)"
  - chmod 600 deploy_rsa
  - ssh-add deploy_rsa
  - echo -e "Host ${DEPLOY_HOST}\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config

deploy:
  provider: script
  skip_cleanup: true
  script: rsync -r --quiet --delete ${TRAVIS_BUILD_DIR}/public/ ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_DIRECTORY}
  on:
    branch: master

env:
  global:
    - PRODUCTION=true
    - HUGO_RELEASE=0.69.2
    - secure: <generated by travis gem>
    - secure: <generated by travis gem>
    - secure: <generated by travis gem>

Conclusion

We have completed all steps. Its time to add this file to our commit and push it to our repository. The build process is triggered by our pushed commit to the repository.