Podstawowe informacje o sposobie wyłączenia
Poniżej znajduje się procedura „graceful shutdown” dla OKD 4.19 (w tym przykładzie 3× master, 3× worker) tak, żeby klaster dało się bezpiecznie uruchomić później (minimalizacja ryzyka korupcji danych, zwłaszcza etcd). To jest dokładnie podejście zalecane w dokumentacji OKD/OpenShift. Krytyczne elementy:
- etcd (quorum na masterach) – dlatego backup przed wyłączeniem
- API VIP (węzeł control-plane, który trzyma VIP) – musi zostać wyłączony ostatni, inaczej kolejne polecenia “shutdown” mogą się nie wykonać
Procedurę wyłączenia należy wykonać wg. podanej kolejności:
- backup etcd
- sprawdzenie ważności certów kubelet-signer jeśli wyłączenie jest na dłużej
- prawidłowe wyłączenie workerów
- prawidłowe wyłączenie masterów
Opis samego klastra OKD można znaleźć we wpisie https://itadmin.vblog.ovh/klaster-okd-openshift-na-maszynach-wirtualnych-proxmox-opis-instalacji/#0-klaster-okd-openshift-na-maszynach-wirtualnych-proxmox
Backup etcd dla OKD 4.19
Więcej przydatnych informacji pod adresem https://docs.okd.io/4.19/backup_and_restore/control_plane_backup_and_restore/backing-up-etcd.html
Najważniejsze zasady :
- Backup robisz TYLKO RAZ i tylko na jednym control-plane hoście (nie na każdym masterze).
- Nie rób backupu przed pierwszą rotacją certów (pierwsza rotacja jest ~24h po instalacji).
Wybieramy 1 z masterów np. control-plane-1.testcluster.okdlab.local.
[bastuser@bastion ~]$ oc get nodes NAME STATUS ROLES AGE VERSION compute-1.testcluster.okdlab.local Ready worker 7d19h v1.32.6 compute-2.testcluster.okdlab.local Ready worker 7d19h v1.32.6 compute-3.testcluster.okdlab.local Ready worker 7d19h v1.32.6 control-plane-1.testcluster.okdlab.local Ready control-plane,master 7d20h v1.32.6 control-plane-2.testcluster.okdlab.local Ready control-plane,master 7d20h v1.32.6 control-plane-3.testcluster.okdlab.local Ready control-plane,master 7d19h v1.32.6
Sprawdamy, czy control-plane i etcd wygląda zdrowo.
[bastuser@bastion ~]$ oc -n openshift-etcd get pods - wide NAME READY STATUS RESTARTS AGE etcd-control-plane-1.testcluster.okdlab.local 5/5 Running 0 7d19h etcd-control-plane-2.testcluster.okdlab.local 5/5 Running 0 7d19h etcd-control-plane-3.testcluster.okdlab.local 5/5 Running 0 7d19h etcd-guard-control-plane-1.testcluster.okdlab.local 1/1 Running 0 7d20h etcd-guard-control-plane-2.testcluster.okdlab.local 1/1 Running 0 7d20h etcd-guard-control-plane-3.testcluster.okdlab.local 1/1 Running 0 7d19h
Sprawdzamy czy występuje cluster-wide proxy. Jeśli występują wartości httpProxy, httpsProxy, noProxy to w należy je poniżej ustawić w debug shellu. W naszym przypadku nie występują.
[bastuser@bastion ~]$ oc get proxy cluster -o yaml
apiVersion: config.openshift.io/v1
kind: Proxy
metadata:
creationTimestamp: "2026-02-04T15:00:45Z"
generation: 1
name: cluster
resourceVersion: "444"
uid: 9cf9823f-e248-4dab-920f-de5176ce73d0
spec:
trustedCA:
name: ""
status: {}Wchodzimy na wybranego control-plane jako root.
[bastuser@bastion ~]$ oc debug --as-root node/control-plane-1.testcluster.okdlab.local Temporary namespace openshift-debug-m4qw5 is created for debugging node... Starting pod/control-plane-1testclusterokdlablocal-debug-5ncft ... To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`. sh-5.1# sh-5.1# chroot /host
Przygotujemy katalog na backup.
BKPDIR="/home/core/assets/backup-$(date +%F_%H%M%S)" mkdir -p "$BKPDIR" df -h "$(dirname "$BKPDIR")"
Ustawiamy zmienne środowiskowych jeśli były zmienne httpProxy, httpsProxy, noProxy.
export HTTP_PROXY=http://<twoj-proxy>:8080 export HTTPS_PROXY=https://<twoj-proxy>:8080 export NO_PROXY=<twoj-no-proxy>
Uruchamiamy backup.
/usr/local/bin/cluster-backup.sh "$BKPDIR"
lub
/usr/local/bin/cluster-backup.sh "/home/core/assets/backup-2026-02-12_114310/"
sh-5.1# /usr/local/bin/cluster-backup.sh "/home/core/assets/backup-2026-02-12_114310/"
Certificate /etc/kubernetes/static-pod-certs/configmaps/etcd-all-bundles/server-ca-bundle.crt is missing. Checking in different directory
Certificate /etc/kubernetes/static-pod-resources/etcd-certs/configmaps/etcd-all-bundles/server-ca-bundle.crt found!
found latest kube-apiserver: /etc/kubernetes/static-pod-resources/kube-apiserver-pod-11
found latest kube-controller-manager: /etc/kubernetes/static-pod-resources/kube-controller-manager-pod-5
found latest kube-scheduler: /etc/kubernetes/static-pod-resources/kube-scheduler-pod-6
found latest etcd: /etc/kubernetes/static-pod-resources/etcd-pod-11
0affcd52d80ce9089714789343efc73ce389500bb72ba5ab4c955bf3b80924a1
etcdctl version: 3.5.21
API version: 3.5
{"level":"info","ts":"2026-02-12T11:46:29.420314Z","caller":"snapshot/v3_snapshot.go:65","msg":"created temporary db file","path":"/home/core/assets/backup-2026-02-12_114310//snapshot_2026-02-12_114628.db.part"}
{"level":"info","ts":"2026-02-12T11:46:29.427082Z","logger":"client","caller":"[email protected]/maintenance.go:212","msg":"opened snapshot stream; downloading"}
{"level":"info","ts":"2026-02-12T11:46:29.427123Z","caller":"snapshot/v3_snapshot.go:73","msg":"fetching snapshot","endpoint":"https://192.168.40.51:2379"}
{"level":"info","ts":"2026-02-12T11:46:29.886097Z","logger":"client","caller":"[email protected]/maintenance.go:220","msg":"completed snapshot read; closing"}
{"level":"info","ts":"2026-02-12T11:46:29.917534Z","caller":"snapshot/v3_snapshot.go:88","msg":"fetched snapshot","endpoint":"https://192.168.40.51:2379","size":"81 MB","took":"now"}
{"level":"info","ts":"2026-02-12T11:46:29.917718Z","caller":"snapshot/v3_snapshot.go:97","msg":"saved","path":"/home/core/assets/backup-2026-02-12_114310//snapshot_2026-02-12_114628.db"}
Snapshot saved at /home/core/assets/backup-2026-02-12_114310//snapshot_2026-02-12_114628.db
{"hash":4072196668,"revision":2472906,"totalKey":6985,"totalSize":81432576}
snapshot db and kube resources are successfully saved to /home/core/assets/backup-2026-02-12_114310/
Przenosimy backup poza klaster np. na maszynę bastion.
# jesli wyszedles juz z poda przez exit / exit oc debug --as-root node/control-plane-1.testcluster.okdlab.local chroot /host # jesli jeszcze nie to nalezy ustawic uprawnienia do pobrania lokalnia z klastra ls -lah /var/home/core/assets/backup-2026-02-12_114310/ chown -R core:core /var/home/core/assets/backup-2026-02-12_114310 chmod 750 /var/home/core/assets/backup-2026-02-12_114310 chmod 640 /var/home/core/assets/backup-2026-02-12_114310/* # na maszynie bastion mkdir -p /home/bastuser/etcd-backup [bastuser@bastion ~]$ scp -r [email protected]:"/home/core/assets/backup-2026-02-12_114310/" ./etcd-backup/ static_kuberesources_2026-02-12_114628.tar.gz 100% 82KB 22.5MB/s 00:00 snapshot_2026-02-12_114628.db 100% 78MB 134.1MB/s 00:00
Sprawdzenie daty ważności certów kubelet-signer
OKD/OpenShift zaleca sprawdzić, do kiedy klaster ma certy tak, by zdążyć go uruchomić przed wygaśnięciem (po roku od instalacji może być potrzebna ręczna akceptacja CSR).
[bastuser@bastion etcd-backup]$ oc -n openshift-kube-apiserver-operator get secret kube-apiserver-to-kubelet-signer \
-o jsonpath='{.metadata.annotations.auth\.openshift\.io/certificate-not-after}'; echo
2027-02-04T13:06:19ZCordon wszystkich węzłów (żeby nic nowego się nie planowało)
Dokumentacja zaleca oznaczyć wszystkie węzły jako unschedulable (cordon).
[bastuser@bastion ~]$
for node in $(oc get nodes -o jsonpath='{.items[*].metadata.name}'); do
echo "cordon $node"
oc adm cordon "$node"
done
cordon compute-1.testcluster.okdlab.local
node/compute-1.testcluster.okdlab.local cordoned
cordon compute-2.testcluster.okdlab.local
node/compute-2.testcluster.okdlab.local cordoned
cordon compute-3.testcluster.okdlab.local
node/compute-3.testcluster.okdlab.local cordoned
cordon control-plane-1.testcluster.okdlab.local
node/control-plane-1.testcluster.okdlab.local cordoned
cordon control-plane-2.testcluster.okdlab.local
node/control-plane-2.testcluster.okdlab.local cordoned
cordon control-plane-3.testcluster.okdlab.local
node/control-plane-3.testcluster.okdlab.local cordoned
[bastuser@bastion etcd-backup]$
Drain tylko workerów (ewakuacja workloadów)
Zgodnie z oficjalną dokumentacją OKD/OpenShift należy ewakuować pody z workerów drainem (z podanymi flagami).
for node in $(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[*].metadata.name}'); do
echo "drain $node"
oc adm drain "$node" \
--delete-emptydir-data \
--ignore-daemonsets=true \
--timeout=15s \
--force
doneShutdown węzłów – najpierw workery, potem mastery, a master z API VIP ostatni
Najbezpieczniej dla układu 3/3 zrobić to jawnie w kolejności, zamiast jednym “loopem po wszystkich”, bo dokumentacja ostrzega, że węzeł z API VIP musi być ostatni, inaczej shutdown polecenia mogą zacząć się wywalać. W przypadku mojej sieci i homelaba adres 192.168.40.15 czyli haproxy.okdlab.local jest adresem, na który idzie ruch api.testcluster.okdlab.local:6443, więc z perspektywy procedury graceful shutdown to on pełni rolę “VIP-a od API”. Czyli kolejność w moim homelab jest:
- workery
- mastery
- HAProxy / LB (192.168.40.15) jako ostatni
Poniżej opisany jest także sposób znalezienia VIP od API w klasycznym układzie OKD (nie dotyczy mojego homelab)
# Sprawdź adres API (zwykle https://api.<cluster>:6443):
oc whoami --show-server
https://api.testcluster.okdlab.local:6443
# odczytaj apiVIP z install-config w klastrze
oc -n kube-system get cm cluster-config-v1 -o jsonpath='{.data.install-config}' | \
egrep 'apiVIP|ingressVIP|baseDomain|metadata'
# Wyciągnij sam hostname i rozwiąż go do IP:
API_HOST=$(oc whoami --show-server | sed -E 's#https?://([^:/]+).*#\1#')
echo "API_HOST=$API_HOST"
getent ahostsv4 "$API_HOST"
# albo:
dig +short "$API_HOST"
# Jeśli dostaniesz jeden IP (np. 192.168.40.50) → to kandydat na VIP/LB.
VIP="192.168.40.50" # <- PODSTAW SWÓJ
for n in control-plane-1.testcluster.okdlab.local \
control-plane-2.testcluster.okdlab.local \
control-plane-3.testcluster.okdlab.local
do
echo "=== $n ==="
oc debug node/$n -- chroot /host sh -c "ip -4 addr | grep -F \"$VIP\" || true"
done
# sprawdzić, czy w ogóle używasz keepalived/VRRP na masterach
oc debug node/control-plane-1.testcluster.okdlab.local -- chroot /host sh -c \
'ps -ef | grep -i keepalived | grep -v grep || true; systemctl status keepalived --no-pager 2>/dev/null || true'
# Jeśli keepalived nie istnieje / nie działa na masterach, to tym bardziej “VIP na masterze” odpada i masz LB gdzie indziejWorkery — shutdown za 2 minuty (dla pełnego bezpieczeństwa można dać 5 minut)
for node in compute-1.testcluster.okdlab.local \
compute-2.testcluster.okdlab.local \
compute-3.testcluster.okdlab.local
do
echo "schedule shutdown worker ${node} (+2min)"
oc debug "node/${node}" -- chroot /host shutdown -h +2
doneMastery — shutdown za 5 minut (dla pełnego bezpieczeństwa można dać 10 minut)
for node in control-plane-1.testcluster.okdlab.local \
control-plane-2.testcluster.okdlab.local \
control-plane-3.testcluster.okdlab.local
do
echo "schedule shutdown master ${node} (+5min)"
oc debug "node/${node}" -- chroot /host shutdown -h +5
doneNa koniec wyłączamy maszynę VIP API czyli w naszym przypadku proxy.okdlab.local (192.168.40.15) w Proxmox 9.
Procedura prawidłowego uruchomienia maszyn klastra OKD 4.19
Kolejność uruchamiania po starcie serwera Proxmox 9.
1) Warstwa „przed klastrem” (musi działać od początku)
- DNS (jeśli masz osobny serwer DNS) – żeby api.testcluster.okdlab.local nadal rozwiązywało się do 192.168.40.15.
- HAProxy / Load Balancer: 192.168.40.15 (to Twój „API VIP” w praktyce).
- Zależności: NFS / iSCSI / zewnętrzne storage / LDAP / itp. (Dokumentacja nazywa je “cluster dependencies”.)
2) Control plane (mastery) – priorytet
Uruchom wszystkie 3 mastery w konsoli Proxmox 9:
control-plane-1.testcluster.okdlab.local
control-plane-2.testcluster.okdlab.local
control-plane-3.testcluster.okdlab.local
Technicznie etcd potrzebuje quorum (2/3), ale w praktyce po maintenance najczyściej jest podnieść wszystkie 3 możliwie blisko siebie. Gdy API zacznie odpowiadać, sprawdź mastery. Na bastionie (tam gdzie masz oc):
oc get nodes -l node-role.kubernetes.io/master
Master jest “OK” gdy ma STATUS=Ready. Uwaga ! Jeśli mastery nie są Ready: sprawdź i ogarnij CSR. To jest najczęstszy „blok” po dłuższym OFF.
oc get csr # Każdy podejrzany CSR obejrzyj: oc describe csr <csr_name> # I dopiero wtedy zatwierdzaj: oc adm certificate approve <csr_name>
Uwaga praktyczna: nie odpalaj w ciemno masowego approve, jeśli nie wiesz co podpisujesz. Najpierw sprawdź REQUESTOR i SIGNERNAME w oc describe csr.
Ponieważ przed shutdownem zrobiliśmy oc adm cordon na wszystkich node’ach, to stan taki zostaje po restarcie.
A wtedy:
- deploymenty/operatorzy (np. authentication, ingress) mogą nie mieć gdzie się uruchomić,
- API (/readyz) działa, ale logowanie OAuth się sypie.
Ponieważ nie możemy się zalogować z bastiona, robimy to od środka z mastera przez localhost-recovery.kubeconfig (oficjalnie używane do działań awaryjnych przy dostępie do API).
# z maszyny bastion [bastuser@bastion ~]$ ssh -i /home/bastuser/ssh/id_ed25519 [email protected] [core@control-plane-1 ~]$ sudo -i [root@control-plane-1 ~]# export KUBECONFIG=/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost-recovery.kubeconfig [root@control-plane-1 ~]# oc whoami system:admin [root@control-plane-1 ~]# oc get nodes NAME STATUS ROLES AGE VERSION compute-1.testcluster.okdlab.local NotReady,SchedulingDisabled worker 7d22h v1.32.6 compute-2.testcluster.okdlab.local NotReady,SchedulingDisabled worker 7d22h v1.32.6 compute-3.testcluster.okdlab.local NotReady,SchedulingDisabled worker 7d22h v1.32.6 control-plane-1.testcluster.okdlab.local Ready,SchedulingDisabled control-plane,master 7d23h v1.32.6 control-plane-2.testcluster.okdlab.local Ready,SchedulingDisabled control-plane,master 7d22h v1.32.6 control-plane-3.testcluster.okdlab.local Ready,SchedulingDisabled control-plane,master 7d22h v1.32.6
Zdejmujemy cordon z masterów.
oc adm uncordon control-plane-1.testcluster.okdlab.local oc adm uncordon control-plane-2.testcluster.okdlab.local oc adm uncordon control-plane-3.testcluster.okdlab.local
Po ok 10 minut uruchomiamy workery, podglądając jak wstają komponenty klastra OKD. Nie musimy czekać, aż oc get clusteroperators pokaże Available=True dla wszystkiego, żeby uruchamiać workery. W praktyce jest wręcz odwrotnie: część operatorów nie ma szans przejść na Available=True bez działających workerów, bo kluczowe workloady (router/ingress, registry, monitoring itd.) zwykle lądują na workerach.
- oc get nodes
- oc -n openshift-ingress get pods -o wide
- oc -n openshift-authentication get pods -o wide
- oc get clusteroperators
[root@control-plane-1 ~]# oc get nodes NAME STATUS ROLES AGE VERSION compute-1.testcluster.okdlab.local NotReady,SchedulingDisabled worker 7d22h v1.32.6 compute-2.testcluster.okdlab.local NotReady,SchedulingDisabled worker 7d22h v1.32.6 compute-3.testcluster.okdlab.local NotReady,SchedulingDisabled worker 7d22h v1.32.6 control-plane-1.testcluster.okdlab.local Ready control-plane,master 7d23h v1.32.6 control-plane-2.testcluster.okdlab.local Ready control-plane,master 7d22h v1.32.6 control-plane-3.testcluster.okdlab.local Ready control-plane,master 7d22h v1.32.6 [root@control-plane-1 ~]# oc -n openshift-ingress get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES router-default-567b4d5485-b8xdd 1/1 Terminating 1 (7d22h ago) 7d23h 192.168.40.62 compute-2.testcluster.okdlab.local <none> <none> router-default-567b4d5485-djkj6 0/1 Pending 0 105m <none> <none> <none> <none> router-default-567b4d5485-hhr82 0/1 Pending 0 138m <none> <none> <none> <none> [root@control-plane-1 ~]# oc -n openshift-authentication get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES oauth-openshift-db87f76bf-5mt9q 1/1 Running 1 7d 10.128.0.135 control-plane-1.testcluster.okdlab.local <none> <none> oauth-openshift-db87f76bf-gzwsp 1/1 Running 1 7d 10.131.0.56 control-plane-3.testcluster.okdlab.local <none> <none> oauth-openshift-db87f76bf-nm52l 1/1 Running 1 7d 10.129.0.184 control-plane-2.testcluster.okdlab.local <none> <none> [root@control-plane-1 ~]# oc get clusteroperators NAME VERSION AVAILABLE PROGRESSING DEGRADED SINCE MESSAGE authentication 4.19.0-okd-scos.9 False False True 112m OAuthServerRouteEndpointAccessibleControllerAvailable: Get "https://oauth-openshift.apps.testcluster.ok dlab.local/healthz": EOF... baremetal 4.19.0-okd-scos.9 True False False 7d23h cloud-controller-manager 4.19.0-okd-scos.9 True False False 7d23h cloud-credential 4.19.0-okd-scos.9 True False False 7d23h cluster-autoscaler 4.19.0-okd-scos.9 True False False 7d23h config-operator 4.19.0-okd-scos.9 True False False 7d23h
Gdybyś nie miał działającego admin.kubeconfig
Oficjalna procedura dopuszcza użycie localhost-recovery.kubeconfig na masterze do wykonania oc adm uncordon.
W praktyce ten plik bywa w takim miejscu:
/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost-recovery.kub
Dokumentacja zaleca po uruchomieniu maszyn odczekać ok. 10 minut zanim zaczniesz oceniać stan control-plane.
(Chodzi o to, że kubelet/static pods/etcd/API potrzebują czasu, żeby się zsynchronizować.)
3) Compute (workery) – dopiero gdy control plane są już uruchomione od ok. 10 minut
Uruchom workery w Proxmox 9 GUI:
compute-1.testcluster.okdlab.local
compute-2.testcluster.okdlab.local
compute-3.testcluster.okdlab.local
Zdejmujemy cordon z workerów.
[bastuser@bastion ~]$ ssh -i /home/bastuser/ssh/id_ed25519 [email protected] [core@control-plane-1 ~]$ sudo -i [root@control-plane-1 ~]# export KUBECONFIG=/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost-recovery.kubeconfig [root@control-plane-1 ~]# oc whoami system:admin [root@control-plane-1 ~]# oc get nodes oc adm uncordon compute-1.testcluster.okdlab.local oc adm uncordon compute-2.testcluster.okdlab.local oc adm uncordon compute-3.testcluster.okdlab.local
| LP | Nazwa DNS | Adres ip | vCPU | vRAM | vHDD | System Operacyjny | Funkcja |
|---|---|---|---|---|---|---|---|
| 1 | dns1.okdlab.local | 192.168.40.10 | 2 | 2 GB | 16 GB | Fedora Server 43 | DNS dla klastra |
| 2 | proxy.okdlab.local | 192.168.40.15 | 2 | 2 GB | 16 GB | Fedora Server 43 | HAProxy / Load balancer / instalacja |
| 3 | storage.okdlab.local | 192.168.40.20 | 2 | 2 GB | 16/64/128 GB | Fedora Server 43 | Storage NFS dla klastra, registry OpenShift |
| 4 | database-1.okdlab.local | 192.168.40.25 | 2 | 4 GB | 32 GB | Fedora Server 43 | Serwer baz danych PostgreSQL |
| 5 | database-2.okdlab.local | 192.168.40.26 | 2 | 4 GB | 32 GB | Fedora Server 43 | Serwer baz danych MariaDB |
| 6 | bastion.okdlab.local | 192.168.40.30 | 4 | 4 GB | 128 GB | Fedora Server 43 | Instalacja i zarządzanie klastrem OKD |
| 7 | gitea.okdlab.local | 192.168.40.35 | 4 | 8 GB | 256 GB | Fedora Server 43 | Gitea |
| 8 | jenkins.okdlab.local | 192.168.40.37 | 8 | 16 GB | 256 GB | Fedora Server 43 | Jenkins |
| 9 | bootstrap.testcluster.okdlab.local | 192.168.40.50 | 4 | 16 GB | 128 GB | CentosOS Stream 9 | Bootstrap node |
| 10 | control-plane-1.testcluster.okdlab.local | 192.168.40.51 | 4 | 16 GB | 128 GB | CentosOS Stream 9 | Master node |
| 11 | control-plane-2.testcluster.okdlab.local | 192.168.40.52 | 4 | 16 GB | 128 GB | CentosOS Stream 9 | Master node |
| 12 | control-plane-3.testcluster.okdlab.local | 192.168.40.53 | 4 | 16 GB | 128 GB | CentosOS Stream 9 | Master node |
| 13 | compute-1.testcluster.okdlab.local | 192.168.40.61 | 4 | 16 GB | 256 GB | CentosOS Stream 9 | Worker node |
| 14 | compute-2.testcluster.okdlab.local | 192.168.40.62 | 4 | 16 GB | 256 GB | CentosOS Stream 9 | Worker node |
| 15 | compute-3.testcluster.okdlab.local | 192.168.40.63 | 4 | 16 GB | 256 GB | CentosOS Stream 9 | Worker node |

