Zaktualizowano 7 min czytania

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.

Rys. 1. Adres zarządzany z wnętrza klastra zależy od API, które sam wystawia; para balancerów sprawdza serwery API z zewnątrz i nie zależy od ich stanu.

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.

Kryteriumkube-vip w klastrzeKeepalived na węzłachZewnętrzny balancer
Dodatkowe maszynybrakbrakdwie lub urządzenie
Zależność od API i etcdtaknienie
Wymaga wspólnej warstwy 2w trybie ARP taktaknie dla węzłów
Rozkład ruchu na węzłynie, jeden lidernie, jeden właścicieltak
Złożoność utrzymanianajniższaśrednianajwyż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.