While using Arch Linux is primarily merry and joy and gives you great fun (in a nerdy way), there are times when living near the bleeding edge is less fun. I was lucky enough to get my Drupal site working mostly after the Drupal package was updated from 7 series to 8. Some of the later updates then broke modules here and there and I ended up creating creative workarounds so that I didn’t need to pin Drupal package to a certain version.

I’m not even sure, whether it was the first Drupal 9 package, which broke my site completely. I noticed that I wasn’t compatible with the new PHP version and neither was Drupal. At that point I decided that there had to be a simpler way to keep a tiny blog running. The end result turned up to be ridiculously over-engineered, but it was also a great learning experience.

My original idea was that I wanted to use static site generator, so that I could keep the frantic security updates to a minimum. I went through different options like Jekyll and such and finally decided to use Pelican. That didn’t end well, since I couldn’t get a working solution for my Kubernetes cluster. Most likely the problem was my lack of skills with Docker.

Anyhow, after several unsuccessful attempts with Pelican I decided I had to try something else. Along came Hugo, which claims to be “the world’s fastest framework for building websites”. I don’t know, how credible the claim is, but I’ve seen that it is plenty fast for me.

Getting a working instance of this static site generator required me to take a different view for how to use blogging platform. Luckily I found a great guide by Chris Liatas, that gave me the direction in the beginning of the process. I got also some guidance from Linode’s excellent instructions.

Combining the aforementioned two guides I was able to come up with the following process.

  • Install Hugo on workstation
  • Create a site somewhere around your workstation
hugo new site hugo-site
  • Initialize git repository in the site directory
git init
  • Add theme as git submodule
git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke
  • Add theme name to config.toml found in the root of the site directory
theme = "ananke"
  • Create a first test post
hugo new content/en/post/test_post.md
  • Edit the file test_post.md to suit your needs

  • Test the site locally by running

hugo server -D
  • When you are satisfied with the end result, build the site with
hugo -v
  • Create a .gitignore file to keep already built pages out of the repository, since they can be rebuilt from the source Markdown files
echo 'public/' >> .gitignore
  • Create a Dockerfile and place it in the root of the repository
# Install the latest Alpine Linux operating system.
FROM alpine:3.12.0 as HUGO

# Install Hugo.
RUN apk update && apk add hugo

# Copy the contents of the current working directory to the
# static-site directory.
COPY . /static-site

# Command Hugo to build the static site from the source files,
# setting the destination to the public directory.
RUN hugo -v --source=/static-site --destination=/static-site/public

# Install NGINX, remove the default NGINX index.html file, and
# copy the built static site files to the NGINX html directory.
FROM nginx:stable-alpine
RUN mv /usr/share/nginx/html/index.html /usr/share/nginx/html/old-index.html
COPY --from=HUGO /static-site/public/ /usr/share/nginx/html/

# Instruct the container to listen for requests on port 80 (HTTP).
EXPOSE 80
  • Create a .dockerignore file
public/
.git/
.gitmodules/
.gitignore
  • Create gitlab repository and add it as upstream to local repository
git remote add origin git@gitlab.com:username/hugo-site.git
  • Create .gitlab-ci.yml
# This is a GitLab CI configuration to build the project as a docker image
# The file is generic enough to be dropped in a project containing a working Dockerfile


stages:
  - deploy

# When using dind, it's wise to use the overlayfs driver for
# improved performance.
variables:
  DOCKER_HOST: tcp://docker:2375/
  CI_BUILD_ARCHS: linux/arm64,linux/amd64
  GIT_SUBMODULE_STRATEGY: recursive

deploy:
  image: jonoh/docker-buildx-qemu
  stage: deploy
  services:
    - docker:dind
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
    # Use docker-container driver to allow useful features (push/multi-platform)
    - docker buildx create --driver docker-container --use
    - docker buildx inspect --bootstrap

  script:
    - update-binfmts --enable # Important: Ensures execution of other binary formats is enabled in the kernel
    - docker buildx build --platform $CI_BUILD_ARCHS --progress plain --pull -t "$CI_REGISTRY_IMAGE" --push .
  only:
    - tags

The last two lines means that you need to push a new tag to the Gitlab repository before it creates a new Docker image.

  • Add the new files (.dockerignore, .gitlab-ci.yml and Dockerfile) to git
git add .
git commit -m "Initial registry commit"

If you want to try creating a new Docker image, you can test it with a tag.

git tag -a v0.01 -m "my test tag"
git push --tags origin master

Then it’s time to deploy Hugo with the newly built Docker image.

  • Create a namespace
kubectl create namespace hugo
  • Optional, but needed, if your image registry needs authentication. Create a deploy token under repository in gitlab with read_registry rights.
kubectl create secret docker-registry regcred --docker-server=registry.gitlab.com --docker-username=<token_username> --docker-password=<token> --namespace=hugo
  • Create a file hugo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: website
  namespace: hugo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: website
  template:
    metadata:
      labels:
        app: website
    spec:
      containers:
      - name: website
        image: registry.gitlab.com/username/hugo-site
        imagePullPolicy: Always
        ports:
        - containerPort: 80
      imagePullSecrets:
      - name: regcred

Note that the imagePullPolicy is Always. This way we can ensure that the new version of the image is always pulled from the registry. There are more sophisticated ways to handle it, but I was pretty exhausted at that stage.

  • Create a file hugo-service.yaml
kind: Service
apiVersion: v1
metadata:
  name: website
  namespace: hugo
spec:
  selector:
    app: website
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: LoadBalancer
  • Apply both of the files
kubectl apply -f hugo.yaml
kubectl apply -f hugo-service.yaml

After all of that done, the workflow looks like this:

  • Working locally with our website files. (Try new features, add/update posts, etc.)
  • When we feel happy with what we have done, commit the changes with a tag. (Or commit as many times as we feel necessary)
  • Push changes to remote repository. (usually origin/master)
  • When CI/CD pipeline is finished, restart the deployment in K8s to get a new version running. (kubectl rollout restart deployment -n hugo website)

Next, we get to the migration part. Right now we have a working blogging platform but no content. Since the Drupal was in a non-functional state, I couldn’t use any export modules or even screen scraping. This means I had to start wandering around MariaDB database and hope to come accross with the content in a suitable format.

I found three tables that contained almost all I needed. As a compromise I left all the multimedia out and just continued with these three tables, which were named:

  • node
  • nodeBody
  • nodeFieldData

With these three I was able to get the titles, contents and the timestamps of my Drupal posts. It was a lot of manual work, but now I have my old content as Markdown files inside git repository, so hopefully they will be easier to migrate next time when I want to change my blogging platform (which won’t be soon, I promise).