There are a few things I consider myself bad at, this blog aims to fix the worst ones. Documentation and motivation. Wait! before you assume that I am not motivated and immediately write this off, I would like to offer a defence; My motivation comes in bursts, sometimes I spend many hours on learning new tech, other times its just more relaxing to fire up a game and switch off. The goal of this blog is to remind myself to document my learning, as well as to keep me building out my skills and knowledge.

Research and scoping

I’ll admit, I’m not the best at web development. So developing my own blog would be difficult and even if I was the best, I’m not sure I’d want to have to maintain a codebase before I can write about anything. So I started out by looking at the usual suspects in the content-management world.

Wordpress is always the first name to come up, along with Drupal. both are large feature-filled applications that can be wrangled into blog sites with a bit of effort and PHP. The issue here for me (other than not knowing any PHP - Which I could largely avoid anyway), is the need to edit within the CMS itself. You log in to write your posts and no matter how many times I am reassured by someone that this is secure, having a login on a simple blog places a very real attack surface out on the web. The simplest way (in my view!) to secure a site like this is to just not take the risk in the first place. So what are the other choices?

Ghost was another name I saw, using Javascript instead I’d be a bit more comfortable, but it still requires editing posts within the running web app! Besides my previous reservations around the security of such a platform, this also creates deployment constraints. The blog becomes stateful and the internal storage of all my posts and configuration managed within the filesystem or within a database, this requires backups - or I risk losing it all!

Finally, I came across the notion of a static site generator which ticked all my boxes. You write content offline in your editor of choice using markdown, test the render locally then generate static HTML to be hosted on a basic web server - simple! So which to choose? in my experience it’s always best to go for something popular, not necessarily because it will be better but because documentation and stack overflow questions (ones with good answers at least) will be readily available. Enter Hugo! A neat and simple static site generator written in Go, using Go templates. This is good for me as I already have experience with Go templates. Many themes are available too, so I can use a nice looking one of those.

The plan

Let’s write down the plan now, before starting, so I can refer back to it if I get lost, and also see how close I was to reality once it’s all up and running!

  • Write the blog locally using a nice editor with markdown preview support (Jetbrains PyCharm)
  • Push the code to gitlab, and have a runner generate the static files, then stuff these into an NGINX docker image.
  • Automatically deploy to kubernetes.

So what things need to be in place to achieve this?

  • A Git repo with the Hugo project in it
  • Gitlab CI/CD config to build the docker
  • Kubernetes cluster
  • A cup of tea!

Setting up Hugo

I installed Hugo from a .deb downloaded from their release page as the apt repos on ubuntu are a few versions behind, but there’s a number of different supported repos if you check here

I then followed this quickstart guide. I’m not going to go too far into how I set up the site, overall the process was painless and before you know it, I had a shiny looking static site going, and guess what… you’re looking at it!

Building a Docker image

Before I get ahead of myself and automate the build process, a dockerfile is needed. I’ll be certain to make a post in the future looking at docker on its own in much more detail at some point, so this will be a brief summary. If you’re unfamiliar with docker, sit through this bit it’ll be worth it - I promise

Docker basically takes all the pain out of installing dependencies on a hosting system, instead you declare how to install the dependencies once, build that into an image, then just deploy that image. Images are stateless, and no matter where it runs, they’re always the same when they start - perfect for a static blog!

The main part of this process is the dockerfile, Here’s one I made earlier:

FROM alpine:3.13 as build
ARG HUGO_VERSION="0.83.1"
RUN apk add --update wget
RUN wget --quiet "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.tar.gz" && \
    tar xzf hugo_${HUGO_VERSION}_Linux-64bit.tar.gz && \
    rm -r hugo_${HUGO_VERSION}_Linux-64bit.tar.gz && \
    mv hugo /usr/bin
COPY ./ /site
WORKDIR /site
RUN hugo

FROM nginx:alpine
COPY --from=build /site/public /usr/share/nginx/html
WORKDIR /usr/share/nginx/html

This might look like a lot to take in, but let’s break it down:

  • FROM alpine:3.13 as build This is the starting point for the container, using a base image. Alpine is a cut-down more lightweight linux, it manages that by not including the majority of libraries and using alternative packages, so be careful if you depend on something specific. Alpine is 5.6MB vs Ubuntu’s 63MB, so it’s a significant saving. as build names this intermediate container, so I can use this as a multi-stage build - we’ve not seen the last of this.
  • ARG HUGO_VERSION="0.83.1" set up a variable so that the version can be updated nicely.
  • RUN apk add --update wget install wget, and the line after this uses wget to download, unzip, delete the zip, then add it into the bin directory.
  • Then COPY the local directory into site.
  • WORKDIR /site switches directories.
  • Then RUN hugo to build the static HTML.
  • FROM nginx:alpine - another starting point. From a fresh NGINX container this time, with everything needed to host our site.
  • COPY across --from=build the output HTML and add that into the NGINX container, note the use of the --from here, allows copying from the build stage container, so the final output container just contains the static HTML, and not the hugo binaries.

That’s everything! a docker image which can built using:

docker built -t griph-blog .

However, building from the command line is casual, lets get this automated.

Gitlab Runner config

Gitlab_logo

My CI/CD stack revolves around Gitlab runners, and they’re pretty neat. You see everything that they’re doing in the same place your code and issues are stored. Plus, you can self-host the runners themselves without hosting gitlab, so you get the best of both worlds. The git repo is stored safe on their platform, along with runner management and won’t disappear if something goes wrong leaving your own servers are offline. The runners however can be easily setup within docker, or can run in virtual machines while still calling out to gitlab.com.

Gitlab’s CI/CD uses a yml file stored within the git repo, this defines the commands to be sent to runners:

stages:
  - dockerbuild
  - deploy

dockerbuild:
  stage: dockerbuild
  image: docker:latest
  services:
    - docker:dind
  tags:
    - dind
  rules:
    - if: '$CI_COMMIT_REF_NAME == "main"'
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    GIT_SUBMODULE_STRATEGY: recursive
  before_script:
    - docker info
    - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
  script:
    - docker pull ${CI_REGISTRY}/${CI_PROJECT_PATH}:latest || true
    - docker build --cache-from ${CI_REGISTRY}/${CI_PROJECT_PATH}:latest -t ${CI_REGISTRY}/${CI_PROJECT_PATH}:latest -t ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHA} --pull .
    - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHA}
    - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:latest
  after_script:
    - docker logout ${CI_REGISTRY}

deploy:
  stage: deploy
  tags:
    - k8s
  rules:
    - if: '$CI_COMMIT_REF_NAME == "main"'
      when: always
  variables:
    GIT_STRATEGY: none
  before_script:
    - kubectl --kubeconfig="$KUBE_CONFIG_FILE" describe deployment/griph-blog
  script:
    - kubectl --kubeconfig="$KUBE_CONFIG_FILE" rollout restart deployment/griph-blog

Here there are two main stages, a docker build, and a deployment stage, so lets go over what they do.

The build stage uses a docker-in-docker container on a gitlab runner using the docker executor with TLS enabled. The important bit of config here is in the variables section GIT_SUBMODULE_STRATEGY: recursive is set because the hugo theme is installed as a git sub-module - so the runner needs to know to pull this before it builds. Everything else in here is a rather typical docker build pattern, note the use of --cache-from in the docker build command, this instructs docker to use the last version of the image as a cache, in an effort to speed up the builds a bit.

EDIT (2021-05-20): Since writing this post, I’ve moved over to Kaniko using the Gitlab Kubernetes runner, read more here

The deploy stage is next, and uses a shell runner on a machine with kubectl installed (it’s actually a rpi4, but more on the hardware another time), on the same network as the cluster. The kube-config is injected as a variable within gitlab - when creating variables set the type to file.

so, that’s the runners going - but it won’t update anything if there’s nothing setup initially in the cluster…

Deploy to kubernetes

This part of this post will feel very much like just draw the rest of the owl But don’t let that put you off! I’ll also be sure to document kubernetes a bit better in another post. So I won’t judge if you decide to deploy the docker container on a server and be done with it.

My kubernetes cluster is using Rancher Kubernetes engine This will be the subject of a post very soon as well, but here I’ll just go over the deployment of the docker image created in the previous sections.

there are two files here, a deployment, and a service. let’s look at the deployment first:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: griph-blog
  labels:
    app: griph-blog
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 0
  selector:
    matchLabels:
      app: griph-blog
  template:
    metadata:
      labels:
        app: griph-blog
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: griph-blog
          image: registry.gitlab.com/.../griph-blog:latest
          imagePullPolicy: Always
          securityContext:
            allowPrivilegeEscalation: false
          ports:
            - containerPort: 80
              name: griph-blog-tcp
              protocol: TCP

A lot of this is boilerplate/fluff but there are two sections I want to highlight. replicas: 3 will create 3 separate instances of the container, all spread out across the cluster. imagePullPolicy: Always means that before starting the container, always pull down the image and update.

The service configuration is as follows:

apiVersion: v1
kind: Service
metadata:
  name: griph-blog-service
  labels:
    app: griph-blog
spec:
  type: NodePort
  ports:
    - port: 8080
      targetPort: 80
      nodePort: 30080
      protocol: TCP
      name: griph-blog-tcp
  selector:
    app: griph-blog

NodePort services bind all instances of a running container to an automatic round-robin load-balancer on the same port ( 30080) on all nodes. To manage this externally, another balancer is needed to loop across each node for full High-Availability, in my case NGINX running on a Raspberry Pi handles this.

Final Thoughts

It goes without saying that if you’re reading this… It works! So with a limited amount of time spent (8 hours across 2 days) I now have a blog. Watch this space for more posts.