Frantisek "Franta" Bartik
Datacenter Tech | Columbus, OH
Introduction
I wanted to have a fully automated setup to deploy a Hugo blog on my K3s cluster. This guide isn’t really about setting up Hugo, more about the underlying infrastructure.
Procedure
Hugo
Prerequisites
- Create your Hugo site first and set up the config to your liking.
- Set up an account on DockerHub (and GitHub if you don’t have one)
- Have a running Kubernetes cluster
Set up the GitHub repo
I’ve chosen GitHub because I already know how to use it but it should be possible to have a similar CI/CD setup on a different host. You’ll need these files at the root of the repo:
Dockerfile
FROM hugomods/hugo:exts as builder # Base URL ARG HUGO_BASEURL="" ENV HUGO_BASEURL=${HUGO_BASEURL} # Build site COPY . /src RUN hugo --minify --gc # Set the fallback 404 page if defaultContentLanguageInSubdir is enabled, please replace the `en` with your default language code. # RUN cp ./public/en/404.html ./public/404.html ##################################################################### # Final Stage # ##################################################################### FROM hugomods/hugo:nginx # Copy the generated files to keep the image as small as possible. COPY --from=builder /src/public /site
.github/workflows/main.yaml
name: ci on: push: branches: - 'master' jobs: docker: env: IMAGE: <image_name> runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive # Necessary to download and apply themes - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: | ${{ env.IMAGE }}:${{ github.sha }}-${{ github.run_number }}
.gitignore
# Added automatically .DS_Store .idea *.log tmp/ # Hugo specifc public/ resources/ .hugo_build.lock
Kubernetes
My cluster is orchestrated using FluxCD, so I’m using their solution for Image Automation. When bootstrapping your cluster with Flux, you’ll need to add the option --components-extra=image-reflector-controller,image-automation-controller
, because it doesn’t get installed default. Another necessity is to use --read-write-key=true
for deploying a repo key that can write (default is read-only). ImageUpdateAutomation
will not be able to run properly without it.
HelmRelease
I’m using the bjw-template
Helm Chart because I’m familiar with it and it’s already used for other releases in my cluster.
---
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2beta2.json
apiVersion: helm.toolkit.fluxcd.io/v2beta2
kind: HelmRelease
metadata:
name: hugo
namespace: apps
spec:
chart:
spec:
chart: app-template
version: 2.6.x # auto-update to semver bugfixes only
sourceRef:
kind: HelmRepository
name: bjw
namespace: flux-system
interval: 15m
timeout: 5m
values: # paste contents of upstream values.yaml below, indented 4 spaces
controllers:
main:
strategy: Recreate
containers:
main:
image:
repository: <image_name> # {"$imagepolicy": "flux-system:hugoblog-repo-policy:name"}
tag: latest # {"$imagepolicy": "flux-system:hugoblog-repo-policy:tag"}
# With tag: latest, the ImageUpdateAutomation will change this automatically
service:
main:
ports:
http:
port: 80 # the hugo:nginx image runs the NGINX server on port 80
ingress:
main:
enabled: true
hosts:
- host: <blog_domain>
paths:
- path: /
pathType: Prefix
service:
name: main
port: http
tls:
- secretName: <TLS_Secret_Name>
hosts:
- <blog_domain>
ImageRepository
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/imagerepository-image-v1beta2.json
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: hugoblog
namespace: flux-system
spec:
image: <image_name>
interval: 5m
ImagePolicy
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/imagepolicy-image-v1beta2.json
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: hugoblog-repo-policy
namespace: flux-system
spec:
imageRepositoryRef:
name: hugoblog
filterTags:
## use "pattern: '[a-f0-9]+-(?P<ts>[0-9]+)'" if you copied the workflow example using github.run_number
pattern: '[a-f0-9]+-(?P<ts>[0-9]+)'
extract: '$ts'
policy:
numerical:
order: asc
ImageUpdateAutomation
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/imageupdateautomation-image-v1beta1.json
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: flux-system
namespace: flux-system
spec:
git:
checkout:
ref:
branch: main
commit:
author:
email: [email protected]
name: fluxcdbot
messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
push:
branch: main
interval: 30m0s
sourceRef:
kind: GitRepository
name: flux-system
update:
path: ./ # Set so it applies to the whole repo
strategy: Setters
Result
Now when you add a new update to your Hugo blog:
- A new docker image should get built.
- It will be pulled in by the
image-automation-controller
in your Kubernetes cluster. - A new pod will be deployed with the updated blog.