Kubernetes上のHolloを自動更新する

Kubernetesでは、Deployment等のPod templateに対しイメージタグにてlatestを指定することは推奨されておりません

そのため、sha256によるコンテナイメージのダイジェスト値指定か、バージョンタグを用いた指定を行う必要があります。

Pod templateにバージョンないしはdigest値を書くということは手動でアップデートの管理を行うということです。別に大した手間ではないですしそれでも良いのですが、せっかく(何が??)ですので自動更新をさせてみたいものです。

但し、X.Y.ZバージョンのうちY部分の更新は何らかの管理者によるマイグレーション操作が発生する可能性を鑑みて、Z版のみ更新できるようにしてみました。(Z版で手動マイグレが必要にならない根拠は、無し。)

本来であれば以前記載した、「Deploymentでのアプリケーション単一性維持が保証できない問題」も含めて解決できる、何らかのOperatorを実装するほうがスマートです。しかし、当たり前ですがそういった実装には手間がかかりますので今回はKubernetesのCronJobでお茶を濁します。

今回の手法を実現するにあたり、JobのPodからKubernetesのDeploymentを書き換える方法を考える必要があります。

kubectl等の汎用クライアントを動かせるようにしてもよいのですが、このコマンドはフットプリントが結構大きく、また、kubeconfigファイルなどを用意する必要がある、応答を基に処理をしようとすると別途jsonの解釈が必要になる、信頼できそうな既製イメージでjqが入っているものが無さそうである、など、少々都合が悪いです。

そこで、今回はcurlコマンドとjqコマンドのみを用いてkubernetes APIを叩き、DeploymentのpodTemplateへ設定されているイメージのバージョン取得、及びその書き換えを行ってみます。

下準備として、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-patch-deploy
  namespace: hollo-1
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "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-patch-deploy
subjects:
- apiGroup: ""
  kind: ServiceAccount
  name: updater
  namespace: hollo-1

次に、Pod内部で実行するスクリプトを用意します。(力技。。。)curlでKubernetes APIを操作する方法については以下のサイト等を参考にしました。

https://qiita.com/iaoiui/items/36e86d173e451a7b18be

patch周り→わすれた…

MastodonへのToot : https://gist.github.com/YuzuRyo61/6388e6e8bde15712348e95714c82b0e1

#!/bin/bash -e

DEPLOYNAME="hollo-app"
TOKEN=$(curl -s "https://ghcr.io/token?service=ghcr.io&scope=repository:dahlia/hollo:pull" | jq -r ".token")
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
SATOKEN=$(cat /run/secrets/kubernetes.io/serviceaccount/token)
API="https://${KUBERNETES_PORT_443_TCP_ADDR}:${KUBERNETES_PORT_443_TCP_PORT}/apis"

function getcurl(){
    curl -s --cacert /run/secrets/kubernetes.io/serviceaccount/ca.crt -H "Accept: application/json" -H "Authorization: Bearer ${SATOKEN}" "${1}"
}

function patchcurl(){
    curl -s --cacert /run/secrets/kubernetes.io/serviceaccount/ca.crt -X PATCH -H "Accept: application/json" -H "Content-Type: application/json-patch+json" -H "Authorization: Bearer ${SATOKEN}" "${1}" -d "${2}"
}

REPOVER=$(curl -s -H "Accept: application/vnd.oci.image.manifest.v1+json" -H "Authorization: Bearer ${TOKEN}" https://ghcr.io/v2/dahlia/hollo/manifests/$(curl -s -H "Accept: application/vnd.oci.image.index.v1+json" -H "Authorization: Bearer ${TOKEN}" https://ghcr.io/v2/dahlia/hollo/manifests/latest | jq -r ".manifests[]|select(.platform.architecture==\"amd64\").digest") | jq -r ".annotations[\"org.opencontainers.image.version\"]")

RUNVER=$(getcurl ${API}/apps/v1/namespaces/${NAMESPACE}/deployments/${DEPLOYNAME} | jq -r ".spec.template.spec.containers[].image" | cut -d":" -f2)

echo REPOVER: ${REPOVER}
echo RUNVER: ${RUNVER}

ACCESS_TOKEN="<Another mastodon account Token>"
INSTANCE_ADDRESS="<Another mastodon server>"

curl https://$INSTANCE_ADDRESS/ >/dev/null 2>&1

case $? in
  0)

  ;;
  6)
    echo "[ERROR] Can't resolve " $INSTANCE_ADDRESS ". Are you connected to the network or are you using the wrong address?"
    exit 1
  ;;
  127)
    echo "[ERROR] curl package is not found. please install curl package."
    exit 1
  ;;
  *)
    echo "[ERROR] An unknown error occurred."
    exit 1
  ;;
esac

PATCH='[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"ghcr.io/dahlia/hollo:'${REPOVER}'"}]'

echo "${PATCH}"

if [[ $(echo ${REPOVER}|cut -d"." -f 1) -eq $(echo ${RUNVER}|cut -d"." -f 1) ]];then
    # X version valid
    if [[ $(echo ${REPOVER}|cut -d"." -f 2) -eq $(echo ${RUNVER}|cut -d"." -f 2) ]];then
        # Y version valid
        if [[ $(echo ${REPOVER}|cut -d"." -f 3) -ne $(echo ${RUNVER}|cut -d"." -f 3) ]];then
            # Z version different
            echo "do upgrade"
            curl -X POST -d "status=Hollo autoUpdater / Upgrade start ${RUNVER} to ${REPOVER}" -d "visibility=private" --header "Authorization: Bearer $ACCESS_TOKEN" -sS https://$INSTANCE_ADDRESS/api/v1/statuses > /dev/null 2>&1
            patchcurl ${API}/apps/v1/namespaces/${NAMESPACE}/deployments/${DEPLOYNAME} "${PATCH}"
        else
            echo "do nothing"
            curl -X POST -d "status=Hollo autoUpdater / do nothing RunVer:${RUNVER}" -d "visibility=private" --header "Authorization: Bearer $ACCESS_TOKEN" -sS https://$INSTANCE_ADDRESS/api/v1/statuses > /dev/null 2>&1
        fi
    else
      echo "Y version different"
      curl -X POST -d "status=Hollo autoUpdater / Y version different RunVer:${RUNVER} / Latest:${REPOVER}" -d "visibility=private" --header "Authorization: Bearer $ACCESS_TOKEN" -sS https://$INSTANCE_ADDRESS/api/v1/statuses > /dev/null 2>&1
      exit 0
    fi
else 
  echo "X version different"
  curl -X POST -d "status=Hollo autoUpdater / X version different RunVer:${RUNVER} / Latest:${REPOVER}" -d "visibility=private" --header "Authorization: Bearer $ACCESS_TOKEN" -sS https://$INSTANCE_ADDRESS/api/v1/statuses > /dev/null 2>&1
  exit 0
fi

上記スクリプトを含めたイメージを作成するのは手間ですので、ConfigMapかSecretあたりを使い無理やりJobで起動されるContainerにファイルとしてマウントします。今回は面倒なのでConfigMapを使用しました。botアカウントに喋らせるためのTokenが含まれるため、気をつけて取り扱う必要があります。他の人も使うような環境などでは絶対にやらないようにしましょうね。(botいらんよって場合は該当部分消せばいい感じになります。)

kubectl create cm -n hollo-1 vercheck-shell --from-file=./vercheck.sh --dry-run=client --save-config -o yaml | kubectl apply -f -

また、Jobについて上記で作成したServiceAccountを使用するようにします。今回、bash、curl、jqが既にインストール済みのイメージとしてnetshootを使用しました。上記コマンドが入っていれば別のイメージでも可です。(とにかく、イメージを自前でビルドしたくない。。。)ConfigMapのマウントも忘れずに行います。

apiVersion: batch/v1
kind: CronJob
metadata:
  name: z-release-autoupdater
  namespace: hollo-1
spec:
  schedule: <Cronのスケジュール> 例:"16 1,13 * * *"
  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: ghcr.io/nicolaka/netshoot:v0.13
            imagePullPolicy: IfNotPresent
            volumeMounts:
              - name: vercheck-shell
                mountPath: /app
            command:
            - 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

色々ガバいですが、一応うごきましたということで。。。

※免責:上記記事が原因で問題が発生しても一切保証しません。よろしくお願いします。