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"
- "[email protected]: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.