Module K5
Dalibo SCOP
26.09
10 septembre 2026
| Formation | Module K5 |
| Titre | Montées de versions |
| Révision | 26.09 |
| https://dali.bo/k5_pdf | |
| EPUB | https://dali.bo/k5_epub |
| HTML | https://dali.bo/k5_html |
| Slides | https://dali.bo/k5_slides |
| TP | https://dali.bo/k5_tp |
| TP (solutions) | https://dali.bo/k5_solutions |
Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :
PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.
Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 14 à 18.
imageName du Clusterimages du ImageCatalog ou
ClusterImageCatalogpg_dump/pg_restorepg_upgradepg_dump/pg_restorebootstrap.initdb.importmicroservice ou monolithCluster
externalClusters dans sa définition YAMLmonolith
microservice
pg_dump -Fd
PGDATAspec.externalClusters
Custom Resource Definitionsinstance-managerCLUSTERS_ROLLOUT_DELAYINSTANCES_ROLLOUT_DELAYprimaryUpdateStrategy et
primaryUpdateMethod
PodsPod Disruption Budget
kubectl describe pdb cluster-18-primary
Name: cluster-18-primary
Namespace: default
Min available: 1
Selector: cnpg.io/cluster=cluster-18,cnpg.io/instanceRole=primary
Status:
Allowed disruptions: 0
Current: 1
Desired: 1
Total: 1
Events: <none>
N’hésitez pas, c’est le moment !
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/k5_solutions.
But : Effectuer une montée de version mineure de PostgreSQL.
La version de PostgreSQL est indiquée dans le nom et le tag de l’image déployée. La modification de celle-ci entraîne une montée de version de l’instance. Cette montée de version peut se faire automatiquement ou de manière supervisée (appelée “manuelle” dans la documentation).
Dans une seconde session SSH, lancer la commande
watch kubectl get podspour voir ce qu’il va se passer pendant la montée de version.
Vous devriez avoir un rafraîchissement toutes les 2 secondes.
Créer un
Clustercluster-productionen version 17.5 composé d’une instance.
vim ~/cluster-production.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-production
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.6-standard-bookworm
instances: 2
storage:
size: 1Gikubectl apply -f ~/cluster-production.yaml
Lorsqu’il est
Running, modifier la version de PostgreSQL de17.5à17.6et appliquer la modification aveckubectl apply.
kubectl apply -f ~/cluster-production.yaml
cluster.postgresql.cnpg.io/cluster-production configured
Le Pod de l’instance secondaire en version 17.5 est
supprimé. Le status du Pod passe à l’état
Terminating.
Un nouveau Pod est créé et la phase d’initialisation est
relancée. Le status du Pod passe donc à l’état
PodInitializing.
Le temps de télécharger l’image et de le redémarrer, il passe à
l’état Running.
Lorsque l’instance secondaire est mise à jour, le Pod de
l’instance primaire est à son tour mise à jour.
Un SELECT version(); indique bien que nous sommes bien
passés en version 17.6.
kubectl cnpg psql cluster-production -- -c 'SELECT version();'
version
-----------------------------------------------------------------------------------------------------------
-----------------
PostgreSQL 17.6 (Debian 17.6-2.pgdg12+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit
(1 row)
Par défaut, un Cluster PostgreSQL est configuré de telle
sorte que la montée de version se fasse automatiquement, c’est-à-dire
que l’opérateur arrête puis redémarre l’instance tout seul. Voyons
maintenant le cas d’une montée de version en mode
supervised.
Le paramètre primaryUpdateStrategy permet de définir la
stratégie à suivre lors d’une mise à jour de l’instance primaire. Il est
positionné par défaut à unsupervised. C’est le comportement
que nous venons de voir dans l’exemple précédent.
Positionner le paramètre
spec.primaryUpdateStrategyàsupervised, modifier la version en la passant de17.6à17.7et tenter de faire la montée de version. Que constatez vous ?
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.7-standard-bookworm
instances: 1
primaryUpdateStrategy: supervisedkubectl apply -f ~/cluster-production.yaml
Aïe…
The Cluster "cluster-production" is invalid: spec.primaryUpdateStrategy: Invalid value: "supervised": supervised update strategy is not allowed for clusters with a single instance
Nous venons de découvrir une première subtilité. Ce paramètre n’est en réalité pas utilisable avec une seule instance. En effet ce paramètre permet de contrôler la manière dont est redémarrée l’instance primaire après que toutes les instances secondaires aient été mis à jour. Ici, nous n’avons qu’une primaire de déployée.
Ceci nous impose donc de déployer un secondaire.
Ajouter un secondaire à votre cluster PostgreSQL en modifiant la ligne
instancesdu fichiercluster-production.yamlet en appliquant la modification.
Un premier Pod join va être créé puis le
Pod de l’instance secondaire sera finalement déployé. Nous
nous retrouvons donc avec deux Pods reprenant le nom du
Cluster, incrémentés de 1.
kubectl get pod
NAME READY STATUS RESTARTS AGE
cluster-production-1 2/2 Running 0 6m16s
cluster-production-2-join-7ckvl 0/1 Pending 0 5s
kubectl get pod
NAME READY STATUS RESTARTS AGE
cluster-production-1 1/1 Running 0 61m
cluster-production-2 1/1 Running 0 20s
Vérifier les versions des deux instances. Que constatez-vous ?
Comme nous avons modifié la version de l’image en 17.7,
l’instance secondaire qui vient d’être déployée est bien dans cette
version. La version du primaire est quant à elle restée en
17.6, ce qui est normal comme nous sommes dans une montée
de version supervisée.
# primaire
kubectl exec -it cluster-production-1 -c postgres -- psql -c 'show server_version;'
server_version
-------------------------------
17.6 (Debian 17.6-2.pgdg12+1)
(1 row)
# secondaire
kubectl exec -it cluster-production-2 -c postgres -- psql -c 'show server_version;'
server_version
-------------------------------
17.7 (Debian 17.7-3.pgdg12+1)
(1 row)
Il nous reste donc à signifier à CloudNativePG que le primaire peut
être mis à jour à son tour. Pour cela, il faut passer par le
plugin CloudNativePG de kubectl et procéder à une
bascule sur le secondaire qui deviendra le nouveau primaire avec la
ligne de commande :
kubectl cnpg promote [cluster] [new_primary].
Faite une bascule manuelle sur l’instance secondaire.
kubectl cnpg promote cluster-production cluster-production-2
L’ancien secondaire est désormais primaire comme on peut le voir dans
la sortie de kubectl cnpg status cluster-production qui
donne l’état du cluster PostgreSQL, en particulier, avec la ligne
Primary instance: cluster-production-2, ou encore dans le
tableau.
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
cluster-production-2 0/D001108 Primary OK Burstable 1.30.0 kind-worker
cluster-production-1 0/D001108 Standby (async) OK Burstable 1.30.0 kind-worker2
Les deux instances sont bien en version 17.7.
kubectl exec -it cluster-production-1 -c postgres -- psql -c 'show server_version;'
server_version
-------------------------------
17.7 (Debian 17.7-3.pgdg12+1)
(1 row)
Il existe d’autres cas où le redémarrage des instances est
nécessaire. Par exemple, si un paramètre comme
max_connections ou shared_buffers a été
modifié.
Si vous faites toujours une montée de version supervisée, il vous est
possible de ne pas faire de bascule mais simplement de redémarrer
l’instance primaire avec la ligne de commande
kubectl cnpg restart [cluster] [current_primary];
But : Effectuer une montée de version majeure de PostgreSQL.
Vous l’avez vu, la version majeure de PostgreSQL est indiquée dans le tag de l’image déployée. La modification de celle-ci entraîne le déclenchement d’une In-Place Major Upgrade. Ce mécanisme se fait nécessairement sur des instances arrêtées. L’opérateur CloudNativePG les arrêtera pour vous.
Pour cet exercice, nous allons mettre à jour un Cluster
en version 16.11 vers la version 18.1.
Créer le fichier
~/postgresql-16-to-18.yamlet ajouter le contenu suivant puis appliquer le aveckubectl.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-test
spec:
instances: 2
imageName: ghcr.io/cloudnative-pg/postgresql:16.11-standard-bookworm
storage:
size: 1Gikubectl apply -f ~/postgresql-16-to-18.yaml
Vérifier que les deux instances soient correctement déployées.
kubectl get pod
NAME READY STATUS RESTARTS AGE
postgresql-test-1 1/1 Running 0 38s
postgresql-test-2 1/1 Running 0 20s
Créer une table et y insérer quelques lignes.
kubectl cnpg psql postgresql-test -- -c "CREATE TABLE t1 (c1 INTEGER PRIMARY KEY, c2 text)";
CREATE TABLE
kubectl cnpg psql postgresql-test -- -c "INSERT INTO t1 SELECT generate_series(1,100), generate_series(1,100)::text";
INSERT 0 100
Ouvrir un nouveau terminal et lancer la commande
kubectl get pod --watch.
Ouvrir un nouveau terminal et lancer la commande
kubectl get pv --watch.
Ouvrir un nouveau terminal et lancer la commande
kubectl get pvc --watch.
Modifier le tag du paramètre
imageNamedu fichier~/postgresql-16-to-18.yamlen le passant de16.11à18.1puis appliquer la modification aveckubectl.
kubectl apply -f ~/postgresql-16-to-18.yaml
cluster.postgresql.cnpg.io/postgresql-test configured
Observez ce qu’il se passe dans les différents terminaux que vous avez ouvert.
Les deux instances passent en Terminating
postgresql-test-1 1/1 Terminating 0 4m26s
postgresql-test-2 1/1 Terminating 0 4m8s
La montée de version est déclenchée et un nouveau Pod
apparait :
postgresql-test-1-major-upgrade-7hl8w 0/1 Init:0/2 0 0s
postgresql-test-1-major-upgrade-7hl8w 0/1 Init:1/2 0 1s
postgresql-test-1-major-upgrade-7hl8w 0/1 PodInitializing 0 2s
postgresql-test-1-major-upgrade-7hl8w 1/1 Running 0 18s
postgresql-test-1-major-upgrade-7hl8w 0/1 Completed 0 45s
Lorsque qu’il a terminé de s’exécuté, le Pod
correspondant à l’instance primaire est relancé :
postgresql-test-1 0/1 Pending 0 0s
postgresql-test-1 0/1 Init:0/1 0 0s
postgresql-test-1 0/1 PodInitializing 0 0s
postgresql-test-1 0/1 Running 0 10s
postgresql-test-1 1/1 Running 0 10s
À ce moment là, le volume (PV) qui existait et qui était rattaché à
l’ancienne instance secondaire postgresql-test-2 sont
supprimés.
pvc-23b021b2-163b-41e3-9791-721b8635bee5 1Gi RWO Delete Released default/postgresql-test-2 standard <unset> 5m3s
pvc-23b021b2-163b-41e3-9791-721b8635bee5 1Gi RWO Delete Terminating default/postgresql-test-2 standard <unset> 5m6s
Même chose pour le PVC :
postgresql-test-2 Terminating pvc-23b021b2-163b-41e3-9791-721b8635bee5 1Gi RWO standard <unset> 5m5s
Pour respecter ce qui est demandé dans la définition YAML, une
instance secondaire postgresql-test-3 est créée et va se
joindre à la nouvelle instance primaire postgresql-test-1.
Cela se fait par le déploiement d’un Pod intermédiaire
suffixé par -join-XXXXX. Il sera détruit à la fin de
l’opération.
postgresql-test-3-join-tcrbs 0/1 Pending 0 0s
postgresql-test-3-join-tcrbs 0/1 Init:0/1 0 5s
postgresql-test-3-join-tcrbs 0/1 PodInitializing 0 6s
postgresql-test-3-join-tcrbs 1/1 Running 0 7s
postgresql-test-3-join-tcrbs 0/1 Completed 0 10s
postgresql-test-3 0/1 Pending 0 0s
postgresql-test-3 0/1 Init:0/1 0 0s
postgresql-test-3 0/1 PodInitializing 0 0s
postgresql-test-3 0/1 Running 0 10s
postgresql-test-3 1/1 Running 0 10s
postgresql-test-3-join-tcrbs 0/1 Terminating 0 22s
Vérifier que les données soient toujours présentes.
kubectl cnpg psql postgresql-test -- -c "SELECT count(*) FROM t1;"
Vérifier la version de PostgreSQL.
kubectl cnpg psql postgresql-test -- -c "show server_version;"
But : Effectuer une montée de version de l’opérateur.
Nous avons déployé la version 1.30.0 de l’opérateur.
Nous allons nous intéresser à la manière de le mettre à jour.
Lorsqu’une nouvelle version de l’opérateur est déployée, un nouveau
Pod se crée. Lorsque celui-ci est prêt, l’ancien opérateur
est tout simplement supprimé. Cette mise à jour déclenche également la
mise à jour d’un composant présentant dans les Pods des
instances PostgreSQL.
Lorsqu’un Pod PostgreSQL est déployé, un
InitContainer est créé en amont et permet de récupérer du
code correspondant à l’instance-manager. Il permet de
contrôler l’instance, son cycle, ses redémarrages, etc. La version de ce
manager est étroitement liée à la version de l’opérateur.
Pour information, c’est ce processus qui va lancer PostgreSQL et qui
aura le pid 1 dans le Pod.
cat /proc/1/cmdline
/controller/manager instance run--status-port-tls--log-level=info
Attention si vous avez utilisé votre opérateur pour
déployer plusieurs instances PostgreSQL, lorsque vous mettez à jour
l’opérateur, tous les Pods seront mis à
jour en même temps (ou quasiment). Il y aura donc une coupure de service
pour chaque instance. C’est le fonctionnement par défaut.
Dans une première console, lancer la commande
watchsuivante :
watch kubectl get pod
Dans une seconde console, lancer la commande
watchsuivante :
watch kubectl get pod -n cnpg-system
Dans une autre console, appliquer les fichiers YAML correspondant à la version
1.31.0de l’opérateur.
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.31/releases/cnpg-1.31.0.yaml
Regarder ce qu’il se passe au niveau des différents
Pods(opérateur et PostgreSQL)
Les instances PostgreSQL déployées par l’opérateur sont redémarrées lors d’une montée de version de l’opérateur. Selon la configuration des instances, une opération manuelle sera nécessaire pour mettre à jour le primaire.
Il existe une méthode pour éviter ce comportement. Cependant elle ne
garantit pas le critère immuable que devrait suivre un Pod.
Nous vous conseillons de l’utiliser qu’à des fins de tests.
À titre d’information, voici la méthode à suivre pour y parvenir.
Pour cela il faut modifier la configuration de l’opérateur en passant le
paramètre ENABLE_INSTANCE_MANAGER_INPLACE_UPDATES à
true. Cela permettra de mettre à jour le
manager sans pour autant redémarrer le Pod
complet. Cette configuration doit être faite dans un objet
ConfigMap.
Créer un
Clusteravec une seule instance dans la dernière version de PostgreSQL disponible.
Le fichier suivant vous permet cela. Créer le dans, par exemple,
~/cluster_pdb.yaml et créer la ressource avec
kubectl apply -f ~/cluster_pdb.yaml.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-pdb
spec:
instances: 1
storage:
size: 1GiRetrouver la ressource
PodDisruptionBudgetet la décrire.
L’abréviation pdb avec kubectl get peut
être utilisée.
kubectl get pdb
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
cluster-pdb-primary 1 N/A 0 6m23s
kubectl describe pdb cluster-pdb-primary
Name: cluster-pdb-primary
Namespace: default
Min available: 1
Selector: cnpg.io/cluster=cluster-pdb,cnpg.io/instanceRole=primary
Status:
Allowed disruptions: 0
Current: 1
Desired: 1
Total: 1
Events:
Trouver le nœud sur lequel fonctionne le
Podprimaire de votre instance primaire.
kubectl get pod cluster-pdb-1 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-1 1/1 Running 0 31s 10.244.1.15 kind-worker <none> <none>
Dans cet exemple, le nœud est kind-worker. Cela peut
être différent pour vous. Comme il n’y a qu’un Pod, il
s’agit forcément de l’instance primaire.
Effectuer un
drainde ce nœud. Que se passe t’il ?
Le drain d’un nœud peut se déclencher avec la commande
kubectl drain. Des options supplémentaires peuvent être
indiquées comme --delete-emptydir-data ou
--ignore-daemonsets.
kubectl drain kind-worker --delete-emptydir-data --ignore-daemonsets
node/kind-worker cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/kindnet-rqjqx, kube-system/kube-proxy-dv4ld
evicting pod default/cluster-pdb-1
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/cluster-pdb-1
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
Le nœud est marqué cordoned. Kubernetes cherche à
évincer les Pods. La politique d’éviction de notre
Cluster n’étant pas respectée
(Cannot evict pod as it would violate the pod's disruption budget.),
le drain du nœud ne peut se faire.
Arrêter le
draindu nœud.
kubectl uncordon kind-worker
Ajouter une instance secondaire à votre
Cluster. Vérifier qu’elle soit correctement démarrée avant de passer à la suite.
Attendez que le Pod de l’instance secondaire soit
marquée comme Running ou que le statut du
Cluster soit bien
Cluster in healthy state.
kubectl get cluster
NAME AGE INSTANCES READY STATUS PRIMARY
cluster-pdb 18m 2 2 Cluster in healthy state cluster-pdb-1
Trouver le nœud sur lequel fonctionne le
Podde votre instance secondaire.
kubectl get pod cluster-pdb-2 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-2 0/1 Running 0 10s 10.244.2.17 kind-worker2 <none> <none>
Ici il s’agit du nœud kind-worker2.
Effectuer un
drainde ce nœud. Que se passe t’il ?
kubectl drain kind-worker2 --delete-emptydir-data --ignore-daemonsets
node/kind-worker2 cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/kindnet-t8c2x, kube-system/kube-proxy-rt6s9
evicting pod default/cluster-pdb-2
evicting pod cnpg-system/cnpg-controller-manager-65bfdb64c9-4rx2v
pod/cnpg-controller-manager-65bfdb64c9-4rx2v evicted
pod/cluster-pdb-2 evicted
node/kind-worker2 drained
Au passage, notons que le drain affecte tous les
Pods du nœud. Donc, dans cet exemple, il y a aussi le
Pod de l’opérateur qui se voit être évincé :
evicting pod cnpg-system/cnpg-controller-manager-65bfdb64c9-4rx2v.
Ce dernier sera recréé sur un autre nœud pouvant l’accueillir.
Le Pod du secondaire est également bien supprimé du
nœud. L’opérateur (après que celui-ci est été lui-même recréé) cherche à
le redéployer ailleurs. S’il n’existe pas de troisième nœud, le
Pod restera à l’état Pending, en attendant
qu’un nouveau nœud soit disponible.
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-1 1/1 Running 0 6m3s 10.244.1.15 kind-worker <none> <none>
cluster-pdb-2 0/1 Pending 0 3m10s <none> <none> <none> <none>
Arrêter le
draindu nœud.
kubectl uncordon kind-worker2
node/kind-worker2 uncordoned
L’instance secondaire doit redevenir opérationnelle.
Effectuer un
draindu nœud où se trouve l’instance primaire. Que se passe t’il ?
kubectl drain kind-worker --delete-emptydir-data --ignore-daemonsets
node/kind-worker cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/kindnet-rqjqx, kube-system/kube-proxy-dv4ld
evicting pod default/cluster-pdb-1
evicting pod cnpg-system/cnpg-controller-manager-65bfdb64c9-n7vgn
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/cnpg-controller-manager-65bfdb64c9-n7vgn evicted
evicting pod default/cluster-pdb-1
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/cluster-pdb-1
pod/cluster-pdb-1 evicted
node/kind-worker drained
Kubernetes cherche a évincer le Pod primaire. Or le
PodDisruptionBudget cluster-pdb-primary
indique qu’il doit y avoir toujours, au sein de cluster
Kubernetes, un Pod ayant le rôle de primaire. Ici ce n’est
pas le cas.
Il faut donc attendre que le Pod secondaire
cluster-pdb-2 soit promu primaire par l’opérateur. Et pour
cela, une opération de switchover est déclenchée. Dès que cette
dernière se termine, l’éviction du Pod peut se faire :
pod/cluster-pdb-1 evicted.
La commande kubectl cnpg status cluster-pdb permet de
nous rendre compte que le primaire à changé.
kubectl cnpg status cluster-pdb
Cluster Summary
Name default/cluster-pdb
System ID: 7600706097499480099
PostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:18.1-system-trixie
Primary instance: cluster-pdb-2
Primary promotion time: 2026-01-29 09:09:33 +0000 UTC (40s)
Status: Waiting for the instances to become active Some instances are not yet active. Please wait.
Instances: 2
Ready instances: 1
Size: 112M
Current Write LSN: 0/6000F40 (Timeline: 2 - WAL File: 000000020000000000000006)
Continuous Backup not configured
Streaming Replication status
Not available yet
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
cluster-pdb-2 0/6000F40 Primary OK BestEffort 1.28.0 kind-worker2
cluster-pdb-1 - - BadRequest BestEffort -
Error(s) extracting status
-----------------------------------
failed to get status by proxying to the pod, you might lack permissions to get pods/proxy: the server rejected our request for an unknown reason (get pods https:cluster-pdb-1:8000)
À cet instant, le nœud où se trouvait le primaire est
drain. Le nœud peut donc être mis à jour.
Le Cluster n’est pas dans un état Healthy
car il attend que toutes les instances soit démarrées.
Arrêter le
draindu nœud.
kubectl uncordon kind-worker
node/kind-worker uncordoned
L’instance cluster-pdb-1 qui maintenant secondaire a été
recréée :
kubectl get pod -o wide lenovo-pro: Thu Jan 29 10:10:54 2026
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-1 1/1 Running 0 75s 10.244.1.17 kind-worker <none> <none>
cluster-pdb-2 1/1 Running 0 6m26s 10.244.2.18 kind-worker2 <none> <none>
et le Cluster a retrouvé un état
Healthy.
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
cluster-pdb-2 0/8000000 Primary OK BestEffort 1.28.0 kind-worker2
cluster-pdb-1 0/8000000 Standby (async) OK BestEffort 1.28.0 kind-worker