KubernetesのCronJobからHugo+rsyncで静的コンテンツを自動更新してみた

# はじめに

Hugo([gohugo.io](https://gohugo.io))は静的コンテンツジェネレータとして、[kubernetes.io](https://kubernetes.io)などのWebサイトを構築するために広く使われています。

通常は手作業で更新、rsyncでWebサーバーに更新といった使い方をすれば良いのですが、外部データベースからの最新データを反映してページを生成したり、RSSフィードと連携してニュース記事などを掲載していると、自動的かつ定期的に静的ページを更新するニーズがあります。

そこでKubernetesのCronJobオブジェクトを利用するため、hugoが稼動するDockerコンテナを生成しました。比較的新しいコードベースを利用しているため、hugoはソースコードからビルドします。

# ワークフロー

ここで対象とする主な処理の流れは以下のとおりです。

* ローカルのGitLabに登録しているHugoプロジェクトがある
* Dockerコンテナが起動時に、Hugoプロジェクトをgit cloneし、静的コンテンツを生成する
* public/ に生成されたコンテンツを、rsyncでリモートサーバーに転送する

GitHubでも同様に動作するはずですし、Hugoをubuntuのdeb packageを利用するなどすれば、わざわざメモを残すほどのことではありませんが、hugoの良いdockerコンテナもなさそうだったのでレシピを残しておきます。

## 前提

以下のような環境を想定しています。

* Hugoプロジェクトでは、build時にgetJSONなどで外部データソースと連携し、変化するコンテンツを生成している
* GitLabは、PrivateプロジェクトをSSH経由でcloneすることを想定している
  * 公開プロジェクトであれば、httpsプロトコルも利用可能です
* GitLabのSSH Keyと、リモートホストのauthorized_keysには、同じSSH公開鍵の情報を登録している
  * つまり、gitlabとリモートホストへのアクセス時には、同じSSH秘密鍵(/app/id_ed25519)を利用している

# 準備したファイルたち

この作業に必要なファイルは以下のとおりです。SSH鍵は既存のものを使う場合があると思うので、実行時に補足しています。

## Dockerfile

alpineとmulti-stage buildを利用して、最終的なコンテナイメージは50〜60MB程度のサイズになっています。

asciidoctorはmarkdownよりも、AsciiDocを好んで使っているので加えていますが、不要であれば省いてください。

Hugoテーマも利用しているので、冗長になっていますが、不要であれば、``run.sh`` を調整してから編集してください。

```Dockerfile:Dockerfile
FROM golang:1.19-alpine3.16 as hugodev

RUN apk --no-cache add git make gcc g++ libc-dev patch

ENV GOPATH /root/go
RUN mkdir ${GOPATH}

RUN mkdir /work
WORKDIR /work
RUN git clone https://github.com/gohugoio/hugo.git
WORKDIR /work/hugo
RUN git checkout refs/tags/v0.108.0 -b t_v0.108.0
RUN go install --tags extended

FROM alpine:3.16

RUN apk update && \
        apk add --no-cache tzdata bash ca-certificates rsync openssh git libstdc++ asciidoctor

COPY --from=hugodev /root/go/bin/hugo /usr/local/bin/hugo

ADD run.sh /run.sh
RUN chmod +x /run.sh

ENV HUGO_GITLAB_SSHKEY_FILEPATH "/app/id_ed25519"
ENV HUGO_GITLAB_PROJECT_URL "git@github.com:YasuhiroABE/example-hugo-project.git"
ENV HUGO_GITLAB_PROJECT_NAME "example-hugo-project"
ENV HUGO_GITLAB_THEME_URL "git@github.com:YasuhiroABE/example-hugo-theme.git"
ENV HUGO_GITLAB_THEME_NAME "theme/mytheme"
ENV HUGO_CONTENTS_DEST_PATH "user01@example.com:public_html/."

RUN addgroup hugo
RUN adduser -S -G hugo hugo
RUN mkdir /work
RUN chown hugo:hugo /work
USER hugo
WORKDIR /work

ENTRYPOINT ["/run.sh"]
```

Hugoのバージョンは、0.108.0を指定してますが、任意の値に変更してください。

## run.sh

インタラクティブにTTY経由でコマンドを実行する場合と比べると、バッチジョブ特有のオプション指定などを行なっています。

また、環境によってrsyncのオプションなどは変化すると思いますので適宜調整してください。

```bash:run.sh
#!/bin/bash -x

## omitting host-key check
env GIT_SSH_COMMAND="ssh -i ${HUGO_GITLAB_SSHKEY_FILEPATH} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
    git clone "${HUGO_GITLAB_PROJECT_URL}" ${HUGO_GITLAB_PROJECT_NAME}

## update the hugo theme
cd "${HUGO_GITLAB_PROJECT_NAME}"
rm -rf "${HUGO_GITLAB_THEME_NAME}"

## build static-contents by hugo 
env GIT_SSH_COMMAND="ssh -i ${HUGO_GITLAB_SSHKEY_FILEPATH} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
    git clone "${HUGO_GITLAB_THEME_URL}" "${HUGO_GITLAB_THEME_NAME}"
hugo

## transfer static contents to the remote host
exec rsync -rcv -e "ssh -i ${HUGO_GITLAB_SSHKEY_FILEPATH} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
     public/. "${HUGO_CONTENTS_DEST_PATH}"
```

# 利用例

## Dockerコンテナのビルドと、DockerHubへの登録

Dockerfileとrun.shを配置してから、通常の方法でbuild、tag, pushなどの操作が可能です。
``<username>`` の部分は、DockerHub上のUsernameです。

```bash:build,tag,pushの実行例
$ sudo docker build . --tag hugo-rsync:1.0.0 --no-cache
$ sudo docker tag hugo-rsync:1.0.0 docker.io/<username>/hugo-rsync:1.0.0
$ sudo docker push docker.io/<username>/hugo-rsync:1.0.0
```

実行は次のような操作で可能です。ssh-keygenコマンドは最初の1回だけ必要ですし、既に利用している鍵ファイルを利用する場合には、``conf/id_ed25519``の名称で配置してください。Dockerfile上の ``HUGO_GITLAB_SSHKEY_FILEPATH`` で指定した値と整合を取る必要があります。

```bash:docker-runの実行例
$ mkdir conf
$ ssh-keygen -t ed25519 -f conf/id_ed25519
$ sudo docker run -it --rm \
      --env HUGO_GITLAB_PROJECT_URL="<gitlab/github clone url>" \
      --env HUGO_GITLAB_PROJECT_NAME="<directory name of the cloned hugo project>" \
      --env HUGO_CONTENTS_DEST_PATH="<username>@<hostname>:<dest-path>" \
      --env HUGO_GITLAB_THEME_URL="https://github.com/theNewDynamic/gohugo-theme-ananke.git" \
      --env HUGO_GITLAB_THEME_NAME="themes/ananke" \
      -v `pwd`/conf:/app --name hugo-rsync hugo-rsync:latest
```

環境によってrun.shの内容はかなり変わりそうですが、getJSONなどを多用するコンテンツ生成機能を利用している場合には、バックグラウンドでコンテンツが更新できることはそれなりに便利だと思われます。

# KubernetesのCronJobからの実行

実際のDockerHubのコンテナはPrivate扱いなので、このままでは実際の利用はできない点に注意してください。

```yaml:deploy-hugo-rsync.yamlファイル
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hugo-rsync
spec:
  schedule: "45 5 * * *"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 30
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: regcred
          containers:
          - name: crawler-scheduler-cron
            image: yasuhiroabe/my-hugo-rsync:1.0.0
            env:
            - name: LC_CTYPE
              value: ja_JP.UTF-8
            - name: HUGO_GITLAB_PROJECT_URL
              value: "ssh://git@example.com:30001/user01/hugo-contents.git"
            - name: HUGO_GITLAB_PROJECT_NAME
              value: "hugo-contents" 
            - name: HUGO_CONTENTS_DEST_PATH
              value: "user01@example.net:/home/web-int/labs/opm/public_html/."
            - name: HUGO_GITLAB_THEME_URL
              value: "ssh://git@example.com:30001/user01/hugo-mytheme.git"
            - name: HUGO_GITLAB_THEME_NAME
              value: "themes/mytheme"
            volumeMounts:
            - name: ssh-keys
              mountPath: /app
              readOnly: true
          volumes:
          - name: ssh-keys
            secret:
              secretName: ssh-seckey
              defaultMode: 0444
```

鍵ファイルはあらかじめ環境で生成し、secretオブジェクトとして登録しています。

```bash:ssh鍵ファイルの生成と登録
$ mkdir conf
$ ssh-keygen -t ed25519 -f conf/id_ed25519
$ sudo kubectl -n mynamespace create secret generic ssh-seckey --from-file=conf/id_ed25519
```
NS(namespace)名（mynamespace)は適宜変更してください。
conf/id_ed25519.pub の内容は、GitLab/GitHubのSSH Keys, リモートホストのauthorized_keysの両方に登録する必要があります。

SSH秘密鍵のパーミッションが格好悪いですが、どうせ外部からアクセスできないので諦めました。

ここまでで、k8s環境で定期的にコンテンツを更新する仕組みが構築できました。

以上