David's Tech Blog

Ramblings of a random Irish nerd.

04 Jan 2020

Continuous Deployment with Hugo on Kubernetes

Over Christmas I thought I'd re-vamp my personal blog. Previously a mostly out of date blog running on Wordpress that ran on a VPS. Some useful entries but mostly out of date.

I decided I didn't want to run anything too heavy - perhaps light enough to run on a Raspberry Pi, some googling and Hugo seemed to be popular.

  • Nice free themes
  • Content as Code approach
  • No DB to manage
  • Very little attack surface area(unlike Wordpress)

As a Software Engineer, git flows are familiar. With Hugo I can have a master branch and a staging branch, and have 2 different Hugo deployments track the 2 different branches. When I'm happy with the staging content I just merge staging into master and they're live.

Now, Hugo doesn't actually know much about git, and neither does Kubernetes. Enter the git-sync project. This does what it says on the tin. Tracks a Git repo and constantly polls for new content and checks it out. Perfect for something like continuous deployment!

One problem, it doesn't have an arm build! So I built one for your convenience davidjmarkey/git-sync-arm:v3.1.3

The next problem is the hugo doesn't have an arm build either. We're going to work around that in a different way later.

The usual flow for a Hugo site is to host is on Github. I have my site as private, so I need to do create a ssh keypair (using ssh-keygen) then upload the public part as a Github Deploy key. Below is an example. The known_hosts piece is simply Github's SSH fingerprint added.

For the ssh portion you'll have to base64 the private key portion of the keypair you added the deployment key for like so:

 base64 -w0 github-deploy

Sample secret manifest:

apiVersion: v1
data:
  known_hosts: Z2l0aHViLmNvbSBzc2gtcnNhIEFBQUFCM056YUMxeWMyRUFBQUFCSXdBQUFRRUFxMkE3aFJHbWRubTl0VURiTzlJRFN3Qks2VGJRYStQWFlQQ1B5NnJiVHJUdHc3UEhrY2NLcnBwMHlWaH
A1SGRFSWNLcjZwTGxWREJmT0xYOVFVc3lDT1Ywd3pmaklKTmxHRVlzZGxMSml6SGhibjJtVWp2U0FIUXFaRVRZUDgxZUZ6TFFOblBIdDRFVlZVaDdWZkRFU1U4NEtlem1ENVFsV3BYTG12VTMxL3lNZitTZTh
4aEhUdktTQ1pJRkltV3dvRzZtYlVvV2Y5bnpwSW9hU2pCK3dlcXFVVW1wYWFhc1hWYWw3MkorVVgyQisyUlBXM1JjVDBlT3pRZ3FsSkwzUktyVEp2ZHNqRTNKRUF2R3EzbEdIU1pYeTI4RzNza3VhMlNtVmkv
dzR5Q0U2Z2JPRHFuVFdsZzcrd0M2MDR5ZEdYQThWSmlTNWFwNDNKWGlVRkZBYVE9PQo=
  ssh: <base64 private key>
kind: Secret
metadata:
  name: git-creds

Now we can start working on the deployment. It should look something like this:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hugo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hugo
  template:
    metadata:
      labels:
        app: hugo
    spec:
      volumes:
      - name: git-secret
        secret:
          secretName: git-creds
          defaultMode: 0440
      - name: git-checkout
        emptyDir: {}
      containers:
      - name: git-sync
        image: davidjmarkey/git-sync-arm:v3.1.3
        args:
         - "-ssh"
         - "-repo=git@github.com:dmarkey/dmarkey.com"
         - "-dest=hugo"
         - "-branch=master"
         - "-depth=1"
         - "-root=/checkout"
        securityContext:
          runAsUser: 65533 # git-sync user
        volumeMounts:
        - name: git-checkout
          mountPath: /checkout
          readOnly: false
        - name: git-secret
          mountPath: /etc/git-secret
          readOnly: false
      - name: hugo
        env:
          - name: HUGO_VERSION
            value: 0.62.0
          - name: BASE_URL
            value: https://dmarkey.com
          - name: ENVIRONMENT
            value: live
        image: busybox:1.31.0-musl
        livenessProbe:
          httpGet:
            path: /favicon.ico
            port: 1313
          initialDelaySeconds: 1
          timeoutSeconds: 10
          failureThreshold: 20
        readinessProbe:
          httpGet:
            path: /favicon.ico
            port: 1313
          initialDelaySeconds: 1
          timeoutSeconds: 10
          failureThreshold: 20
        args:
          - sh
          - -c
          - >
            cd /tmp;
            wget https://github.com/gohugoio/hugo/releases/download/v$HUGO_VERSION/hugo_${HUGO_VERSION}_Linux-ARM.tar.gz;
            tar zxvf hugo_${HUGO_VERSION}_Linux-ARM.tar.gz;
            cd /checkout;
            /tmp/hugo server -s hugo --appendPort=false -e $ENVIRONMENT --bind 0.0.0.0 --baseURL $BASE_URL --buildDrafts
        securityContext:
          runAsUser: 65533 # git-sync user
        volumeMounts:
        - name: git-checkout
          mountPath: /checkout
          readOnly: false
      securityContext:
        fsGroup: 65533 # to make SSH key readable

Lets talk through this deployment.

Containers

git-sync container checks out our hugo site from Github.

For your site you will have to change the -branch and -repo arguments

The hugo container runs the hugo server.

As hugo is a go application, we can start with a minimal busy box container and simply download the binary distribution from their releases page on Github.

We then boot the hugo built-in webserver. This builds the static pages and then serves them on port 1313.

This offers fast reloading based on filesystem changes, so really suits the flow here. The documentation says that it's fine to use this in-built server in production.

Every time git-sync checks out a new version the hugo server will instantly deploy the changes.

Volumes

git-secret is the secret we added earlier. This is mounted in the git-sync container only.

git-checkout is an empty volume which is used to share the checked out code from Git between the git-sync container and the hugo container.

Probes

To ensure the pod doesn't get any traffic before it's ready, simple liveness and readiness probes try to download the favicon. Once it succeeds it will start routing traffic to the pod.

Conclusion

I really like this flow. Hugo is great! It's lightweight and really suits my style of working. It will probably encourage me to write more blog posts than before.

comments powered by Disqus