★2025/1/29 GithubのURLを変更しました
★2026/5/28 curlだけで更新するのをやめて、kubectlを都度installして使うように変更しました
Kubernetesでは、Deployment等のPod templateに対しイメージタグにてlatestを指定することは推奨されておりません
そのため、sha256によるコンテナイメージのダイジェスト値指定か、バージョンタグを用いた指定を行う必要があります。
Pod templateにバージョンないしはdigest値を書くということは、手動でアップデートの管理を行うということです。
別に大した手間ではないですしそれでも良いのですが、せっかく(何が??)ですので自動更新をさせてみたいものです。
但し、X.Y.ZバージョンのうちY部分の更新は何らかの管理者によるマイグレーション操作が発生する可能性を鑑みて、Z版のみ更新できるようにしてみました。(Z版で手動マイグレが必要にならない根拠は、無し。)
本来であれば以前記載した、「Deploymentでのアプリケーション単一性維持が保証できない問題」も含めて解決できる、何らかのOperatorを実装するほうがスマートです。しかし、当たり前ですがそういった実装には手間がかかりますので今回はKubernetesのCronJobでお茶を濁します。
下準備として、PodからkubeAPIを叩くためにServiceAccountやRole等を作成します。以下に例を示します。
apiVersion: v1
kind: ServiceAccount
metadata:
name: updater
namespace: hollo-1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: get-edit-deploy
namespace: hollo-1
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: updater
namespace: hollo-1
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: get-edit-deploy
subjects:
- apiGroup: ""
kind: ServiceAccount
name: updater
namespace: hollo-1
次に、Pod内部で実行するスクリプトを用意します。
Workerを分離している関係でTARGET_DEPLOYMENTSに複数記載がありますが、適宜変更してください。
なお、hollo-webとhollo-workerが同時にDBのマイグレーションを実行しようとする動作が発生しうるため、おそらく本当は下記コードだと良くないと思います。Worker側のargs変えてマイグレを止める+何らかの方法でWEB側のマイグレが終わるのを待つ をしないといけないですが面倒。一旦replicas=0にScaleさせて、マイグレだけ実行させた方が早いかも。
MastodonへのToot参考 : https://gist.github.com/YuzuRyo61/6388e6e8bde15712348e95714c82b0e1
#!/usr/bin/env bash
set -Eeuo pipefail
[[ "${DEBUG:-0}" == "1" ]] && set -x
TARGET_DEPLOYMENTS="${TARGET_DEPLOYMENTS:-hollo-web hollo-worker}"
IMAGE_REPO="fedify-dev/hollo"
IMAGE_ARCH="${IMAGE_ARCH:-amd64}"
INSTANCE_ADDRESS="${INSTANCE_ADDRESS:-<mastodon server fqdn>}"
ACCESS_TOKEN="${ACCESS_TOKEN:-}"
ROLLOUT_TIMEOUT="${ROLLOUT_TIMEOUT:-180s}"
SA_NAMESPACE_FILE="/run/secrets/kubernetes.io/serviceaccount/namespace"
if [[ ! -f "${SA_NAMESPACE_FILE}" ]]; then
SA_NAMESPACE_FILE="/var/run/secrets/kubernetes.io/serviceaccount/namespace"
fi
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "[ERROR] command not found: $1" >&2
exit 1
}
}
for cmd in curl jq kubectl; do
require_cmd "${cmd}"
done
NAMESPACE="${NAMESPACE:-}"
if [[ -z "${NAMESPACE}" && -f "${SA_NAMESPACE_FILE}" ]]; then
NAMESPACE="$(<"${SA_NAMESPACE_FILE}")"
fi
if [[ -z "${NAMESPACE}" ]]; then
echo "[ERROR] namespace is empty. Set NAMESPACE env or run in-cluster with serviceaccount namespace file." >&2
exit 1
fi
post_status() {
local msg="$1"
if [[ -z "${ACCESS_TOKEN}" ]]; then
echo "[WARN] ACCESS_TOKEN is empty. Skip status post." >&2
return 0
fi
if ! curl -fsS --connect-timeout 5 --max-time 20 \
-X POST \
-d "status=${msg}" \
-d "visibility=private" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
"https://${INSTANCE_ADDRESS}/api/v1/statuses" >/dev/null 2>&1; then
echo "[WARN] Failed to post status to ${INSTANCE_ADDRESS}" >&2
fi
}
parse_semver() {
local v="$1"
if [[ "${v}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"
else
echo "[ERROR] version is not strict semver (x.y.z): ${v}" >&2
exit 1
fi
}
TOKEN="$(curl -fsS "https://ghcr.io/token?service=ghcr.io&scope=repository:${IMAGE_REPO}:pull" | jq -er '.token')"
MANIFEST_DIGEST="$(
curl -fsS \
-H "Accept: application/vnd.oci.image.index.v1+json" \
-H "Authorization: Bearer ${TOKEN}" \
"https://ghcr.io/v2/${IMAGE_REPO}/manifests/latest" | \
jq -er --arg arch "${IMAGE_ARCH}" '.manifests[] | select(.platform.architecture == $arch) | .digest' | \
head -n 1
)"
REPOVER="$(
curl -fsS \
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
-H "Authorization: Bearer ${TOKEN}" \
"https://ghcr.io/v2/${IMAGE_REPO}/manifests/${MANIFEST_DIGEST}" | \
jq -er '.annotations["org.opencontainers.image.version"]'
)"
echo "REPOVER: ${REPOVER}"
read -r REPO_MAJOR REPO_MINOR REPO_PATCH <<< "$(parse_semver "${REPOVER}")"
read -r -a DEPLOYMENT_LIST <<< "${TARGET_DEPLOYMENTS}"
if [[ "${#DEPLOYMENT_LIST[@]}" -eq 0 ]]; then
echo "[ERROR] TARGET_DEPLOYMENTS is empty" >&2
exit 1
fi
TARGET_IMAGE="ghcr.io/${IMAGE_REPO}:${REPOVER}"
for DEPLOYNAME in "${DEPLOYMENT_LIST[@]}"; do
CONTAINER_NAME="$(kubectl -n "${NAMESPACE}" get deployment "${DEPLOYNAME}" -o jsonpath='{.spec.template.spec.containers[0].name}')"
RUN_IMAGE="$(kubectl -n "${NAMESPACE}" get deployment "${DEPLOYNAME}" -o jsonpath='{.spec.template.spec.containers[0].image}')"
RUNVER="${RUN_IMAGE##*:}"
if [[ -z "${CONTAINER_NAME}" || -z "${RUN_IMAGE}" ]]; then
echo "[ERROR] failed to read deployment/container info via kubectl: ${DEPLOYNAME}" >&2
exit 1
fi
echo "${DEPLOYNAME} RUNVER: ${RUNVER}"
read -r RUN_MAJOR RUN_MINOR RUN_PATCH <<< "$(parse_semver "${RUNVER}")"
if [[ "${REPO_MAJOR}" -ne "${RUN_MAJOR}" ]]; then
echo "${DEPLOYNAME}: X version different"
post_status "Hollo autoUpdater / ${DEPLOYNAME} X version different RunVer:${RUNVER} / Latest:${REPOVER}"
continue
fi
if [[ "${REPO_MINOR}" -ne "${RUN_MINOR}" ]]; then
echo "${DEPLOYNAME}: Y version different"
post_status "Hollo autoUpdater / ${DEPLOYNAME} Y version different RunVer:${RUNVER} / Latest:${REPOVER}"
continue
fi
if [[ "${REPO_PATCH}" -eq "${RUN_PATCH}" ]]; then
echo "${DEPLOYNAME}: do nothing"
post_status "Hollo autoUpdater / ${DEPLOYNAME} do nothing RunVer:${RUNVER}"
continue
fi
echo "${DEPLOYNAME}: do upgrade"
echo "set image: ${DEPLOYNAME} ${CONTAINER_NAME}=${TARGET_IMAGE}"
post_status "Hollo autoUpdater / ${DEPLOYNAME} Upgrade start ${RUNVER} to ${REPOVER}"
kubectl -n "${NAMESPACE}" set image "deployment/${DEPLOYNAME}" "${CONTAINER_NAME}=${TARGET_IMAGE}" >/dev/null
kubectl -n "${NAMESPACE}" rollout status "deployment/${DEPLOYNAME}" --timeout="${ROLLOUT_TIMEOUT}" >/dev/null
post_status "Hollo autoUpdater / ${DEPLOYNAME} Upgrade completed ${RUNVER} to ${REPOVER}"
done
上記スクリプトを含めたイメージを作成するのは手間ですので、ConfigMapを使いJobで起動されるContainerにファイルとしてマウントします。
kubectl create cm -n hollo-1 vercheck-shell --from-file=./vercheck.sh --dry-run=client --save-config -o yaml | kubectl apply -f -
bot機能を使う場合は、別途Mastodonへ投稿するためのアクセストークンを作成し、以下のようなSecretを作成します。
apiVersion: v1
kind: Secret
metadata:
name: updater-secrets
namespace: hollo-1
type: Opaque
stringData:
access_token: "〜〜〜"
それではUpdater本体をDeployしましょう。
apiVersion: batch/v1
kind: CronJob
metadata:
name: z-release-autoupdater
namespace: hollo-1
spec:
schedule: "16 * * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: updater
volumes:
- name: vercheck-shell
configMap:
name: vercheck-shell
items:
- key: vercheck.sh
path: vercheck.sh
mode: 0777
containers:
- name: updater
image: alpine:latest
imagePullPolicy: Always
env:
- name: ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: updater-secrets
key: access_token
- name: TARGET_DEPLOYMENTS
value: "hollo-web hollo-worker"
volumeMounts:
- name: vercheck-shell
mountPath: /app
command:
- /bin/sh
- -c
- apk add --no-cache bash ca-certificates curl jq kubectl >/dev/null && exec bash /app/vercheck.sh
restartPolicy: Never
それではテストしてみましょう。CronJobからJobを手動でcreateしてみます。
kubectl create job -n hollo-1 --from=cronjob/z-release-autoupdater test
以下のようにPodが生成され、無事に動作しました。(^o^)v
user@k-cp-1:~/hollo-1$ k get pod -n hollo-1 | grep test
test-mshz4 0/1 Completed 0 3m
※免責:本記事が原因で問題が発生しても一切保証しません。よろしくお願いします。
