Revinate’s engineering blog (the very blog you’re reading right now) was originally hosted on Ghost. As we start to migrate more and more of our applications to Kubernetes, we decided it would be a good experiment to migrate our engineering blog to our Kubernetes cluster as well. Since this is an engineering blog, every contributor is also a Git committer. This allows us to use a simple, static website as the blog. Contributors will commit their blog posts to Git, and our CI/CD pipeline will build the static website and deploy it to Kubernetes.

For the static site generator, we chose Jekyll, a blog-aware generator that does most of the heavylifting of generating our new blog. By default, Jekyll uses Markdown for authoring pages. However, at Revinate we author much of our internal documentation using AsciiDoc. For consistency we would like to use AsciiDoc for the blog as well. Fortunately we came across jekyll-asciidoc, an AsciiDoc plugin for Jekyll powered by Asciidoctor, as well as jekyll-asciidoc-quickstart, a starter project based on the jekyll-asciidoc plugin. We chose the jekyll-asciidoc-quickstart project as the starting point for our new blog. This project is based on an older version of Jekyll, and a number of fixes and cleanups were needed to make it work, but we were able to modify it to suit our purposes.

Next we needed to migrate our existing blog posts from Ghost to Jekyll. Since we were using hosted Ghost, we used jekyll_ghost_importer to extract the blog posts from a Ghost export. The extracted blog posts are in Markdown, but it was an easy task to convert them to AsciiDoc due to overlaps in syntax.

Generating the static site locally is as simple as running jekyll build. But we decided to make the process even simpler for blog contributors by running the static site generation as part of our CI/CD pipeline. We use Jenkins with the Pipeline Plugin, combined with a Docker engine on the Jenkins workers, to accomplish this. We therefore have the following stage in the Jenkinsfile:

stage 'Generate site'
node {
    checkout scm
    sh "docker run --rm -v `pwd`:/usr/src/app -w /usr/src/app ruby:2.1 ./build.sh"
    stash name: 'site', includes: '_site/**'
}

Where build.sh is a simple script that installs node.js (required by Jekyll 2) and the Rubygem dependencies, and runs jekyll build:

#!/bin/sh

apt-get update && apt-get install -y --no-install-recommends nodejs
bundle install
jekyll build

We decided to use a standard Ruby Docker image and install the Rubygem dependencies on-the-fly, rather than using one of the Jekyll Docker images, because we wanted to ensure the Rubygems installed have versions specified in our Gemfile.lock file.

Once the site has been generated, Jenkins can build and push the Docker image that will be used by Kubernetes. This is the second stage in the Jenkinsfile:

stage 'Docker build and push'
node {
    checkout scm
    unstash 'site'
    sh "docker build -t engineering-blog --no-cache ."
    sh "docker push engineering-blog"
}

The Dockerfile is a very simple static web server based on nginx:

FROM nginx:latest

COPY _site /usr/share/nginx/html

With the new Docker image in our registry, the final stage of the Jenkinsfile triggers another Jenkins job to deploy the Docker image to Kubernetes:

stage 'Kubernetes deploy'
node {
    build job: 'kube-deploy', parameters: [
        [$class: 'StringParameterValue', name: 'application', value: 'engineering-blog'],
        [$class: 'StringParameterValue', name: 'namespace',   value: 'public'],
        [$class: 'StringParameterValue', name: 'environment', value: 'prod']
    ], propagate: false, wait: false
}

The details of the kube-deploy Jenkins job are beyond the scope of the present blog post and may be covered in a future blog post. The end result of this build pipeline is the blog you’re reading right now.