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-deploySample secret manifest:
apiVersion: v1
data:
known_hosts: Z2l0aHViLmNvbSBzc2gtcnNhIEFBQUFCM056YUMxeWMyRUFBQUFCSXdBQUFRRUFxMkE3aFJHbWRubTl0VURiTzlJRFN3Qks2VGJRYStQWFlQQ1B5NnJiVHJUdHc3UEhrY2NLcnBwMHlWaH
A1SGRFSWNLcjZwTGxWREJmT0xYOVFVc3lDT1Ywd3pmaklKTmxHRVlzZGxMSml6SGhibjJtVWp2U0FIUXFaRVRZUDgxZUZ6TFFOblBIdDRFVlZVaDdWZkRFU1U4NEtlem1ENVFsV3BYTG12VTMxL3lNZitTZTh
4aEhUdktTQ1pJRkltV3dvRzZtYlVvV2Y5bnpwSW9hU2pCK3dlcXFVVW1wYWFhc1hWYWw3MkorVVgyQisyUlBXM1JjVDBlT3pRZ3FsSkwzUktyVEp2ZHNqRTNKRUF2R3EzbEdIU1pYeTI4RzNza3VhMlNtVmkv
dzR5Q0U2Z2JPRHFuVFdsZzcrd0M2MDR5ZEdYQThWSmlTNWFwNDNKWGlVRkZBYVE9PQo=
ssh: <base64 private key>
kind: Secret
metadata:
name: git-credsNow 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 readableLets 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.