Suboptymalne rozmieszczenie podów między węzłami roboczymi klastra

Składnik warstwy sterowania klastra Kubernetes odpowiedzialny za przypisywanie nowo tworzonych podów do węzłów roboczych, na których powinny zostać uruchomione nosi nazwę kube-scheduler. Bez doprecyzowania odpowiedniej konfiguracji, kube-scheduler nie gwarantuje przypisania pary podów tego samego rodzaju do różnych węzłów roboczych klastra. W rezultacie, dwa lub więcej podów będących replikami tego samego Deployment’u lub należących do tego samego StatefulSet’u mogą zostać umieszczone na tym samym węźle pogarszając tym samym odporność na awarie.

Opisane zachowanie klastra najłatwiej prześledzić na przykładzie. Wyobraźmy sobie, że tworząc klaster Kafki, na który składają się trzy instancje brokerów oraz trzy instancje Apache Zookeeper, uruchamiane pody zostają przypisane do odpowiednich węzłów klastra Kubernetes zgodnie z poniższym schematem: przypisanie podów do węzłów roboczych klastra Kubernetes Takie przypisanie istotnie zmniejsza odporność klastra Kafki na awarie węzłów działającego pod spodem klastra Kubernetes. W przypadku niedostępności drugiego węzła roboczego, dostępna pozostanie tylko jedna instancja Apache Zookeeper, co uniemożliwi uzyskanie kworum, w konsekwencji zatrzymując operacje zapisu do brokerów Kafki. Awaria trzeciego węzła roboczego również mogłaby zatrzymać klaster Kafki (przy pewnych założeniach dotyczących wymagań ISR). W podsumowaniu - dostępność usługi zostanie przerwana, jeżeli awarii ulegnie drugi lub trzeci węzeł roboczy klastra Kubernetes.

Możliwym rozwiązaniem pozwalającym na uniknięcie przedstawionej jest zdefiniowanie wymagań anti-affinity podów Kafki oraz Zookeepera, tak aby dwa pody tego samego typu nie mogły zostać umieszczone na tym samym węźle klastra. Otrzymane w rezultacie rozmieszczenie podów zostało zilustrowane poniższą grafiką. przypisanie podów do węzłów roboczych klastra Kubernetes z pod anti-affinity Przedstawione rozmieszczenie jest optymalne. W przypadku awarii dowolnego z węzłów, dostępne pozostaną dwie instancje Zookeepera oraz dwie instancje brokera, w konsekwencji utrzymując poprawne działanie Kafki.

Tworzenie osobnego Load Balancera dla każdego serwisu

W większości klastrów Kubernetes, mamy do czynienia z więcej niż jednym serwisem, który powinien być wyeksponowany (dostępny z publicznego Internetu). Jeżeli dany serwis zostanie udostępniony z wykorzystaniem imperatywnego polecenia:

kubectl expose deploy nazwa-deploymentu --type=LoadBalancer --name=nazwa-svc 

W rezultacie zostanie wydzierżawiony Load Balancer odpowiedni dla danego cloud providera (np. Network Load Balancer w przypadku Amazon EKS, Azure Load Balancer dla Azure Kubernetes Service czy NodeBalancer dla Linode Kubernetes Engine). Jeżeli w klastrze posiadamy kilkanaście serwisów wyeksponowanych wspomnianą metodą, dla każdego z nich zostanie wydzierżawiony osobny Load Balancer istotnie podnosząc kwotę na fakturze wystawianą na koniec miesiąca przez cloud providera.

Aby uniknąć niepotrzebnego mnożenia Load Balancerów, w większości przypadków znacznie lepszym rozwiązaniem będzie zastosowanie kontrolera ruchu przychodzącego (Ingress Controller), którego repliki wyeksponowane będą jako NodePort do zewnętrznego Load Balancera. W rezultacie, za routing wewnątrz klastra odpowiadać będzie Ingress Controller, do którego ruch kierowany będzie przez tylko jeden zewnętrzny Load Balancer. Ponadto, Ingress Controller może dokonywać terminacji TSL.

Niedostateczna replikacja kontrolera ruchu przychodzącego (Ingress Controller)

Jeżeli dany klaster wykorzystuje kontroler ruchu przychodzącego, nie bez znaczenia pozostaje liczba jego replik. Niektóre opracowania i poradniki dostępne w Internecie zawierają przykładowe konfiguracje posiadające tylko jedną replikę Ingress Controller’a, do której kierowany jest ruch z publicznego Internetu za pośrednictwem Load Balancera. W przypadku klastra Kubernetes posiadającego więcej niż jeden węzeł roboczy, takie rozwiązanie jest suboptymalne z punktu widzenia dostępności aplikacji uruchomionych w klastrze. W przypadku awarii węzła, na którym uruchomiony jest jedyny pod Ingress Controller’a, może wystąpić następująca sekwencja zdarzeń:

  1. Węzeł A nie odpowiada na heartbeat wysyłany przez Load Balancer.
  2. Load Balancer oznacza dany węzeł klastra jako martwy. Ponieważ był to jedyny węzeł klastra odpowiadający na heartbeat LB, klaster zostaje całkowicie odcięty od ruchu przychodzącego z LB.
  3. Warstwa sterowania klastra Kubernetes uruchamia pod Ingress Controllera na węźle B.
  4. Load Balancer otrzymuje odpowiedź na heartbeat od węzła B i rozpoczyna kierowanie ruchu z publicznego Internetu do Ingress Controllera uruchomionego na tym węźle klastra.

W rezultacie dostępność usługi zostaje przywrócona dopiero w kroku czwartym, po przerwie mogącej trwać nawet kilka minut. Zwiększenie liczby replik Ingress Controller’a pozwoliłoby na niemal całkowite zachowanie dostępności aplikacji w opisanym scenariuszu (poza utratą części ruchu przychodzącego do LB przed drugim krokiem).

Brak konfiguracji zasad sieciowych (Network Policies)

Zasady sieciowe Kubernetes to istotne zagadnienie bezpieczeństwa, o którym mówi się zdecydowanie za mało. Co może wydawać się nieoczywiste, domyślnie wewnątrz klastra Kubernetes, każdy pod może komunikować się dowolnym innym podem.

Prześledźmy pożądaną komunikację między poszczególnymi podami na przykładzie aplikacji składającej się z następujących składowych:

  • Bazy danych PostgreSQL (app: postgres)
  • Aplikacji backendowej w Node.js (app: node)
  • Frontendu renderowanego po stronie serwera w Next.js (app: next)

W opisanym przykładzie, konieczne jest dopuszczenie ruchu wychodzącego z aplikacji serwerowej do bazy danych, natomiast bezpośrednia komunikacja między kontenerem odpowiedzialnym za prerenderowanie frontendu a bazą danych nie jest potrzebna.

Tutaj z pomocą przychodzą zasady sieciowe (Network Policies), które umożliwiają wprowadzenie ograniczeń w komunikacji między poszczególnymi podami. Oto przykładowa definicja zasady sieciowej, która ogranicza ruch przychodzący do bazy danych jedynie do podów aplikacji backendowej:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
[...]
spec:
  podSelector:
    matchLabels:
      app: postgres
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: node

Stosowanie Network Policies w klastrze Kubernetes podnosi poziom bezpieczeństwa. W sytuacji, w której atakujący pozyskałby kontrolę nad jednym z podów, utrudnione byłoby spenetrowanie pozostałych składników aplikacji.

Wszystkie przedstawione w tym zestawieniu niedociągnięcia w konfiguracji klastra Kubernetes posiadają jedną cechę wspólną. Pomimo ich obecności, aplikacje uruchomione w klastrze będą działać poprawnie, aczkolwiek z mniejszą odpornością na awarie, niższym poziomem bezpieczeństwa czy wyższymi kosztami infrastruktury.