Kubernetes #2

Maciej Lelusz
16. października 2018
Reading time: 7 min
Kubernetes #2

Co zatem umożliwia ten Kubernetes? 

Przede wszystkim daje nam wysoką prędkość działania. Nie mówię tutaj o takiej surowej prędkości aplikacji – że 2 sek. szybciej się coś wyświetla, nie. Chodzi o prędkość, z jaką możemy wykonywać zmiany na infrastrukturze, pozostawiając aplikację wciąż dostępną i produkującą. To, co teraz jest ogromnym problemem – okna serwisowe, zmiany wersji przy Kubernetesie idą niejako w niepamięć. Umożliwia to idea działania Kubernetesa oparta o trzy główne filary – niezmienność (immutability), deklaratywna konfiguracja (declarative configuration) i samo leczenie (self-healing).

Zacznijmy od niezmienności. Idea ta w świecie Kubernetesa, jak i konteneryzacji polega na tym, że raz powołana infrastruktura nie może być modyfikowana przez użytkownika. Jedyny sposób zmiany konfiguracji, to zniszczenie starej infrastruktury i powołanie nowej ze zmienionymi parametrami. Brzmi nieco przerażająco, wiem, ale gdy zastanowić się nad tym dłużej, to ma to ogromną przewagę. Brak zmian w potencjalnie produkującej infrastrukturze zapobiega błędom ludzkim związanym z tzw. „no, coś kiedyś tam zmieniliśmy, ale Józek już nie pracuje, a my nie wiemy… to może nie aktualizujmy”. Takich sytuacji już nie ma, bo dzięki temu, że infrastruktura jest powołana, ma ona swoją konkretną konfigurację, którą można przejrzeć i w razie awarii lub nieudanej aktualizacji prześledzić gdzie jest błąd. Druga sprawa to kwestia tego, że gdy posiada się starą wersję aplikacji (np. starą wersję kontenera w repozytorium, wraz z jego konfiguracją), to zawsze można do niej wrócić, czy nawet zostawić ją uruchomioną na czas aktualizacji i w przypadku problemu, przepiąć się w kilka sekund na stary stos technologiczny. Myślę, że teraz brzmi to lepiej, jednak nasuwa się pytanie, jak to ogarniać w kontekście hyperskali. Odpowiedzią na to jest deklaratywna konfiguracja, która reprezentuje całość jako kod.

Tak! Nie klikamy, tylko ładnie, wszystko linia po linii wypisujemy, a Kubernetes dla nas wykonuje zgodnie z opisem całą infrastrukturę. Zanim rozpali się w waszej jaźni bunt, że „przecież teraz tak jest, żadna zmiana! Apage, Satanas!”, to jednak jest to zmiana. Dotychczas byliśmy przyzwyczajeni, że klapiąc skrypt, który ma coś konfigurować, należało napisać linia po linii dokładnie każdy krok, który system miał wykonać i to nazywało się konfiguracją imperatywną. Czyli np. zrób folder, skopiuj z tego, a tego miejsca to i to, a w linii 37 ustaw tak. Jak się pomyliliśmy, to skrypt się wywalał i w najlepszym przypadku nic nie było zrobione, a w najgorszym mieliśmy 99,7% skryptu wykonane, ale nic nie działało. Imperatywna konfiguracja mówi jak dojść z punktu A do B i do C. Przy konfiguracji deklaratywnej sprawa ma się odwrotnie – mówimy od razu, że chcemy C. To znaczy, definiujemy stan docelowy, a platforma, w tym przypadku Kubernetes dąży do osiągnięcia tego celu swoimi, zaszytymi w nim wewnętrznie metodami.

Ma to szereg zalet, pierwsza z nich to fakt, że nie musimy znać wszystkich mechanizmów, które za powołanie infrastruktury odpowiadają. Co przekłada się na czas, który możemy poświęcić na dopieszczenie konfiguracji. Kuberentes jest dla nas swoistym frameworkiem z przygotowanymi przez kogoś funkcjami, z których my grzecznie korzystamy. Jak chcemy się dowiedzieć jak dokładnie one działają, można się zanurzyć w kod samego Kubernetesa, gdyż jest on dostępny publicznie. Drugi zasadniczy plus to fakt, że to nie my dbamy o to, jak dojść do pożądanego stanu, tylko platforma. W wolnym tłumaczeniu oznacza to, że jak konfiguracja gdzieś jest niepoprawna i infrastruktura nie zostanie powołana, to Kubernetes posprząta po sobie i powie nam, że coś jest nie tak w linii tej i tej. Wszystko to wdrożeniowa strona medalu, ale jest też i operacyjna. Dzięki temu, że wszystko trzymamy w kodzie, posiadamy kontrolę wersji konfiguracji infrastruktury i możliwość jej faktycznego audytowania. Transfer wiedzy w końcu również staje się możliwy, a wraz z tym automatyczna dokumentacja.

Kiedy Kubernetes otrzyma konfigurację, będzie utrzymywać stan, czyli jeżeli będziemy chcieli powołać 5 instancji jakiegoś kontenera i dajmy na to, jedna z tych instancji przestanie działać, to Kubernetes ubije ją i postawi na nowo. Bo zadeklarowaliśmy ich 5, a nie 4, więc trzeba działać! Oznacza to, że Kubernetes nie będzie tylko powoływał z konfiguracji Twoją infrastrukturę, ale również będzie dbał nieprzerwanie o jej zdrowie.

Skoro postanowiliśmy, że do naszej przykładowej platformy do korepetycji transoceanicznych zastosujemy technologię konteneryzacyjną, stworzymy ją w architekturze dystrybuowanych mikroserwisów, to idea niezmienności, deklaratywnej konfiguracji i samoleczenia przy pomocy Kubernetesa staje się dość seksowna. Zachęcające może być również to, jak będziemy mogli podejść do skalowania aplikacji w godzinach wieczornych na drugiej półkuli, gdy żądni wiedzy Panowie i Panie, oraz ich karty kredytowe zaczną atakować naszą infrastrukturę. Dzięki zastosowaniu deklaratywnej konfiguracji jest to banalnie proste – wystarczy zmiana w konfigu, a Kubernetes sam wykona pracę. Niezmienne obrazy kontenerów, ich konfiguracja w odpowiedniej równie stałej wersji pozwolą na skalowanie automatyczne, tylko zmiana cyfry ze starej ilości instancji do nowej… lub zastosowanie wbudowanego wK8S (Kubernetesa) autoskalingu i pozwolenie mu samemu na zarządzanie bałaganikiem.

Oczywiście uruchamiając autoskaling należy mieć odpowiednie zasoby sprzętowe i jakiś pomysł na DDoS. Ponieważ czasem ilość chętnych na korepetycje może znacząco wzrosnąć i będzie wymagane przeskalowanie samego klastra Kubernetesa. Tutaj również widać rękę doświadczenia, która spoczywała na twórcach tego projektu, zrytą wieloma bruzdami po niezliczonych awariach i katastrofach, po EoF na przepełnionych dyskach i All Path Down na macierzach. Wszystkie węzły klastra Kubernetesowego są identyczne, a aplikacje w kontenerach są od nich całkowicie niezależne. Zatem autodeployment kolejnego węzła może się odbyć z wcześniej przygotowanego obrazu i prostego one-linera, który dołączy go do klastra.

Zacznijmy odwrotnie, niż by się wydawało logicznie, czyli nie od architektury, ale od tego jakie byty logiczne musimy poznać i zawrzeć w naszej konfiguracji, aby Kubernetes wiedział jak powołać dla nas zadeklarowaną infrastrukturę.

Na początek Pod, jest to jeden lub zbiór kontenerów opisanych konfiguracją w pewnej grupie. Jest to najmniejszy byt konfiguracyjny reprezentujący proces, jaki można stworzyć i uruchomić na klastrze. Pody dzieli się na dwa rodzaje:

  1. Pod, który uruchamia jeden kontener– Model zwany „one-container-per-Pod” jest najbardziej popularną metodą używaną w Kubernetesie. Pod można to przyrównać, do swego rodzaju enkapsulacji, w której środku działa kontener, a K8S nie zarządza nim bezpośrednio, a za pośrednictwem Pod’a właśnie. 
  2. Pod, który uruchamia wiele kontenerów– Gdy mamy aplikację, która składa się z kilku kontenerów, które są blisko ze sobą związane i muszą współdzielić zasoby, wówczas warto zapakować je w jeden Pod i traktować to jako wątek zbudowany z kilku kontenerów.  

Pod ma jeden adres IP i zakres portów, który jest współdzielony przez kontenery w nim się znajdujące, który nie jest gwarantowany ze względu na to, że istnienie poda z założenia jest krótkotrwałe. Poza wspomnianymi, kontenery w Podzie mogą dzielić ze sobą volumeny z danymi, należy jednak pamiętać, że istnieją one tylko tyle, ile żyje Pod, czyli przy zniszczeniu Poda, zniszczony jest również volumen, a dane na nim są usuwane. Można temu zapobiec poprzez zastosowanie sterowników zewnętrznych do tzw. Persistent Storage.

Kolejnym bytem są serwisy. Tak jak pody są śmiertelne – żyją i umierają, tak serwisy są bardziej trwałą jednostką. Jest to warstwa abstrakcji, która pozwala zgrupować Pody wraz z zasadami dostępu do nich. Dla przykładu, mamy prosta aplikację, która przetwarza obrazki i składa się ona z trzech replik Poda. Frontend aplikacji nie bardzo zastanawia się nad tym do jakiego backendu się łączy – ma produkować, to produkuje. Jednakże klienci nie mogą zauważyć, żadnej zmiany. Właśnie takie zastosowanie jest idealne, aby skonfigurować Serwis – będzie on posiadał jedno wejście, a w tle będzie szereg przetwarzających podów jako backend.

 class=