Wirtualny adres API klastra: kube-vip, keepalived czy zewnętrzny balancer
Trzy węzły control plane nic nie dają, jeśli klienci łączą się z API pod adresem jednego z nich. Trzy sposoby utrzymania wspólnego adresu, ich konfiguracje, certyfikat z właściwymi nazwami i sprawdzenie, kto naprawdę z adresu korzysta.
Przewodnik Odporność małego środowiska Część 4 z 9
- DNS jako pojedynczy punkt awarii: co się psuje, gdy resolver nie odpowiada
- Dwa łącza internetowe w małej firmie: failover, który naprawdę przełącza
- Wysoka dostępność w małym klastrze: co naprawdę pada, gdy znika węzeł
- Wirtualny adres API klastra: kube-vip, keepalived czy zewnętrzny balancer
- Wygasający certyfikat to najbardziej przewidywalna awaria: inwentarz i monitoring
- Backup, którego nikt nie odtwarzał, nie istnieje: protokół testu odtworzenia
- Checkpoint to nie kopia zapasowa: jak działają punkty kontrolne Hyper-V
- Kopia Active Directory i odtwarzanie lasu: procedura, której nikt nie chce ćwiczyć
- Pierwsza godzina awarii: kto prowadzi, co komunikować i co wycofać
Klaster ma trzy węzły control plane i wirtualny adres API. Po restarcie pierwszego węzła w całym klastrze przestaje działać sieć: pody nie rozwiązują nazw, nowe nie startują, a aplikacje zgłaszają błędy połączeń. Po godzinie okazuje się, że wtyczka sieciowa została zainstalowana z adresem API tego węzła, a nie z adresem wirtualnym. Przez rok działała bez zarzutu.
Klienci klastra potrzebują jednego adresu API, który przetrwa awarię dowolnego węzła. Mechanizm, który go utrzymuje, i to, czy naprawdę korzysta z niego każdy klient, decydują o tym, czy awaria jednego węzła pozostanie awarią jednego węzła.
Trzy podejścia
Adres zarządzany z wnętrza klastra. kube-vip działa jako statyczny pod lub DaemonSet na węzłach control plane, wybiera lidera przez dzierżawę (obiekt Lease) w API Kubernetes i ogłasza adres w sieci: przez ARP w warstwie drugiej albo przez BGP do routera. Nie wymaga dodatkowych maszyn.
Adres zarządzany na węzłach, poza Kubernetes. Keepalived działa jako usługa systemowa na węzłach control plane i protokołem VRRP wybiera węzeł, który trzyma adres. Skrypt sprawdzający stan serwera API decyduje, czy węzeł nadaje się do tej roli.
Zewnętrzny balancer. Para maszyn z HAProxy i keepalived albo urządzenie sieciowe stoi przed węzłami, sprawdza każdy serwer API i rozdziela połączenia. Adres wirtualny należy do balancera, nie do węzłów.
| Kryterium | kube-vip w klastrze | Keepalived na węzłach | Zewnętrzny balancer |
|---|---|---|---|
| Dodatkowe maszyny | brak | brak | dwie lub urządzenie |
| Zależność od API i etcd | tak | nie | nie |
| Wymaga wspólnej warstwy 2 | w trybie ARP tak | tak | nie dla węzłów |
| Rozkład ruchu na węzły | nie, jeden lider | nie, jeden właściciel | tak |
| Złożoność utrzymania | najniższa | średnia | najwyższa |
kube-vip: dzierżawa i jej parametry
Adres zarządzany z wnętrza klastra zależy od API, które sam wystawia. Lider musi regularnie odnawiać dzierżawę w API, a API potrzebuje etcd. Gdy etcd odpowiada wolno, na przykład przy przeciążonym dysku, odnowienie nie zdąży, adres przeskakuje na inny węzeł, a wszyscy klienci zrywają połączenia i obciążają API jeszcze bardziej. Rosnący z tygodnia na tydzień licznik restartów kube-vip to najczęstszy objaw tej pętli.
Manifest generuje sam kube-vip. Bez parametru --services obsługuje on wyłącznie adres API, co oddziela tę rolę od adresów usług typu LoadBalancer:
kube-vip manifest daemonset \
--interface eth0 \
--address 10.10.0.50 \
--inCluster \
--taint \
--controlplane \
--arp \
--leaderElection
Domyślne parametry dzierżawy (5, 3 i 1 sekunda) są dobrane pod szybkie przełączenie. W środowisku, w którym etcd bywa wolniejsze, warto dać im zapas w zmiennych środowiskowych kontenera:
env:
- name: vip_leaseduration
value: "15"
- name: vip_renewdeadline
value: "10"
- name: vip_retryperiod
value: "2"
Keepalived: sprawdzanie gotowości API
Keepalived nie zależy od etcd ani od API. Adres przenosi się wtedy, gdy skrypt kontrolny na danym węźle przestaje zwracać sukces. Skrypt powinien pytać o /readyz, a nie sprawdzać otwarty port 6443, bo otwarty port mówi tylko, że proces nasłuchuje. W k3s anonimowy dostęp do API jest domyślnie wyłączony, więc skrypt korzysta z lokalnego kubeconfig przez k3s kubectl:
vrrp_script chk_apiserver {
script "/usr/local/bin/k3s kubectl get --raw=/readyz --request-timeout=2s"
interval 3
timeout 3
fall 3
rise 2
}
vrrp_instance K8S_API {
state BACKUP
nopreempt
interface eth0
virtual_router_id 51
# 100, 90 i 80 na kolejnych węzłach
priority 100
advert_int 1
virtual_ipaddress {
10.10.0.50/24
}
track_script {
chk_apiserver
}
}
fall 3 przy interwale 3 sekund oznacza przełączenie po około 10 sekundach braku gotowości. nopreempt sprawia, że węzeł o wyższym priorytecie po powrocie nie odbiera adresu od razu, co oszczędza klientom drugiego przełączenia. Skrypt musi działać z uprawnieniami pozwalającymi odczytać /etc/rancher/k3s/k3s.yaml.
Zewnętrzny balancer
Pojedyncza maszyna z HAProxy jest nowym pojedynczym punktem awarii, dlatego balancer to para z keepalived, który przenosi adres między nimi. Na maszynie zapasowej HAProxy musi móc nasłuchiwać na adresie, którego jeszcze nie ma, co umożliwia ustawienie net.ipv4.ip_nonlocal_bind = 1.
frontend k8s_api
bind 10.10.0.50:6443
mode tcp
default_backend k8s_api_servers
backend k8s_api_servers
mode tcp
balance roundrobin
default-server inter 3s fall 3 rise 2
server cp1 10.10.0.11:6443 check
server cp2 10.10.0.12:6443 check
server cp3 10.10.0.13:6443 check
Certyfikat API i --tls-san
Adres wirtualny i nazwa DNS, pod którą klienci go znają, muszą być w polu SAN certyfikatu serwera API. W k3s dodaje się je parametrem --tls-san albo w pliku konfiguracyjnym, na każdym węźle serwerowym:
tls-san:
- k8s-api.corp.example.com
- 10.10.0.50
Listę najlepiej ustalić przed instalacją. Dodanie pozycji później wymaga restartu usługi k3s na każdym serwerze i sprawdzenia, czy certyfikat zawiera nową nazwę. Brak wpisu objawia się błędem „x509: certificate is valid for …, not 10.10.0.50” dopiero wtedy, gdy ktoś pierwszy raz połączy się przez adres wirtualny.
Klienci, którzy omijają adres wirtualny
Plik k3s.yaml na serwerze k3s wskazuje https://127.0.0.1:6443. Kopiując go na stację administratora albo do CI, trzeba zmienić adres na nazwę wirtualną, a nie na adres węzła, z którego plik pochodzi. Agenci k3s używają adresu z parametru server przy rejestracji, a po dołączeniu łączą się z serwerami przez wbudowany balancer. Jeśli jednak server wskazuje jeden konkretny serwer, dołączenie nowego węzła podczas jego awarii się nie powiedzie.
Co wybrać
W małym klastrze w jednej sieci kube-vip jest rozsądnym startem, pod warunkiem że jego restarty są monitorowane, a dzierżawa ma zapas. Gdy klaster obsługuje coś, czego niedostępność boli, keepalived na węzłach usuwa najgroźniejszą zależność kosztem jednej usługi systemowej. Para zewnętrznych balancerów ma sens, gdy węzły są w różnych sieciach, gdy potrzebny jest rozkład ruchu albo gdy ten sam balancer obsługuje inne usługi.
Test
Wyłączenie węzła, który w danej chwili trzyma adres, na co najmniej dwadzieścia minut. Przerwę mierzy pętla uruchomiona z innej maszyny: kod 000 oznacza brak odpowiedzi, każdy kod HTTP oznacza, że adres odpowiada.
while true; do
printf '%s %s\n' "$(date +%T)" \
"$(curl -sk -o /dev/null -w '%{http_code}' --max-time 1 https://10.10.0.50:6443/readyz)"
sleep 1
done
Po teście sprawdza się liczbę restartów komponentów w kube-system i to, czy po powrocie węzła adres nie przeskoczył na niego bez potrzeby.
Sprawdź u siebie
Pierwsze polecenie pokazuje, z jakim adresem łączy się bieżący kubeconfig.
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}{"\n"}'
Dobry wynik to nazwa lub adres wirtualny, na przykład https://k8s-api.corp.example.com:6443. Adres konkretnego węzła oznacza, że ten klient straci dostęp razem z węzłem. Polecenie warto uruchomić na każdej stacji administratora i w każdym pipeline.
Drugi blok sprawdza, czy API odpowiada przez adres wirtualny.
curl -k --max-time 3 https://10.10.0.50:6443/readyz; echo
kubectl --server=https://10.10.0.50:6443 get --raw='/readyz?verbose' | tail -n 1
Odpowiedź ok z curl oznacza gotowe API. Odpowiedź JSON z kodem 401 oznacza, że adres działa, ale anonimowy dostęp jest wyłączony (jak w k3s); wtedy rozstrzyga drugie polecenie, które powinno zwrócić readyz check passed. Brak odpowiedzi oznacza, że adres nie działa, a błąd x509 w drugim poleceniu oznacza brak adresu w certyfikacie.
Trzeci blok pokazuje nazwy w certyfikacie serwera API (OpenSSL 1.1.1 lub nowszy).
openssl s_client -connect 10.10.0.50:6443 -servername k8s-api.corp.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
Lista powinna zawierać zarówno DNS:k8s-api.corp.example.com, jak i IP Address:10.10.0.50.
Czwarty blok szuka w konfiguracji komponentów systemowych adresów konkretnych węzłów.
# Adresy węzłów control plane zamiast adresu wirtualnego
kubectl -n kube-system get daemonset,deployment,configmap -o yaml | grep -nE '10\.10\.0\.1[123]'
Dobry wynik jest pusty. Każde trafienie trzeba obejrzeć: jeśli to adres API, komponent zależy od jednego węzła.