Klaster OKD 4.19 (OpenShift) – Prawidłowe wyłączanie i włączanie klastra

 

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:19Z

Cordon 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
done

Shutdown 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 indziej

Workery — 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
done

Mastery — 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
done

Na 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
LPNazwa DNSAdres ipvCPUvRAMvHDDSystem OperacyjnyFunkcja
1dns1.okdlab.local192.168.40.1022 GB16 GBFedora Server 43DNS dla klastra
2proxy.okdlab.local192.168.40.1522 GB16 GBFedora Server 43HAProxy / Load balancer / instalacja
3storage.okdlab.local192.168.40.2022 GB16/64/128 GBFedora Server 43Storage NFS dla klastra, registry OpenShift
4database-1.okdlab.local192.168.40.2524 GB32 GBFedora Server 43Serwer baz danych PostgreSQL
5database-2.okdlab.local192.168.40.2624 GB32 GBFedora Server 43Serwer baz danych MariaDB
6bastion.okdlab.local192.168.40.3044 GB128 GBFedora Server 43Instalacja i zarządzanie klastrem OKD
7gitea.okdlab.local192.168.40.3548 GB256 GBFedora Server 43Gitea
8jenkins.okdlab.local192.168.40.37816 GB256 GBFedora Server 43Jenkins
9bootstrap.testcluster.okdlab.local192.168.40.50416 GB128 GBCentosOS Stream 9Bootstrap node
10control-plane-1.testcluster.okdlab.local192.168.40.51416 GB128 GBCentosOS Stream 9Master node
11control-plane-2.testcluster.okdlab.local192.168.40.52416 GB128 GBCentosOS Stream 9Master node
12control-plane-3.testcluster.okdlab.local192.168.40.53416 GB128 GBCentosOS Stream 9Master node
13compute-1.testcluster.okdlab.local192.168.40.61416 GB256 GBCentosOS Stream 9Worker node
14compute-2.testcluster.okdlab.local192.168.40.62416 GB256 GBCentosOS Stream 9Worker node
15compute-3.testcluster.okdlab.local192.168.40.63416 GB256 GBCentosOS Stream 9Worker node