Or is it
No (of course not)#
This whole thing runs on kubernetes using the k3s distribution and is managed using Flux for gitops. At some other time I’ll go into further details on how that all works. Here I’ll just focus on the parts closer into getting Hugo running. Kustomize to provide further templating.
I swear that kubernetes is at the same place in its lifecycle that JavaScript was at 10 years ago. A million different solutions to transpile and bundle your code because the end result is not something anyone wants to touch directly. Anyway…
Step 1: A webserver#
The web server being used is Ingress NGINX Controller. Certificates are managed using cert-manager. Metallb is being used as a load balancer. There’s a lot here that I won’t go into detail now but when requests come in on port 80 or 443 they get routed to the address metallb assigned the ingress controller which handles the SSL portion and proxies the request to the hugo service.
Step 2: A Hugo#
Here’s the directory structure of the service definition:
1
2
3
4
5
6
7
| .
├── app
│ ├── hugo.env
│ ├── hugo.yaml
│ └── kustomization.yaml
├── ks.yaml
└── kustomization.yaml
|
The top level kustomization.yaml
is trivial and just simple resource lists like this
1
2
3
4
5
6
| ---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- app
- ks.yaml
|
The ks.yaml
is a flux Kustomization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: hugo
namespace: flux-system
spec:
path: './cluster/andor/apps/public/hugo/app/'
interval: 1m0s
prune: true
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-gpg
postBuild:
substituteFrom:
- kind: ConfigMap
name: global-vars
- kind: ConfigMap
name: hugo-site-version
- kind: Secret
name: global-secrets
|
This ensures I can use variables in the definition of hugo.
The other kustomization.yaml
file is a little more interesting. This one creates a config map for me
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- hugo.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: hugo-site-version
namespace: flux-system
envs:
- hugo.env
|
This reads hugo.env
1
| HUGO_VERSION=e8dd92a014a9c7be9a4f0132241d1644953a7805
|
And creates a config map like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ❯ kubectl -n flux-system get configmaps hugo-site-version -o yaml
apiVersion: v1
data:
HUGO_VERSION: e8dd92a014a9c7be9a4f0132241d1644953a7805
kind: ConfigMap
metadata:
creationTimestamp: "2023-08-10T23:57:47Z"
labels:
kustomize.toolkit.fluxcd.io/name: hugo
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: hugo-site-version
namespace: flux-system
resourceVersion: "34262521"
uid: 7791cb6e-ccf9-4550-bc1c-658a031e28e3
|
The wonderful helm chart by bjw-s is used for creating a helm release of a docker image.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| ---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: hugo
namespace: public
spec:
chart:
spec:
chart: app-template
version: 1.5.1
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: bjw-s
namespace: flux-system
interval: 5m0s
install:
createNamespace: true
values:
image:
repository: code.example.com/org/hugosite
tag: ${HUGO_VERSION}
pullPolicy: IfNotPresent
controller:
strategy: RollingUpdate
replicas: 2
service:
main:
type: LoadBalancer
ports:
http:
enabled: true
port: 80
targetPort: 80
protocol: TCP
ingress:
main:
enabled: true
ingressClassName: "nginx"
annotations:
hajimari.io/icon: simple-icons:hugo
hosts:
- host: &host "hugo.${SECRET_DOMAIN2}"
paths:
- path: /
pathType: Prefix
tls:
- secretName: wildcard2-cert-tls
hosts:
- *host
resources:
requests:
cpu: 20m
memory: 125M
limits:
cpu: 50m
memory: 256M
|
The image tag is defined as ${HUGO_VERSION}
. This value comes from hugo.env. And given that this is a Rube Goldberg machine no way am I setting this by hand.
Step 3: The CD (continuous delivery)#
Continuous delivery for Hugo is managed using a self-hosted gitea instance. That’s a separate topic and I’m not sold on my own hosted solution for this because of bootstrap problems. But, for Hugo I think it isn’t too egregious. It has a great package registry. The action runner isn’t designed for kubernetes and has some rough edges. But I have it working.
Here’s the Dockerfile for hugo that I’m using.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| FROM alpine:3.9 AS build
# The Hugo version
ARG VERSION=0.113.0
ADD https://github.com/gohugoio/hugo/releases/download/v${VERSION}/hugo_${VERSION}_Linux-64bit.tar.gz /hugo.tar.gz
RUN tar -zxvf hugo.tar.gz
RUN /hugo version
# We add git to the build stage, because Hugo needs it with --enableGitInfo
RUN apk add --no-cache git
# The source files are copied to /site
COPY . /site
WORKDIR /site
# And then we just run Hugo
RUN /hugo --minify --enableGitInfo
# stage 2
FROM nginx:1.15-alpine
WORKDIR /usr/share/nginx/html/
# Clean the default public folder
RUN rm -fr * .??*
# Finally, the "public" folder generated by Hugo in the previous stage
# is copied into the public fold of nginx
COPY --from=build /site/public /usr/share/nginx/html
RUN chown -R 100:101 /usr/share/nginx/html
|
At this time this container image is 7.7 MiB. That’s a nice lean container. 😄
Here’s the workflow file in the hugo repository:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| name: Build and push image
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
submodules: recursive
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: code.example.com
username: name
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Docker build and push
run: |
docker build --build-arg GIT_COMMIT=$(git rev-parse HEAD) -t org/hugosite:$(git rev-parse HEAD) --progress=plain .
docker tag org/hugosite:$(git rev-parse HEAD) code.example.com/org/hugosite:$(git rev-parse HEAD)
docker image push code.example.com/org/hugosite:$(git rev-parse HEAD)
docker tag org/hugosite:$(git rev-parse HEAD) code.example.com/org/hugosite:latest
docker image push code.example.com/org/hugosite:latest
- name: Checkout homelab
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ID_RSA }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "${{ secrets.ID_RSA_PUB }}" > ~/.ssh/id_rsa.pub
chmod 644 ~/.ssh/id_rsa.pub
echo "${{ secrets.KNOWN_HOSTS }}" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
git clone git@gitlab.com:name/homeserver.git
- name: Set hugosite version
run: |
echo "HUGO_VERSION=$(git rev-parse HEAD)" > homeserver/cluster/andor/apps/public/hugo/app/hugo.env
git config --global user.email "name@example.com"
git config --global user.name "name (CI bot)"
cd homeserver
git add cluster/andor/apps/public/hugo/app/hugo.env
git commit -m "Hugo version update from CI"
git push origin main
|
I’ve replaced some references here with generic example and name but you see what’s happening. The docker image is being built and pushed to the private registry. And then the hash of the last git commit is written out to the hugo.env file in the homelab repo (which kustomize turns into a configmap) and that’s pushed up which flux will make go live. The pipeline takes 14 seconds to run (which is great) but because of how I have flux setup it can take more than 5 minutes for updates to be reflected live.
Is It Working?#
Yes. Here’s a performance chart.
Conclusion#
All this for a blog I’ll update twice.