Preface

Firstly welcome, I'm going to talk through how I've come about making this blog and the processes involved.

Many times I've wanted to start and continue making a blog but either life or the process has gotten in the way. My main focus is a secure static HTML artifact, along with an easy to use Content Management System (CMS). Which provides the usual enriched content options (images, videos etc) but with code snippet inclusion which can be any programming language.

Architecture

Basic overview of end goal services

I wanted to keep the CMS virtual machine privately accessible from my home network, then make use of my already existing Jenkins CI/CD virtual machine. This would allow me to communicate with the CMS and then compile it into static HTML, deploy it to a simple web server virtual machine and expose to the internet. In the diagram above the red lines indicate data which takes place on the private network and the green lines are public traffic.

Technology Choices

Firstly for the CMS if possible I'd like to try a new technology that I hadn't previously tried, for this I settled on Ghost it looked easy to setup, use, developer friendly and I knew that I could plug it directly in to Gatsby a fast and modern site generator for React.

Ghost is an open source, professional publishing platform built on a modern Node.js technology stack
Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps

Setting up the CMS

To install Ghost it has a couple of prerequisites

  • A computer running MacOS, Windows or Linux - Debian 9
  • A supported version of Node.js - Node 10
  • Either yarn or npm to manage packages - NPM
  • A clean, empty directory on your machine
# Install NodeJS APT repository using their provided script
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash

# Install Node.js
sudo apt-get install -y nodejs
 
# Check installation
# should return v10.21.0
node -v
# should return v6.14.4
npm -v
Install NodeJS and NPM

Once this has been installed you are ready to install the Ghost package on the machine/computer.

# Install the Ghost-CLI tool globally
sudo npm install ghost-cli@latest -g
 
# I'm using ghost in this example; you can use whatever you want
sudo mkdir -p /var/www/ghost
 
# Replace ansible with the name of your user who will own this directory
sudo chown ansible:ansible /var/www/ghost
 
# Set the correct permissions
sudo chmod 775 /var/www/ghost
 
# Change directory into it the directory you've created
cd /var/www/ghost
 
# Install ghost with SQlite 3 as ORM Layer and its internal IP:Port
# by default Ghost will listen on localhost but I changed this to come up on the VMs private address so its available to HAProxy
ghost install --url "https://ghost.atathome.me" --ip "GHOST_VM_IP" --port 2368 --db "sqlite3"
Install Ghost-CLI tool

The Ghost-cli installer will ask a few questions during install, we will need the systemd service file and we do want to start ghost an example output should look like the following once completed. There are many ways to setup Ghost using NGINX for example, more can be found in their documentation here.

ghost install --url "https://ghost.atathome.me" --ip "GHOST_VM_IP" --port 2368 --db "sqlite3"
✔ Checking system Node.js version
✔ Checking logged in user
✔ Checking current folder permissions
System checks failed with message: 'Linux version is not Ubuntu 16 or 18'
Some features of Ghost-CLI may not work without additional configuration.
For local installs we recommend using `ghost install local` instead.
? Continue anyway? Yes
System stack check skipped
ℹ Checking operating system compatibility [skipped]
✔ Checking memory availability
✔ Checking for latest Ghost version
✔ Setting up install directory
✔ Downloading and installing Ghost v3.22.1
✔ Finishing install process
✔ Configuring Ghost
✔ Setting up instance
+ sudo useradd --system --user-group ghost
+ sudo chown -R ghost:ghost /var/www/ghost/content
✔ Setting up "ghost" system user
Nginx is not installed. Skipping Nginx setup.
ℹ Setting up Nginx [skipped]
Nginx setup task was skipped, skipping SSL setup
ℹ Setting up SSL [skipped]
? Do you wish to set up Systemd? Yes
✔ Creating systemd service file at /var/www/ghost/system/files/ghost_ghost-atathome-me.service
+ sudo ln -sf /var/www/ghost/system/files/ghost_ghost-atathome-me.service /lib/systemd/system/ghost_ghost-atathome-me.service
+ sudo systemctl daemon-reload
✔ Setting up Systemd
+ sudo systemctl is-active ghost_ghost-atathome-me
? Do you want to start Ghost? Yes
+ sudo systemctl start ghost_ghost-atathome-me
+ sudo systemctl is-enabled ghost_ghost-atathome-me
+ sudo systemctl enable ghost_ghost-atathome-me --quiet
✔ Starting Ghost

Ghost uses direct mail by default. To set up an alternative email method read our docs at https://ghost.org/docs/concepts/config/#mail

------------------------------------------------------------------------------

Ghost was installed successfully! To complete setup of your publication, visit: 

    https://ghost.atathome.me/ghost/

ghost-cli install output

Internally I'm already using HAProxy as an application load balancer for some other internal/external services. So it was a simple case of adding the ACL and backend, by default Ghost runs on port 2368 so using HAProxy this is proxied to https/443.

frontend https
...
	acl frontend-ghost hdr_sub(host) -i ghost.atathome.me
	use_backend backend-ghost if frontend-ghost

...
backend backend-ghost
	server dc0-ghost-p1a GHOST_VM_IP:2368 check
	timeout server 30s
haproxy.cfg

This completes the infrastructure setup for Ghost, if we head over to the URL  (https://ghost.atathome.me/ghost/) given in the install output we should now see the welcome page if all was successful.

Setting up the Jenkins Project

This section of the tutorial is going with the understanding that you know the basics of setting up Jenkins projects and that you have already configured Jenkins with a ssh key which allows access to the repository, this process is very similar for both GitHub and BitBucket (I'm using BitBucket for the purposes of this tutorial).

Pipeline Diagram

The above diagram shows how the build project is connected up. It starts with Git as a source control which Jenkins checks out into a workspace, this then will invoke a Jenkinsfile Pipeline. The steps for this pipeline are as follows

  • Install NPM Dependencies
  • Run the Gatsby Build Step
  • Synchronise static website to destination server

The Gatsby Build Step communicates via the Ghost API to retrieve information needed to generate the static website.

// Give ourselves a global version, so things can be versioned together
def artifactVersion = "0.0.0"
def scmVars

timestamps {
  try {
    node {
      // Make the dir nice and fresh
      deleteDir()

      // Checkout
      scmVars = checkout scm
      milestone 1

      // little bit of a fudge so branch name is populated when not in a multi-branch project
      env.BRANCH_NAME = scmVars.GIT_BRANCH.replace('origin/', '')
      echo "Running ${env.BUILD_TAG} - ${env.BRANCH_NAME}"

      // Store the version we're building
      artifactVersion = version(scmVars);
      currentBuild.description = artifactVersion
    }
    
    milestone 2

    stage('Install') {
      node {
	    sh "npm ci"
      }
    }

    milestone 3

    stage('Build') {
      node {
        sh "npm run build"
      }
    }

    milestone 4    
    
    if (env.BRANCH_NAME.startsWith('master')) {
      stage('Deploy') {
        node {
          // Sync compiled blog to target
          sh "rsync -avh public/ GATSBY_VM_IP:/var/www/gatsby"
        }
      }

      milestone 5
    }

  } catch (e) {
    // Tests failed
    currentBuild.result = 'FAILURE'
    throw e
  }
}

String version(scmVars) {
  // Read the version number from the version file and manipulate it if needed
  def v = sh "node -p \"require('./package.json').version\""
  def suffix = ''

  // If the file is empty, or doesnt exist, default to the Jenkins build tag so we can still trace it back
  if (v == null) {
    v = env.BUILD_TAG
  }

  if(env.BRANCH_NAME.startsWith('develop')) {
    suffix = '-dev.' + env.BUILD_NUMBER
  } else if(env.BRANCH_NAME.startsWith('master')) {
    // do nothing, the version is the version
    // TODO: make versioning automatic!!
  } else {
    suffix = '-' + scmVars.GIT_BRANCH + '+build.'  + env.BUILD_NUMBER
  }

  v = "${v}${suffix}".replace(" ",".").replace("/", "_")
  return v
}

Example Jenkinsfile from repository.

I have included the Jenkinsfile for reference above. To set this up start by clicking 'New Item' when logged into your Jenkins instance.

Jenkins create job item

Enter your Jenkins job name and select Pipeline and click 'OK' this will present you with the configuration page. Then select 'Do not allow concurrent builds' and scroll down to the Pipeline section. For Definition select 'Pipeline script from SCM', use Git as SCM value which will present you with information for entering the Repository URL, Credentials and Source Branch as pictured below.

Jenkins pipeline configuration settings

At the point you've entered your own details click 'Save' and it will go to the project overview page. This should then trigger a branch scan and if your Jenkinsfile is already in the repository it will start building.

Setting up the Web

The web machine is a very simple NGINX web server, which just serves static html. Its sat behind my existing HAProxy instance with the following frontend/backend configuration.

frontend https
...
	acl frontend-blog hdr_sub(host) -i blog.atathome.me
	use_backend backend-blog if frontend-blog

...
backend backend-blog
	server dc0-gatsby-p1a GATSBY_VM_IP:80 check
	timeout server 30s
haproxy.cfg

To setup the virtual machine, I installed NGINX and created a directory to server the static content from. The caveat being that the user needs to be accessible via Jenkins or a jenkins user will need permission to write in the content directory.

# I'm using gatsby in this example; you can use whatever you want
sudo mkdir -p /var/www/gatsby
 
# Replace ansible with the name of your user who will own this directory
sudo chown ansible:ansible /var/www/gatsby
 
# Set the correct permissions
sudo chmod 775 /var/www/gatsby
 
# Change directory into it the directory you've created
cd /var/www/gatsby

Then to get NGINX to server the static files I added the following conf file to the sites-enabled directory for NGINX.

server {
    server_name blog.atathome.me;
 
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
 
    location / {
        root 	/var/www/gatsby;
        index	index.htm index.html;
    }
}
/etc/nginx/sites-enabled/blog.conf
Once this is added you can enable the conf by reloading NGINX.
sudo systemctl reload nginx

In conclusion

Hopefully this article has talked you through the basics of setting up Ghost CMS and then integrating it into a Jenkins CI/CD Pipeline and then hosting on a static NGINX server.