Przeprowadzka control-plane'u bez dramatu: etcd na nowy węzeł

Przed końcem umowy na dwie małe maszyny produkcyjnego klastra przeniosłem etcd i wszystko inne na jeden serwer dedykowany, bez stawiania klastra od nowa. Metoda zajęła jedną noc, a dwa zaskoczenia po drodze nauczyły mnie więcej niż sam plan.

· 4 min czytania

Spisane w listopadzie 2026 na podstawie notatek z tamtego czasu.

Produkcyjny klaster stał wtedy na dwóch małych maszynach wirtualnych: jedna niosła control-plane, druga warstwę webową. Ten podział powstał we wrześniu, kiedy control-plane’owi zaczęło brakować pamięci i odciążyłem go, przenosząc strony na osobną maszynę. W październiku pojawiła się okazja na prostszy układ: jeden serwer dedykowany zamiast dwóch maszyn wirtualnych. Zanim cokolwiek ruszyłem, sprawdziłem w Prometheusie szczyty z ostatniego tygodnia. Obie maszyny razem zużywały w szczycie wyraźnie mniej pamięci, niż miał nowy serwer, więc konsolidacja miała sens. Termin wyznaczał koniec umowy na mniejsze maszyny.

Najprostsza droga to postawić nowy klaster i przenieść aplikacje. Wybrałem trudniejszą, ale czystszą: ten sam klaster, ta sama baza etcd, te same certyfikaty, tylko na innym sprzęcie.

Etap 0: endpoint na nazwę, zanim cokolwiek się ruszy

Klaster znał swój adres API jako goły adres IP starego serwera, a ten adres miał zniknąć. Dlatego pierwszy krok, zrobiony osobno i sprawdzony przed resztą, nie miał nic wspólnego z nowym serwerem: przełączyłem endpoint klastra na nazwę DNS, którą certyfikat API już znał. Nazwa wskazywała na ten sam stary adres, więc teoretycznie nic się nie zmieniało.

Talos pozwala zastosować taką zmianę w trybie próbnym, z automatycznym wycofaniem, i dopiero potem na stałe. Workery nawet tego nie zauważyły, bo kubelet łączy się przez lokalny proxy KubePrism. Na control-plane zmiana zrestartowała statyczne pody i API na chwilę zniknęło, czego podgląd zmian w ogóle nie zapowiadał.

Etap 1: drugi control-plane

Nowy serwer zainstalowałem z obrazu Talosa pobranego z oficjalnego wydania, sprawdzając sumę kontrolną i podpis cosign. Obrazy generowane na żądanie nie miały sum do porównania, a nie chciałem instalować systemu produkcyjnego z pliku, którego nie umiem zweryfikować. Konfigurację przygotowałem tą samą wersją talosctl, na której stoi klaster, z sekcją etcd identyczną jak na starym węźle.

Nowy węzeł dołączył do etcd sam: najpierw jako learner, który tylko dogania dane, potem jako pełny członek. Od tej chwili etcd ma dwóch członków i quorum wymaga obu. Awaria któregokolwiek zatrzymałaby cały klaster, więc dwa kolejne etapy zrobiłem tej samej nocy.

Etap 2: aplikacje i wolumeny

Aplikacje webowe przypinam do węzłów po etykiecie, a nie po nazwie hosta. Wystarczyło nadać etykietę nowemu serwerowi, zdjąć ją ze starego i zrestartować pody. Wolumeny przeniosłem skryptem z sumą sha256 po obu stronach, przy zatrzymanej aplikacji. Na koniec przepiąłem rekordy DNS stron na nowy adres.

Tu jest pułapka, o której warto wiedzieć: PersistentVolume z polityką Delete po zwolnieniu zostaje skasowany przez provisioner razem z danymi. Przy kopiowaniu oznaczało to, że źródłowa kopia zniknęłaby w chwili przełączenia aplikacji na nowy wolumen. Przed zwolnieniem każdego takiego PV przełączyłem go na Retain.

Etap 3: etcd zmienia dom

Nazwa endpointu poszła na nowy adres. Potem ręczny snapshot etcd, sprawdzony, i stary węzeł opuścił etcd.

Przed oddaniem etcd: snapshot, zawsze, nawet jeśli wszystko wygląda dobrze
$ talosctl -n $NEW_NODE etcd snapshot etcd-before-handover.db
$ talosctl -n $NEW_NODE etcd members
# stary węzeł opuszcza etcd...
$ talosctl -n $OLD_NODE etcd leave
# ...i od razu się zatrzymuje; kasuję tylko partycje systemowe, dane zostają jako kopia awaryjna
$ talosctl -n $OLD_NODE reset --system-labels-to-wipe STATE --system-labels-to-wipe EPHEMERAL --reboot=false
$ kubectl delete node $OLD_NODE

Stare maszyny nie zostały wymazane do zera. Ich dyski z danymi zostały nietknięte jako kopia awaryjna aż do rezygnacji z tych serwerów.

Dwa zaskoczenia

Zmiana endpointu zmienia też wystawcę tokenów kont serwisowych. W Talosie adres endpointu trafia do --service-account-issuer i --api-audiences API servera. Wszystkie tokeny wydane przed zmianą zaczęły dostawać Unauthorized, aż kubelet je odnowił. Agent Cilium na jednym z węzłów nie widział przez to nowego węzła, kilka operatorów zgłaszało błędy, dwa pody wpadły w CrashLoop. Restart dotkniętych podów naprawił to od ręki. Następnym razem podam stary issuer jako dodatkowy na czas przełączenia albo od razu zaplanuję restart.

Stary API server bez etcd dalej przyjmuje połączenia. Po etcd leave stary węzeł nadal odpowiadał na porcie API, a KubePrism dalej kierował tam część ruchu. Kontrolery z wyborem lidera przez kilkadziesiąt sekund nie mogły odnowić dzierżaw, dopóki nie zatrzymałem starego węzła. Stąd w komendach wyżej reset zaraz po leave, a nie „za chwilę”.

Czego się nauczyłem

  • Rozdziel zmiany, które da się rozdzielić. Przełączenie endpointu na nazwę jako osobny, sprawdzony krok sprawiło, że dalsza część nocy miała o jedną niewiadomą mniej.
  • Snapshot etcd przed każdym ruchem w quorum. Nie dlatego, że coś pójdzie źle, tylko dlatego, że jeśli pójdzie, to bez niego nie ma dokąd wrócić.
  • Planuj skutki uboczne, nie tylko kroki. Oba zaskoczenia wynikały z zachowań udokumentowanych, ale niewidocznych w diffie. Teraz są w README repozytorium, żeby nikt, łącznie ze mną, nie odkrywał ich drugi raz.

Wszystkie wpisy