Professional Documents
Culture Documents
Kubernetes. Wzorce Projektowe - Bilgin Ibryam, Roland Huss
Kubernetes. Wzorce Projektowe - Bilgin Ibryam, Roland Huss
Kubernetes
Wzorce projektowe
Komponenty wielokrotnego
użycia do projektowania
natywnych aplikacji
chmurowych
Tytuł oryginału: Kubernetes Patterns: Reusable Elements for Designing Cloud-Native
Applications
Tłumaczenie: Krzysztof Rychlicki-Kicior
ISBN: 978-83-283-6404-2
© 2020 Helion SA
Authorized Polish translation of the English edition of Kubernetes Patterns ISBN
9781492050285 © 2019 Bilgin Ibryam and Roland Huß
This translation is published and sold by permission of O’Reilly Media, Inc., which owns or
controls all rights to publish and sell the same.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by
any means, electronic or mechanical, including photocopying, recording or by any information
storage retrieval system, without permission from the Publisher.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu
niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą
kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym
lub innym powoduje naruszenie praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi
ich właścicieli.
Autor oraz Helion SA dołożyli wszelkich starań, by zawarte w tej książce informacje były
kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani
za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Helion
SA nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z
wykorzystania informacji zawartych w książce.
HELION SA
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Pliki z przykładami omawianymi w książce można znaleźć pod adresem:
ftp://ftp.helion.pl/przyklady/kubewp.zip
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/kubewp_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Poleć książkę
Kup w wersji papierowej
Oceń książkę
Księgarnia internetowa
Lubię to! » nasza społeczność
Przedmowa
Gdy Craig, Joe i ja zaczęliśmy tworzyć Kubernetesa prawie pięć lat temu, chyba wszyscy
zdawaliśmy sobie sprawę z jego możliwości w zakresie przekształcenia sposobu tworzenia i
wdrażania oprogramowania. Nie wydaje mi się, abyśmy zdawali sobie sprawę czy nawet
wierzyli, jak szybko ta transformacja nastąpi. Kubernetes stanowi podstawę procesu tworzenia
przenośnych i stabilnych systemów, będąc używanym w wielu chmurach publicznych,
prywatnych, a także na czystych serwerach (ang. bare-metal). Mimo że Kubernetes stał się
wszechobecny do tego stopnia, że obecnie możesz uruchomić klaster w chmurze w mniej niż
pięć minut, wciąż nie jest łatwo określić, co zrobić po stworzeniu takiego klastra. Wspaniałą
rzeczą jest obserwować intensywne wysiłki skierowane w stronę operacjonalizacji samego
Kubernetesa, jednak to nie wszystko. Jest to podstawa, baza, na której można budować
aplikacje, dostarczając bogatą bibliotekę API i narzędzi do tworzenia tych aplikacji. Nie
zapewni ona jednak pożytecznych dla architekta czy programisty wskazówek i wytycznych,
mówiących jak skorzystać z tych różnych fragmentów układanki, aby stworzyć kompletny,
stabilny system, który spełnia ich założenia biznesowe.
Choć niezbędną perspektywę i doświadczenie można zdobyć „w boju” lub korzystając z
doświadczeń z poprzednich systemów, wiąże się to z długim czasem, jaki musi w tym celu
upłynąć, nie mówiąc o wpływie na jakość systemów dostarczanych użytkownikom końcowym.
Kiedy uczysz się dostarczać krytyczne usługi na podstawie systemu takiego jak Kubernetes,
nauka na własnych błędach zabiera zbyt dużo czasu i powoduje zbyt wiele poważnych
problemów związanych z brakiem dostępności usługi.
Właśnie dlatego książka Bilgina i Rolanda jest tak cenna. Książka ta pozwala Ci uczyć się na
bazie naszych doświadczeń, które zawarliśmy w narzędziach i API wchodzących w skład
Kubernetesa. Kubernetes to efekt uboczny doświadczenia szerokiej społeczności w zakresie
budowania i doświadczania różnorodnych, stabilnych systemów rozproszonych na różnych
środowiskach. Każdy obiekt i możliwość dostępne w Kubernetesie reprezentują podstawowe
narzędzie, które zostało zaprojektowane w celu rozwiązania konkretnego problemu projektanta
oprogramowania. Z tej książki dowiesz się, jak koncepty Kubernetesa rozwiązują praktyczne,
rzeczywiste problemy, a także jak zaadaptować je do systemów, nad którymi pracujesz na co
dzień.
Tworząc Kubernetesa zawsze mówiliśmy, że naszym głównym celem było uczynienie procesu
tworzenia systemów rozproszonych dziecinnie prostym, możliwym do nauczenia w ramach
przedmiotu takiego, jak Wprowadzenie do informatyki. Gdybyśmy spełnili nasz cel, książki takie
jak ta, mogłyby być używane jako podręcznik do takiego przedmiotu. Bilgin i Roland uchwycili
narzędzia najistotniejsze dla programisty Kubernetesa i podzielili je na czytelne i łatwe do
zrozumienia części. Po zakończeniu lektury tej książki będziesz wiedzieć nie tylko, jakie
komponenty w Kubernetesie masz do dyspozycji, ale także dlaczego i jak budować przy ich
pomocy systemy.
Kubernetes
Kubernetes to platforma do orkiestracji kontenerów. Jej początki biorą się z centrów danych
Google’a, gdzie opracowano wewnętrzne narzędzie do orkiestracji kontenerów — Borg
(https://ai.google/research/pubs/pub43438). W Google korzystano z Borga przez wiele lat do
uruchamiania aplikacji. W 2014 roku kierownictwo Google postanowiło przekazać swoje
doświadczenia z Borgiem do nowo powstałego projektu otwartego oprogramowania o nazwie
Kubernetes (gr. κυβερνήτης — kapitan, sternik, nawigator). W 2015 roku Kubernetes został
przekazany jako pierwszy projekt do nowo powołanej Cloud Native Computing Foundation
(CNCF — fundacja natywnych obliczeń w chmurze).
Od samego początku Kubernetes miał szerokie grono użytkowników, a liczba kontrybutorów
rosła w niezwykle szybkim tempie. Obecnie Kubernetes jest uważany za jeden z najaktywniej
rozwijanych projektów w serwisie GitHub. Nie będzie przesadą stwierdzenie, że w momencie
pisania tych słów Kubernetes stał się najczęściej używaną i najbogatszą platformą do
orkiestracji kontenerów. Kubernetes stanowi także podstawę dla wielu innych platform,
zbudowanych na jego bazie. Najbardziej znanym z tych systemów typu PaaS (ang. Platform-as-
a-Service — platforma jako usługa) jest Red Hat OpenShift, który rozszerza Kubernetesa o
szereg dodatkowych opcji, w tym o możliwość tworzenia aplikacji w ramach platformy. Jest to
jeden z wielu powodów, dla których to właśnie Kubernetes został przez nas wybrany jako
wzorcowa platforma do omówienia wzorców chmurowych w tej książce.
Wzorce projektowe
Pojęcie wzorców projektowych (ang. design patterns) pojawiło się w latach 70. ubiegłego
stulecia. Wywodzi się ono z architektury. W 1977 roku Christopher Alexander, architekt i
teoretyk systemów, opublikował wraz z zespołem pracę pt. A Pattern Language1, opisującą
wzorce architekturalne możliwe do zastosowania podczas projektowania miast, budynków i
innych projektów konstrukcyjnych. Podobny pomysł został wykorzystany na gruncie nowo
powstającego przemysłu produkcji oprogramowania. Najpopularniejszą pozycją w tym zakresie
jest książka Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku
autorstwa Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa (tzw. Bandy
Czterech — Gang of Four), wydana w oryginale w 1994 roku2. To właśnie dzięki tej pracy
posługujemy się wzorcami takimi jak Singleton, Fabryki czy Delegacje. Od tamtego czasu
powstało wiele interesujących publikacji, opisujących wzorce na różnym poziomie dokładności,
np. Enterprise Integration Patterns Gregora Hohpego i Bobby’ego Woolfa czy Patterns of
Enterprise Application Architecture Martina Fowlera.
Mówiąc krótko, wzorzec opisuje powtarzalne rozwiązanie pewnego problemu3. Od przepisu czy
też recepty różni go formuła — zamiast przedstawienia precyzyjnych instrukcji krok po kroku w
celu rozwiązania problemu, wzorzec opisuje schemat rozwiązania całej klasy podobnych
problemów. W książce Alexandra znajdziemy np. wzorzec Beer Hall (hala piwna), opisujący taki
sposób konstruowania publicznych przestrzeni przeznaczonych do spożywania różnego rodzaju
napojów, w którym zarówno znajomi, jak i nieznajomi stają się kompanami we wspólnej
konsumpcji. Wszystkie przestrzenie zaprojektowane zgodnie z tym wzorcem będą wyglądać
inaczej, ale znajdą się w nich wspólne cechy, takie jak otwarte wnęki dla grup kilkuosobowych
(od 4 do 8), a także wspólna przestrzeń dla 100 osób.
Wzorzec nie składa się wyłącznie z samego rozwiązania. Niezwykle ważne jest również
właściwe słownictwo. Wzięte razem, unikatowe nazwy wzorców tworzą precyzyjny, oparty w
dużej mierze na rzeczownikach język. Po upowszechnieniu się nazw wzorców, niezwykle łatwo
jest dzięki nim komunikować się między sobą różnym specjalistom. Mówiąc np. o stole, prawie
każdy pomyśli o konstrukcji, w skład której wchodzą cztery nogi i płaska powierzchnia na nich
osadzona, na której można umieszczać rozmaite przedmioty. To samo daje się zaobserwować w
świecie inżynierii oprogramowania — na przykład gdy wprowadzono doń pojęcie „fabryki” (ang.
factory). W językach zorientowanych obiektowo „fabryka” oznacza obiekt, który tworzy
(produkuje) inne obiekty. Skoro natychmiast wiemy, jakie rozwiązanie jest związane z danym
wzorcem, możemy przejść do rozwiązywania właściwych problemów.
Język wzorców ma jeszcze inne ważne cechy. Wzorce wiążą się ze sobą, a niekiedy nawet
nakładają się na siebie — dzięki czemu razem pokrywają one większość przestrzeni problemów.
Podobnie jak w książce Język wzorców, wzorce mogą operować na innym poziomie
szczegółowości i zasięgu problemu. Bardziej ogólne wzorce omawiają szeroką przestrzeń
problemu i dostarczają bardzo ogólne wytyczne dotyczące jego rozwiązania. Wzorce dokładne
przedstawiają precyzyjne rozwiązania problemu, ale też nie zawsze można je zastosować. W tej
książce znajdziesz cały szereg wzorców, a także odwołań pomiędzy nimi — w niektórych
przypadkach skorzystanie z jednego wzorca będzie fragmentem rozwiązania innego!
Kolejną cechą wzorców jest definiowanie ich w precyzyjny sposób. Niestety, każdy autor
korzysta z własnego formatu i nie istnieje jeden standard, który byłby wspólny dla wszystkich
wzorców. Martin Fowler omawia różne formaty prezentacji wzorców w artykule Writing
Software Patterns4.
Nazwa
Każdy wzorzec jest definiowany za pomocą nazwy, stanowiącej zarazem tytuł rozdziału.
Nazwa stanowi istotę języka wzorców.
Problem
W tej części opisujemy szeroko kontekst i przestrzeń problemu.
Rozwiązanie
Dyskusja
Więcej informacji
Na zakończenie przedstawimy dodatkowe źródła nt. danego wzorca, z którymi warto się
zapoznać.
Nie każdy wzorzec będzie pasował tylko do jednej kategorii. W zależności od kontekstu, ten
sam wzorzec może być przypisany do kilku różnych kategorii. Każdy rozdział (poświęcony
pojedynczemu wzorcowi) jest samowystarczalny, dlatego można czytać je niezależnie od siebie i
w dowolnej kolejności.
Przed rozpoczęciem naszej przygody podsumujmy jeszcze, czego nie znajdziesz w tej książce:
Ta książka nie jest przewodnikiem, z którego dowiesz się, jak skonfigurować klaster
Kubernetesa. Każdy wzorzec i każdy przykład zakłada, że dysponujesz działającą instancją
Kubernetesa. Jeśli chcesz przetestować przykłady, możesz skorzystać z kilku różnych
metod. Informacje na temat konfiguracji klastra Kubernetesa znajdziesz np. w książce
Managing Kubernetes Brendana Burnsa i Craiga Traceya. Przepisy na konfigurację
klastra Kubernetesa od podstaw znajdziesz również w książce Kubernetes Cookbook
Michaela Hausenblasa i Sébastiena Goasguena.
Ta książka nie jest wprowadzeniem do Kubernetesa, jak również nie stanowi podręcznika
referencyjnego. Poruszamy w niej wiele funkcji Kubernetesa, wyjaśniając je do pewnego
stopnia, jednak przede wszystkim skupiamy się na konceptach, które kryją się za tymi
mechanizmami. W rozdziale 1. odświeżamy podstawowe informacje dotyczące
Kubernetesa. Jeśli chcesz dowiedzieć się więcej na temat ogólnych zasad używania
Kubernetesa, polecamy książkę Kubernetes in Action Marko Lukšy.
Treści zawarte w tej książce są ze sobą powiązane dość luźno, dzięki czemu poszczególne
rozdziały można czytać niezależnie od siebie.
Dowolny tekst, który można wprowadzić w konsoli lub edytorze, jest formatowany za
pomocą czcionki monotypicznej.
Nazwy zasobów Kubernetesa są zapisywane wielką literą (np. Pod). Jeśli zasób ma nazwę
stanowiącą połączenie kilku słów (np. ConfigMap), zapisujemy ją łącznie (zamiast
stosować po prostu sformułowanie „słownik konfiguracji”), aby wyrazić jednoznacznie, że
odwołujemy się do obiektu Kubernetesa.
W sytuacji, gdy nazwa zasobu jest jednocześnie ogólnym określeniem pewnej idei (np.
service — usługa lub node — węzeł), formatowanie stosujemy tylko wtedy, gdy
odwołujemy się ściśle do samego zasobu.
Zastosowanie przykładowych kodów
Każdy z wzorców zawiera w pełni wykonywalne przykłady, które znajdziesz w serwisie GitHub,
w zasobie poświęconym tej książce, pod adresem https://github.com/k8spatterns/examples.
Łącze do każdego przykładu znajdziesz też w sekcji „Więcej informacji”, będącej częścią
każdego rozdziału. Sekcja ta zawiera także łącza do innych przydatnych źródeł związanych z
danym wzorcem. Listy te są aktualizowane w naszym repozytorium. Znajdują się w nim również
informacje na temat uruchomienia klastra Kubernetesa w celu wykonania kodu przykładów.
Eksperymentując z przykładami, pamiętaj o zapoznaniu się z załączonymi plikami zasobów.
Zawierają one wiele przydatnych komentarzy, które pomogą Ci zrozumieć kod.
Do określania pól w obrębie zasobów stosujemy notację JSON, np. określenie .spec.replicas
wskazuje na pole replicas w sekcji spec zasobu.
Podziękowania
Pisanie tej książki było długą podróżą, rozciągającą się na przestrzeni dwóch lat. W tym miejscu
chcielibyśmy podziękować wszystkim recenzentom, którzy pomogli nam prowadzić prace we
właściwym kierunku. Specjalne podziękowania kierujemy do Paolo Antinoriego i Andrei
Tarocchiego za wszelką okazaną pomoc. Na podziękowania zasłużyły także następujące osoby:
Marko Lukša, Brandon Philips, Michael Hüttermann, Brian Gracely, Andrew Block, Jiri Kremser,
Tobias Schneck i Rick Wagner, którzy wspierali nas swoim doświadczeniem i radami. Wreszcie,
gdyby nie redaktorzy wydawnictwa O’Reilly — Virginia Wilson, John Devins, Katherine Tozer,
Christina Edwards, a także wielu innych pracowników tego wydawnictwa — nie
doprowadzilibyśmy chyba prac nad książką do udanego końca.
W tej książce nie będziemy zajmować się czystym kodem, projektowaniem sterowanym
modelem czy też mikrousługami. Skupiamy się wyłącznie na wzorcach i praktykach związanych
z orkiestracją kontenerów. Musisz jednak pamiętać, że same wzorce nie zapewnią właściwego
działania — Twoja aplikacja musi być zaprojektowana dobrze pod każdym względem,
począwszy od zastosowania technik czystego kodu, przez projektowanie sterowane modelem,
aż po wzorce mikrousług i inne istotne techniki projektowania.
Rozproszone prymitywy
Aby właściwie zrozumieć to, czym są nowe abstrakcje i prymitywy, warto porównać je z
konceptami dobrze znanymi z programowania zorientowanego obiektowo (OOP), na przykładzie
Javy. W świecie OOP posługujemy się pojęciami takimi jak klasa, obiekt, pakiet, dziedziczenie,
enkapsulacja czy polimorfizm. To środowisko uruchomieniowe Javy implementuje wszystkie te
mechanizmy i gwarantuje właściwy sposób zarządzania cyklem życia obiektów, a także aplikacji
jako zamkniętej całości.
Język Java i maszyna wirtualna Javy (JVM) dostarczają lokalne konstrukcje do tworzenia
aplikacji, funkcjonujące w obrębie pojedynczego procesu. Kubernetes dodaje do tego schematu
zupełnie nowy wymiar, umożliwiając stosowanie rozproszonych prymitywów i środowiska
uruchomieniowego przeznaczonych do tworzenia systemów rozproszonych, rozlokowanych na
przestrzeni wielu węzłów i procesów. Dzięki Kubernetesowi nie musimy ograniczać się jedynie
do lokalnych prymitywów w celu zaimplementowania kompletnego zachowania aplikacji.
Prymitywy wewnątrzprocesowe (lokalne) i rozproszone mają sporo wspólnego, ale nie są one w
żaden sposób wymienne. Operują one na różnych poziomach abstrakcji, mają także różne
wymagania i gwarancje. Niektóre prymitywy z założenia są używane razem. Klasy są potrzebne
do tworzenia obiektów, które następnie mogą być umieszczone w obrazach kontenerów. Z
drugiej strony, prymitywy takie jak CronJob, mogą zastąpić mechanizm Javy ExecutorService
w całości.
Teraz zapoznajmy się z kilkoma rozproszonymi abstrakcjami i prymitywami z Kubernetesa,
które są interesujące zwłaszcza dla programistów aplikacji.
Timer,
Zadanie okresowe CronJob (zadanie Crona)
ScheduledExecutorService
Kontenery
Kontenery stanowią podstawowy rodzaj elementów, z których budowane są natywne aplikacje
chmurowe na platformie Kubernetes. Stosując analogię z OOP i Javą, obrazy kontenerów można
porównać do klas, a kontenery — do obiektów. Tak jak w przypadku klas możemy tworzyć klasy
pochodne, które wykorzystują zachowania klasy bazowej, również obrazy kontenerów mogą
rozszerzać inne obrazy w celu dodania nowego zachowania. Z drugiej strony, w przypadku
programowania zorientowanego obiektowo możemy mówić o złożeniu (kompozycji) obiektów —
tak samo kontenery mogą być składane razem i umieszczane w kapsule.
Pogłębiając analogię, Kubernetesa można porównać do maszyny wirtualnej Javy (JVM), ale
rozproszonej na wiele hostów i odpowiedzialnej za uruchamianie i zarządzanie kontenerami.
Kapsuły
Po zapoznaniu się z opisem działania kontenerów łatwo dojść do wniosku, że stanowią one
świetny środek do implementacji mikrousług. Obraz kontenera dostarcza pojedynczy fragment
funkcjonalny, należy do jednego zespołu, ma niezależny cykl publikacji, a także zapewnia
izolację czasu wykonania. W związku z tym, jeden obraz kontenera reprezentuje z reguły jedną
mikrousługę.
Mimo to, większość natywnych platform chmurowych udostępnia jeszcze jeden prymityw do
zarządzania cyklem życia grup kontenerów — w Kubernetesie nosi on nazwę kapsuły (ang.
Pod). Kapsuła to niepodzielna jednostka, umożliwiająca planowanie, wdrożenie i izolację czasu
wykonania dla grupy kontenerów. Wszystkie kontenery w kapsule trafiają do tego samego hosta
i są wdrażane razem — niezależnie od tego, czy wynika to z powodu skalowania, czy też
migracji hosta. Kontenery z kapsuły mogą współdzielić system plików, interfejsy sieciowe i
przestrzeni nazw procesów. Złączony cykl życia pozwala kontenerom w ramach jednej kapsuły
współpracować ze sobą przy użyciu systemu plików, za pomocą lokalnego interfejsu sieciowego
(ang. localhost), czy też międzyprocesów mechanizmów komunikacji (np. z powodów
wydajnościowych).
Jak widać na rysunku 1.2, w czasie tworzenia i budowania, mikrousługa odpowiada obrazowi
kontenera, rozwijanemu i publikowanemu przez jeden zespół. W czasie wykonania następuje
zmiana — mikrousługa jest powiązana z kapsułą, która stanowi najmniejszą jednostkę
wdrożenia, skalowania i rozmieszczenia. Jedynym sposobem na uruchomienie kontenera — z
powodu skalowania lub migracji — jest skorzystanie z abstrakcji kapsuły. Czasami kapsuła
będzie zawierać jeden kontener. Jednym z przykładów jest skonteneryzowana mikrousługa,
która wykorzystuje pomocnicze kontenery w czasie wykonania (por. rozdział 15., „Przyczepa”).
Rysunek 1.2. Kapsuła jako jednostka zarządzania i wdrażania
Kontenery i kapsuły, dzięki swoim wyjątkowym możliwościom, oferują nowy zbiór wzorców i
reguł, przeznaczony specjalnie do projektowania aplikacji opartych na mikrousługach.
Przeanalizowaliśmy przed chwilą niektóre z własności dobrze zaprojektowanych kontenerów —
teraz przyjrzymy się w podobny sposób kapsułom:
Kapsuła stanowi niepodzielną jednostkę planowania. Oznacza to, że planista stara się
znaleźć host spełniający wymagania wszystkich kontenerów należących do kapsuły
(pewne rozbieżności występują w przypadku kontenerów inicjalizacji, które omawiamy w
rozdziale 14.). Jeśli stworzysz kapsułę zawierającą wiele kontenerów, planista musi
znaleźć host dysponujący zasobami wystarczającymi do spełnienia wszystkich wymagań
kontenerów. Proces planowania jest opisany w rozdziale 6.
Kapsuła zapewnia kolokację kontenerów. Dzięki kolokacji, kontenery w tej samej kapsule
zyskują dodatkowe sposoby na komunikowanie się ze sobą. Do typowych metod
komunikacji należą współdzielony system plików, zastosowanie lokalnego interfejsu
sieciowego lub komunikacja międzyprocesowa (IPC — inter-process communication) w
celu osiągnięcia wysokiej wydajności.
Kapsuła zawiera adres IP, nazwę i zakres portów, współdzielone przez wszystkie należące
do niej kontenery. Oznacza to, że kontenery w tej samej kapsule muszą być
skonfigurowane w taki sposób, aby uniknąć konfliktu portów, podobnie jak procesy
uniksowe funkcjonujące w jednej przestrzeni sieciowej w ramach jednego hosta.
Usługi
Kapsuły są ulotne — pojawiają się i znikają z wielu różnych przyczyn, takich jak skalowanie w
górę lub w dół, niespełniona kontrola kondycji kontenera, czy też migracja węzła. Adres IP
kapsuły staje się znany dopiero po jej zaplanowaniu i uruchomieniu w węźle. Kapsuły może być
przypisana do innego węzła, jeśli istniejący węzeł, na którym jest ona uruchomiona, przestaje
być sprawny. Niezależnie od przyczyny, adres sieciowy kapsuły może zmieniać się w trakcie
życia aplikacji, dlatego konieczne jest wprowadzenie kolejnego prymitywu do wykrywania usług
i równoważenia obciążenia.
W tym momencie do gry wchodzą usługi (ang. services) Kubernetesa. Usługa to kolejna prosta,
ale funkcjonalna abstrakcja Kubernetesa, która wiąże na stałe nazwę usługi z adresem IP i
portem. Usługa reprezentuje więc nazwany punkt dostępowy do aplikacji. Zazwyczaj stanowi
punkt wejściowy dla zbioru kapsuł. Usługa to prymityw ogólnego przeznaczenia — może ona
wskazywać na funkcje i systemy spoza klastra Kubernetesa. W związku z tym, prymityw ten
może być używany do wykrywania usług i równoważenia obciążenia, dopuszczając różne
implementacje i umożliwiając skalowanie bez wpływu na konsumentów usługi. Ten mechanizm
jest omówiony szczegółowo w rozdziale 12.
Etykiety
Dowiedzieliśmy się już, że mikrousługa jest reprezentowana w formie kontenera w trakcie jej
tworzenia, ale już w czasie wykonania — w formie kapsuły. W jaki sposób można zatem
skonstruować aplikację składającą się z wielu mikrousług? Kubernetes udostępnia dwa kolejne
prymitywy, które pomagają określić strukturę aplikacji. Są to etykiety i przestrzenie nazw.
Przestrzenie nazw omawiamy szczegółowo w podrozdziale o takim samym tytule.
Przed rozpowszechnieniem się mikrousług, aplikacja sprowadzała się do pojedynczej jednostki
wdrożenia, zawierającej jeden schemat wersjonowania i publikacji. Była ponadto
reprezentowana za pomocą pliku .war, .ear lub podobnego. Zmiana nastąpiła dopiero dzięki
mikrousługom, które mogą być niezależnie tworzone, publikowane, uruchamiane, restartowane
lub skalowane. Wraz z rozwojem mikrousług, pojęcie aplikacji uległo zmianie. Trudno obecnie
znaleźć kluczowe czynności, które wykonujemy na jej poziomie. Jeśli jednak wciąż musisz mieć
możliwość wskazania, że niektóre z niezależnych usług należą do aplikacji, możesz skorzystać z
etykiet (ang. labels). Załóżmy, że jedna aplikacja została podzielona na trzy mikrousługi, a
druga — na dwie.
W tym momencie mamy pięć definicji kapsuł (i prawdopodobnie o wiele więcej instancji tych
kapsuł), które zarówno z punktu widzenia tworzenia aplikacji, jak i czasu wykonania, są
niezależne. Może jednak zaistnieć potrzeba oznaczenia trzech pierwszych kapsuł jako
powiązanych z pierwszą aplikacją, a dwóch kolejnych — z drugą. Nawet jeśli kapsuły są
technicznie niezależne od siebie, mogą od siebie zależeć w celu dostarczenia wartości
użytkowej lub biznesowej. Jedna kapsuła może zawierać frontend aplikacji, a pozostałe dwie
mogą odpowiadać za backend. Jeśli którakolwiek z tych kapsuł przestaje działać, aplikacja — z
biznesowego punktu widzenia — staje się bezużyteczna. Zastosowanie selektorów etykiet daje
nam możliwość odpytania i zidentyfikowania zbioru kapsuł, w celu zarządzania nim jako
logiczną, spójną jednostką. Rysunek 1.3 przedstawia sposób stosowania etykiet w celu
zgrupowania fragmentów rozproszonego systemu w poszczególne podsystemy.
Rysunek 1.3. Etykiety zastosowane do przypisania kapsuł do poszczególnych aplikacji
Adnotacje
Kolejnym prymitywem, podobnym w działaniu do etykiet, jest adnotacja. Podobnie jak etykiety,
także adnotacje mają postać słownika. W przeciwieństwie jednak do nich, są one przeznaczone
do definiowania metadanych nieprzeszukiwalnych — do użycia przez system, a nie przez
człowieka.
Informacje zawarte w adnotacjach nie są przeznaczone do odpytywania i dopasowywania
obiektów. Ich celem jest dołączanie dodatkowych metadanych do obiektów pochodzących z
rozmaitych narzędzi i bibliotek, z których czasami korzystamy. Dobrymi przykładami użycia
adnotacji będzie dołączanie identyfikatorów wersji roboczych i publikowanych, informacji nt.
obrazów, znaczników czasu, nazw gałęzi Gita, numerów zleceń przesłania zmian (ang. pull
request), haszy obrazów, adresów rejestrów, danych o autorach, informacji o użytych
narzędziach, itd. A zatem, o ile etykiety są z powodzeniem stosowane do wykonywania akcji i
zapytań dopasowujących, adnotacje są używane do załączania metadanych wykorzystywanych
przez rozmaite systemy.
Przestrzenie nazw
Kolejnym prymitywem, który pomaga w zarządzaniu grupami zasobów, jest przestrzeń nazw.
Jak już wspominaliśmy, przestrzeń nazw może przypominać etykietę, jednak w rzeczywistości
prymitywy te różnią się swoimi własnościami i przeznaczeniem.
Przestrzenie nazw Kubernetesa pozwalają na podział klastra Kubernetesa (rozprzestrzenionego
na wiele hostów) na logiczną pulę zasobów. Przestrzenie nazw udostępniają zakresy dla
zasobów Kubernetesa z możliwością zastosowania autoryzacji i różnorodnych reguł dla każdej
podsekcji klastra. Typowym przykładem użycia przestrzeni nazw jest utworzenie różnych
środowisk, takich jak deweloperskie, testowe, testowe integracyjne i produkcyjne. Przestrzenie
nazw mogą być używane do osiągnięcia wielopodmiotowości (ang. multitenancy) i dostarczenia
izolacji na poziomie przestrzeni roboczych dla zespołów, projektów, a nawet całych aplikacji.
Trzeba przy tym jednak pamiętać, że jeżeli zależy nam na naprawdę dużym poziomie izolacji,
przestrzenie nazw okazują się być niewystarczające — konieczne staje się oddzielenie klastrów.
Z reguły mamy do czynienia z jednym nieprodukcyjnym klastrem, wspólnym dla kilku środowisk
(deweloperskiego, testowego i testowego integracyjnego), a także odrębnym klastrem
produkcyjnym, używanym do testowania wydajności aplikacji i środowisk produkcyjnych.
Przeanalizujmy własności przestrzeni nazw i sytuacje, w których mogą one okazać się pomocne:
Dyskusja
W tym rozdziale zajęliśmy się pokrótce kilkoma głównymi pojęciami związanymi z
Kubernetesem, których będziemy używać w tej książce. W codziennej pracy zetkniesz się
zapewne z jeszcze z kilkoma prymitywami. Na przykład, tworząc konteneryzowaną usługę,
możesz skorzystać z różnych obiektów Kubernetesa, które pozwolą Ci wykorzystać jego
potencjał. Pamiętaj, że te obiekty są używane przez programistów do zintegrowania
konteneryzowanej usługi w ramach Kubernetesa. Można także skorzystać z innych narzędzi,
używanych przez administratorów w celu ułatwienia programistom efektywnego zarządzania
platformą. Rysunek 1.4 przedstawia zarys wielu zasobów Kubernetesa przydatnych dla
programistów.
Więcej informacji
Zasady projektowania aplikacji opartych na kontenerach: https://red.ht/2HBKqYI
Dwanaście aspektów aplikacji: https://12factor.net/
Projektowanie sterowane modelem — rozwiązywanie problemów ze złożonością w
oprogramowaniu: https://dddcommunity.org/book/evans_2003/
Najlepsze praktyki konteneryzacji: http://bit.ly/2TUyNTe
Najlepsze praktyki pisania plików Dockerfile: https://dockr.ly/2TFZBaL
Wzorce kontenerów: http://bit.ly/2TFjsH2
Ogólne wytyczne dotyczące obrazów kontenerów: https://red.ht/2u6Ahvo
Kapsuły: https://kubernetes.io/docs/user-guide/pods/
1 J. Opóźnione kontenery (ang. defer containers) nie zostały jeszcze zaimplementowane, ale
istnieje duże prawdopodobieństwo, że zostaną dołączone w kolejnych wersjach Kubernetesa.
Haki cyklu życia omawiamy w rozdziale 5., „Zarządzany Cykl Życia”.
Część I. Wzorce podstawowe
Wzorce podstawowe opisują szereg podstawowych reguł, które muszą spełniać aplikacje
skonteneryzowane, aby mogły być uznane za zgodne ze standardami natywnych platform
chmurowych. Zapewnienie zgodności z tymi zasadami da nam pewność, że aplikacje są
dostosowane do automatyzacji w ramach takich platform.
Wzorce opisane w kolejnych rozdziałach przedstawiają podstawowe zasady tworzenia
rozproszonych, kontenerowych, natywnych aplikacji kubernetesowych:
Problem
Kubernetes jest w stanie zarządzać aplikacjami napisanymi w przeróżnych językach
programowania, o ile tylko aplikacja może być uruchomiona w kontenerze. Różne języki
charakteryzują się jednak różnymi wymaganiami dotyczącymi zasobów. Z reguły, program
napisany w języku kompilowanym działa szybciej i wymaga mniej pamięci w porównaniu do
środowisk uruchomieniowych działających według metody dokładnie-na-czas (ang. just-in-time)
lub w językach interpretowanych. Biorąc pod uwagę, że wiele nowoczesnych języków
programowania należących do tej samej kategorii ma podobne wymagania dotyczące zasobów,
najważniejsze aspekty to domena, logika biznesowa aplikacji i szczegóły implementacyjne.
Bardzo trudno jest przewidzieć dokładną ilość zasobów potrzebnych do optymalnego
funkcjonowania — najlepszą wiedzę na ten temat powinien mieć programista, który w wyniku
przeprowadzania testów z reguły zna wymagania usługi dotyczące zasobów. Niektóre usługi
mają stałe zapotrzebowanie na czas procesora i zużycie pamięci, a inne będą się zmieniać w
czasie. Niektóre potrzebują do działania pamięci trwałej w celu przechowywania danych, a inne
będą oczekiwać stałego numeru portu w komputerze hosta. Określenie tych szczegółowych
wymagań na poziomie platformy zarządzającej jest kluczowe dla prawidłowego funkcjonowania
natywnych aplikacji chmurowych.
Poza wymaganiami dotyczącymi zasobów, środowiska uruchomieniowe aplikacji mogą także
zależeć od możliwości zarządzanych na poziomie platformy, takich jak przechowywanie danych
czy konfiguracja aplikacji.
Rozwiązanie
Wiedza na temat wymagań uruchomieniowych kontenera jest istotna z dwóch powodów. Po
pierwsze, dysponując wszystkimi wymaganiami uruchomieniowymi i oczekiwaniami
dotyczącymi zasobów, Kubernetes jest w stanie podjąć inteligentne decyzje odnośnie do
rozmieszczenia kontenera w klastrze w celu najbardziej efektywnego użycia zasobów
sprzętowych. W środowisku o zasobach współdzielonych, w których funkcjonuje duża liczba
procesów o różnych priorytetach, jedynym sposobem na skuteczne współistnienie jest
znajomość oczekiwań każdego procesu. Inteligentne rozmieszczenie to — niestety — nie jedyna
kwestia, o której musimy pamiętać.
Drugą przyczyną, dla której profile zasobów kontenera są kluczowe, jest planowanie
pojemności. W zależności od wymagań konkretnej usługi i łącznej liczby usług, możemy
zaplanować założenia dotyczące pojemności dla różnych środowisk, aby uzyskać najbardziej
efektywne kosztowo profile hostów, które zaspokoją oczekiwania całego klastra. Profile
zasobów usług i planowanie pojemności są kluczowe dla długoterminowego, skutecznego
zarządzania klastrem.
Zależności uruchomieniowe
Jedną z najważniejszych zależności uruchomieniowych jest system plików, używany do
utrwalania stanu aplikacji. Systemy plików kontenerów są ulotne — ulegają usunięciu w
momencie wyłączenia kontenera. Kubernetes umożliwia tworzenie wolumenów jako przestrzeni
dostępnych na poziomie kapsuły, które przetrwają ponowne uruchomienie kontenera.
Najprostszym rodzajem wolumenu jest emptyDir, który jest obecny przez cały czas życia
kapsuły, a jego usunięcie następuje w momencie usunięcia kapsuły z nim powiązanej. Wolumen
musi być wsparty przez pamięć innego rodzaju, aby przetrwać restart kapsuły. Jeśli aplikacja
musi mieć możliwość odczytu plików z pamięci trwałej i zapisywania ich w niej, konieczne jest
zadeklarowanie jej w definicji kontenera za pomocą własności volumes (listing 2.1).
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
volumeMounts:
- mountPath: “/logs”
name: log-volume
volumes:
- name: log-volume
persistentVolumeClaim:
claimName: random-generator-log
Nieco innym rodzajem zależności są konfiguracje. Niemal każda aplikacja wymaga dostępu do
pewnych informacji konfiguracyjnych. Rozwiązaniem zalecanym w Kubernetesie jest
zastosowanie obiektów ConfigMap. Twoje usługi wymagają określenia sposobu użycia tych
ustawień — albo za pomocą zmiennych środowiskowych, albo przy użyciu systemu plików. W
obu przypadkach powstaje zależność uruchomieniowa od nazwanych obiektów ConfigMap. Jeśli
nie wszystkie obiekty ConfigMap zostaną utworzone, kontenery zostaną rozplanowane na
węzłach, ale nie będą uruchomione. Obiekty ConfigMap i Secret są szczegółowo omówione w
rozdziale 19. Na listingu 2.2 pokazujemy, jak zastosować te zasoby jako zależności
uruchomieniowe.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: PATTERN
valueFrom:
name: random-generator-config
key: pattern
Choć tworzenie obiektów ConfigMap i Secret jest proste i należy do typowych zadań
administracyjnych, które trzeba wykonać, węzły klastra dostarczają pamięć trwałą i numery
portów. Niektóre z tych zależności ograniczają możliwość rozplanowania kapsuły, a inne mogą
całkowicie uniemożliwić jej uruchomienie. Projektując skonteneryzowane aplikacje z tego
rodzaju zależności, zawsze pamiętaj o ograniczeniach uruchomieniowych, które mogą się z nimi
wiązać.
Profile zasobów
Określanie zależności kontenera, takich jak obiekty ConfigMap, Secret czy wolumeny, jest
całkiem proste. Więcej pracy i eksperymentów wymagać będzie właściwe określenie wymagań
zasobów dla kontenera. Zasoby obliczeniowe — w kontekście Kubernetesa — to wszystko to,
czego możemy zażądać, zaalokować i skonsumować w kontenerze. Zasoby mogą być
kompresowalne (tj. mogą być ograniczane, np. procesor czy przepustowość łącza) lub
niekompresowalne (np. pamięć RAM).
Rozróżnianie tych dwóch rodzajów zasobów jest niezwykle ważne. Jeśli Twój kontener zużywa
zbyt dużo zasobów kompresowalnych, np. procesora, zostanie on ograniczony. Jeśli jednak
dochodzi do zużycia zbyt dużej ilości zasobów niekompresowalnych — np. pamięci — kontener
zostanie wyłączony, ponieważ nie można zażądać od aplikacji zwolnienia zajętej pamięci.
Parametr requests jest używany przez planistę przy rozmieszczaniu kapsuł, w przeciwieństwie
do parametru limits. Przy rozplanowywaniu danej kapsuły, planista rozważy tylko tę węzły,
które mają na tyle dużo dostępnych zasobów, aby spełnić wymagania tej kapsuły i wszystkich jej
kontenerów; dodaje w tym celu wartości parametru requests ze wszystkich kontenerów. W
związku z tym można powiedzieć, że pole requests każdego z kontenerów ma istotny wpływ na
możliwość rozplanowania kapsuły. Na listingu 2.3 pokazujemy, jak takie limity są określane w
ramach pojedynczej kapsuły.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
resources:
requests: D:\_____Helion\Wicked Cool Ruby Scripts\01.tif
cpu: 100m
memory: 100Mi
limits: Obraz2404.PNG
cpu: 200m
memory: 200Mi
W zależności od tego, czy określisz jedynie parametr requests bądź limits, czy też obydwa,
platforma pozwoli na zastosowanie różnych zasad jakości usługi (ang. Quality of Service —
QoS).
Kapsuła, która nie ma ustawionych żadnych żądań ani limitów w swoich kontenerach, ma
najniższy priorytet i jest wyłączana w pierwszej kolejności, gdy węzeł, na którym została
umieszczona, nie będzie dysponować zasobami.
Burstable (rozrywalne)
Guaranteed (gwarantowane)
Priorytety kapsuł
Przed chwilą wyjaśniliśmy, jak deklaracje zasobów kontenerów wpływają na QoS kapsuł i
determinują kolejność, w której kontenery są wyłączane w razie problemów z zasobami. Kolejną
powiązaną funkcją, która w trakcie pisania tej książki była wciąż w fazie beta, jest
priorytetyzacja i wywłaszczanie kapsuł (ang. priority and preemption). Priorytet kapsuły
pozwala na określenie jej wagi względem innych kapsuł, co z kolei przekłada się na kolejność
ich rozplanowywania. Przeanalizujmy listing 2.4.
kind: PriorityClass
metadata:
globalDefault: false
---
apiVersion: v1
kind: Pod
metadata:
name: random-generator
labels:
env: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
PriorityClass
W tym momencie dochodzimy do kluczowego zagadnienia. Jeśli nie mamy do dyspozycji węzłów
o pojemności wystarczającej do umieszczenia kapsuły, planista może usunąć kapsuły o niższym
priorytecie, aby zwolnić zasoby i następnie móc umieścić te o wyższym priorytecie. W związku z
tym, kapsuła o wyższym priorytecie może zostać rozplanowana szybciej niż kapsuły o niższym
priorytecie — o ile wszystkie wymagania związane z planowaniem zostaną spełnione. Ten
algorytm pozwala administratorom klastrów na lepszą kontrolę nad kapsułami — kapsuły o
strategicznym znaczeniu dla działania całego systemu są traktowane w specjalny sposób, w
porównaniu do kapsuł o niższym priorytecie. Jeśli kapsuła nie może być rozplanowana, planista
będzie kontynuował swoją pracę, rozmieszczając kapsuły o innych priorytetach.
Jakość usługi i priorytet kapsuły to dwa pojęcia ortogonalne, niezależne względem siebie. QoS
jest używana przez Kubelet do zapewnienia stabilności węzła, gdy ilość dostępnych zasobów
jest niska. Kubelet przed podjęciem działań najpierw przeanalizuje QoS, a następnie własność
PriorityClass kapsuł. Z drugiej strony, logika działania planisty zupełnie pomija aspekt
jakości usług w kapsułach, wybierając obiekty do wywłaszczenia. Planista stara się wybrać
zestaw kapsuł o najniższych możliwych priorytetach tak, aby spełnić wymagania oczekujących
na rozmieszczenie kapsuł o wyższych priorytetach.
Gdy określasz priorytety dla niektórych kapsuł, możesz jednocześnie uzyskać niepożądany efekt
w przypadku innych kapsuł, poddanych wywłaszczaniu. Choć zasada bezpiecznego wyłączania
kapsuł jest brana pod uwagę, element PodDistruptionBudget (omówiony w rozdziale 10.) nie
jest gwarantowany, co może doprowadzić do błędów w działaniu aplikacji klastrowej o niższym
priorytecie, która wykorzystuje do swojego działania kworum kapsuł.
Kolejnym problemem może być złośliwy lub niezorientowany użytkownik, który tworzy kapsuły
o najwyższym możliwym priorytecie i wywłaszcza wszystkie inne. Aby tego uniknąć, obiekt
ResourceQuota wspiera mechanizm PriorityClass. Wyższe numery priorytetów są
zarezerwowane dla krytycznych, systemowych kapsuł, które z reguły nie powinny być
wywłaszczane.
Podsumowując, priorytety kapsuł powinny być używane ostrożnie, ponieważ liczbowe priorytety
nadawane przez użytkownika mają wpływ na planistę i Kubeleta, a w konsekwencji mogą
doprowadzić do zmian w działaniu wielu kapsuł, uniemożliwiając spełnienie warunków
działania platformy zgodnie z umowami o gwarantowanym poziomie usług (ang. service-level
agreements — SLA).
Zasoby projektowe
Kubernetes to samoobsługowa platforma, która pozwala programistom na uruchamianie
aplikacji w dogodny dla siebie sposób w ramach wyznaczonych, izolowanych środowisk. Z
drugiej strony, praca na współdzielonej, wielopodmiotowej platformie, wymaga obecności
ograniczeń i jednostek kontrolnych, które powstrzymają niektórych użytkowników przed
skonsumowaniem wszystkich zasobów dostępnych na platformie. Jednym z takich narzędzi jest
obiekt ResourceQuota, który dostarcza mechanizm ograniczenia konsumpcji zasobów w ramach
danej przestrzeni nazw. Dzięki obiektom ResourceQuota, administratorzy klastra mogą
ograniczać łączną ilość zasobów obliczeniowych (procesor, pamięć) i przestrzeni trwałej. Można
także ograniczać maksymalną liczbę obiektów (takich jak ConfigMap, Secret, kapsuły czy
usługi), utworzonych w przestrzeni nazw.
Kolejnym użytecznym narzędziem jest LimitRange, które pozwala na określenie limitów użycia
dla każdego rodzaju zasobu. Poza możliwością zdefiniowania wartości minimalnej, maksymalnej
i domyślnej dla każdego rodzaju zasobu, istnieje możliwość określenia stosunku wartości
parametrów requests i limits, nazywanego poziomem nadmiernych zobowiązań (ang.
overcommit level). Tabela 2.1 przedstawia przykłady doboru wartości parametrów requests i
limits.
Tabela 2.1. Zakresy parametrów requests i limits
Domyślna Domyślna
Stosunek
Rodzaj Zasób Minimum Maksimum wartość wartość
limits/requests
limits requests
Obiekty LimitRange są użyteczne do kontroli profilów zasobów kontenerów, dzięki czemu nie
zostanie umieszczony żaden kontener, który ma wymagania przekraczające to, co węzeł klastra
może dostarczyć. Ponadto możemy zapobiec tworzeniu kontenerów, które konsumują znaczną
ilość zasobów, przez co węzły nie będą w stanie uruchomić innych kontenerów. Biorąc pod
uwagę, że główne wytyczne co do rozplanowywania kontenerów wynikają z parametru
requests (a nie limits), obiekt LimitRequestRatio pozwala na określenie różnicy, jaka dzieli
parametry requests i limits kontenerów. Utworzenie dużej luki pomiędzy parametrami
requests i limits zwiększa ryzyko stworzenia nadmiernych zobowiązań na węźle, co
doprowadzi do niższej wydajności, ponieważ wiele kontenerów może w tym samym momencie
wymagać więcej zasobów, niż pierwotnie zażądano.
Planowanie pojemności
Biorąc pod uwagę to, że kontenery mogą mieć różne profile zasobów w różnych środowiskach,
a także liczba instancji kontenera może się zmieniać, zaplanowanie pojemności dla złożonego
środowiska może nie być proste. Aby osiągnąć optymalne wykorzystanie sprzętu w
nieprodukcyjnym klastrze, rozsądnym wyborem może być skorzystanie z kontenerów typu Best-
Effort i Burstable. W takim dynamicznym środowisku, wiele kontenerów może być
uruchamianych i zamykanych w tym samym czasie. Nawet jeśli kontener zostanie zlikwidowany
w okresie problemów z zasobami, nie jest to duży problem. W klastrze produkcyjnym z kolei,
nad którym chcemy mieć znacznie większą kontrolę, kontenery powinny być głównie typu
Guaranteed, z niewielkim udziałem Burstable. Jeśli kontener jest poddawany likwidacji,
oznacza to, że pojemność klastra powinna być zwiększona.
Tabela 2.2 przedstawia kilka usług wraz z ich zapotrzebowaniem na czas procesora i pamięć.
Tabela 2.2. Przykład planowania pojemności
Kapsuła Żądane CPU Limit CPU Żądana pamięć Limit pamięci Instancje
Dyskusja
Kontenery przydają się nie tylko do izolowania procesów i opakowywania aplikacji. Dysponując
określonymi profilami zasobów, jesteś w stanie zaprojektować struktury właściwe pod kątem
pojemności dla swojej aplikacji. Nie bój się wykonywać wiele testów, aby znaleźć ilość zasobów
optymalną dla każdego kontenera. Korzystaj z tych informacji, przewidując i planując
pojemność swojej aplikacji.
Więcej informacji
Przykład wzorca Przewidywalnych Wymagań: http://bit.ly/2CrT8FJ
Zastosowanie obiektu ConfigMap: http://kubernetes.io/docs/user-guide/configmap/
Kwoty (ograniczenia) zasobów: http://kubernetes.io/docs/admin/resourcequota/
Dobre praktyki Kubernetesa — żądania i limity zasobów: http://bit.ly/2ueNUc0
Konfiguracja limitów pamięci i procesora kapsuły:
http://kubernetes.io/docs/admin/limitrange/
Konfiguracja obsługi sytuacji braku zasobów: http://bit.ly/2TKEYKz
Priorytety i wywłaszczanie kapsuł: http://bit.ly/2OdBcU6
Jakość usługi zasobów w Kubernetesie: http://bit.ly/2HGimUq
Problem
Jesteśmy w stanie dostarczać izolowane środowiska w formie przestrzeni nazw i rozmieszczać
usługi w tych środowiskach za pomocą planisty, z minimalną interakcją ze strony człowieka.
Wraz z rosnącą liczbą mikrousług, ciągła aktualizacja i częste podmiany na nowsze wersje stają
się coraz większym problemem.
Aktualizacja usługi do następnej wersji wiąże się z aktywnościami takimi jak uruchomienie
nowej wersji kapsuły, bezpieczne zatrzymanie jej starej wersji, odczekanie i weryfikacja
skutecznego uruchomienia kapsuły lub wycofanie zmian w razie jakichkolwiek problemów.
Wszystkie te aktywności można wykonać dopuszczając pewien czas przestoju (ang. downtime) i
brak jednocześnie działających różnych wersji usługi, lub zapewniając brak przestoju, ale z
koniecznością użycia większej ilości zasobów z uwagi na uruchomienie więcej niż jednej wersji
usługi w danej chwili. Wykonywanie tych operacji ręcznie, przez człowieka, może prowadzić do
różnorodnych błędów, a z kolei oskryptowanie procesu wymaga sporej ilości pracy, przez co
proces wydania niepotrzebnie się wydłuży.
Rozwiązanie
Na szczęście cała operacja została zautomatyzowana przez Kubernetesa. Korzystając z
Wdrożenia, możemy opisać sposób, w jaki nasza aplikacja ma być aktualizowana, dostosowując
strategię i rozmaite aspekty procesu aktualizacji. Zauważ, że każda z instancji mikrousług jest
wdrażana wielokrotnie w każdym cyklu wydania (który, w zależności od zespołu i projektu,
może rozciągać się od kilku minut do wielu miesięcy).
Ciągłe wdrażanie
Deklaratywny sposób aktualizowania aplikacji w Kubernetesie jest realizowany za pomocą
Wdrożenia (Deployment). Obiekt Deployment tworzy obiekty ReplicaSet, które obsługują
selektory etykiet oparte na zbiorach. Abstrakcja Deployment pozwala na dostosowanie
zachowania procesu aktualizacji, z wykorzystaniem strategii RollingUpdate (domyślnej) lub
Recreate. Listing 3.1 przedstawia najważniejsze elementy konfiguracji Wdrożenia dla strategii
ciągłej aktualizacji.
kind: Deployment
metadata:
name: random-generator
spec:
rollingUpdate:
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
exec:
D:\_____Helion\Wicked Cool Ruby Scripts\01.tif Deklaracja trzech replik. Aby operacja ciągłej
D:\_____Helion\Wicked Cool Ruby Scripts\02.tif Liczba kapsuł, które mogą być uruchamiane
tymczasowo poza replikami określonymi w ramach aktualizacji. W tym przykładzie mogą istnieć
maksymalnie cztery repliki.
D:\_____Helion\Wicked Cool Ruby Scripts\03.tif Liczba kapsuł, które mogą być niedostępne w
czasie aktualizacji. W tym przypadku dopuszczamy możliwość istnienia jedynie dwóch kapsuł w
trakcie aktualizacji.
ciągłego wdrażania, aby zapewnić brak przestojów — nie zapominaj o nich (por. rozdział 4.)!
03_01
zastąpić cały obiekt Deployment nową wersją za pomocą polecenia kubectl replace,
uzupełnić (kubectl patch) lub interaktywnie zmodyfikować (kubectl edit) obiekt
wdrożenia, aby ustawić obraz kontenera na nową wersję,
skorzystać z polecenia kubectl set image, aby ustawić nowy obraz w obiekcie
Deployment.
Zapoznaj się z przykładem w pełnej wersji, dostępnym w naszym repozytorium1. Dzięki temu
dowiesz się, jak korzystać z tych poleceń, a także jak monitorować lub wycofywać aktualizację
za pomocą polecenia kubectl rollout.
Poza rozwiązaniem wcześniej opisanych problemów imperatywnego wdrażania usług, obiekt
Deployment przynosi następujące korzyści:
Deployment jest obiektem zasobu Kubernetesa, którego status jest zarządzany w całości
przez Kubernetesa. Cały proces aktualizacji jest wykonywany po stronie serwera, bez
interakcji z klientem.
Deklaratywny charakter obiektu Deployment skupia Twoją uwagę na stanie, w jakim
powinna znaleźć się aplikacja po wdrożeniu, a nie na krokach, które trzeba wykonać, aby
osiągnąć ten efekt.
Definicja wdrożenia stanowi wykonywalny obiekt, przetestowany i wypróbowany w wielu
środowiskach przed osiągnięciem etapu produkcyjnego.
Proces aktualizacji jest w całości rejestrowany i wersjonowany, z możliwością
wstrzymania, kontynuowania lub wycofania do poprzednich wersji.
Stałe wdrażanie
Strategia RollingUpdate jest użyteczna, gdy trzeba zapewnić brak jakichkolwiek przestojów
podczas procesu aktualizacji. Efektem ubocznym takiego podejścia jest doprowadzenie do
sytuacji, w której dwie wersje tego samego kontenera działają w tym samym czasie. Taka
sytuacja może spowodować problemy u konsumentów usług, zwłaszcza, gdy proces aktualizacji
wprowadza zmiany likwidujące kompatybilność wsteczną w API usług, na które klient nie jest
gotowy. W takiej sytuacji warto skorzystać ze strategii Recreate, przedstawionej na rysunku
3.2.
03_02
Wydanie niebiesko-zielone
Wdrażanie niebiesko-zielone (ang. blue-green) to strategia wydania używana do wdrażania
oprogramowania w środowiskach produkcyjnych, minimalizująca czas przestoju i obniżająca
ryzyko wystąpienia problemów. Abstrakcja Deployment stanowi fundamentalny mechanizm,
który pozwala na określanie przejść pomiędzy różnymi wersjami niemodyfikowalnych
kontenerów. Skorzystamy z prymitywu Deployment, a także z kilku innych prymitywów, aby
zaimplementować bardziej zaawansowaną strategię wydania, jaką jest wdrażanie niebiesko-
zielone.
Jeżeli nie skorzystamy z rozszerzeń, takich jak Service Mesh lub Knative, wdrożenie niebiesko-
zielone musi być zrealizowane ręcznie. Teoretycznie możemy po prostu stworzyć drugi obiekt
Deployment, zawierający najnowszą wersję kontenerów (nazywaną zieloną), która nie obsługuje
jeszcze żądań. W tym momencie repliki starej kapsuły (nazywanej niebieską), pochodzące z
oryginalnego obiektu Deployment, są wciąż uruchomione i obsługują żądania na bieżąco.
Gdy tylko jesteśmy pewni, że nowa wersja kapsuł działa prawidłowo i może obsługiwać
rzeczywiste żądania, przekierowujemy ruch z replik starej kapsuły na nowe repliki. Tę
aktywność w Kubernetesie można osiągnąć aktualizując selektor usługi, aby dopasować nowe
kontenery (oznaczone jako zielone). Jak widać na rysunku 3.3, w momencie, gdy zielone
kontenery obsługują cały ruch, niebieskie kontenery mogą być usunięte, a zasoby — zwolnione
do obsługi wdrożeń niebiesko-zielonych w przyszłości.
03_03
Wydanie kanarkowe
Wydanie kanarkowe (ang. canary release) stanowi metodę łagodnego wdrażania nowej wersji
aplikacji w środowisku produkcyjnym przez zastąpienie jedynie małego podzbioru starych
instancji nowymi. Ta technika obniża ryzyko wprowadzenia nowej wersji w środowisku
produkcyjnym, ponieważ tylko nieliczni klienci są w stanie skorzystać ze zaktualizowanej
wersji. Gdy jesteśmy zadowoleni z tego, jak nowa usługa działa z wybraną grupą klientów,
możemy zastąpić wszystkie stare instancje nową wersją. Rysunek 3.4 przedstawia wydanie
kanarkowe w praktyce.
03_04
Dyskusja
Użycie prymitywu Deployment stanowi przykład sytuacji, gdy Kubernetes zastępuje żmudny
proces ręcznego aktualizowania aplikacji aktywnością o charakterze deklaratywnym, która jest
powtarzalna i automatyzowalna. Strategie wdrożenia działające od razu (RollingUpdate i
Recreate) kontrolują proces zastępowania starych kontenerów nowymi, a strategie wydania
aplikacji (niebiesko-zielona i kanarkowa) kontrolują proces udostępniania nowej wersji
konsumentom. Ostatnie dwie ze strategii publikacji bazują całkowicie na decyzji operatora w
celu dokonania przejścia — w związku z tym nie są one w pełni zautomatyzowane. Rysunek 3.5
przedstawia podsumowanie strategii publikacji i wdrożenia, z uwzględnieniem liczby instancji
podczas procesu przejścia.
03_05
Każde oprogramowanie jest inne, a wdrażanie złożonych systemów z reguły wymaga wykonania
dodatkowych kroków i sprawdzeń. Mechanizmy omówione w tym rozdziale uwzględniają proces
aktualizacji kapsuły, ale nie zawierają operacji aktualizacji i wycofywania innych zależności
kapsuł, takich jak obiekty ConfigMap, Secret czy inne usługi będące zależnościami.
Więcej informacji
Przykład Deklaratywnego Wdrażania: http://bit.ly/2Fc6d6J
Ciągłe aktualizacje: http://bit.ly/2r06Ich
Obiekty Deployment: http://bit.ly/2q7vR7Y
Uruchomienie aplikacji bezstanowej za pomocą obiektu Deployment: http://bit.ly/2XZZhlL
Wdrażanie niebiesko-zielone: http://bit.ly/1Gph4FZ
Wydanie kanarkowe: https://martinfowler.com/bliki/CanaryRelease.html
DevOps przy użyciu OpenShift: https://red.ht/2W7fdAQ
1 http://bit.ly/2Fc6d6J
Rozdział 4. Sonda Kondycji
Wzorzec Sonda Kondycji pozwala na przekazywanie informacji na temat stanu aplikacji do
Kubernetesa. Aby osiągnąć pełną automatyzację, natywna aplikacja chmurowa musi
udostępniać informacje na temat swojego stanu, dzięki czemu Kubernetes jest w stanie
określić, czy aplikacja działa prawidłowo i może obsługiwać żądania. Te obserwacje wpływają
na zarządzanie cyklem życia kapsuł i sposobem kierowania ruchu do aplikacji.
Problem
Kubernetes regularnie sprawdza stan procesu kontenera i restartuje go, jeśli zostaną wykryte
jakiekolwiek problemy. Z praktyki wiadomo jednak, że samo sprawdzenie stanu procesu z
reguły nie wystarcza, aby powiedzieć, że aplikacja działa prawidłowo. W wielu sytuacjach
aplikacja zawiesza się, co nie zmienia faktu, że proces dalej działa. Aplikacja Javy może na
przykład rzucić błąd OutOfMemoryError, ale proces JVM będzie dalej działać. Z drugiej strony,
aplikacja może ulec zamrożeniu, ponieważ znajdzie się w nieskończonej pętli, może wystąpić
zakleszczenie lub innego rodzaju problemy z pamięcią podręczną, stertą czy samym procesem.
W związku z tym, Kubernetes musi dysponować wiarygodną metodą weryfikacji kondycji
aplikacji. Nie chodzi o to, aby w pełni rozumieć, jak aplikacja działa wewnątrz, ale by
sprawdzać, czy działa ona jak należy i jest w stanie obsługiwać klientów.
Rozwiązanie
W przemyśle związanym z wytwarzaniem oprogramowania zaakceptowano fakt, że nie jest
możliwe pisanie kodu niezawierającego błędów. Ryzyko ich powstawania jest szczególnie duże
w przypadku aplikacji rozproszonych. W związku z tym, sporo wysiłku wkłada się nie tylko w
unikanie błędów, ale także w wykrywanie ich i przywracanie sprawności systemów po awariach.
Wykrycie błędu nie jest prostą operacją, przebiegającą identycznie w przypadku różnych
aplikacji, ponieważ każda z nich może mieć swoje własne, specyficzne definicje błędów. Różne
rodzaje błędów mogą wymagać różnych działań naprawczych. Problemy chwilowe mogą
zniknąć samoistnie, w wyniku upływu czasu, ale inne usterki mogą spowodować konieczność
zrestartowania aplikacji. Sprawdźmy, z jakich mechanizmów weryfikujących korzysta
Kubernetes, aby wykrywać i naprawiać problemy z aplikacjami.
Sonda żywotności
Jeśli Twoja aplikacja znajdzie się w stanie zakleszczenia, z punktu widzenia kontroli działania
procesu wciąż będzie ona uznawana za działającą. Aby wykryć tego rodzaju problem (jak
również wiele innych podobnych), Kubernetes wprowadza mechanizm sondy żywotności (ang.
liveness probe) — regularne prośby wysyłane przez agenta Kubelet o potwierdzenie stanu
kontenera. Wykonanie takiej operacji z zewnątrz (a nie wewnętrznie, z poziomu samej aplikacji)
jest niezwykle ważne, ponieważ w przypadku niektórych błędów, aplikacja nie będzie w stanie
sprawdzać sama siebie, bo zaistniały problem wpłynie również na wbudowany w nią
mechanizm weryfikacji. Działanie naprawcze jest identyczne, jak w przypadku kontroli
działania procesu — kontener zostaje zrestartowany. Ta sonda daje większe możliwości w
zakresie metod sprawdzania stanu aplikacji:
Sonda HTTP wysyła żądanie GET na adres IP kontenera i oczekuje w odpowiedzi kodu
HTTP pomiędzy 200 a 399, świadczącego o pozytywnym wyniku.
Sonda gniazda TCP próbuje nawiązać połączenie TCP i oznajmia sukces w przypadku
skutecznego zakończenia tej operacji.
Sonda Exec wykonuje dowolne polecenie w przestrzeni nazw jądra kontenera i oczekuje
poprawnego kodu wyjścia (zakończenia), równego 0.
kind: Pod
metadata:
name: pod-with-liveness-check
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: DELAY_STARTUP
value: “20”
ports:
- containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
W zależności od specyfiki Twojej aplikacji, możesz wybrać metodę, która najlepiej do niej
pasuje. To od Ciebie zależy określenie, kiedy o Twojej aplikacji można powiedzieć, że działa
prawidłowo. Pamiętaj, że jeśli test stanu aplikacji nie powiedzie się, kontener zostanie
zrestartowany. Jeśli restart kontenera nie pomoże, tego rodzaju mechanizm kontrolny nie
będzie miał sensu, ponieważ przyczyna problemu nie zostanie usunięta.
Sondy gotowości
O ile sondy żywotności pomagają nam utrzymać aplikacje w dobrym stanie przez ubicie
problematycznych kontenerów i zastąpienie ich nowymi, o tyle nie zawsze takie zachowanie
pomoże w rozwiązaniu problemów. Na przykład kontener może mieć długi czas rozruchu i przy
pierwszym teście żywotności może nie być gotowy na obsługę żądań. Kontener może też być
przeładowany, co prowadzi do zwiększenia latencji — w takiej sytuacji powinien być chroniony
przed dodatkowym obciążeniem, a nie zamykany.
W takiej sytuacji Kubernetes pozwala na użycie sond gotowości (ang. readiness probes).
Metody pozwalające na weryfikację gotowości nie różnią się od kontroli żywotności (HTTP, TCP i
Exec), ale za to działanie naprawcze jest już w ich przypadku inne. Zamiast restartu kontenera,
dojdzie do usunięcia go z końcówki usługi, wskutek czego nie otrzyma on nowych żądań —
nowy ruch do tego kontenera zostanie powstrzymany. Sondy gotowości sygnalizują, gdy
kontener jest gotowy do pracy, dzięki czemu może on przygotować się do obsługi żądań
pochodzących z usługi. Ponadto tego rodzaju sondy pomagają w ochronie przed wzmożonym
ruchem — testy są bowiem wykonywane regularnie, podobnie jak w przypadku sond
żywotności. Listing 4.2 przedstawia sposób implementacji sondy gotowości poprzez
sprawdzenie istnienia pliku tworzonego przez aplikację w momencie, gdy jest ona gotowa do
działania.
apiVersion: v1
kind: Pod
metadata:
name: pod-with-readiness-check
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
readinessProbe:
exec:
Sprawdź obecność pliku tworzonego przez aplikację, aby określić, czy jest ona gotowa do
obsługi żądań. Polecenie stat zwróci błąd, jeżeli plik nie istnieje, w związku z czym także sonda
gotowości zwróci błąd.
Ponownie, to do Ciebie należy określenie stanu, w którym aplikacja powinna być gotowa na
obsługę żądań. O ile kontrola działania procesu i sonda żywotności przywracają sprawność
kontenera przez jego restart, sonda gotowości spróbuje „kupić” trochę czasu dla Twojej
aplikacji, licząc na to, że sama odzyska ona sprawność. Pamiętaj, że Kubernetes próbuje
zapobiec przesyłaniu nowych żądań do kontenera (np. w trakcie jego wyłączania), niezależnie
od tego, czy sonda gotowości działa prawidłowo po otrzymaniu sygnału SIGTERM.
W wielu przypadkach sondy żywotności i gotowości wykonują te same testy. Dzięki obecności
sondy gotowości kontener ma więcej czasu na uruchomienie. Dopiero przejście testu gotowości
pozwoli na stwierdzenie, że Wdrożenie zostało wykonane prawidłowo, dzięki czemu — na
przykład — kapsuły o starszej wersji mogą być wyłączone w ramach operacji ciągłej
aktualizacji.
Dyskusja
Aby osiągnąć pełną automatyzację, natywne aplikacje chmurowe muszą być obserwowalne — to
znaczy udostępniać informacje na swój temat platformie, na której się znajdują, dzięki czemu
możliwy jest ich odczyt, interpretacja i — ewentualnie — podjęcie działań naprawczych. Testy
stanu aplikacji odgrywają kluczową rolę w automatyzacji takich działań jak wdrażanie,
samodzielne naprawianie, skalowanie i wiele innych. Informacje na temat swojego stanu
aplikacja może udostępniać również na inne sposoby.
Oczywistą i dość wiekową metodą jest rejestrowanie wpisów w dzienniku zdarzeń (logowanie
ich). Dobrą praktyką w przypadku kontenerów jest rejestrowanie wszystkich istotnych zdarzeń
na systemowym wyjściu i strumieniu błędów, oraz gromadzenie ich w jednym, centralnym
miejscu do dalszej analizy. Wpisy dziennika nie są używane do podejmowania
zautomatyzowanych działań — służą one głównie do ostrzegania i pomagają w analizie
problemu. Użyteczną funkcją logów jest analiza całej problematycznej sytuacji po fakcie (ang.
post mortem) w celu wykrycia niezauważonych wcześniej błędów.
Nawet dobrze napisane aplikacje muszą udostępniać także metody zarządzania platformą w
celu obserwowania stanu aplikacji. Można to osiągnąć przez implementację bibliotek do
śledzenia i zbierania wskaźników, takich jak OpenTracing czy Prometheus. Traktuj swoją
aplikację jako czarną skrzynkę, ale jednocześnie nie zapominaj o implementowaniu wszystkich
niezbędnych API, które pomogą w obserwowaniu jej działania i zarządzaniu nią.
Więcej informacji
Przykład sondy kondycji:
https://github.com/k8spatterns/examples/tree/master/foundational/HealthProbe
Konfigurowanie sond żywotności i gotowości: https://kubernetes.io/docs/tasks/configure-
pod-container/configure-liveness-readiness-startup-probes
Ustawianie testów stanu aplikacji za pomocą sond gotowości i żywotności:
https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-setting-up-health-
checks-with-readiness-and-liveness-probes
Zasób QoS: https://github.com/kubernetes/community/blob/master/contributors/design-
proposals/node/resource-qos.md
Bezpieczne wyłączanie aplikacji Node.js w ramach Kubernetesa:
https://blog.risingstack.com/graceful-shutdown-node-js-kubernetes
Zaawansowane wzorce sprawdzania stanu aplikacji w Kubernetesie:
https://ahmet.im/blog/advanced-kubernetes-health-checks
Rozdział 5.Zarządzany Cykl
Życia
Aplikacje konteneryzowane, zarządzane przez natywne platformy chmurowe, nie mają wpływu
na swój cykl życia. Aby dobrze spełniać swoje obowiązki, muszą nasłuchiwać w oczekiwaniu na
zdarzenia emitowane przez platformę zarządzającą i dostosowywać do niej swój cykl życia.
Wzorzec Zarządzanego Cyklu Życia pokazuje, jak aplikacje mogą i jak powinny reagować na te
zdarzenia.
Problem
W rozdziale 4. pokazaliśmy, dlaczego kontenery muszą udostępniać API w celu wykonania
testów kondycji systemu. API do weryfikowania stanu to końcówki przeznaczone tylko do
odczytu, używane w sposób ciągły. W ten sposób, platforma jest w stanie uzyskiwać informacje
na temat aplikacji.
Poza monitorowaniem stanu kontenera, platforma może czasami wydawać specyficzne
polecenia, oczekując od aplikacji, że na nie zareaguje. W zależności od określonych reguł i
zewnętrznych czynników, natywna platforma chmurowa może zadecydować o uruchomieniu lub
zatrzymaniu aplikacji w dowolnym momencie. To od skonteneryzowanej aplikacji zależy sposób
reakcji na zdarzenia. Można powiedzieć, że to API jest używane przez platformę do komunikacji
i wysyłania poleceń do aplikacji. Aplikacje mogą korzystać z cyklu życia, ale mogą też
kompletnie go ignorować.
Rozwiązanie
Zauważyliśmy już, że sprawdzanie procesu tylko pod kątem jego działania nie jest
wystarczająco dobrym rozwiązaniem. Z tego względu wprowadzono różne API do
monitorowania stanu kontenera. Na podobnej zasadzie, zastosowanie podejścia procesowego
do uruchamiania i zatrzymywania procesów nie jest wystarczająco dobre. Rzeczywiste aplikacje
wymagają więcej interakcji o znacznie większym poziomie szczegółowości. Niektóre aplikacje
mogą potrzebować pomocy podczas uruchomienia, a inne — przy zamykaniu. W tym i wielu
innych przypadkach mogą pomóc zdarzenia emitowane przez platformę. Kontener może
nasłuchiwać w celu wychwycenia ich wystąpienia i reagować w miarę potrzeby (rysunek 5.1).
Rysunek 5.1. Zarządzany cykl życia kontenera
Jednostką wdrażania aplikacji jest kapsuła. Jak wiesz, składa się ona z co najmniej jednego
kontenera. Na poziomie kapsuły możemy mówić o innych konstrukcjach, takich jak kontenery
inicjalizacji, omówione w rozdziale 14. (a także kontenery opóźnione, które w czasie
powstawania tej książki były wciąż na etapie koncepcyjnym). Pomagają one w zarządzaniu
cyklem życia kontenera. Zdarzenia i haki opisane w tym rozdziale są stosowane na poziomie
pojedynczego kontenera, a nie kapsuły.
Sygnał SIGTERM
Gdy Kubernetes decyduje się zamknąć kontener — niezależnie od tego, czy jest to
konsekwencją zamknięcia kapsuły, czy też nieudanego działania sondy żywotności i wynikającej
z tego konieczności restartu — dochodzi do otrzymania przez kontener sygnału SIGTERM.
SIGTERM stanowi dość subtelny sygnał zamknięcia kontenera, wysyłany przed bardziej
kategorycznym komunikatem SIGKILL. Po otrzymaniu sygnału SIGTERM, aplikacja powinna
zamknąć się tak szybko, jak to możliwie. Oczywiście jedna aplikacja zamknie się dość szybko, a
inna będzie musiała zakończyć przetwarzane żądania, zwolnić otwarte połączenia i wyczyścić
pliki tymczasowe, co może zająć trochę więcej czasu. We wszystkich przypadkach otrzymanie
sygnału SIGTERM powinno spowodować bezpieczne zakończenie działania aplikacji.
Sygnał SIGKILL
Jeśli proces kontenera nie został zamknięty po otrzymaniu sygnału SIGTERM, nastąpi jego siłowe
wyłączenie, za pomocą sygnału SIGKILL. Kubernetes nie wysyła tego sygnału od razu, ale
odczekuje domyślnie 30 sekund od wysłania SIGTERM. Okres zwłoki (ang. grace period) można
zdefiniować odrębnie dla każdej kapsuły, korzystając z pola
.spec.terminationGracePeriodSeconds. Nie można go jednak zagwarantować, ponieważ
może on zostać przesłonięty w trakcie wydawania poleceń Kubernetesowi. Naszym celem jest
uzyskanie takiego projektu i takiej implementacji konteneryzowanych aplikacji, aby ich procesy
startu i zatrzymania były możliwie krótkie.
Hak postartowy
Zarządzanie cyklem życia tylko za pomocą sygnałów procesów to niezbyt rozbudowane
rozwiązanie. W związku z tym możemy skorzystać z dodatkowych haków cyklu życia, takich jak
postStart czy preStop, które udostępnia Kubernetes. Manifest kapsuły zawierający hak
postStart wygląda jak na listingu 5.1.
apiVersion: v1
kind: Pod
metadata:
name: post-start-hook
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
lifecycle:
postStart:
exec:
command:
- sh
- -c
httpGet — wykonuje żądanie HTTP GET, korzystając z portu otwartego przez jeden z
kontenerów.
Jeśli stosujesz hak postStart, musisz być niezwykle ostrożny, ponieważ nie ma gwarancji co do
jego wykonania. Hak jest wykonywany równolegle do procesu kontenera, dlatego jest możliwe,
że zostanie wykonany przed uruchomieniem kontenera. Ponadto hak jest wykonywany zgodnie
z zasadą „minimum raz”, co oznacza, że trzeba zająć się potencjalnymi zduplikowanymi
wywołaniami. Należy też pamiętać o tym, że platforma nie ponawia prób wykonania nieudanych
żądań HTTP, które nie otrzymały odpowiedzi.
apiVersion: v1
kind: Pod
metadata:
name: pre-stop-hook
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
lifecycle:
preStop:
httpGet:
port: 8080
path: /shutdown
W rozdziale 14. omawiamy szczegółowo ten mechanizm — tj. kontenery inicjalizacji; póki co
przedstawimy go pokrótce, porównując go do haków cyklu życia. W przeciwieństwie do
zwykłych kontenerów aplikacji, kontenery inicjalizacji są wykonywane sekwencyjnie i
funkcjonują do momentu zakończenia. Są one wykonywane przed uruchomieniem kontenerów
w kapsule. Te reguły pozwalają na zastosowanie kontenerów inicjalizacji do zadań inicjalizacji
na poziomie kapsuły. Zarówno haki cyklu życia, jak i kontenery inicjalizacji, operują na innym
poziomie szczegółowości (odpowiednio — kontenera i kapsuły) i mogą być używane wymiennie
w niektórych sytuacjach, a w ramach uzupełnienia w innych. Tabela 5.1 podsumowuje główne
różnice pomiędzy tymi pojęciami.
Tabela 5.1. Haki cyklu życia i kontenery inicjalizacji
Moment
Fazy cyklu życia kontenera Fazy cyklu życia kapsuły
aktywacji
Moment
działania przy Polecenie postStart Lista obiektów initContainer do wykonania
uruchomieniu
Moment
działania przy Polecenie preStop Brak analogicznej funkcji
zamknięciu
Wykonywanie niekrytycznych
Wykonywanie sekwencyjnych operacji przy
operacji związanych z
Zastosowania użyciu kontenerów; korzystaj z kontenerów
uruchomieniem/zamknięciem
wielokrotnie do wykonywania zadań
kontenera
Nie ma ścisłych reguł związanych ze stosowaniem jednego lub drugiego rozwiązania — jedyna
istotna różnica tkwi w gwarancjach czasowych. Moglibyśmy w ogóle pominąć haki cyklu życia i
kontenery inicjalizacji, a zamiast nich korzystać ze skryptów powłoki w celu wykonania
specyficznych zadań w trakcie uruchomienia lub zamknięcia kontenera. Jest to możliwe, ale
jednocześnie związałoby to ściśle kontener ze skryptem, znacząco utrudniając utrzymanie i
konserwację aplikacji.
Moglibyśmy także skorzystać z haków cyklu życia Kubernetesa, aby wykonać niektóre działania
opisane w tym rozdziale. Możemy pójść nawet dalej i uruchamiać kontenery, które wykonują
pojedyncze działania za pomocą kontenerów inicjalizacji. W tym przypadku przedstawione
opcje wymagają więcej wysiłku, ale jednocześnie dają lepsze gwarancje i pozwalają na ponowne
użycie.
Zrozumienie etapów działania aplikacji i dostępnych haków kontenera, a także cyklu życia
kapsuły, to kluczowe kwestie związane z tworzeniem aplikacji, które korzystają z faktu bycia
zarządzanymi przez Kubernetesa.
Dyskusja
Jedną z kluczowych zalet natywnych platform chmurowych jest możliwość uruchamiania i
skalowania aplikacji w przewidywalny i stabilny sposób, mimo stosowania wewnętrznie
potencjalnie niestabilnej architektury chmurowej. Platformy te pozwalają na określenie
ograniczeń i kontraktów dla aplikacji, które są w ich ramach uruchamiane. To w interesie
aplikacji jest przestrzegać tych kontraktów, dzięki czemu możliwe staje się skorzystanie z
możliwości oferowanych przez natywne platformy chmurowe. Obsługa zdarzeń i reakcja na nie
zapewniają, że aplikacja może być uruchamiana i zamykana bezpiecznie, z minimalnym
wpływem na usługi będące konsumentami aplikacji. W tym momencie, w podstawowej postaci
oznacza to, że kontenery powinny zachować się jak typowe, dobrze zaprojektowane procesy
POSIX. W przyszłości być może pojawi się jeszcze więcej zdarzeń, które poinformują aplikację
np. o skalowaniu lub poproszą o zwolnienie zasobów, aby uniknąć konieczności zamknięcia
kontenera. Najważniejsze jest to, aby pamiętać, że cykl życia aplikacji nie jest zarządzany przez
człowieka, ale jest on w pełni zautomatyzowany przez platformę.
Poza zarządzaniem cyklem życia aplikacji, innym obowiązkiem platform orkiestracji, takich jak
Kubernetes, jest dystrybuowanie kontenerów na dużych grupach węzłów. Wraz z kolejnym
wzorcem, Automatyczne Rozmieszczanie, omówimy możliwości, jakie mamy w zakresie
wpływania na ten proces z zewnątrz.
Więcej informacji
Przykład Zarządzanego Cyklu Życia: http://bit.ly/2udxws4
Haki cyklu życia kontenera: http://bit.ly/2Fb38Uk
Załączanie mechanizmów obsługi do zdarzeń cyklu życia kontenera: http://bit.ly/2Jn9ANi
Bezpieczne wyłączanie: http://bit.ly/2TcPnJW
Bezpieczne zamykanie kapsuł w Kubernetesie: http://bit.ly/2CvDQjs
Opóźnione kontenery: http://bit.ly/2TegEM7
Rozdział 6. Automatyczne
Rozmieszczanie
Automatyczne Rozmieszczanie stanowi główną funkcję planisty Kubernetesa, używaną do
przypisywania nowych kapsuł do węzłów spełniających oczekiwania kontenera w zakresie
zasobów i uwzględniających zasady planowania. Ten wzorzec opisuje reguły algorytmu
planowania Kubernetesa, a także możliwość wpłynięcia na sposób rozmieszczania.
Problem
Typowy system zbudowany na bazie mikrousług może składać się z dziesiątków, a nawet setek
niezależnych, wyizolowanych procesów. Kontenery i kapsuły dostarczają abstrakcje do
opakowywania i wdrażania, ale nie rozwiązują problemu umieszczenia procesów w
odpowiednich węzłach. Wraz z rosnącą liczbą mikrousług, przypisywanie i rozmieszczanie ich
pojedynczo do węzłów przestaje być praktycznie możliwe.
Kontenery są zależne od siebie nawzajem, od węzłów, a także mają one oczekiwania wobec
zasobów — wszystko to również zmienia się w czasie. Dostępność zasobów w klastrze również
może się zmieniać, przez dodanie lub odjęcie zasobów z klastra lub przez skorzystanie z
zasobów przez uprzednio rozmieszczone kontenery. Sposób, w jaki rozmieszczamy kontenery,
wpływa na dostępność, wydajność i pojemność rozproszonych systemów. Wszystko to sprawia,
że rozmieszczanie kontenerów w węzłach jest dość trudne — i nie możemy zadowolić się
półśrodkami.
Rozwiązanie
Rozmieszczanie kapsuł w węzłach w Kubernetesie jest wykonywane przez planistę. Jest to
obszar działania, na który mamy duży wpływ, ale jednocześnie należy pamiętać o tym, że
podlega on stałej ewolucji i nieustannym zmianom. W tym rozdziale omawiamy główne
mechanizmy kontroli planowania — te, które mają wpływ na rozmieszczanie kontenerów.
Omawiamy ich zalety, a także zastosowania i konsekwencje podjętych wyborów. Planista
Kubernetesa to potężne narzędzie, które pozwala zaoszczędzić sporo czasu. Odgrywa ono
kluczową rolę w działaniu Kubernetesa jako całości, ale podobnie jak w przypadku pozostałych
komponentów Kubernetesa (API serwera czy Kubeleta), może być ono uruchamiane bez
związku z pozostałymi narzędziami; można również w ogóle z niego nie korzystać.
Na bardzo ogólnym poziomie planista Kubernetesa pobiera definicje każdej nowo utworzonej
kapsuły z serwera API i przypisuje ją do węzła. Każda kapsuła otrzymuje odpowiedni węzeł (o
ile tylko on istnieje), niezależnie od tego, czy operacja ta wynika z pierwotnego rozmieszczenia
aplikacji, skalowania w górę, czy też przemieszczenia aplikacji z węzła problematycznego do
prawidłowo działającego. Aby zrealizować tę operację, Kubernetes wykonuje analizę zależności
uruchomieniowych, wymagań co do zasobów, a także zasad związanych z zapewnieniem
wysokiej dostępności, rozmieszczając kapsuły poziomo i kolokując je blisko siebie, aby
zwiększyć wydajność i zmniejszyć latencję. Aby planista był w stanie prawidłowo wykonywać
swoją pracę z wykorzystaniem deklaratywnego rozmieszczenia, potrzebne są węzły o określonej
pojemności, a także kontenery o odpowiednich profilach zasobów i zasad. Teraz
przeanalizujemy szczegółowo wszystkie te aspekty.
Jeśli nie zarezerwujesz zasobów dla demonów systemu operacyjnego, a także samego
Kubernetesa, kapsuły wypełnią cały węzeł, co spowoduje wyścig o zasoby pomiędzy kapsułami i
demonami systemu operacyjnego. Jest to najprostszy sposób prowadzący do wystąpienia braku
zasobów w węźle. Warto pamiętać, że w przypadku, gdy kontenery są uruchomione na węźle
niezarządzanym przez Kubernetesa, nie są one uwzględnione w wykonywanych przez niego
obliczeniach pojemności węzła.
Rozwiązaniem tego ograniczenia jest uruchomienie kapsuły zastępczej, która nie robi nic poza
rezerwacją czasu procesora i pamięci wynikającej z wymagań kontenerów, które nie są
obserwowane. Taka kapsuła jest tworzona tylko po to, aby zarezerwować zasoby wynikające ze
zużycia kontenerów niepodlegających obserwacji. Dzięki temu planista jest w stanie lepiej
określić model użycia zasobów w węźle.
Zasady rozmieszczenia
Ostatnim elementem układanki jest odpowiednie zdefiniowanie zasad priorytetów lub
filtrowania w swojej aplikacji. Planista dysponuje domyślnym zestawem zasad predykatów i
priorytetów, który w większości przypadków okazuje się wystarczający. Można przesłonić go
podczas startu planisty, wprowadzając własny zestaw zasad, jak na listingu 6.2.
“kind” : “Policy”,
“apiVersion” : “v1”,
“predicates” : [
{“name” : “PodFitsHostPorts”},
{“name” : “PodFitsResources”},
{“name” : “NoDiskConflict”},
{“name” : “NoVolumeZoneConflict”},
{“name” : “MatchNodeSelector”},
{“name” : “HostName”}
],
“priorities” : [
Tuż po utworzeniu kapsuły, która nie została jeszcze przypisana do węzła, zostaje ona wybrana
przez planistę wraz z dostępnymi węzłami, a także zbiorem zasad filtrowania i priorytetów. Na
początku planista stosuje zasady filtrowania i usuwa wszystkie węzły, które nie odpowiadają
wymaganiom kapsuły. Następnie pozostałe węzły są porządkowane według wagi. W ostatnim
etapie kapsuła trafia do węzła, co jest głównym efektem procesu rozplanowania.
Z reguły to planista powinien przypisywać kapsuły do węzłów — nie powinniśmy zbyt często
ingerować w logikę rozmieszczania. W niektórych sytuacjach może zaistnieć konieczność
przypisania kapsuły do konkretnego węzła lub grupy węzłów. To przypisanie można osiągnąć za
pomocą selektora węzła. .spec.nodeSelector to pole konfiguracji kapsuły, które określa
słownik par klucz-wartość, jakie muszą być obecne w węźle w formie etykiet, aby możliwe było
uruchamianie kapsuły. Na przykład, jeśli chcesz wymusić uruchomienie kapsuły na węźle, który
dysponuje pamięcią SSD lub kartą graficzną z wydajnym GPU, możesz skorzystać z konstrukcji
podobnej do tej z listingu 6.3. W tym przykładzie oczekujemy, że etykieta disktype będzie
miała wartość ssd i tylko węzły spełniające tę regułę będą w stanie uruchamiać kapsułę.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
nodeSelector:
disktype: ssd
Zbiór etykiet, do których węzeł musi pasować, aby móc obsłużyć kapsułę.
Poza określaniem własnych etykiet w węzłach, możesz skorzystać z domyślnych etykiet, które
są dostępne w każdym węźle. Każdy węzeł ma swoją etykietę kubernetes.io/hostname, używaną
do rozmieszczania kapsuły w węźle na podstawie nazwy hosta. Ponadto możemy korzystać z
takich etykiet jak system operacyjny, architektura czy rodzaj instancji (instance-type), które
również są przydatne w procesie rozmieszczania.
Przypisanie węzła
Kubernetes obsługuje wiele różnych sposobów konfiguracji procesu planowania. Jedną z nich
jest przypisanie węzła, stanowiące uogólnienie opisanego przed chwilą mechanizmu selektora
węzła. Dzięki niemu jesteśmy w stanie określać reguły jako wymagane lub preferowane. Reguły
wymagane muszą być spełnione, aby kapsuła została przydzielona do węzła, podczas gdy reguły
preferowane mają wpływ na proces decyzyjny, zwiększając wagę węzłów spełniających takie
reguły — nie muszą być one jednak koniecznie spełnione. Ponadto przypisanie węzła znacząco
zwiększa rodzaje ograniczeń, które możesz wyrazić, korzystając z operatorów takich jak In,
NotIn, Exists, DoesNotExist, Gt lub Lt. Listing 6.4 przedstawia sposób deklaracji przypisania
węzła.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: numberCores
operator: Gt
values: [ “3” ]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchFields:
- key: metadata.name
operator: NotIn
values: [ “master” ]
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
Wymaganie miękkie, będące listą selektorów z wagami. Dla każdego węzła jest obliczana
suma wag zgodnie z pasującymi selektorami. Następuje wybór węzła o największej wartości, o
ile rzecz jasna spełnione jest wymaganie twarde.
Dopasowanie według pola (określonego za pomocą ścieżki JSON). Zwróć uwagę na to, że w
tym miejscu możemy korzystać jedynie z operatorów In i NotIn, a ponadto możliwe jest
podanie tylko jednej wartości w ramach zadeklarowanej listy.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
confidential: high
topologyKey: security-zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
confidential: none
topologyKey: kubernetes.io/hostname
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
Reguły wymagane do rozmieszczenia kapsuły, związane z innymi kapsułami uruchomionymi
w docelowym węźle.
Reguły rozdzielności określają węzły, na których kapsuła nie może być rozmieszczona.
Reguła określa, że kapsuła nie powinna (choć teoretycznie może) być umieszczona na węźle,
na którym są uruchomione kapsuły z etykietą confidential=none.
Podobnie jak w przypadku przypisania węzłów, także i w przypadku przypisania i rozdzielności
kapsuł możemy mówić o twardych i miękkich wymaganiach (odpowiednio
requiredDuringSchedulingIgnoredDuringExecution i
preferredDuringSchedulingIgnoredDuringExecution). Analogicznie jak podczas przypisania
węzłów, w nazwie pola pojawia się sufiks IgnoredDuringExecution, który istnieje w celu
zachowania możliwości rozszerzenia w przyszłości. W tym momencie, jeśli etykiety w węźle
ulegną zmianie i reguły przypisania nie są aktualne, kapsuły będą dalej działać1. W przyszłości
może się to jednak zmienić.
Skazy i tolerancje
Bardziej zaawansowaną funkcją, która kontroluje miejsce rozmieszczenie kapsuł do
uruchomienia, jest mechanizm skaz (ang. taints) i tolerancji (ang. tolerations). O ile z
przypisania węzłów korzystamy, aby określić na jakich węzłach chcemy uruchamiać kapsuły, o
tyle skaz i tolerancji używamy w odwrotnej sytuacji. To dzięki nim jesteśmy w stanie określić,
które kapsuły powinny lub nie powinny być uruchamiane na węzłach. Skaza to cecha kapsuły,
która sprawia, że nie można uruchomić danej kapsuły w ramach węzła, chyba że ma ona
tolerancję dla danej skazy. Można powiedzieć, że skazy i tolerancje pozwalają kapsułom na
bycie rozplanowanymi na węzłach, na których domyślnie nie powinno mieć to miejsca (ang. opt-
in). Reguły przypisania stanowią mechanizm wyboru, na których węzłach ma nastąpić
uruchomienie kapsuły — jest to więc rodzaj mechanizmu opt-out.
Skazę dodaje się do węzła za pomocą polecenia kubectl: kubectl taint nodes master node-
role.kubernetes.io/master=”true”:NoSchedule (listing 6.6). Powiązaną tolerancję dodajemy
do kapsuły na listingu 6.7. Zwróć uwagę, że wartości atrybutów key i effect w sekcji taints
na listingu 6.6, a także sekcji tolerations na listingu 6.7, mają te same wartości.
kind: Node
metadata:
name: master
spec:
taints:
- effect: NoSchedule
key: node-role.kubernetes.io/master
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
tolerations:
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
Tolerujemy (tj. akceptujemy przy wyborze węzła do zaplanowania) węzły, które mają skazę z
kluczem node-role.kubernetes.io/master. W klastrach produkcyjnych skaza ta jest
ustawiana na węzłach typu master w celu uniemożliwienia rozplanowania kapsuł na takim
węźle. Tolerancja dla tej skazy pozwoli kapsule na bycie zainstalowaną nawet na węźle master.
Tolerujemy węzeł tylko wtedy, gdy skaza zawiera efekt NoSchedule. Pole to można
pozostawić pustym, dzięki czemu tolerancja będzie dotyczyć wszystkich efektów.
Istnieją skazy twarde, które uniemożliwiają rozplanowanie kapsuł na węźle
(effect=NoSchedule), miękkie, które starają się uniemożliwić rozplanowanie kapsuł
(effect=PreferNoSchedule), a także takie, które potrafią wywłaszczyć już uruchomione
kapsuły z węzła (effect=NoExecute).
Skazy i tolerancje pozwalają na obsługę złożonych przypadków użycia, np. zastosowanie
dedykowanych węzłów dla ściśle określonego zbioru kapsuł lub siłowe wywłaszczenie kapsuł z
problematycznych węzłów przez skażenie tych węzłów.
Możesz wpływać na rozmieszczanie, bazując na potrzebach w zakresie wysokiej dostępności i
wydajności aplikacji, jednak staraj się nie ograniczać zbytnio planisty, ponieważ może to
doprowadzić do sytuacji, w której kapsuły nie mogą być rozplanowane, przy jednoczesnej zbyt
dużej ilości opuszczonych zasobów. Na przykład jeśli wymagania zasobów kontenerów są zbyt
ogólne lub węzły są zbyt małe, możesz skończyć w sytuacji, w której zasoby zostały opuszczone
w węzłach, które nie są do końca wykorzystane.
Rysunek 6.2 przedstawia węzeł A zawierający 4 GB pamięci, która nie może być użyta,
ponieważ nie ma już dostępnych rdzeni dla innych kontenerów. Pomocne w tej sytuacji może
być tworzenie kontenerów o mniejszych wymaganiach dotyczących zasobów. Innym
rozwiązaniem jest zastosowanie deplanisty (ang. descheduler), który pomaga w defragmentacji
węzłów i usprawnieniu ich użycia.
Po przypisaniu kapsuły do węzła praca planisty kończy się — umiejscowienie kapsuły nie
zostanie zmienione, chyba że zostanie ona usunięta lub utworzona na nowo bez określonego
przypisania do węzła. Wraz z upływem czasu, takie zachowanie może prowadzić do
fragmentacji zasobów i nieoptymalnego ich użycia w klastrze. Kolejny problem wynika z faktu,
że decyzje planisty są podejmowane na podstawie widoku klastra w momencie, gdy nowa
kapsuła jest rozplanowywana.
Jeśli klaster jest dynamiczny, a profil zasobu węzłów zmienia się lub dodawane są nowe węzły,
planista nie skoryguje poprzednich rozmieszczeń kapsuł. Poza zmianą pojemności węzła,
możesz także zmienić etykiety w węzłach (co może wpłynąć na rozmieszczenie), jednak
poprzednie rozmieszczenia nie zostaną skorygowane.
Wszystkie te problemy można rozwiązać za pomocą deplanisty. Deplanista Kubernetesa to
opcjonalny mechanizm, który jest wykonywany jako zadanie (Job), gdy tylko administrator
klastra postanowi oczyścić i zdefragmentować klaster, wykonując ponowne rozmieszczenie
kapsuł. Deplanista zawiera predefiniowane zasady, które można włączać, wyłączać i dostrajać.
Zasady są przekazywane w formie pliku do deplanisty kapsuły. Oto ich aktualny wykaz:
RemoveDuplicates
Ta strategia zapewnia, że tylko jedna kapsuła z przypisanym obiektem ReplicaSet lub
Deployment jest uruchomiona na pojedynczym węźle. Jeśli kapsuł jest więcej, wszystkie
nadmiarowe kapsuły zostaną wywłaszczone. Ta strategia jest przydatna w sytuacji, gdy
węzeł przestaje być w dobrym stanie, a kontroler zarządzający uruchamia nowe kapsuły na
innych, zdrowych węzłach. Gdy problematyczny węzeł wraca do prawidłowego stanu i
dołącza do klastra, liczba uruchomionych kapsuł jest większa niż potrzebujemy, stąd
deplanista jest w stanie zmniejszyć liczbę kapsuł zgodnie z wartością właściwości
replicas. Usunięcie duplikatów w węzłach pomaga w równomiernym rozprzestrzenianiu
kapsuł na większej liczbie węzłów, gdy zasady planowania i topologia klastra zmieniają się
po początkowym rozmieszczeniu.
LowNodeUtilization
Ta strategia wyszukuje węzły, które nie są wykorzystane w pełni, i wywłaszcza kapsuły z
innych, przepełnionych węzłów, zakładając, że kapsuły te zostaną umieszczone w
niewykorzystanych w pełni węzłach. W ten sposób możemy osiągnąć lepsze wykorzystanie
zasobów. Węzły niewykorzystane w całości to te, w przypadku których zużycie CPU,
pamięci czy liczby kapsuł jest mniejsze niż w wartościach atrybutu thresholds.
Analogicznie, przeciążone węzły mają wartości większe niż te określone w wartościach
atrybutu targetThresholds. Każdy węzeł, którego wartości mieszczą się pomiędzy tymi
dwoma atrybutami, jest traktowany jako dobrze wykorzystany i nie jest poddawany żadnym
zmianom.
RemovePodsViolatingInterPodAntiAffinity
Ta strategia wywłaszcza kapsuły, które naruszają reguły rozdzielności międzykapsułowej.
Może się to zdarzyć w sytuacji, gdy reguły rozdzielności są dodawane po rozmieszczeniu
kapsuł w węzłach.
RemovePodsViolatingNodeAffinity
Dyskusja
Rozmieszczanie to ten aspekt wdrażania aplikacji w Kubernetesie, w który nie powinno się
zbytnio ingerować. Jeśli stosujesz się do wytycznych z rozdziału 2. i deklarujesz wszystkie
wymagania dotyczące zasobów kontenera, planista zrobi co do niego należy i umieści kapsułę
na najlepszym możliwym węźle. Jeśli jednak takie podejście okaże się niewystarczające, istnieje
wiele sposobów na zasugerowanie planiście pożądanej topologii wdrożenia. Poniższe
zestawienie zawiera podsumowanie przedstawiające sposoby kontroli planowania kapsuły,
zaczynając od najprostszego, aż do najbardziej skomplikowanego (pamiętaj, że wraz z
publikacją kolejnych wersji Kubernetesa, lista ta może ulec zmianie):
nodeName
Jest to najprostsza metoda powiązania kapsuły z węzłem. To pole powinno być wypełniane
przez planistę, co powinno być efektem zastosowania różnorodnych zasad, a nie ręcznym
przypisaniem węzła. Przypisanie kapsuły do węzła znacząco ogranicza możliwości
planowania. W ten sposób cofamy się do czasów przedkubernetesowych, kiedy jawnie
określaliśmy węzły, na których mają być uruchamiane nasze aplikacje.
nodeSelector
Specyfikacja będąca słownikiem par klucz-wartość. Aby kapsuła mogła być uruchomiona w
ramach węzła, musi zawierać pary klucz-wartość jako etykiety w ramach węzła. Selektor
węzła jest jednym z najprostszych rozsądnych mechanizmów do kontroli działania planisty.
Domyślna zmiana planowania
Przypisanie węzłów
Ta reguła pozwala na określanie zależności od węzłów. Do uwzględnianych kryteriów
należą m.in. parametry sprzętowe czy lokalizacja węzła.
Skazy i tolerancje
Skazy i tolerancje pozwalają na określanie, które kapsuły powinny być lub nie być
uruchamiane w ramach węzła. W ten sposób można przeznaczyć węzeł dla specjalnie
wybranej grupy kapsuł, a nawet wywłaszczać kapsuły w czasie działania. Kolejną zaletą
skaz i tolerancji jest możliwość rozszerzania klastra Kubernetesa przez dodanie nowych
węzłów z nowymi etykietami bez konieczności dodawania nowych etykiet do wszystkich
kapsuł — wystarczy dodać je do tych, które będą umieszczane na nowych węzłach.
Własny planista
Jeśli żadne z przedstawionych podejść nie jest wystarczająco dobre lub Twoje wymagania
dotyczące planowania są zbyt skomplikowane, możesz napisać własnego planistę. Własny
planista może działać zamiast lub obok standardowego planisty Kubernetesa. Podejście
hybrydowe polega na posiadaniu procesu „rozszerzenia planisty”, który jest wywoływany
przez standardowego planistę na sam koniec w trakcie podejmowania decyzji. W ten
sposób nie musisz implementować pełnego planisty, a jedynie zapewnić API http do
filtrowania i priorytetyzacji węzłów. Posiadanie własnego planisty pozwala na wzięcie pod
uwagę czynników wykraczających poza ramy klastra Kubernetesa, takich jak koszt sprzętu,
opóźnienia sieci czy lepsze użycie zasobów w trakcie przypisywania kapsuł do węzłów.
Możesz także skorzystać z wielu własnych planistów obok domyślnego i zadecydować o
przypisaniu planisty do każdej z kapsuł. Każdy planista może mieć inny zestaw zasad,
przeznaczony dla konkretnego podzbioru kapsuł.
Jak widać, istnieje wiele metod kontroli rozmieszczenia kapsuł. Wybór odpowiedniego podejścia
lub połączenie wielu podejść jednocześnie bywa trudne. Wniosek z tego rozdziału jest
następujący: deklaruj profile zasobów kontenerów, etykietuj kapsuły i węzły we właściwy
sposób, a przede wszystkim staraj się ograniczać ingerencje w mechanizm planisty
Kubernetesa do minimum.
Więcej informacji
Przykład Automatycznego Rozmieszczania: http://bit.ly/2TTJUMh
Przypisywanie kapsuł do węzłów: https://kubernetes.io/docs/user-guide/node-selection
Objaśnienie rozmieszczenia i planowania węzłów: https://red.ht/2TP1ceB
Budżet zakłóceń kapsuły: https://kubernetes.io/docs/admin/disruptions
Gwarantowane planowanie dla krytycznych dodatkowych kapsuł:
https://kubernetes.io/docs/admin/rescheduler
Planista Kubernetesa: http://bit.ly/2Hrq8lJ
Algorytm planisty: http://bit.ly/2F9Vfi2
Konfiguracja wielu planistów: http://bit.ly/2HLv5Fk
Deplanista w Kubernetesie: http://bit.ly/2YMQzYn
Zrównoważ swój klaster Kubernetesa — sekret wysokiej dostępności: http://bit.ly/2zuecKk
Wszystko, co chciałbyś wiedzieć o planowaniu zasobów, ale boisz się zapytać:
http://bit.ly/2FNkBT9
1 Jeśli jednak etykiety węzłów ulegną zmianie i dopuszczą do sytuacji, gdy nierozplanowane
kapsuły zostaną dopasowane do selektora przypisania węzła, kapsuły zostaną umieszczone na
tym węźle.
Część II. Wzorce zachowań
Wzorce z tej kategorii są skoncentrowane na mechanizmach interakcji i komunikacji pomiędzy
kapsułami i platformą zarządzającą. W zależności od kontrolera, kapsuły mogą być
uruchomione aż do zakończenia działania lub funkcjonować cyklicznie. Można uruchamiać je w
formie Usługi Demona lub zapewniać unikalne gwarancje do ich replik. Istnieją różne metody
uruchamiania kapsuły, a wybór właściwych prymitywów do zarządzania kapsułami wymaga
zrozumienia ich zachowania. W kolejnych rozdziałach omawiamy następujące wzorce:
Problem
Głównym prymitywem używanym w Kubernetesie do uruchamiania kontenerów i zarządzania
nimi jest kapsuła. Można wyróżnić różne sposoby tworzenia kapsuł o różnych cechach:
Naga kapsuła
Istnieje możliwość ręcznego utworzenia kapsuły w celu uruchamiania kontenerów. Jeśli
jednak węzeł, na którym jest uruchomiona kapsuła, zawiedzie, kapsuła nie zostanie
zrestartowana. Uruchamianie kapsuł w taki sposób nie jest zalecane poza etapem
tworzenia oprogramowania lub do testów. Mechanizm ten znany jest pod nazwą nagich lub
niezarządzanych kapsuł (odpowiednio naked/unmanaged Pods).
ReplicaSet
Ten kontroler jest używany do tworzenia kapsuł (i zarządzania ich cyklem życia), które
powinny działać w sposób ciągły (np. kontenera serwera WWW). Kontroler zarządza
stabilnym zbiorem replik kapsuł, gwarantując dostęp do określonej liczby identycznych
kapsuł.
DaemonSet
Kontroler odpowiedzialny za uruchomienie pojedynczej kapsuły na każdym węźle.
Zazwyczaj jest on używany do zarządzania różnymi funkcjami platformy, takimi jak
monitorowanie, agregacja logów, kontenery pamięci trwałej itd. Obiekty DaemonSet
omawiamy szczegółowo w rozdziale 9.
Typową cechą tego rodzaju kapsuł jest to, że reprezentują one długotrwałe procesy, które z
założenia nie kończą swojego działania w określonym momencie. Czasami istnieje jednak
konieczność wykonania pewnego określonego zadania w skończonym czasie, a następnie
zamknięcia kontenera. W tym celu można skorzystać z zasobu zadania (Job).
Rozwiązanie
Obiekt Job w Kubernetesie przypomina obiekt ReplicaSet, który tworzy co najmniej jedną
kapsułę i zapewnia, że każda z utworzonych kapsuł zostanie uruchomiona prawidłowo. Różnica
polega na tym, że po zakończeniu działania określonej liczby kapsuł, zadanie jest traktowane
jako skutecznie zakończone, w związku z czym nie będą tworzone dodatkowe kapsuły. Definicja
zadania jest przedstawiona na listingu 7.1.
Listing 7.1. Specyfikacja obiektu Job
apiVersion: batch/v1
kind: Job
metadata:
name: random-generator
spec:
completions: 5
parallelism: 2
template:
metadata:
name: random-generator
spec:
restartPolicy: OnFailure
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
Zadanie powinno uruchomić pięć kapsuł i wszystkie powinny zakończyć swe działanie z
sukcesem.
Czemu więc do uruchomienia kapsuły jeden raz w ogóle rozważamy obiekt Job, skoro
moglibyśmy po prostu skorzystać z nagich kapsuł? Zastosowanie obiektu Job wnosi wymienione
poniżej korzyści w postaci stabilności i skalowalności.
Zadanie nie jest ulotne — jest przechowywane w sposób trwały i jest w stanie przetrwać
restart klastra.
Po zakończeniu zadania nie jest ono usuwane — zostaje zachowane w celu śledzenia.
Kapsuły, które zostały utworzone w ramach zadania, także nie są usuwane — są dostępne
do późniejszej analizy (np. do sprawdzenia logów). Ta reguła jest również prawdziwa w
przypadku nagich kapsuł, jednak tylko przy ustawieniu atrybutu
restartPolicy:OnFailure.
Zadanie może być wykonywane wiele razy. Zastosowanie pola .spec.completions
pozwala na określenie liczby skutecznych zakończeń działania kapsuły przed
zakończeniem zadania.
Gdy zadanie musi być ukończone wiele razy (co określamy za pomocą atrybutu
.spec.completions), możemy także skalować je i uruchamiać wiele kapsuł w tym samym
czasie. W tym celu możemy skorzystać z atrybutu .spec.parallelism.
Jeśli węzeł zawiedzie lub kapsuła zostanie wywłaszczona w trakcie działania, planista
umieści kapsułę na zdrowym węźle i uruchomi ją ponownie. Nagie kapsuły pozostałyby w
takiej sytuacji w stanie nieprawidłowym, ponieważ istniejące kapsuły nie zostaną
przemieszczone do innych węzłów.
Wszystko to sprawia, że prymityw Job jest niezwykle rozsądnym wyborem w sytuacji, gdy
konieczne jest skuteczne zakończenie pewnego określonego działania.
.spec.completions
.spec.parallelism
Określa ile replik kapsuły może być uruchomionych jednocześnie. Ustawienie wysokiej
liczby nie gwarantuje wysokiego poziomu zrównoleglenia, a faktyczna liczba kapsuł może
być mniejsza (a niekiedy nawet większa) niż pożądana (np. z powodu dławienia, limitów
zasobów, niewystarczającej liczby pozostałych wykonań itd.). Ustawienie wartości tego pola
na 0 spowoduje wstrzymanie zadania.
Rysunek 7.1 przedstawia sposób przetworzenia zadania z listingu 7.1, z liczbą wykonań równą
5 i zrównolegleniem wynoszącym 2.
Kolejka robocza (ang. work queue) jest używana w przypadku zadań równoległych, gdy
pominiesz deklarację atrybutu .spec.completions, a atrybut .spec.parallelism ustawisz
na wartość większą niż 1. Zadanie kolejki roboczej zostanie uznane za zakończone, gdy co
najmniej jedna kapsuła zostanie zakończona prawidłowo, a wszystkie inne kapsuły również
zostaną zakończone. Ta konfiguracja wymaga możliwości koordynacji działań pośród kapsuł
i określenia, które z nich są aktywne, aby mogły one skoordynować zakończenie swojego
działania. Na przykład jeśli w kolejce znajduje się określona, ale nieznana liczba elementów
do przetworzenia, równoległe kapsuły mogą przetwarzać elementy jeden po drugim.
Pierwsza kapsuła, która wykryje, że kolejka jest pusta, może zakończyć swoje działanie z
sukcesem, wskazując tym samym koniec zadania. Kontroler Job musi poczekać na
zakończenie również innych kapsuł. Skoro jedna kapsuła przetwarza wiele elementów, ten
rodzaj zadania jest świetnym wyborem w przypadku mniejszych zadań — gdy narzut
związany z poświęcaniem jednej kapsuły dla jednego elementu nie może być racjonalnie
uzasadniony.
Dyskusja
Abstrakcja Job to prosty mechanizm, ale jednocześnie jest zasadniczym, podstawowym
prymitywem, na którym zostały zbudowane takie prymitywy jak CronJob. Zadania pozwalają
przekształcić pojedyncze operacje czy działania do wykonania w stabilne i skalowalne jednostki
wykonawcze. Zadanie nie określa jednak, w jaki sposób zamieniać pojedyncze elementy do
przetworzenia (operacje robocze) na zadania czy kapsuły. Decyzję trzeba podjąć samemu,
rozważając wady i zalety każdej z opcji:
Jedno zadanie — jeden element do przetworzenia
Ta opcja jest właściwa w przypadku dużej liczby elementów, które nie muszą być
niezależnie śledzone i zarządzane przez platformę. W tym przypadku elementy do
przetworzenia muszą być zarządzane z poziomu aplikacji za pomocą frameworku
wsadowego.
Nie wszystkie usługi muszą działać cały czas. Niektóre powinny być uruchamiane na żądanie,
inne — w określonym czasie, a jeszcze inne — co jakiś czas. Zastosowanie zadań pozwala na
selektywne uruchamianie kapsuł tylko na czas wykonania zadań. Zadania są rozplanowywane
na węzłach, które spełniają określone założenia co do pojemności, reguły rozmieszczania
kapsuł, a także inne zależności pojemników. Zastosowanie zadań dla krótkotrwałych operacji
(zamiast używania abstrakcji długotrwałych, takich jak ReplicaSet) oszczędza zasoby dla
innych działań na platformie. Wszystko to sprawia, że zadanie jest prymitywem unikatowym i
dowodem na to, że Kubernetes obsługuje zróżnicowane rodzaje działań.
Więcej informacji
Przykład Zadania Wsadowego: http://bit.ly/2Jnloz6
Uruchamianie aż do zakończenia: http://bit.ly/2W1ZTW2
Równoległe przetwarzanie za pomocą rozszerzeń: http://bit.ly/2Y563GL
Zgrubne przetwarzanie równoległe za pomocą kolejki roboczej: http://bit.ly/2Y29cqS
Drobne przetwarzanie równoległe za pomocą kolejki roboczej: http://bit.ly/2Obtutr
Zadanie indeksowane utworzone za pomocą metakontrolera: http://bit.ly/2FkjQSA
Frameworki i biblioteki do przetwarzania wsadowego w Javie: https://github.com/jberet
Rozdział 8. Zadanie Okresowe
Wzorzec Zadanie Okresowe rozszerza wzorzec Zadanie Wsadowe, dodając do niego wymiar
czasowy i pozwalając na wykonanie operacji w wyniku zaistnienia zdarzenia czasowego.
Problem
W świecie rozproszonych systemów i mikrousług istnieje wyraźna tendencja do tworzenia
interakcji w aplikacjach odbywających się w czasie rzeczywistym i sterowanych zdarzeniami z
wykorzystaniem protokołu HTTP i lekkich komunikatów (bez dużego narzutu na komunikację).
Niezależnie od trendów w tworzeniu oprogramowania, planowanie zadań istnieje od dawna i
wciąż jest niezwykle ważnym aspektem tworzenia oprogramowania. Zadania Okresowe są
używane do automatyzacji operacji związanych z konserwacją i administracją systemu. Są one
także niezwykle istotne z punktu widzenia tych mechanizmów logiki biznesowej, które muszą
być wykonywane cyklicznie. Do typowych przykładów należą integracje oparte na transferze
plików, pobieraniu danych z bazy danych, wysyłaniu newslettera w formie wiadomości e-mail
czy też czyszczeniu i archiwizacji starych plików.
Typowym sposobem obsługi Zadań Okresowych związanych z konserwacją systemu było
zastosowanie programu Cron lub specjalistycznego oprogramowania do planowania.
Specjalistyczne oprogramowanie potrafi sporo kosztować, a z kolei zadania Crona wykonywane
na jednym serwerze są trudne w utrzymaniu i mogą łatwo zawieść. Z tego względu programiści
często implementują rozwiązania, które radzą sobie dobrze zarówno z planowaniem, jak i
wykonywaniem logiki biznesowej. W świecie Javy można skorzystać z bibliotek takich jak
Quartz, Spring Batch czy własnych implementacji klasy ScheduledThreadPoolExecutor, które
potrafią uruchamiać zadania czasowe. Podobnie jak w programie Cron, główna trudność polega
na uodpornieniu możliwości planowania na problemy i zapewnieniu wysokiej dostępności. W
takiej sytuacji, planista zadań staje się częścią aplikacji, przez co zapewnienie wysokiej
dostępności planisty wiąże się z koniecznością zagwarantowania wysokiej dostępności całej
aplikacji. Jest to związane zazwyczaj z uruchomieniem wielu instancji aplikacji i jednocześnie
zapewnieniem, że tylko jedna instancja jest aktywna i planuje zadania — co wiąże się z
koniecznością wyboru lidera i innymi typowymi problemami systemów rozproszonych.
Koniec końców, prosta usługa, która musi skopiować kilka plików raz na dzień, może
spowodować rezerwację kilku węzłów, zastosowanie rozproszonego mechanizmu wyboru lidera
itd. Implementacja obiektu CronJob w Kubernetesie rozwiązuje te wszystkie problemy,
pozwalając programistom skupić się na faktycznej pracy, a nie na planowaniu jej wykonania.
Rozwiązanie
W rozdziale 7. zapoznaliśmy się z przypadkami użycia i możliwościami Zadań Wsadowych
Kubernetesa. Wszystkie omówione tam cechy odnoszą się również do tego rozdziału, ponieważ
prymityw CronJob jest zbudowany na bazie zwykłego obiektu Job. Instancja CronJob jest
podobna do pojedynczego wpisu z tabeli Cron (crontab) z Uniksa, jako że odpowiada za
czasowe aspekty wykonania zadania. Pozwala ona na cykliczne wykonanie zadania zawsze o
wyznaczonej porze (listing 8.1).
Listing 8.1. Zasób CronJob
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: random-generator
spec:
# Co trzy minuty
schedule: “*/3 * * * *”
jobTemplate:
spec:
template:
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
Szablon Zadania Okresowego, które korzysta z tej samej specyfikacji, co Zadanie Wsadowe.
Pomijając definicję zadania, obiekt CronJob udostępnia wymienione poniżej dodatkowe pola,
aby określić swoje własności związane z czasem.
.spec.schedule
.spec.startingDeadlineSeconds
Maksymalny czas wykonania (w sekundach) do uruchomienia zadania, jeśli nie zostanie
ono wykonane w zaplanowanym czasie. W niektórych sytuacjach zadanie jest prawidłowe
tylko wtedy, gdy zostanie wykonane w określonym przedziale czasowym, i nie ma ono
sensu, jeśli zostanie wykonane z opóźnieniem. Na przykład, jeśli zadanie nie zostanie
wykonane w określonym czasie z powodu braku zasobów lub zależności, lepszym
rozwiązaniem może być pominięcie opóźnionego wykonania, ponieważ przetwarzane przez
zadanie dane będą i tak, na ten moment, nieaktualne.
.spec.concurrencyPolicy
Określa sposób zarządzania współbieżnymi wykonaniami zadań utworzonych przez ten sam
obiekt CronJob. Domyślne zachowanie Allow spowoduje utworzenie nowych instancji
zadania, nawet jeśli poprzednie nie zostały zakończone. Jeśli nie jest to zachowanie
pożądane, istnieje możliwość pominięcia kolejnego wykonania, o ile bieżące nie zostało
zakończone. Wystarczy skorzystać z opcji Forbid. Można też anulować aktualnie
wykonywane zadanie i uruchomić nowe, dzięki opcji Replace.
.spec.suspend
.spec.successfulJobsHistoryLimit i .spec.failedJobsHistoryLimit
Dyskusja
Jak widać, CronJob to dość prosty prymityw, który dodaje przypominające Crona zachowanie
klastrowane do istniejących definicji zadania. Jeśli jednak połączy się go z innymi prymitywami,
takimi jak kapsuły, czy też z mechanizmem izolacji zasobów kontenera, a także innymi
funkcjami Kubernetesa, jak omówione w rozdziale 6. Automatyczne Rozmieszczanie czy Sonda
Kondycji z rozdziału 4., ten prymityw może okazać się niezwykle potężny. Dzięki temu
programiści mogą skupić się na rozwiązaniu problemów z danej dziedziny i implementacji logiki
biznesowej w aplikacjach konteneryzowanych. Planowanie jest wykonywane poza aplikacją i
obsługiwane przez platformę, dzięki czemu zyskujemy wszystkie jej możliwości, takie jak
wysoka dostępność, odporność na błędy, pojemność czy rozmieszczenie kapsuł według
przyjętych reguł. Podobnie jak w przypadku implementacji zadań, implementując kontener
CronJoba, aplikacja musi uwzględnić różne przypadki brzegowe i problemy związane z
wywołaniami duplikowanymi, równoległymi, czy też z anulowaniem uruchomienia.
Więcej informacji
Przykład Zadania Okresowego: http://bit.ly/2HGXAnh
Zadania Crona: https://kubernetes.io/docs/concepts/jobs/cron-jobs/
Cron: https://en.wikipedia.org/wiki/Cron
Rozdział 9. Usługa Demona
Wzorzec Usługa Demona pozwala na rozmieszczenie i uruchamianie na docelowych węzłach
priorytetyzowanych kapsuł skupionych na infrastrukturze. Jest on używany głównie przez
administratorów do uruchamiania kapsuł charakterystycznych dla konkretnych węzłów, aby
rozszerzać możliwości platformy Kubernetesa.
Problem
Pojęcie demona (ang. daemon) w systemach informatycznych istnieje na wielu poziomach. Na
poziomie systemu operacyjnego, demon to długotrwały, samoprzywracający się w razie
problemów program, który działa jako proces w tle. W systemach uniksowych nazwy demonów
kończą się na literę d, np. httpd, named i sshd. W innych systemach operacyjnych można
spotkać się z pojęciem zadań usługowych lub zadań-duchów.
Niezależnie od nazwy, typową cechą, łączącą wszystkie tego rodzaju zadania, jest brak
interakcji z urządzeniami wejścia-wyjścia (monitor, klawiatura, mysz i inne), a także moment
uruchomienia — przy starcie systemu. Podobny koncept funkcjonuje również na poziomie
aplikacji. Wątki demona JVM są uruchomione w tle i dostarczają wsparcie dla wątków
użytkownika. Wątki demona mają niski priorytet, są uruchomione w tle, nie mają żadnego
wpływu na aplikację, wykonując zadania takie jak odśmiecanie czy finalizacja.
Na podobnej zasadzie w Kubernetesie funkcjonuje obiekt DaemonSet. Biorąc pod uwagę to, że
Kubernetes jest platformą rozproszoną na przestrzeni wielu węzłów, a głównym celem jest
zarządzanie kapsułami aplikacji, obiekt DaemonSet jest reprezentowany za pomocą kapsuł na
węzłach klastra i dostarcza szereg możliwości funkcjonujących w tle dla reszty klastra.
Rozwiązanie
Obiekt ReplicaSet i jego poprzednik — ReplicationController — to struktury kontrolne,
odpowiedzialne za zapewnienie odpowiedniej liczby uruchomionych kapsuł. Kontrolery te stale
monitorują listę uruchomionych kapsuł i upewniają się, że ich liczba jest na odpowiednim
poziomie. Pod tym względem DaemonSet działa podobnie, będąc odpowiedzialnym za
zapewnienie, że pewna liczba kapsuł jest stale uruchomiona. Różnica polega na tym, że
ReplicaSet i ReplicationController uruchamiają określoną liczbę kapsuł na podstawie
wymagań aplikacji w zakresie wysokiej dostępności i obciążenia użytkownika, niezależnie od
liczby węzłów.
Z drugiej strony, obiekt DaemonSet nie uwzględnia w swoim działaniu obciążenia ze strony
konsumentów. Jego głównym celem jest zapewnienie działania pojedynczej kapsuły na
wszystkich bądź wybranych węzłach. Przeanalizujmy definicję obiektu DaemonSet z listingu 9.1.
kind: DaemonSet
metadata:
name: random-refresher
spec:
selector:
matchLabels:
app: random-refresher
template:
metadata:
labels:
app: random-refresher
spec:
nodeSelector:
feature: hw-rng
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
command:
- sh
- -c
- >-
“while true; do
java -cp / RandomRunner /host_dev/random 100000;
volumeMounts:
- mountPath: /host_dev
name: devices
volumes:
- name: devices
hostPath:
path: /dev
Korzystaj tylko z węzłów z etykietą feature o wartości hw-rng.
Obiekty DaemonSet często montują fragment systemu plików węzła w celu wykonywania
zadań związanych z utrzymaniem aplikacji.
Biorąc pod uwagę tego rodzaju działanie, głównymi kandydatami do wdrożenia w ramach
obiektu DaemonSet są z reguły procesy związane z infrastrukturą, takie jak mechanizmy
zbierania logów, eksportery wskaźników, a także kube-proxy, które wykonuje operacje na
całym klastrze. Istnieje wiele różnic w sposobie zarządzania, odróżniających obiekty DaemonSet
i ReplicaSet. Do głównych należą następujące:
Obiekt DaemonSet zazwyczaj tworzy jedną kapsułę na wszystkich węzłach lub na pewnej
podgrupie węzłów. W konsekwencji istnieje wiele metod pozwalających na dostęp do kapsuł
zarządzanych przez obiekty DaemonSet.
Usługa
Utwórz usługę o takim samym selektorze kapsuły co DaemonSet, a następnie skorzystaj z
usługi, aby uzyskać dostęp do kapsuły demona, która w ramach procesu równoważenia
obciążenia została przydzielona do osobnego węzła.
DNS
Utwórz usługę typu headless o takim samym selektorze kapsuły co DaemonSet, aby uzyskać
wiele rekordów typu A z usługi DNS zawierającej wszystkie adresy IP i porty kapsuł.
Kapsuły z obiektem DaemonSet mogą określić własność hostPort, dając możliwość dostępu
za pomocą adresów IP węzła i określonego portu. Skoro kombinacja własności hostIp,
hostPort i protocol musi być unikatowa, liczba miejsc, w których można umieścić
kapsułę, jest ograniczona.
Wypychanie
Aplikacja umieszczona w kapsułach typu DaemonSet może wypychać dane do dobrze znanej
lokalizacji lub usługi, umieszczonej na zewnątrz kapsuły. Dzięki temu nikt nie musi
samodzielnie kontaktować się z kapsułami DaemonSet.
Kapsuły statyczne mogą uruchamiać konteneryzowaną wersję procesów systemu Kubernetesa
lub innych kontenerów. Obiekty DaemonSet z kolei są — w porównaniu do kapsuł statycznych —
lepiej zintegrowane z resztą platformy i z tego względu ich użycie jest zalecane.
Kapsuły statyczne
Kolejną metodą uruchamiania kontenerów, podobną do tej używanej w obiektach
DaemonSet, jest mechanizm kapsuł statycznych. Kubelet, pomijając komunikację z
serwerem Kubernetes API i pobieraniem manifestów, potrafi pobierać definicje zasobów z
lokalnego katalogu. Kapsuły zdefiniowane w ten sposób są zarządzane jedynie przez
narzędzie Kubelet i mogą być uruchamiane tylko na jednym węźle. Usługa API nie
obserwuje tych kapsuł, a także nie są wobec nich wykonywane żadne mechanizmy
kontroli stanu czy kontrolera. Kubelet obserwuje te kapsuły i restartuje je, gdy dochodzi
do zawieszenia. Analogicznie, Kubelet okresowo skanuje ustalony katalog, sprawdzając,
czy nie doszło do zmiany definicji, dodając lub usuwając kapsuły w miarę potrzeby.
Dyskusja
W tej książce opisujemy wzorce i funkcje Kubernetesa stosowane głównie przez programistów,
a nie administratorów platform. Obiekt DaemonSet jest w tym podziale ulokowany gdzieś
pośrodku; należy raczej do narzędzi administracyjnych, ale z drugiej strony ma zastosowanie
również dla programistów aplikacji. Obiekty DaemonSet i CronJob są świetnymi przykładami
tego, jak Kubernetes potrafi zaadaptować koncepty jednowęzłowe (takie jak Crontab czy
skrypty demonów) do postaci wielowęzłowych, klastrowych prymitywów do zarządzania
rozproszonymi systemami. Są to nowe, rozproszone koncepty, z którymi programiści powinni
być obeznani.
Więcej informacji
Przykład Usługi Demona: http://bit.ly/2TMX3rc
Obiekty DaemonSet: http://bit.ly/2r07CWx
Wykonywanie ciągłej aktualizacji z obiektem DaemonSet: http://bit.ly/2CAZ13F
DaemonSet a zadania: http://bit.ly/2HLeHof
Kapsuły statyczne: https://kubernetes.io/docs/tasks/administer-cluster/static-pod/
Rozdział 10. Usługa Singleton
Wzorzec Usługa Singleton zapewnia obecność tylko jednej aktywnej instancji aplikacji w danej
chwili przy zachowaniu jej wysokiej dostępności. Ten wzorzec można zaimplementować w
samej aplikacji albo oddelegować w pełni do Kubernetesa.
Problem
Jedną z głównych funkcji Kubernetesa jest zdolność do łatwego i nieinwazyjnego skalowania
aplikacji. Kapsuły można skalować imperatywnie, za pomocą jednego polecenia (np. kubectl
scale), deklaratywnie, za pomocą definicji kontrolera (np. ReplicaSet), a nawet dynamicznie,
na podstawie obciążenia aplikacji (por. rozdział 24.). Uruchomienie wielu instancji tej samej
usługi (mamy na myśli komponent rozproszonej aplikacji reprezentowany za pomocą kapsuły, a
nie usługę Kubernetesa) powoduje z reguły zwiększenie przepustowości i dostępności.
Dostępność zwiększa się, ponieważ w przypadku problemów z jedną instancją usługi,
dyspozytor żądań przekazuje nowe żądania do innych zdrowych instancji. W Kubernetesie
instancje są replikami kapsuły, a zasób usługi jest odpowiedzialny za przekazywanie żądań.
W niektórych sytuacjach tylko jedna instancja usługi może działać w danej chwili. Na przykład,
jeśli w ramach usługi jest wykonywane okresowo pewne zadanie i dojdzie do uruchomienia
wielu instancji tej samej usługi, każda instancja wyzwoli zadanie w zaplanowanych interwałach,
doprowadzając do powstania duplikatów. Kolejny przykład stanowi usługa, która pobiera dane z
określonych zasobów (np. systemu plików lub bazy danych). Chcemy zapewnić obecność jednej
instancji, a nawet jednego wątku, który wykonuje pobieranie i przetwarzanie danych. Trzeci
przykład to konsumpcja komunikatów przekazywanych przez brokera w ustalonej kolejności, z
konsumentem uruchomionym w pojedynczym wątku — to też dobry przykład usługi typu
singleton.
W takich i podobnych sytuacjach musimy zachować kontrolę nad liczbą instancji faktycznie
działających, aktywnych w danej chwili (choć zazwyczaj wymagana jest tylko jedna),
niezależnie od liczby tylko uruchomionych, których może być więcej.
Rozwiązanie
Uruchomienie wielu replik tej samej kapsuły tworzy topologię aktywny-aktywny, w której
wszystkie instancje tej usługi są aktywne. My potrzebujemy skorzystać z topologii aktywny-
pasywny (znanej też jako master-slave), w której tylko jedna instancja jest aktywna, a wszystkie
inne są pasywne. Możemy to osiągnąć na dwa sposoby — blokadą pozaaplikacyjną i
wewnątrzaplikacyjną.
Blokada pozaaplikacyjna
Jak sama nazwa wskazuje, ten mechanizm deleguje kwestię zapewnienia obecności tylko jednej
instancji aplikacji poza aplikację. Implementacja aplikacji nie jest świadoma tego ograniczenia
— funkcjonuje ona jako instancja typu singleton. Z tej perspektywy można porównać tę sytuację
do dysponowania klasą Javy, która jest tworzona raz przez mechanizm zarządzający (np.
framework Spring). Implementacja klasy nie jest świadoma, że działa jako singleton — nie
zawiera ona żadnego kodu, który uniemożliwiłby tworzenie wielu instancji.
Aby osiągnąć ten efekt w Kubernetesie, musimy uruchomić kapsułę z jedną repliką. W ten
sposób nie zapewnimy jednak jej wysokiej dostępności. Musimy także wesprzeć kapsułę
kontrolerem ReplicaSet, który przekształci kapsułę-singleton w wysoko dostępny singleton. Ta
topologia nie stanowi dokładnie modelu aktywny-pasywny (ponieważ nie mamy tutaj instancji
pasywnej), ale osiągamy ten sam efekt, ponieważ Kubernetes zapewnia nas, że w dowolnym
momencie działać będzie jedna instancja kapsuły. Co więcej, ta pojedyncza instancja będzie
wysoce dostępna, dzięki kontrolerowi wykonującemu testy kondycji i podejmującemu działania
w razie problemów, co szerzej opisujemy w rozdziale 4.
Stosując to podejście, przede wszystkim trzeba obserwować liczbę replik, która nie powinna
przypadkowo wzrastać, ponieważ na poziomie platformy nie dysponujemy mechanizmem
zdolnym do tego, by zapobiec zmianie liczby replik.
Nie jest do końca prawdą, że przez cały czas jest uruchomiona jedna instancja — zwłaszcza gdy
coś zacznie się dziać nie po naszej myśli. Prymitywy Kubernetesa, takie jak ReplicaSet,
preferują dostępność, a nie spójność — wszystko w celu osiągnięcia wysokiej dostępności i
skalowalności systemów rozproszonych. Oznacza to, że kontroler ReplicaSet zastosuje regułę
„co najmniej”, a nie „co najwyżej” jednej instancji dla swoich replik. Jeśli skonfigurujemy obiekt
ReplicaSet, aby działał jak singleton (ustawiając wartość parametru replicas na 1), kontroler
zapewni działanie minimum jednej instancji — może się jednak zdarzyć, że przez krótki czas
będzie działać ich więcej.
Typowym przypadkiem brzegowym jest sytuacja, w której węzeł z kapsułą zarządzaną przez
kontroler zaczyna mieć problemy i odłącza się od reszty klastra Kubernetesa. W takiej sytuacji
kontroler ReplicaSet uruchomi kolejną instancję kapsuły na zdrowym węźle (zakładając, że są
dostępne zasoby), bez upewnienia się, że kapsuła na rozłączonym węźle jest wyłączona.
Analogicznie, gdy zmieni się liczba replik lub kapsuły zostaną przeniesione do innych węzłów,
liczba kapsuł może okresowo przekroczyć pożądaną. Ten tymczasowy wzrost jest uzasadniony
chęcią zapewnienia wysokiej dostępności i braku przerw, co jest konieczne w przypadku
stanowych i skalowalnych aplikacji.
Singletony mogą być odporne na błędy i zdolne do przywracania swojego działania, ale z
definicji nie są one wysoce dostępne. Singletony są stosowane w celu zachowania spójności, a
nie dostępności. Zasobem Kubernetesa, który również spełnia tę regułę, a także zapewnia
gwarancję jednej instancji, jest obiekt StatefulSet. Jeśli obiekty ReplicaSet nie spełniają
oczekiwanych wymagań, a konieczność zapewnienia działania tylko jednej instancji jest
kluczowa, StatefulSet może być odpowiedzią na Twoje problemy. Obiekt ten jest używany w
aplikacjach stanowych, udostępniając wiele rozmaitych funkcji, w tym gwarancje działania
singletona. Z drugiej strony, obsługa takich obiektów wiąże się z większą złożonością. Więcej na
ich temat znajdziesz w rozdziale 11.
Usługi Kubernetesa omawiamy szczegółowo w rozdziale 12. W tym momencie zajmiemy się
nimi pokrótce, w zakresie związanym z singletonami. Typowa usługa (o wartości type:
ClusterIP) tworzy wirtualny adres IP i wykonuje równoważenie obciążenia wśród instancji
kapsuł o pasującej wartości selektora. Kapsuła-singleton zarządzana przez obiekt StatefulSet
zawiera tylko jedną kapsułę, dysponując stabilną tożsamością sieciową. W takiej sytuacji
znacznie lepiej jest utworzyć usługę typu headless (ustawiając własność type na wartość
ClusterIP, a clusterIP na wartość None). Usługa ta nosi miano headless (bezgłowej),
ponieważ nie dysponuje ona wirtualnym adresem IP; kube-proxy nie obsługuje takich usług, a
platforma nie wykonuje operacji przekazywania.
Mimo wszystko taka usługa nadal jest użyteczna, ponieważ usługa typu headless z selektorami
utworzy rekordy końcówek w serwerze API i wygeneruje wpisy A na serwerze DNS do
pasujących kapsuł. W związku z tym wyszukiwanie DNS dla usługi nie zwróci jej wirtualnego
adresu IP, ale adresy IP wspierających kapsuł. Dzięki temu możliwy jest bezpośredni dostęp do
kapsuły-singletona za pomocą rekordu DNS usługi, bez konieczności przechodzenia przez adres
IP usługi. Jeśli utworzymy usługę typu headless o nazwie my-singleton, możemy skorzystać z
nazwy my-singleton.default.svc.cluster.local, aby uzyskać dostęp bezpośrednio do
adresu IP kapsuły.
Podsumowując, w przypadku singletonów elastycznych, wystarczy obiekt ReplicaSet z jedną
repliką i zwykłą usługą. W przypadku singletonu ścisłego i wydajniejszego procesu wykrywania
usługi, zdecydowanie lepiej skorzystać z usługi typu headless i obiektu StatefulSet.
Kompletny przykład znajdziesz w rozdziale 11, w którym wystarczy zmienić liczbę replik, aby
uzyskać singleton.
Blokada wewnątrzaplikacyjna
W środowisku rozproszonym liczbę instancji usługi można kontrolować za pomocą blokady
rozproszonej, co przedstawiamy na rysunku 10.2. Gdy zostaje aktywowana instancja usługi (lub
komponent wewnątrz instancji), może spróbować uzyskać blokadę. Jeśli operacja to powiedzie
się, usługa staje się aktywna. Każda kolejna instancja usługi nie będzie w stanie pozyskać
blokady — w takiej sytuacji będzie ona oczekiwać na jej zwolnienie i sprawdzać regularnie, czy
blokada nie została zwolniona.
Rysunek 10.2. Mechanizm blokady wewnątrzaplikacyjnej
Implementacja narzędzia Camel korzysta z tego zabezpieczenia, aby zapewnić aktywność tylko
jednej instancji trasy. Wszystkie inne instancje muszą poczekać na uzyskanie blokady przed
aktywacją. Jest to własna implementacja blokady, ale osiąga taki sam efekt: gdy ta sama
aplikacja Camel jest skojarzona z wieloma kapsułami, tylko jedna z nich stanie się aktywnym
singletonem, podczas gdy inne będą oczekiwać w trybie pasywnym.
Obiekt PodDisruptionBudget zapewnia, że pewna liczba (lub procent) kapsuł nie zostanie
świadomie wywłaszczona z żadnego węzła w żadnym momencie. Świadomie w tym kontekście
oznacza, że wywłaszczenie może być opóźnione przez jakiś czas — np. gdy jest wywołane przez
udrażnianie węzła w celu konserwacji lub aktualizacji (kubectl drain), czy też gdy klaster
podlega skalowaniu w dół. Gdy węzeł zaczyna być niestabilny i ma problemy w działaniu —
jesteśmy w sytuacji, której nie da się przewidzieć ani kontrolować.
Obiekt PodDisruptionBudget na listingu 10.1 jest stosowany do kapsuł, które pasują do jego
selektora, zapewniając, że dwie kapsuły muszą być dostępne przez cały czas.
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: random-generator-pdb
spec:
selector:
matchLabels:
app: random-generator
minAvailable:
Minimum dwie kapsuły muszą być dostępne. Możesz także określić wartość procentową, np.
80%, aby określić, że tylko 20% pasujących kapsuł może być wywłaszczonych.
Dyskusja
Jeśli w Twojej sytuacji konieczne jest zapewnienie silnych gwarancji singletona, nie możesz
polegać na blokadach pozaaplikacyjnych, takich jak ReplicaSet. Obiekty ReplicaSet w
Kubernetesie zostały zaprojektowane, aby zapewniać dostępność kapsuł, a nie być w zgodzie z
zasadą „maksymalnie jedna dostępna kapsuła” (ang. at-most-one). W rezultacie może się
zdarzyć wiele problematycznych sytuacji (np. gdy węzeł, na którym jest uruchamiany singleton,
oddziela się od reszty klastra lub gdy usunięta kapsuła jest podmieniana na nową), w których
przez krótki czas będą działać dwie kopie kapsuły. Jeśli nie jest to akceptowalna sytuacja,
skorzystaj z obiektów StatefulSet lub przeanalizuj możliwości blokady wewnątrzaplikacyjnej,
które dadzą Ci więcej kontroli nad procesem wyboru lidera z silniejszymi gwarancjami.
Ostatnia opcja pozwoli na uniknięcie przypadkowego skalowania kapsuł z powodu zmiany liczby
replik.
W innych okolicznościach tylko fragment aplikacji konteneryzowanej powinien stać się
singletonem. Możemy na przykład dysponować skonteneryzowaną aplikacją, która udostępnia
końcówkę HTTP możliwą do wyskalowania na wiele instancji. Jednocześnie w tej samej aplikacji
może istnieć komponent do odpytywania (pobierania danych), który musi być singletonem.
Zastosowanie blokady pozaaplikacyjnej uniemożliwi skalowanie usługi. Musimy więc albo
wyodrębnić komponent singletona, aby zachować jego charakter (w teorii jest to dobre
rozwiązanie, ale nie zawsze ma sens i jest warte zachodu). Możemy też skorzystać z
mechanizmu blokady wewnątrzaplikacyjnej i zablokować tylko komponent, który musi być
singletonem. W ten sposób możemy wyskalować całą aplikację przezroczyście — skalując
końcówki HTTP, przy zachowaniu innych części aplikacji jako singletonów aktywno-pasywnych.
Więcej informacji
Przykład Usługi Singletona: http://bit.ly/2TKp5nm
Prosty wybór lidera za pomocą Kubernetesa i Dockera: http://bit.ly/2FwUS1a
Wybór lidera w kliencie Go: http://bit.ly/2UatejW
Konfiguracja budżetu zakłóceń kapsuły: http://bit.ly/2HDKcR3
Tworzenie klastrowanych Usług Singletonów w Kubernetesie: http://bit.ly/2TKm1HR
Złącze Apache Camel do Kubernetesa: http://bit.ly/2JoL6mT/
Rozdział 11. Usługa Stanowa
Rozproszone aplikacje stanowe wymagają obsługi funkcji takich jak utrwalenie tożsamości,
sieć, pamięć trwała czy uporządkowanie. Wzorzec Usługa Stanowa opisuje prymityw
StatefulSet, który spełnia te wymagania, gwarantując wszystko to, co potrzebne do obsługi
aplikacji stanowych.
Problem
Do tej pory poznaliśmy wiele prymitywów Kubernetesa, używanych do tworzenia aplikacji
rozproszonych: kontenery z sondami kondycji i limitami zasobów, kapsuły zawierające wiele
kontenerów, dynamiczne rozmieszczenia kapsuł na przestrzeni całego klastra, zadania wsadowe
i zaplanowane, singletony i wiele więcej. Typową cechą tych wszystkich prymitywów jest fakt,
że z ich punktu widzenia zarządzana aplikacja jest aplikacją bezstanową, złożoną z
identycznych i wymienialnych kontenerów, spełniającą regułę dwunastu aspektów.
Choć oddelegowanie rozmieszczenia, odporności i skalowania aplikacji bezstanowych do
platformy daje wiele korzyści, wciąż mamy na głowie całkiem sporo: w przypadku aplikacji
stanowych każda instancja jest unikatowa i ma pewne cechy, które charakteryzują się
wyjątkową trwałością.
W prawdziwym świecie, za każdą wysoko skalowalną, bezstanową usługą kryje się usługa
stanowa, z reguły pełniąca rolę magazynu danych. Na początku istnienia Kubernetesa, gdy
brakowało obsługi rozwiązań stanowych, konieczne było wprowadzenie podziału, polegającego
na umieszczeniu aplikacji bezstanowych w Kubernetesie (w celu uzyskania korzyści natywnej
chmury) przy jednoczesnym utrzymaniu komponentów stanowych poza klastrem — w chmurze
publicznej lub na własnych serwerach (ang. on-premise), zarządzanych za pomocą
tradycyjnych, niechmurowych mechanizmów. Biorąc pod uwagę, że w każdym środowisku
aplikacji biznesowych (korporacyjnych) można wyróżnić szereg mechanizmów stanowych
(zarówno przestarzałych, jak i nowoczesnych), brak obsługi aplikacji stanowych był istotnym
ograniczeniem Kubernetesa, znanego jako uniwersalna, natywna platforma chmurowa.
Jak możemy opisać wymagania aplikacji stanowej? Wdrożenie aplikacji stanowych, takich jak
Apache ZooKeeper, MongoDB, Redis czy MySQL, można by przeprowadzić za pomocą obiektu
Deployment, który w rezultacie utworzyłby obiekt ReplicaSet z wartością atrybutu replicas
równą 1, aby zapewnić ich stabilność. Do tego można by skorzystać z usługi do wykrywania
końcówki, a także obiektów PersistentVolumeClaim i PersistentVolume do utrwalenia stanu
w pamięci trwałej.
Choć taki scenariusz jest z grubsza poprawny w przypadku aplikacji stanowej zawierającej
jedną instancję, problem tkwi w obiekcie ReplicaSet, który nie gwarantuje spełnienia reguły
„co-najwyżej-raz” (ang. at-most-once), przez co liczba replik może się nieznacznie zmieniać.
Taka sytuacja może prowadzić do utraty danych. Oprócz tego mogą pojawić się inne wyzwania.
Aplikacja stanowa złożona z wielu sklastrowanych usług wymaga wieloaspektowych gwarancji
od bazowej infrastruktury. Przeanalizujmy najpopularniejsze trwałe wymagania wstępne dla
aplikacji stanowych.
Pamięć trwała
Formalnie rzecz biorąc, utworzenie rozproszonej aplikacji stanowej wymaga jedynie
zwiększenia parametru replicas w obiekcie ReplicaSet. Jak jednak w takiej sytuacji poradzić
sobie z wymaganiami dotyczącymi pamięci trwałej? Z reguły w takich okolicznościach aplikacje
stanowe (takie jak te wymienione wcześniej) wymagają utworzenia dedykowanej pamięci
trwałej dla każdej instancji. Obiekt ReplicaSet z parametrem replicas=3 i definicją obiektu
PersistentVolumeClaim (PVC) spowodowałby powiązanie wszystkich trzech kapsuł z tym
samym wolumenem PersistentVolume (PV). Choć obiekty ReplicaSet i PVC zapewniają
uruchomienie instancji i powiązanie pamięci trwałej z tymi węzłami, na których instancje są
uruchomione, przestrzeń dyskowa nie jest dedykowana, tylko współdzielona przez wszystkie
instancje.
Sieć
Podobnie jak w przypadku wymagań dotyczących pamięci trwałej, rozproszona aplikacja
stanowa wymaga zdefiniowania stabilnej tożsamości sieciowej. Poza kwestią przechowywania
danych aplikacji w pamięci trwałej, aplikacje stanowe dysponują także szczegółami na temat
konfiguracji, takimi jak nazwa hosta czy szczegóły połączenia do innych pokrewnych hostów.
Oznacza to, że każda instancja powinna być osiągalna za pomocą adresu, który nie zmienia się
dynamicznie, jak w przypadku adresów IP kapsuł w ramach obiektu ReplicaSet. W tym
przypadku znów moglibyśmy rozwiązać ten problem za pomocą obejścia: moglibyśmy utworzyć
po jednej usłudze dla każdego obiektu ReplicaSet i z atrybutem replicas=1. Niestety,
zarządzanie taką konfiguracją nie może być zautomatyzowane, a aplikacja nie może sama z
siebie polegać na nazwie hosta, ponieważ po każdym restarcie ulega ona zmianie. Oprócz tego
aplikacja nie zna nazwy usługi, z której pochodzi dostęp.
Tożsamość
Jak wynika z powyższej analizy, działanie klastrowanych aplikacji stanowych zależy w dużej
mierze od dysponowania trwałą przestrzenią dyskową i tożsamością sieciową. Wynika to z
faktu, że w aplikacjach stanowych każda instancja jest unikatowa i zna swoją tożsamość — a
źródłem tej tożsamości jest trwała pamięć i sieciowe „koordynaty”. Do tej listy możemy dodać
także tożsamość/nazwę instancji (niektóre aplikacje stanowe mogą wymagać unikatowych nazw
trwałych), co w Kubernetesie wyraża się za pomocą nazwy kapsuły. Kapsuła utworzona za
pomocą obiektu ReplicaSet charakteryzuje się losową nazwą, która nie przetrwa restartu
kapsuły.
Uporządkowanie
Poza unikatową i długotrwałą tożsamością, instancje klastrowanych aplikacji stanowych mają
stałe położenie w kolekcji instancji. Porządek instancji ma wpływ na sekwencję, w jakiej
instancje są skalowane w górę i w dół. Można też skorzystać z niego do rozpowszechniania lub
dostępu do danych, a także wewnątrzklastrowego pozycjonowania, związanego np. z
blokadami, singletonami lub węzłami typu master.
Inne wymagania
Stabilne i trwałe pamięć, sieć, tożsamość i uporządkowanie stanowią wspólne wymagania dla
klastrowanych aplikacji stanowych. Zarządzanie aplikacjami stanowymi wiąże się ze
specyficznymi wymaganiami, które mogą pojawiać się w każdym przypadku. Niektóre aplikacje
mogą wymagać istnienia kworum, tj. minimalnej liczby instancji zawsze dostępnych. Inne będą
wrażliwe na uporządkowanie, a jeszcze inne — nie będą miały problemu ze współbieżnymi
Wdrożeniami. Niektóre aplikacje mogą działać z duplikatami instancji, a inne nie. Zaplanowanie
tych wszystkich szczególnych sytuacji i obsługa za pomocą ogólnych mechanizmów są w
praktyce niemożliwe, dlatego Kubernetes pozwala na tworzenie zasobów
CustomResourceDefinition i Operatorów do zarządzania aplikacjami stanowymi. Więcej na ich
temat znajdziesz w rozdziale 23.
Rozwiązanie
Możliwości prymitywu StatefulSet w zakresie zarządzania aplikacjami stanowymi będziemy
czasem przedstawiać na zasadzie porównania do dobrze znanego nam prymitywu ReplicaSet,
za pomocą którego uruchamiamy aplikacje bezstanowe. Można powiedzieć, że obiekt
StatefulSet służy do zarządzania zwierzętami domowymi, a ReplicaSet — do zarządzania
bydłem. To porównanie jest znaną (ale jednocześnie kontrowersyjną) metaforą świata DevOps:
identyczne i wymienialne serwery są określane mianem bydła, podczas gdy unikatowe,
wyjątkowe serwery, do których trzeba odnosić się z troską i indywidualnie, noszą nazwę
zwierząt domowych. Na podobnej zasadzie, obiekt StatefulSet (początkowo nazywany PetSet)
jest używany do zarządzania kapsuł unikatowych, w przeciwieństwie do ReplicaSet, który
stosuje się do zarządzania identycznymi, wymienialnymi kapsułami.
Przeanalizujmy jak działają obiekty StatefulSet i jak rozwiązują one problemy aplikacji
stanowych. Listing 11.1 to usługa generatora liczb pseudolosowych, przedstawiona jako
StatefulSet1.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: rg
spec:
serviceName: random-generator
replicas: 2
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: logs
mountPath: /logs
volumeClaimTemplates:
- metadata:
name: logs
spec:
accessModes: [ “ReadWriteOnce” ]
resources:
requests:
storage: 10Mi
Nazwa prymitywu StatefulSet jest używana jako prefiks dla generowanych nazw węzłów.
Szablon do tworzenia PVC dla każdej kapsuły (podobnie jak w przypadku szablonu kapsuły).
Zamiast analizować definicję z listingu 11.1 wiersz po wierszu, przeanalizujemy ogólne
zachowanie i gwarancje dostarczone przez tę definicję obiektu StatefulSet.
Pamięć trwała
Choć nie jest to zawsze konieczne, większość aplikacji stanowych przechowuje stan w
dedykowanej, powiązanej z daną instancją pamięci trwałej. Jeżeli kapsuła w Kubernetesie
potrzebuje pamięci trwałej, należy skorzystać z obiektów PV i PVC. Aby utworzyć PVC w ten sam
sposób, co kapsuły, StatefulSet korzysta z elementu volumeClaimTemplates. Dodatkowa
własność stanowi jedną z kluczowych różnic pomiędzy obiektami StatefulSet i ReplicaSet,
który zawiera element persistentVolumeClaim.
Zamiast odwoływać się do predefiniowanego PVC, obiekty StatefulSet tworzą PVC korzystając
z elementów volumeClaimTemplates w locie podczas tworzenia kapsuły. Ten mechanizm
pozwala każdej kapsule na uzyskanie dedykowanego PVC podczas początkowego procesu
tworzenia, jak również w przypadku skalowania w górę wynikającego ze zmiany wartości
atrybutu replicas obiektu StatefulSet.
Zwróć uwagę na swego rodzaju brak symetrii: skalowanie w górę obiektu StatefulSet (tj.
zwiększenie wartości parametru replicas) tworzy nowe kapsuły i skojarzone PVC. Z drugiej
strony, skalowanie w dół usuwa kapsuły, ale nie usuwa żadnych PVC (ani PV), co oznacza, że PV
nie mogą być ponownie użyte ani usunięte, a Kubernetes nie może zwolnić pamięci. To
zachowanie nie jest przypadkowe. Wynika ono z założenia, że pamięć trwała aplikacji
stanowych ma znaczenie krytyczne i przypadkowe skalowanie w dół nie powinno powodować
utraty danych. Jeśli jesteśmy pewni, że aplikacje stanowe zostały wyskalowane w dół celowo, a
dane zostały przeniesione do innych instancji, możemy usunąć PVC ręcznie, co pozwoli na
ponowne użycie obiektu PV.
Sieć
Każda kapsuła utworzona przez obiekt StatefulSet ma stabilną tożsamość, wygenerowaną na
podstawie nazwy obiektu StatefulSet i indeksu porządkowego (zaczynając od 0). W
poprzednim przykładzie zostały utworzone dwie kapsuły o nazwach rg-0 i rg-1. Nazwy kapsuł
są generowane w przewidywalny sposób, w formacie, który różni się od mechanizmu
generowania nazw obiektów ReplicaSet, wykorzystującego losowo wybrany sufiks.
Dedykowana, skalowalna przestrzeń trwała stanowi kluczowy aspekt aplikacji stanowych — tak
samo jak sieć.
Na listingu 11.2 definiujemy usługę typu headless. W takiej sytuacji atrybut clusterIP ma
wartość None, co oznacza, że nie chcemy, aby kube-proxy obsługiwało usługę. Nie chcemy też
skorzystać z alokacji klastrów IP ani zrównoważenia obciążenia. Czemu więc usługa jest
potrzebna?
Listing 11.2. Usługa używana w celu uzyskania dostępu do obiektu StatefulSet
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
clusterIP: None
selector:
app: random-generator
ports:
- name: http
port: 8080
Jak widać na rysunku 11.1, obiekty StatefulSet oferują zróżnicowany zbiór funkcji,
gwarantując zachowanie niezbędne w przypadku zarządzania aplikacjami stanowymi w
środowisku rozproszonym. To od Ciebie zależy decyzja dotycząca tego, jak użyć ich w sposób
najbardziej korzystny dla Twojej aplikacji stanowej.
Tożsamość
Tożsamość (ang. identity) to specjalny składnik obiektu StatefulSet, na którym budowane są
wszystkie gwarancje dotyczące działania. Używając nazwy obiektu, możesz uzyskać
przewidywalną nazwę i tożsamość kapsuły. Następnie dzięki tej tożsamości można nazwać
obiekty PVC, korzystać z konkretnych kapsuł za pomocą usług typu headless itd. Możesz
przewidzieć tożsamość kapsuły przed jej utworzeniem i w miarę potrzeby wykorzystać tę
wiedzę w swojej aplikacji.
Uporządkowanie
Rozproszone aplikacji stanowe z definicji składają się z wielu instancji, które są unikalne i
niewymienialne. Pomijając kwestię unikalności, instancje mogą odnosić się do siebie, opierając
się na kolejności kapsuł — w takiej sytuacji pojawia się wymaganie dotyczące uporządkowania
(ang. ordinality).
Z punktu widzenia obiektu StatefulSet, jedynym zastosowaniem, w którym uporządkowanie
ma znaczenie, jest skalowanie. Kapsuły mają nazwy, które zawierają liczbowy sufiks
(poczynając od 0), a proces tworzenia kapsuł określa także kolejność, w której są one
skalowane w górę i w dół (w odwrotnej kolejności, od n–1 do 0).
Aby umożliwić właściwą synchronizację danych podczas skalowania w górę i w dół, obiekt
StatefulSet domyślnie wykonuje sekwencyjnie procedury startu i zamknięcia. Oznacza to, że
kapsuły są uruchamiane począwszy od pierwszej (z indeksem 0) i dopiero po skutecznym
starcie tej kapsuły zostanie wykonana następna (z indeksem 1) itd. Podczas skalowania w dół
kolejność jest odwracana — pierwsza zamykana jest kapsuła z największym indeksem, a po jej
zamknięciu zamykana jest kapsuła o indeksie o jeden mniejszym od pierwszej itd., aż do
kapsuły z indeksem 0.
Inne funkcje
Obiekty StatefulSet mają także inne możliwości, które da się dostosować do wymagań
aplikacji stanowych. Każda aplikacja stanowa jest wyjątkowa i wymaga starannej analizy, gdy
próbuje się ją dopasować do modelu StatefulSet. Przeanalizujmy kilka funkcji Kubernetesa,
które mogą przydać się w trakcie procesu tworzenia aplikacji stanowych.
Podzielone aktualizacje
Dyskusja
W tym rozdziale zapoznaliśmy się ze standardowymi wymaganiami i wyzwaniami w zarządzaniu
rozproszonymi aplikacjami stanowymi na natywnych platformach chmurowych. Odkryliśmy, że
obsługa aplikacji stanowych zawierających jedną instancję jest względnie prosta; w przypadku
rozproszonego stanu sytuacja niestety znacząco się komplikuje. Choć „stan” z reguły kojarzymy
z pamięcią trwałą, zapoznaliśmy się z wieloma aspektami stanowości i różnymi gwarancjami,
które mogą być wymagane przez różne aplikacje stanowe. W tym zakresie można uznać, że
StatefulSet to świetny prymityw, który pozwala na implementowanie rozproszonych aplikacji
stanowych w generyczny sposób. Za jego pomocą rozwiązujemy problem pamięci trwałej, sieci
(za pomocą usług), tożsamości, uporządkowania i kilku innych aspektów. StatefulSet
dostarcza zbiór różnych funkcji do automatycznego zarządzania aplikacjami stanowymi, czyniąc
go kluczowym elementem natywnego świata chmur.
Obiekty StatefulSet stanowią dobry początek i krok naprzód, ale świat aplikacji stanowych
jest w dużej mierze unikatowy i złożony. Pomijając aplikacje stanowe, które zostały
zaprojektowane z myślą o rzeczywistości natywnej chmury (przez co pasują do obiektów
StatefulSet), istnieje ogromna ilość przestarzałych, stanowych aplikacji, które nie były pisane
pod kątem chmur i ich obsługa jest jeszcze trudniejsza. Na szczęście Kubernetes ma odpowiedź
także i na ten problem. Społeczność Kubernetesa zdała sobie sprawę, że zamiast modelowania
różnych rodzajów zadań za pomocą zasobów Kubernetesa i implementacji zachowań przy
użyciu generycznych kontrolerów, należy pozwolić użytkownikom na implementowanie
własnych kontrolerów, co pozwoli na modelowanie zasobów aplikacji za pomocą własnych
definicji zasobów i zachowań. Ten mechanizm nosi nazwę operatorów.
W rozdziale 22. i 23. dowiesz się więcej na temat wzorców Kontroler i Operator, które znacznie
lepiej poradzą sobie z zarządzaniem złożonymi aplikacjami stanowymi w środowiskach
chmurowych.
Więcej informacji
Przykład Usługi Stanowej: http://bit.ly/2Y7SUN2
Podstawy obiektu StatefulSet: http://bit.ly/2r0boiA
Obiekty StatefulSet: http://bit.ly/2HGm6oE
Wdrażanie Cassandry za pomocą obiektów StatefulSet: http://bit.ly/2HBLNXA
Uruchamianie ZooKeepera, koordynatora systemów rozproszonych: http://bit.ly/2JmNPNQ
Usługi typu headless: http://bit.ly/2v7Z19P
Siłowe usuwanie kapsuł z obiektów StatefulSet: http://bit.ly/2OeuRrh
Bezpieczne skalowanie w dół aplikacji stanowych w Kubernetesie: http://bit.ly/2Fk0mgK
Konfiguracja i wdrażanie aplikacji stanowych: http://bit.ly/2UsbkJt
1 Załóżmy, że udało nam się opracować niezwykle wyszukaną metodę generowania liczb
losowych w rozproszonym klastrze, z wieloma instancjami naszej usługi jako węzłami.
Oczywiście nie jest to prawda, ale dla dobra przykładu możemy uczynić takie założenie.
Rozdział 12. Wykrywanie Usług
Wzorzec Wykrywanie Usług dostarcza stabilną końcówkę, za pośrednictwem której klienci
usługi mogą uzyskać dostęp do konkretnych instancji zapewniających usługę. W związku z tym
Kubernetes dostarcza liczne mechanizmy, stosowane w zależności od tego, czy konsumenci i
producenci usługi znajdują się wewnątrz czy na zewnątrz klastra.
Problem
Aplikacje wdrożone w Kubernetesie rzadko funkcjonują samodzielnie. Z reguły dochodzi do
interakcji z innymi usługami wewnątrz klastra lub z systemami poza nim. Do interakcji może
dochodzić w wyniku działań wewnętrznych lub z inicjatywy zewnętrznej. Wewnętrzne
interakcje są wykonywane zazwyczaj za pomocą konsumenta odpytującego: aplikacja łączy się z
innym systemem w momencie startu (lub nieco później), a następnie zaczyna wysyłać i odbierać
dane. Dobry przykład takiego zachowania stanowi aplikacja uruchomiona wewnątrz kapsuły,
która łączy się z serwerem plików i rozpoczyna konsumpcję plików, lub łączy się z brokerem
komunikatów, po czym rozpoczyna pobieranie lub wysyłanie komunikatów. To samo może
dotyczyć połączenia z relacyjną bazą danych lub magazynem typu klucz-wartość w celu
rozpoczęcia procesu odczytu i zapisu danych.
Najważniejszą cechą wspólną tych wszystkich przypadków jest fakt, że aplikacja uruchomiona
w ramach kapsuły w pewnym momencie swojego działania decyduje się nawiązać połączenie z
inną kapsułą lub systemem zewnętrznym, po czym rozpoczyna transfer danych. W takiej
sytuacji aplikacja nie otrzymuje żadnego bodźca z zewnątrz i nie potrzebujemy żadnej
dodatkowej konfiguracji w Kubernetesie.
Często korzystamy z tej techniki implementując wzorce przedstawione w rozdziałach 7. i 8. Co
więcej, długo działające kapsuły w ramach kontrolerów DaemonSet i ReplicaSet czasami
aktywnie łączą się z innymi systemami przez sieć. Najbardziej popularnym przypadkiem w
Kubernetesie są długo działające procesy, które oczekują na otrzymanie sygnału z zewnątrz,
najczęściej w postaci połączeń HTTP przychodzących z innych kapsuł z klastra lub zupełnie z
zewnątrz. W takiej sytuacji konsumenci usługi muszą w jakiś sposób wykryć kapsuły
rozmieszczane dynamicznie przez planistę, podlegające niekiedy skalowaniu w górę i w dół.
Samodzielne śledzenie, rejestrowanie i wykrywanie końcówek dynamicznych kapsuł byłoby
niewątpliwie dużym wyzwaniem. W związku z tym Kubernetes implementuje wzorzec
Wykrywanie Usług za pomocą różnych mechanizmów, które omawiamy w tym rozdziale.
Rozwiązanie
Jeśli przeanalizujemy rozwiązania tego problemu z czasów „przed Kubernetesem”,
najpopularniejszym z nich było wykrywanie realizowane po stronie klienta. W takim modelu,
gdy konsument usługi musi wywołać inną, wyskalowaną na wiele instancji usługę, konieczne
jest skorzystanie ze specjalnego agenta wykrywania, odpowiedzialnego za przeszukanie
rejestru instancji usług i wybór jednej z nich. Zazwyczaj odbywa się to za pośrednictwem
agenta wbudowanego w konsumenta usługi (np. klienta ZooKeepera, klienta Consula czy
Ribbona) lub za pomocą innego, kolokowanego procesu — takiego jak Prana — poszukującego
usługi w rejestrze (rysunek 12.1).
W świecie Kubernetesa wszystko to dzieje się za kulisami, dzięki czemu konsument usługi
wywołuje jej wirtualną końcówkę, która dynamicznie wykrywa instancje usług implementowane
przez kapsuły. Rysunek 12.2 przedstawia sposób obsługi rejestracji wyszukiwania przez
Kubernetesa.
Na pierwszy rzut oka Wykrywanie Usług może wydawać się prostym w obsłudze wzorcem. W
celu jego implementacji można skorzystać z wielu mechanizmów, w zależności od tego, czy
konsument i dostawca usługi znajdują się wewnątrz czy na zewnątrz klastra.
To wyzwanie rozwiązuje zasób usługi Kubernetesa. Usługa dostarcza stały i stabilny punkt
wejścia dla kolekcji kapsuł oferujących te same funkcje. Najprostszym sposobem na utworzenie
usługi jest użycie polecenia kubectl expose, które tworzy usługę dla pojedynczej kapsuły lub
wielu kapsuł na podstawie obiektu Deployment lub ReplicaSet. Polecenie tworzy wirtualny
adres IP, określany jako clusterIP, i pobiera selektory kapsuły wraz z numerami portów z
zasobu, aby utworzyć definicję usługi. Aby uzyskać pełną kontrolę nad tworzoną definicją,
należy utworzyć usługę ręcznie (listing 12.1).
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
protocol: TCP
W tym momencie musimy pamiętać, że po utworzeniu usługi otrzymuje ona clusterIP, który
jest dostępny tylko z wewnątrz klastra Kubernetesa (stąd nazwa). Adres IP pozostanie
niezmieniony, o ile istnieje definicja usługi. W jaki sposób możemy przekazać tę informację do
innych aplikacji wewnątrz klastra? Mamy do wyboru jedną z dwóch metod:
RANDOM_GENERATOR_SERVICE_HOST=10.109.72.32
RANDOM_GENERATOR_SERVICE_PORT=8080
Wiele portów
Pokrewieństwo sesji
W momencie pojawienia się nowego żądania, sesja losowo wybiera kapsułę, która zajmie
się jego obsługą. To zachowanie można zmienić ustawiając atrybut sessionAffinity na
wartość ClientIP, co sprawi, że wszystkie żądania z tego samego IP klienta trafią do tej
samej kapsuły. Pamiętaj, że usługi Kubernetesa równoważą obciążenie na poziomie
czwartej warstwy modelu ISO/OSI (transportowej), przez co nie są w stanie przeanalizować
pakietów sieciowych i realizować pokrewieństwa sesji np. na podstawie ciastek HTTP.
Sondy gotowości
Wirtualny adres IP
Kiedy usługa utworzona jest z atrybutem type: ClusterIP, otrzymuje ona stabilny,
wirtualny adres IP. Ten adres nie ma jednak powiązania z żadnym interfejsem sieciowym i
— w praktyce — nie istnieje. To kube-proxy, uruchomione na wszystkich węzłach,
odpowiada za wykrycie nowej usługi i dodanie do iptables w węźle reguł, które
przechwycą pakiety przeznaczone do danego wirtualnego adresu IP i podmienią go na
prawdziwy adres IP kapsuły. Reguły w iptables nie dodają reguł ICMP, a jedynie reguły
protokołu określonego w definicji usługi (TCP lub UDP). W związku z tym nie będziesz w
stanie odpytać adresu IP usługi za pomocą polecenia ping, ponieważ korzysta ono z
protokołu ICMP. Naturalnie jest możliwy dostęp do adresu IP usługi za pomocą protokołu
TCP (np. poprzez wykonanie żądania HTTP).
Podczas tworzenia usługi możemy określić adres IP, który zostanie użyty w polu
.spec.clusterIP. Musi to być prawidłowy adres IP z określonego zakresu. Choć nie jest to
zalecane, dzięki tej opcji możemy poradzić sobie z przestarzałymi aplikacjami, które
korzystają z określonego adresu IP, lub gdy chcemy użyć ponownie istniejącego wpisu DNS.
Usługi Kubernetesa z atrybutem type: ClusterIP są dostępne tylko z wnętrza klastra. Są one
używane do wykrywania kapsuł przez dopasowanie selektorów i jednocześnie są najczęściej
stosowanym rodzajem usług. Teraz możemy przeanalizować pozostałe rodzaje usług, które
pozwalają na wykrywanie końcówek zdefiniowanych ręcznie.
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ClusterIP
ports:
- protocol: TCP
port: 80
Następnie, za pomocą kodu z listingu 12.4, definiujemy zasób końcówek z tą samą nazwą, jaką
ma usługa (zasób ten zawiera docelowe adresy IP i porty).
kind: Endpoints
metadata:
name: external-service
subsets:
- addresses:
- ip: 1.1.1.1
- ip: 2.2.2.2
ports:
- port: 8080
Nazwa musi być identyczna z tą, jaką ma usługa, która uzyskuje dostęp do tych końcówek.
Ta usługa jest dostępna tylko wewnątrz klastra i może być konsumowana w ten sam sposób co
poprzednie, za pomocą zmiennych środowiskowych lub wyszukiwania wpisów DNS. Różnica
polega na tym, że lista końcówek jest zarządzana ręcznie, a wartości w niej umieszczone
wskazują z reguły na adresy IP spoza klastra (rysunek 12.4).
Choć połączenie z zasobem zewnętrznym stanowi najczęstszy przypadek użycia, nie jest on
jedynym. Końcówki mogą przechowywać adresy IP kapsuł, ale nie wirtualne adresy IP innych
usług. Zaletą usług jest możliwość dodawania i usuwania selektorów, a także wskazywania na
zewnętrznych lub wewnętrznych dostawców bez potrzeby usuwania definicji zasobu, co
prowadziłoby do zmiany adresu IP usługi. W związku z tym konsumenci usług mogą korzystać z
tego samego adresu IP usługi, na który wskazywali na początku, mimo że faktyczna
implementacja dostawcy usługi jest przenoszona ze środowiska on-premise na Kubernetesa bez
wpływu na klienta.
metadata:
name: database-service
spec:
type: ExternalName
externalName: my.database.example.com
ports:
- port: 80
Ta definicja usługi również nie ma definicji selektora, ale za to jej typ ma wartość
ExternalName. Z punktu widzenia implementacji jest to istotna różnica. Ta definicja usługi
wskazuje na treści określone w atrybucie externalName jedynie za pomocą usługi DNS. W ten
sposób tworzymy alias dla zewnętrznej końcówki za pomocą rekordu DNS CNAME zamiast
przechodzić przez proxy przy użyciu adresu IP. Koniec końców, jest to kolejna metoda
dostarczenia abstrakcji Kubernetesa dla końcówek zlokalizowanych poza klastrem.
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: NodePort
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
nodePort: 30036
protocol: TCP
Skoro ta metoda otwiera port we wszystkich węzłach, rozsądne wydaje się skonfigurowanie
dodatkowych reguł zapory sieciowej, aby umożliwić zewnętrznym klientom dostęp do
portów węzła.
Wybór węzła
Zewnętrzny klient może otworzyć połączenie do dowolnego węzła w klastrze. Jeśli jednak
węzeł nie jest dostępny, to do aplikacji klienckiej należy obowiązek połączenia się z innym
zdrowym węzłem. W związku z tym dobrym pomysłem jest umieszczenie równoważnika
obciążenia przed węzłami, aby to jego zadaniem był wybór zdrowych węzłów.
Wybór kapsuły
Gdy klient nawiązuje połączenie przy użyciu portu węzła, jest ono kierowane do losowo
wybranej kapsuły — może ono trafić do tego samego węzła, na którym połączenie zostało
otwarte, lub do innego. Można uniknąć tego dodatkowego przeskoku i wymusić na
Kubernetesie wybór węzła, na którym połączenie zostało otwarte, przez dodanie atrybutu
externalTrafficPolicy: Local do definicji usługi. Po ustawieniu tej opcji Kubernetes nie
pozwoli połączyć się z kapsułami zlokalizowanymi na innych węzłach, co czasami może być
problemem. Aby rozwiązać ten problem, musisz upewnić się, że kapsuły są rozmieszczone
na wszystkich węzłach (np. za pomocą Usług Demona) lub upewniając się, że klient wie, na
których węzłach zostały rozmieszczone zdrowe kapsuły.
Rysunek 12.5. Wykrywanie usług typu NodePort
Adresy źródłowe
Z adresami źródłowymi pakietów wysyłanych do różnych typów usług wiążą się pewne
osobliwości. Gdy korzystamy z typu NodePort, źródłowe adresy klientów w pakietach
sieciowych, zawierające adresy IP klientów, są zamieniane na adresy wewnętrzne węzła (za
pomocą mechanizmu NAT — Network Address Translation, translacja adresów sieciowych).
Gdy aplikacja kliencka wysyła pakiet do węzła nr 1, adres źródłowy jest podmieniany na
adres węzła, a adres docelowy — na adres kapsuły. Następnie pakiet jest przekierowywany
do węzła nr 2, na którym znajduje się kapsuła. Gdy kapsuła otrzymuje pakiet sieciowy,
adres źródłowy nie zawiera oryginalnego adresu klienta — zamiast tego znajdziemy w nim
adres węzła nr 1. Aby uniknąć tego zachowania, możemy ustawić atrybut
externalTrafficPolicy na wartość Local, przekierowując ruch tylko do kapsuł
zlokalizowanych na węźle nr 1.
Innym sposobem obsługi procesu wykrywania usług dla zewnętrznych klientów jest użycie
równoważnika obciążenia (ang. load balancer). Widzieliśmy już, jak usługa typu NodePort
funkcjonuje na bazie zwykłej usługi (typu ClusterIP), przez otwarcie portu w każdym węźle.
Ograniczeniem tego podejścia jest konieczność zastosowania mechanizmu równoważenia
obciążenia dla aplikacji klienckich w celu wyboru zdrowego węzła. Usługa typu LoadBalancer
rozwiązuje ten problem.
Poza utworzeniem zwykłej usługi i otwarciem portu na wszystkich węzłach typu NodePort,
udostępniamy usługę na zewnątrz, za pomocą mechanizmu równoważenia obciążenia
dostarczonego przez dostawcę chmury. Rysunek 12.6 przedstawia ten schemat: zamknięty
(dostarczony przez dostawcę chmury) load balancer funkcjonuje jako brama do klastra
Kubernetesa.
Rysunek 12.6. Wykrywanie usług z równoważeniem obciążenia
Ten rodzaj usługi działa tylko jeżeli dostawca chmury obsługuje Kubernetesa i dostarcza
rozwiązanie do równoważenia obciążenia.
W kodzie z listingu 12.7 tworzymy usługę równoważącą obciążenie, nadając jej typ
LoadBalancer. Kubernetes następnie doda adresy IP do pól .spec i .status.
kind: Service
metadata:
name: random-generator
spec:
type: LoadBalancer
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
protocol: TCP
status:
loadBalancer:
ingress:
- ip: 146.148.47.155
Kubernetes przypisuje adresy clusterIP i loadBalancerIP, gdy staną się one dostępne.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: random-generator
spec:
backend:
serviceName: random-generator
servicePort: 8080
kind: Ingress
metadata:
name: random-generator
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: random-generator
servicePort: 8080
- path: /cluster-status
backend:
serviceName: cluster-status
servicePort: 80
Trasy OpenShift
OpenShift firmy Red Hat to popularna, korporacyjna dystrybucja Kubernetesa. Poza pełną
zgodnością ze standardowym Kubernetesem, OpenShift dostarcza dodatkowe funkcje.
Jedną z nich jest mechanizm tras (ang. Routes), który bardzo przypomina w działaniu
Ingressa. Oba mechanizmy są tak podobne, że w praktyce trudno zauważyć różnice.
Przede wszystkim mechanizm Routes powstał przed wdrożeniem obiektu Ingress w
Kubernetesie, dlatego można go określać mianem przodka Ingressa.
Mimo licznych podobieństw, można jednak odnotować wiele różnic pomiędzy obydwoma
rodzajami obiektów:
Trasa jest wykrywana automatycznie przez zintegrowany z OpenShiftem równoważnik
obciążenia HAProxy, dlatego nie ma konieczności instalowania dodatkowego kontrolera
Ingress. Mimo to możesz podmienić zbudowaną wersję także w równoważniku
obciążenia w OpenShift.
Możesz skorzystać z dodatkowych trybów terminacji TLS, takich jak ponowne
szyfrowanie (ang. re-encryption) czy przejście (ang. pass-through) do usługi.
Można skorzystać z wielu ważonych komponentów serwerowych do dzielenia ruchu.
Obsługiwane są domeny wieloznaczne (typu wildcard).
Nic nie stoi na przeszkodzie, aby w OpenShift również korzystać z Ingressa. Wszystko jest
kwestią wyboru.
Dyskusja
W tym rozdziale omówiliśmy ulubione mechanizmy wykrywania usług w Kubernetesie.
Wykrywanie dynamicznych kapsuł z poziomu klastra realizujemy za pomocą zasobu usługi, choć
różne opcje w tym zakresie mogą poprowadzić nas do różnych implementacji. Abstrakcja usługi
jest wysokopoziomowym, natywnym, typowym dla środowiska chmurowego sposobem na
konfigurację szczegółów niskopoziomowych, takich jak wirtualne adresy IP, iptables, rekordy
DNS czy zmienne środowiskowe. Wykrywanie Usług spoza klastra funkcjonuje na podstawie
abstrakcji usługi, koncentrując się na udostępnianiu usług na zewnątrz klastra. Mimo że usługa
typu NodePort dostarcza podstawowe mechanizmy w zakresie udostępniania usług, stworzenie
wysoce dostępnej konfiguracji wymaga integracji z dostawcą infrastruktury.
Tabela 12.1 podsumowuje różne omówione w tym rozdziale sposoby implementacji Wykrywania
Usług w Kubernetesie, począwszy od prostszych, aż do tych bardziej skomplikowanych. Mamy
nadzieję, że dzięki niej łatwiej będzie Ci je zrozumieć.
Tabela 12.1. Mechanizmy wykrywania usług
Rodzaj
Nazwa Konfiguracja Podsumowanie
klienta
type: ClusterIP
Ręczny IP Wewnętrzny Wykrywanie zewnętrznego IP
kind: Endpoints
type: ClusterIP
Usługa typu Wykrywanie na podstawie usługi
.spec.clusterIP: Wewnętrzny
headless DNS bez wirtualnego adresu IP
None
Więcej informacji
Przykład Wykrywania Usług: http://bit.ly/2TeXzcr
Usługi Kubernetesa: http://bit.ly/2q7AbUD
Usługa DNS dla usług i kapsuł: http://bit.ly/2Y5jUwL
Usługi pomocne w debugowaniu: http://bit.ly/2r0igMX
Stosowanie źródłowego IP: https://kubernetes.io/docs/tutorials/services/
Utwórz zewnętrzny mechanizm typu load balancer: http://bit.ly/2Gs05Wh
Porównanie — NodePort kontra LoadBalancer kontra Ingress: http://bit.ly/2GrVio2
Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/
Ingress w Kubernetesie kontra Route w OpenShift: https://red.ht/2JDDflo
Rozdział 13. Samoświadomość
Niektóre aplikacje muszą być samoświadome, tj. mieć dostęp do informacji na swój temat.
Wzorzec Samoświadomość opisuje Downward API Kubernetesa, które dostarcza prosty
mechanizm do introspekcji i wstrzykiwania metadanych do aplikacji.
Problem
W większości przypadków natywne aplikacje chmurowe są bezstanowe i możliwe do
błyskawicznego usunięcia, ponieważ nie mają one tożsamości istotnej z punktu widzenia innych
aplikacji. Czasami nawet tego rodzaju aplikacje muszą mieć dostęp do informacji na swój temat,
a także dotyczących środowiska, w którym funkcjonują. Do tych informacji zaliczamy te znane
tylko w czasie wykonania, takie jak nazwa kapsuły, jej adres IP czy nazwa hosta, na którym
znajduje się aplikacja. Z drugiej strony mamy do czynienia z licznymi informacjami statycznymi,
zdefiniowanymi na poziomie kapsuły, takimi jak żądania i limity zasobu, a także z informacjami
dynamicznymi, takie jak adnotacje i etykiety, możliwe do zmiany przez użytkownika w czasie
działania aplikacji.
Na przykład, w zależności od zasobów udostępnionych kontenerowi, konieczne może okazać się
dostrojenie rozmiaru puli wątków lub zmiana algorytmu odśmiecania (ang. garbage collection)
czy alokacji pamięci. Możesz chcieć skorzystać z nazwy kapsuły i nazwy hosta w trakcie
rejestrowania informacji diagnostycznych lub wysłać wskaźniki do serwera centralnego.
Możesz chcieć wykrywać kapsuły w tej samej przestrzeni nazw z określoną etykietą i złączyć je
w klastrowanej aplikacji. We wszystkich tych przypadkach warto skorzystać z Downward API
Kubernetesa.
Rozwiązanie
Wymagania, które opisaliśmy, i rozwiązanie, które przedstawimy, nie są ograniczone jedynie do
kontenerów — można zastosować je we wszelkich dynamicznych środowiskach, w których
dochodzi do zmiany metadanych zasobów. AWS udostępnia usługi metadanych instancji i
danych użytkownika, które można odpytywać z dowolnej instancji EC2, aby uzyskać metadane
na temat samej instancji. Na podobnej zasadzie AWS ECS udostępnia API, które kontenery
mogą odpytywać, uzyskując informacje nt. klastra kontenerów.
Podejście oparte na Kubernetesie jest nawet bardziej eleganckie i łatwe w użyciu. Downward
API pozwala na przekazywanie metadanych dotyczących kapsuły do kontenerów i klastra za
pomocą zmiennych środowiskowych i plików. Są to te same mechanizmy, z których
korzystaliśmy do przekazywania danych związanych z aplikacją z obiektów ConfigMap i Secret.
W tym przypadku jednak dane nie są tworzone przez nas. Zamiast tego określamy klucze, które
nas interesują, a Kubernetes wypełnia wartości dynamicznie. Rysunek 13.1 przedstawia ogólny
proces wstrzykiwania informacji o zasobach i ustawienia czasu wykonania za pomocą
Downward API do zainteresowanych kapsuł.
Rysunek 13.1. Mechanizmy introspekcji aplikacji
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: MEMORY_LIMIT
valueFrom:
resourceFieldRef:
container: random-generator
resource: limits.memory
Zmienna środowiskowa POD_IP jest pobierana z właściwości kapsuły i staje się dostępna w
momencie startu kapsuły.
Nazwa Opis
Nazwa Opis
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
volumeMounts:
- name: pod-info
mountPath: /pod-info
volumes:
- name: pod-info
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: annotations
fieldRef:
fieldPath: metadata.annotations
Dzięki wolumenom, jeśli metadane ulegną zmianie w trakcie działania kapsuły, zmiana zostanie
odnotowana w plikach wolumenów. Trzeba jednak pamiętać, że to aplikacja korzystająca z tych
informacji musi wykrywać zmiany w plikach i odczytywać zaktualizowane dane we właściwy
sposób. Jeśli nie zaimplementujesz takiego mechanizmu w aplikacji, restart kapsuły może
okazać się jedyną opcją.
Dyskusja
W wielu przypadkach może się okazać, że aplikacja musi być w stanie pozyskiwać informacje na
temat siebie i otaczającego środowiska w trakcie swojego działania. Kubernetes dostarcza
nieinwazyjne mechanizmy do introspekcji i wstrzykiwania zależności. Jedną z wad Downward
API jest stała, ograniczona liczba kluczy, do których można się odnosić. Jeśli aplikacja
potrzebuje więcej danych, w szczególności dotyczących pozostałych zasobów lub metadanych
związanych z klastrem, konieczne będzie odpytanie serwera API.
Ta technika jest stosowana przez wiele aplikacji, które odpytują serwer API w celu wykrycia
dodatkowych kapsuł w tej samej przestrzeni nazw, charakteryzujących się określonymi
etykietami lub adnotacjami. Następnie aplikacja może utworzyć klaster z odkrytych kapsuł i na
przykład zsynchronizować stan. Można także skorzystać z tego mechanizmu w aplikacjach
monitorujących, w których użyjemy go do wykrywania kluczowych kapsuł, a następnie
instrumentowania ich.
Więcej informacji
Przykład Samoświadomości: http://bit.ly/2TYBXpc
Udostępnianie kontenerom informacji nt. kapsuły za pomocą plików: http://bit.ly/2CoZyFy
Udostępnianie kontenerom informacji nt. kapsuły za pomocą zmiennych środowiskowych:
http://bit.ly/2JpuHPe
Introspekcja agentowa kontenera Amazon ECS: https://amzn.to/2JnVXgX
Metadane instancji i dane użytkownika: http://amzn.to/1Cu0fTl
Część III. Wzorce strukturalne
Obrazy kontenerów i kontenery przypominają klasy i obiekty znane ze świata programowania
obiektowego. Obrazy kontenerów stanowią wzorce — szablony, na podstawie których tworzone
są obiekty. Kontenery nie działają jednak w oderwaniu od świata — są uruchamiane w innych
abstrakcjach, takich jak kapsuły, które dostarczają unikatowe możliwości w czasie wykonania
aplikacji.
Wzorce z tej kategorii są skoncentrowane na nadaniu struktury i zorganizowaniu kontenerów w
kapsule, aby spełnić różne przypadki użycia. Mechanizmy, które mają wpływ na kontenery w
kapsułach, skutkują powstawaniem wzorców, opisanych w następujących rozdziałach:
Rozdział 14., „Kontener inicjalizacji”, wprowadza odrębny cykl życia dla zadań
związanych z inicjalizacją i głównymi kontenerami aplikacji.
Rozdział 15., „Przyczepka”, opisuje jak rozszerzyć i wzbogacić funkcje istniejącego
kontenera bez jego zmiany.
Rozdział 16., „Adapter”, przekształca heterogeniczny system i czyni go zgodnym ze
spójnym, jednolitym interfejsem, który może być konsumowany z zewnątrz systemu.
Rozdział 17., „Ambasador”, opisuje proxy, które ogranicza powiązania z zewnętrznymi
usługami.
Rozdział 14. Kontener
Inicjalizacji
Kontenery Inicjalizacji pozwalają na separację zagadnień, dostarczając odrębny cykl życia dla
zadań związanych z inicjalizacją, niezależnych od głównych kontenerów aplikacji. W tym
rozdziale przyjrzymy się blisko kluczowemu konceptowi Kubernetesa, który jest używany w
wielu innych wzorcach wymagających zastosowania logiki inicjalizacji.
Problem
Inicjalizacja stanowi niezwykle istotne zagadnienie w wielu językach programowania. W
niektórych językach jest ono obsługiwane na poziomie samego języka, a w innych stosuje się
pewne ściśle określone konwencje nazewnicze, aby oznaczyć konstrukcję jako inicjalizator. W
języku Java, aby utworzyć obiekt, który wymaga pewnej konfiguracji, musimy skorzystać z
konstruktora (lub bloków inicjalizacji w bardziej wymyślnych przypadkach). Wykonanie kodu
konstruktorów jest pierwszą czynnością podczas tworzenia obiektu. Masz też gwarancję, że taki
konstruktor będzie uruchamiany tylko raz przez środowisko uruchomieniowe (to tylko przykład
— nie będziemy teraz szczegółowo omawiać wszystkich języków programowania). Możemy też
skorzystać z konstruktora, aby zweryfikować wymagania konieczne do spełnienia, np. obecność
obowiązkowych parametrów. Korzystamy także z konstruktorów, aby zainicjalizować pola
instancji, korzystając z przekazanych argumentów lub wartości domyślnych.
Kontenery Inicjalizacji działają podobnie, ale na poziomie kapsuły, a nie klasy. Jeśli w Twojej
kapsule masz co najmniej jeden kontener, który tworzy Twoją aplikację, przed rozpoczęciem
pracy może być konieczne jego skonfigurowanie. Do koniecznych w tym celu operacji mogą
należeć: ustawienie uprawnień do systemu plików, konfiguracja schematu bazy danych,
określenie danych początkowych aplikacji. Logika inicjalizacji może wymagać skorzystania z
narzędzi i bibliotek, których nie ma w obrazie aplikacji. Ze względu na bezpieczeństwo obraz
aplikacji może nie mieć uprawnień do wykonywania czynności inicjalizacyjnych. Może też
zaistnieć konieczność opóźnienia startu aplikacji, dopóki zależność zewnętrzna nie zostanie
spełniona. We wszystkich przypadkach Kubernetes korzysta z kontenerów inicjalizacji do
realizacji tego wzorca, co pozwala na oddzielenie czynności inicjalizacyjnych od głównych
obowiązków aplikacji.
Rozwiązanie
Kontenery Inicjalizacji w Kubernetesie stanowią element definicji kapsuły, dzieląc kontenery w
kapsule na dwie grupy: kontenery inicjalizacji i kontenery aplikacji. Wszystkie kontenery
inicjalizacji są wykonywane sekwencyjnie, jeden po drugim, wszystkie też muszą zakończyć się
pomyślnie przed uruchomieniem kontenerów aplikacji. W tym sensie kontenery inicjalizacji
można porównać do konstruktorów z Javy, które wspierają inicjalizację obiektów. Z drugiej
strony, kontenery aplikacji są wykonywane równolegle, a kolejność uruchomienia jest dowolna.
Przepływ wykonania jest przedstawiony na rysunku 14.1.
Rysunek 14.1. Kontenery inicjalizacji i aplikacji w kapsule
Kontenery inicjalizacji są z reguły niewielkie, uruchamiają się szybko, a ich wykonanie trwa
krótko, z wyjątkiem sytuacji, gdy kontener inicjalizacji służy do opóźnienia startu kapsuły w
oczekiwaniu na zależność — wtedy jego działanie może nie zostać zakończone dopóty, dopóki
zależność nie zostanie spełniona. Jeśli kontener inicjalizacji zawiedzie, cała kapsuła jest
restartowana (chyba że jest oznaczona etykietą RestartNever), co powoduje ponowne
uruchomienie wszystkich kontenerów inicjalizacji. W związku z tym, aby uniknąć efektów
ubocznych, kontenery inicjalizacji powinny być z założenia idempotentne.
W związku z tym, jeśli Twoje kontenery inicjalizacji mają wysokie wymagania dotyczące
zasobów, a Twoje kontenery aplikacji — niskie, wartości limitów i żądań na poziomie kapsuły,
które mają wpływ na planowanie, zostaną wybrane na podstawie kontenerów inicjalizacji. Taka
sytuacja nie jest korzystna. Nawet jeśli kontenery inicjalizacji są uruchamiane przez krótki czas
i pojemność węzła przez większość czasu jest duża, żadna inna kapsuła nie będzie mogła z niej
skorzystać.
Listing 14.1 przedstawia kontener inicjalizacji, który kopiuje dane do pustego wolumenu.
Listing 14.1. Kontener inicjalizacji
apiVersion: v1
kind: Pod
metadata:
name: www
labels:
app: www
spec:
initContainers:
- name: download
image: axeclbr/git
command:
- git
- clone
- https://github.com/mdn/beginner-html-site-scripted
- /var/lib/data
volumeMounts:
- mountPath: /var/lib/data
name: source
containers:
- name: run
image: docker.io/centos/httpd
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/www/html
name: source
volumes:
- emptyDir: {}
name: source
command:
- /bin/sh
- “-c”
- “sleep 3600”
Podobny efekt można osiągnąć korzystając z Przyczepki, opisanej w rozdziale 15., w której
kontener serwera HTTP i kontener Gita są uruchomione równolegle, jako kontenery aplikacji.
W przypadku Przyczepki nie można stwierdzić, który z kontenerów zostanie uruchomiony jako
pierwszy — Przyczepka jest używana, gdy kontenery uruchomione równolegle działają w
sposób ciągły (np. jak w kodzie z listingu 15.1, w którym kontener synchronizacji Gita stale
aktualizuje katalog lokalny). Moglibyśmy skorzystać zarówno z wzorca Przyczepka, jak i wzorca
Kontener Inicjalizacji, jeśli konieczne jest zagwarantowanie procesu inicjalizacji, a także stałej
aktualizacji danych.
Dyskusja
Podsumowując: czemu dzielimy kontenery w kapsule na dwie grupy? Czemu w razie potrzeby
nie możemy po prostu skorzystać z kontenera aplikacji do celów inicjalizacji? Odpowiedź jest
prosta — obie grupy kontenerów mają inne cykle życia, cele, a nawet są tworzone przez różne
osoby.
Kontenery inicjalizacji są uruchamiane przed kontenerami aplikacji i — co ważniejsze — są
wykonywane etapami, jeden po drugim, dzięki czemu możesz mieć pewność, że kolejny krok
zostanie wykonany dopiero po skutecznym wykonaniu poprzednich. Kontenery aplikacji są z
kolei uruchamiane równolegle i nie dają takich samych gwarancji co kontenery inicjalizacji.
Pamiętając o tej różnicy, możemy tworzyć kontenery skoncentrowane na zadaniach
inicjalizacyjnych lub aplikacyjnych, umieszczając je w tych samych kapsułach.
Obiekty PodPreset
Obiekty PodPreset są ewaluowane przez kontroler dopuszczający, który pomaga
wstrzykiwać do kapsuł pola określone w obiekcie PodPreset w czasie ich tworzenia. Pola
mogą zawierać wolumeny, podmontowania wolumenów lub zmienne środowiskowe. W
związku z tym obiekty PodPreset wstrzykują dodatkowe zależności czasu wykonania do
kapsuły w czasie jej tworzenia, korzystając z selektorów etykiet w celu określenia
pasujących kapsuł. Obiekty PodPreset pozwalają autorom szablonów kapsuł na
automatyzację dodawania powtarzalnych informacji, które muszą znaleźć się w wielu
kapsułach.
Więcej informacji
Przykład Kontenera Inicjalizacji: http://bit.ly/2TW7ckN
Kontenery Inicjalizacji: http://bit.ly/2TR7OsD
Konfiguracja inicjalizacji kapsuły: http://bit.ly/2TWMEbL
Wzorzec inicjalizatora w JavaScripcie: http://bit.ly/2TYF14G
Inicjalizacja obiektów w Swifcie: https://apple.co/2FdSLPN
Stosowanie kontrolerów dopuszczających: http://bit.ly/2ztKrJM
Dynamiczna kontrola dopuszczenia: http://bit.ly/2DwR2Y3
Jak działają inicjalizatory w Kubernetesie: http://bit.ly/2TeYz0k
Presety kapsuł: https://kubernetes.io/docs/concepts/workloads/pods/podpreset/
Wstrzykiwanie informacji do kapsuł za pomocą obiektu PodPreset: http://bit.ly/2Fh7QzV
Poradnik dotyczący inicjalizatorów w Kubernetesie: http://bit.ly/2FfEu4W
Rozdział 15. Przyczepka
Przyczepka rozszerza możliwości istniejącego kontenera bez zmiany jego zawartości. Ten
wzorzec stanowi jeden z najważniejszych wzorców kontenerowych, pozwalając na współpracę
kontenerów skoncentrowanych na wykonaniu jednego zadania. W tym rozdziale omówimy
koncept Przyczepki. Wyspecjalizowane wzorce powiązane — Adapter i Ambasador — są
omawiane odpowiednio w rozdziałach 16. i 17.
Problem
Kontenery stanowią popularne narzędzie do pakowania aplikacji, pozwalające programistom i
administratorom budować, dostarczać i uruchamiać aplikacje w jednolity sposób. Kontener
reprezentuje naturalne granice dla pojedynczej jednostki funkcjonalnej, oferując własne
środowisko uruchomieniowe, cykl wydawniczy, API i zespół do niego przypisany. Dobrze
zdefiniowany kontener zachowuje się jak pojedynczy proces systemu Unix — rozwiązuje jeden
problem i robi to dobrze, będąc tworzonym z myślą o wymienialności i ponownym użyciu.
Ostatnia z wymienionych cech jest niezwykle istotna, ponieważ pozwala na szybsze budowanie
aplikacji, z wykorzystaniem istniejących, wyspecjalizowanych kontenerów.
Obecnie, jeśli chcemy wykonać wywołanie protokołu HTTP, nie musimy pisać biblioteki
klienckiej — wystarczy skorzystać z już istniejącej. Na tej samej zasadzie, tworząc stronę
internetową, nie musimy utworzyć kontenera dla serwera webowego — możemy skorzystać z
istniejącego. To podejście pozwala programistom uniknąć wynajdywania koła na nowo. Dzięki
temu możliwe jest tworzenie ekosystemu z mniejszą liczbą lepszej jakości kontenerów do
zarządzania. Nawet jeśli dysponujemy reużywalnym, skoncentrowanym na jednym zadaniu
kontenerem, czasami musimy rozszerzyć jego funkcje, udostępniając metody współpracy
pomiędzy kontenerami. Do tego celu służy wzorzec Przyczepka — pozwala on na rozszerzenie
funkcji jednego kontenera przez dodanie doń innego istniejącego już kontenera.
Rozwiązanie
W rozdziale 1. opisaliśmy, jak kapsuła pozwala na połączenie wielu kontenerów w pojedynczą,
niepodzielną jednostkę. W czasie wykonania aplikacji kapsuła także stanowi kontener — jej
działanie zaczyna się jako proces wstrzymany (dosłownie, za pomocą polecenia pause), zanim
wszystkie inne kontenery w kapsule zostaną uruchomione. Kapsuła nie robi nic poza
utrzymaniem wszystkich przestrzeni nazw, z których kontenery aplikacji korzystają do
interakcji w czasie życia kapsuły. Poza tym szczegółem implementacyjnym, zdecydowanie
bardziej interesujące jest to, co dostarcza abstrakcja kapsuły.
Kapsuła jest tak ważnym, fundamentalnym pojęciem, że można znaleźć ją na wielu natywnych
platformach chmurowych pod różnymi nazwami, jednak zawsze z podobnymi możliwościami.
Kapsuła jako jednostka wdrożenia wprowadza pewne ograniczenia czasu wykonania w
kontenerach, które do niej należą. Na przykład wszystkie kontenery są wdrażane na tym samym
węźle i współdzielą ten sam cykl życia kapsuły. Ponadto kapsuła pozwala swoim kontenerom na
współdzielenie wolumenów i komunikację za pomocą sieci lokalnej lub mechanizmu
komunikacji międzyprocesowej (IPC). To właśnie z tych powodów użytkownicy umieszczają
kontenery w kapsule. Przyczepka (nazywana też czasami Pomocnikiem) jest używana w
przypadku, gdy kontener umieszczony w kapsule rozszerza i wzbogaca zachowanie innego
kontenera.
apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
containers:
- name: app
image: docker.io/centos/httpd
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/www/html
name: git
- name: poll
image: axeclbr/git
volumeMounts:
- mountPath: /var/lib/data
name: git
env:
- name: GIT_REPO
value: https://github.com/mdn/beginner-html-site-scripted
command:
- “sh”
- “-c”
workingDir: /var/lib/data
volumes:
- emptyDir: {}
name: git
W tym przykładzie pokazujemy, jak synchronizator Gita wzbogaca działanie serwera HTTP,
przekazując do niego treści i utrzymując je w aktualnym stanie. Możemy powiedzieć, że oba
kontenery współpracują i są równie ważne, jednak we wzorcu Przyczepka z reguły wyróżniamy
kontener główny i pomocniczy, który wzbogaca główne zachowanie. Kontener główny jest
zazwyczaj umieszczany pierwszy na liście i jest to jednocześnie kontener domyślny (gdy
uruchamiamy polecenie kubectl exec).
Dyskusja
Poprzednio powiedzieliśmy, że obrazy kontenerów są jak klasy, a kontenery jak obiekty w
świecie programowania obiektowego. Kontynuując to porównanie można powiedzieć, że
rozszerzenie kontenera w celu wzbogacenia jego aspektów funkcjonalnych jest podobne do
dziedziczenia w programowaniu obiektowym, a współpraca wielu kontenerów w kapsule
przypomina złożenie (kompozycję) w OOP. Choć oba podejścia pozwalają na ponowne użycie
kodu, dziedziczenie wiąże się ze ściślejszym powiązaniem kontenerów, ponieważ reprezentuje
ono związek „jest” (ang. is-a) pomiędzy kontenerami.
Z drugiej strony, złożenie w kapsule reprezentuje relację „ma” (ang. has-a) i jest ono bardziej
elastyczne, ponieważ nie wiąże ze sobą kontenerów w czasie budowania, dając możliwość
późniejszej podmiany kontenerów w definicji kapsuły. Złożenia wiążą się też z tym, że
dysponujesz wieloma uruchomionymi kontenerami (procesami), sprawdzaniem kondycji,
restartowaniem i zużyciem większej ilości zasobów niż w przypadku pojedynczego kontenera
aplikacji. Nowoczesne kontenery Przyczepek są małe i zużywają mało zasobów, jednak to Ty
musisz zdecydować, czy warto uruchomić odrębny proces, czy też lepiej połączyć go z głównym
kontenerem.
Patrząc na to zagadnienie z jeszcze innego punktu widzenia, można pokusić się o stwierdzenie,
że złożenie kontenerów jest podobne do programowania aspektowego — dodając kolejne
kontenery, wprowadzamy ortogonalne, niezwiązane, nowe możliwości do kapsuły bez
modyfikacji głównego kontenera. W ostatnim czasie zastosowanie wzorca Przyczepka staje się
coraz bardziej popularne, zwłaszcza w przypadku obsługi sieci, monitorowania i śledzenia
różnych aspektów usług, gdzie każda usługa dostarcza ze sobą kontenery Przyczepek.
Więcej informacji
Przykład przyczepki: http://bit.ly/2FqSZUV
Wzorce projektowe dla rozproszonych systemów opartych na kontenerach:
http://bit.ly/2Odan24
Prana — Przyczepka dla Twoich Netfliksowych aplikacji i usług typu PaaS (ang. Platform-
as-a-Service): http://bit.ly/2Y9PRnS
Telefon z puszki — wzorce dodawania autoryzacji i szyfrowania w przestarzałych
aplikacjach: https://www.feval.ca/posts/tincan-phone/
Wszechmocny kontener pauzy: http://bit.ly/2FOYH21
Rozdział 16. Adapter
Wzorzec Adapter potrafi dostosować heterogeniczny, konteneryzowany system, aby zapewnić
jego zgodność ze spójnym, jednolitym interfejsem o ustandaryzowanym i znormalizowanym
formacie, z którego mogą korzystać zewnętrzni klienci. Wzorzec Adapter ma te same cechy, co
wzorzec Przyczepka, jednak ma jeden dodatkowy cel — dać przekształcony dostęp do aplikacji.
Problem
Kontenery pozwalają na zapakowanie i uruchamianie aplikacji napisanych przy użyciu różnych
bibliotek i języków w jednolity sposób. Obecnie różne zespoły często współpracują ze sobą,
korzystając z różnych technologii w celu stworzenia systemów rozproszonych, zbudowanych z
heterogenicznych komponentów. Ta heterogeniczność może spowodować trudności w sytuacji,
gdy wszystkie komponenty muszą być traktowane w jednolity sposób przez inne systemy.
Wzorzec Adapter rozwiązuje ten problem, pozwalając na ukrycie złożoności systemu i
udostępnienie go w jednolity sposób.
Rozwiązanie
Najlepszym sposobem na zilustrowanie tego wzorca jest skorzystanie z przykładu. Gdy chcemy
skutecznie uruchamiać i obsługiwać systemy rozproszone, musimy dostarczyć mechanizmy
monitorowania i ostrzegania o problemach. Co więcej, jeśli dysponujemy systemem
rozproszonym złożonym z wielu usług, które chcemy monitorować, możemy skorzystać z
zewnętrznego narzędzia monitorującego w celu cyklicznego pobierania i rejestrowania
wskaźników z rozmaitych usług.
Usługi opracowane w różnych językach mogą nie dysponować tymi samymi możliwościami i nie
udostępniać wskaźników w tym samym formacie, oczekiwanym przez narzędzie monitorujące.
Ta różnorodność tworzy dla nas wyzwanie, spowodowane koniecznością monitorowania
heterogenicznej aplikacji przez narzędzie, dostosowane do otrzymywania jednolitego widoku
całego systemu. Korzystając z wzorca Adapter, możemy dostarczyć jednolity interfejs
monitorujący, który eksportuje wskaźniki z różnych kontenerów aplikacji do jednego,
standardowego formatu i protokołu. Pokazany na rysunku 16.1 kontener adaptera przekształca
lokalne wskaźniki do zewnętrznego formatu, zrozumiałego dla serwera monitorującego.
Rysunek 16.1. Wzorzec Adapter
Korzystając z tego podejścia, każda usługa reprezentowana przez kapsułę — poza głównym
kontenerem aplikacji — dysponuje innym kontenerem, który potrafi odczytać wskaźniki
aplikacji i udostępnić je w generycznym formacie, zrozumiałym przez narzędzie monitorujące.
Możemy skorzystać z jednego adaptera, który potrafi wyeksportować wskaźniki z języka Java za
pomocą protokołu HTTP, a z innego (w innej kapsule) do udostępnienia wskaźników Pythona,
również za pomocą protokołu HTTP. Dla narzędzia monitorującego, wszystkie wskaźniki będą
dostępne za pomocą protokołu HTTP, we wspólnym, znormalizowanym formacie.
Przechodząc do właściwego przykładu, powróćmy do naszej przykładowej aplikacji —
generatora liczb losowych — i utwórzmy adapter pokazany na rysunku 16.1. Jeżeli będzie on
prawidłowo skonfigurowany, otrzymamy plik logów z losowo wygenerowanymi liczbami, a także
z dokładnym momentem, w którym doszło do wygenerowania losowej liczby. Chcemy
odnotować ten czas za pomocą Prometheusa. Niestety, nasz format logów nie pasuje do formatu
tego narzędzia. Musimy także udostępnić tę wartość za pomocą końcówki HTTP, dzięki czemu
serwer Prometheusa będzie w stanie ją pobrać.
apiVersion: apps/v1
kind: Deployment
metadata:
name: random-generator
spec:
replicas: 1
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: LOG_FILE
value: /logs/random.log
ports:
- containerPort: 8080
protocol: TCP
volumeMounts:
- mountPath: /logs
name: log-volume
# --------------------------------------------
- image: k8spatterns/random-generator-exporter
name: prometheus-adapter
env:
- name: LOG_FILE
value: /logs/random.log
ports:
- containerPort: 9889
protocol: TCP
volumeMounts:
- mountPath: /logs
name: log-volume
volumes:
- name: log-volume
emptyDir: {}
Główny kontener aplikacji z usługą generatora liczb losowych, uruchomioną na porcie 8080.
Ścieżka do pliku logów, zawierającego informacje czasowe nt. generowanych liczb losowych.
Kolejnym zastosowaniem tego wzorca jest rejestrowanie zdarzeń. Różne kontenery mogą
rejestrować informacje w różnych formatach i o różnym poziomie szczegółów. Adapter potrafi
znormalizować te informacje, oczyścić je, wzbogacić informacjami kontekstowymi dzięki
wzorcowi Samoświadomość (por. rozdział 13.), a następnie udostępnić do pobrania przez
centralny agregator logów.
Dyskusja
Adapter stanowi wyspecjalizowaną wersję wzorca Przyczepka, omówionego w rozdziale 15.
Funkcjonuje on jako odwrotne proxy do heterogenicznego systemu, ukrywając jego złożoność
za pomocą jednolitego interfejsu. Zastosowanie odrębnej nazwy dla tego wzorca pozwala na
precyzyjne zdefiniowanie celu, jaki za jego pomocą osiągamy.
Problem
Usługi konteneryzowane nie działają w izolacji — często korzystają z innych usług, do których
dostęp nie zawsze jest w pełni stabilny i pewny. Trudność w dostępie do innych usług może
wynikać z dynamicznych i zmieniających się adresów, konieczności równoważenia obciążenia
instancji klastrowanej usługi, niestabilnego protokołu lub problematycznych formatów danych.
W idealnym świecie kontenery mają jeden, ściśle określony cel i mogą być używane
wielokrotnie w różnych kontekstach. Jeśli jednak nasz kontener dostarcza pewien zakres
funkcjonalny i korzysta z zewnętrznej usługi w specyficzny sposób, będzie on miał do
wykonania dwa istotne obowiązki.
Konsumpcja usługi zewnętrznej może wymagać zastosowania specjalnej biblioteki do
wykrywania usług, której nie chcemy umieszczać w naszym kontenerze. Możemy też chcieć
podmienić różne rodzaje usług, korzystając z różnych rodzajów bibliotek i metod do
wykrywania usług. Ta technika tworzenia abstrakcji i izolowania logiki związanej z dostępem do
innych usług znajdujących się na zewnątrz naszego systemu stanowi cel istnienia wzorca
Ambasador.
Rozwiązanie
Aby zademonstrować ten wzorzec, skorzystamy z pamięci podręcznej w naszej aplikacji.
Skonfigurowanie dostępu do lokalnej pamięci podręcznej w środowisku deweloperskim może
być proste, jednak w środowisku produkcyjnym możemy być zmuszeni do skorzystania z
konfiguracji klienta, który potrafi połączyć się z różnymi fragmentami (ang. shards) pamięci
podręcznej. Kolejnym przykładem jest użycie usługi realizowane przez wyszukanie jej w
rejestrze i wykonanie wykrywania usług po stronie klienta. Trzeci przykład polega na użyciu
usługi za pośrednictwem niestabilnego protokołu, takiego jak HTTP — w związku z tym,
ochrona naszej aplikacji wiąże się z użyciem logiki wyłącznika obwodu (ang. circuit breaker),
konfiguracji limitu czasu, wykonywania ponownych prób połączenia itd.
We wszystkich tych przypadkach możemy skorzystać z kontenera Ambasadora, który ukrywa
skomplikowane aspekty związane z dostępem do zewnętrznych usług i dostarcza uproszczony
widok i dostęp do głównego kontenera aplikacji za pośrednictwem hosta lokalnego (ang.
localhost). Rysunki 17.1 i 17.2 przedstawiają sposób rozluźnienia dostępu do magazynu klucz-
wartość dzięki podłączeniu kontenera ambasadora, nasłuchującego na porcie lokalnym. Na
rysunku 17.1 widzimy, jak oddelegować dostęp do danych do w pełni rozproszonego magazynu
zdalnego, takiego jak Etcd.
Rysunek 17.1. Ambasador, który pozwala na dostęp do zdalnej, rozproszonej pamięci podręcznej
Dla celów tworzenia aplikacji, ten kontener Ambasadora można zamienić na działający lokalnie
w pamięci magazyn klucz-wartość, taki jak memcached (rysunek 17.2).
Rysunek 17.2. Ambasador używany w celu uzyskania dostępu do lokalnej pamięci podręcznej
Korzyści z zastosowania tego wzorca są podobne do tych płynących z użycia wzorca Przyczepka
— oba pozwalają na utrzymanie kontenerów skoncentrowanych na realizacji jednego zadania, a
także zapewniają reużywalność. Realizując taki wzorzec, kontener aplikacji może
skoncentrować się na logice biznesowej, a szczegóły użycia usługi zewnętrznej delegować do
innego, wyspecjalizowanego kontenera. W ten sposób tworzymy wyspecjalizowane i reużywalne
kontenery Ambasadora, które można połączyć z innymi kontenerami aplikacji.
Listing 17.1 przedstawia Ambasadora, który jest uruchomiony równolegle do usługi REST.
Przed zwróceniem odpowiedzi, usługa REST dodaje log z wygenerowanymi danymi, wysyłając
go na ściśle określony adres URL: http://localhost:9009. Proces Ambasadora nasłuchuje na tym
porcie i przetwarza dane. Na tym listingu wyświetlamy dane w konsoli, jednak moglibyśmy
zrobić coś bardziej skomplikowanego — np. przekierować dane do w pełni funkcjonalnej
infrastruktury do rejestrowania zdarzeń. Co do usługi REST, nie ma dla niej znaczenia, co dzieje
się z logami — naszego obecnego ambasadora można zastąpić niezwykle łatwo, konfigurując
ponownie kapsułę, bez potrzeby modyfikowania kontenera głównego.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: main
env:
- name: LOG_URL
value: http://localhost:9009
ports:
- containerPort: 8080
protocol: TCP
- image: k8spatterns/random-generator-log-ambassador
name: ambassador
Główny kontener aplikacji, który dostarcza usługę REST do generowania liczb losowych.
Dyskusja
Prowadząc dyskusję na ogólnym poziomie szczegółowości, można stwierdzić, że Ambasador to
po prostu — z grubsza — inna forma wzorca Przyczepki. Główna różnica pomiędzy obydwoma
polega na tym, że wzorzec Ambasador nie rozszerza głównej aplikacji o dodatkowe możliwości.
Funkcjonuje on jedynie jako inteligentne proxy do świata zewnętrznego i stąd pochodzi jego
nazwa (można się również spotkać z nazwą wzorca Proxy — Pośrednik).
Więcej informacji
Przykład ambasadora: http://bit.ly/2FpjBFS
Jak korzystać z wzorca Ambasador do dynamicznej konfiguracji usług:
https://do.co/2HxGIQG
Dynamiczne łącza Dockera z użyciem ambasadora: http://bit.ly/2TQ1uBO
Łącz się za pomocą kontenera ambasadora: https://dockr.ly/2UdTGKc
Modyfikacje do wzorca Ambasador CoreOS: http://bit.ly/2Ju4zmb
Część IV. Wzorce konfiguracyjne
Każda aplikacja musi być w jakiś sposób skonfigurowana — najprościej jest to osiągnąć,
umieszczając konfigurację w kodzie źródłowym aplikacji. To podejście generuje, niestety, spore
skutki uboczne, ponieważ kod i jego konfiguracja żyją ze sobą na dobre i na złe, co zostało
opisane w artykule na temat niemodyfikowalnego serwera (http://bit.ly/2CoH5cj). Niezwykle
istotna jest dla nas możliwość modyfikowania konfiguracji bez konieczności tworzenia od nowa
obrazu aplikacji. Ciągłe tworzenie obrazów byłoby niezwykle czasochłonne, dlatego takie
zachowanie jest traktowane jako antywzorzec w kontekście ciągłego dostarczania aplikacji —
aplikacja powinna być tworzona raz, a następnie powinna przechodzić przez kolejne etapy
wdrożenia, aż do osiągnięcia środowiska produkcyjnego, bez zmian.
Jak zatem możemy dostosować aplikację pod względem konfiguracji do różnych środowisk,
takich jak deweloperskie, integracyjne czy produkcyjne? Odpowiedź tkwi w użyciu
zewnętrznych danych konfiguracji, które mogą być różne dla różnych środowisk. Wzorce
przedstawione w kolejnych rozdziałach dotyczą dostosowywania i adaptowania aplikacji za
pomocą zewnętrznych konfiguracji w różnych środowiskach:
Rozdział 18., „Konfiguracja EnvVar”, zawiera opis zmiennych środowisk pod kątem
przechowywania w nich danych konfiguracyjnych.
Rozdział 19., „Zasoby konfiguracji”, przedstawia zasoby Kubernetesa, takie jak obiekty
ConfigMap czy Secret, używane do przechowywania informacji nt. konfiguracji.
Rozdział 20., „Konfiguracja niezmienna”, porusza kwestię niezmienności
(niemutowalności) dużych zbiorów konfiguracji, uzyskiwanej przez umieszczenie ich w
kontenerach, wiązanych z aplikacją w czasie wykonania.
Rozdział 21., „Szablon konfiguracji”, przydaje się, gdy trzeba zarządzać dużymi plikami
konfiguracyjnymi w zróżnicowanych środowiskach, które różnią się od siebie tylko
nieznacznie.
Rozdział 18. Konfiguracja
EnvVar
We wzorcu Konfiguracja EnvVar przyglądamy się najprostszej metodzie konfigurowania
aplikacji. W przypadku niedużych zbiorów wartości konfiguracyjnych, najprościej jest umieścić
je w powszechnie obsługiwanych zmiennych środowiskowych. Przeanalizujemy różne sposoby
deklarowania zmiennych środowiskowych w Kubernetesie; omówimy też ograniczenia
stosowania zmiennych w złożonych konfiguracjach.
Problem
Każda nietrywialna aplikacja wymaga pewnej liczby ustawień konfiguracyjnych, związanych z
dostępem do danych, zewnętrznymi usługami lub dostrajaniem działania w środowisku
produkcyjnym. Ta wiedza była znana na długo przed opublikowaniem manifestu Aplikacji
dwunastu czynników (ang. The Twelve-Factor App), zgodnie z którym konfiguracja nie powinna
być zapisywana na sztywno w kodzie aplikacji. Zamiast tego, wszelkie ustawienia należy
umieszczać na zewnątrz aplikacji, dzięki czemu można je modyfikować nawet po jej
zbudowaniu. W ten sposób tworzymy jeszcze lepsze aplikacje konteneryzowane, promując ideę
niezmiennych artefaktów aplikacji. Jak najlepiej można to osiągnąć w świecie
konteneryzowanym?
Rozwiązanie
Manifest Aplikacji dwunastu czynników zaleca stosowanie zmiennych środowiskowych do
przechowywania konfiguracji aplikacji. To podejście jest proste i sprawdza się w dowolnym
środowisku i na dowolnej platformie. Każdy system operacyjny ma możliwość definiowania
zmiennych środowiskowych i ich propagacji do aplikacji, a każdy język programowania pozwala
na łatwy dostęp do tych zmiennych. Można więc z pewnością stwierdzić, że zmienne
środowiskowe stanowią narzędzie uniwersalne. Typowy sposób użycia tych zmiennych polega
na ustawieniu na sztywno wartości domyślnych w czasie kompilacji, które mogą być
przesłonięte w czasie wykonania aplikacji. Przeanalizujmy konkretne przykłady zastosowania
zmiennych środowiskowych w Dockerze i Kubernetesie.
FROM openjdk:11
ENV PATTERN “Konfiguracja EnvVar”
Dzięki takiej konfiguracji aplikacja Javy uruchomiona w kontenerze może z łatwością uzyskać
dostęp do zmiennych środowiskowych, korzystając z wywołań biblioteki standardowej Javy
(listing 18.2).
-e LOG_FILE=”/tmp/random.log” \
-e SEED=”147110834325” \
k8spatterns/random-generator:1.0
W Kubernetesie tego rodzaju zmienne środowiskowe można ustawić bezpośrednio w
specyfikacji kapsuły kontrolera, np. obiektu Deployment czy ReplicaSet (listing 18.4).
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: LOG_FILE
value: /tmp/random.log
- name: PATTERN
valueFrom:
configMapKeyRef:
name: random-generator-config
key: pattern
- name: SEED
valueFrom:
secretKeyRef:
name: random-generator-secret
key: seed
EnvVar z obiektu Secret (procedura pobierania wartości jest taka sama, jak w obiekcie
ConfigMap).
W szablonie kapsuły możemy nie tylko wiązać wartości ze zmiennymi środowiskowymi (takimi
jak LOG_FILE), ale także skorzystać z delegacji do obiektów Secret Kubernetesa (np. do
ochrony wrażliwych danych) i ConfigMap (dla jawnych ustawień konfiguracji). Zaletą
pośredniego zastosowania obiektów ConfigMap i Secret jest fakt, że zmienne środowiskowe
mogą być zarządzane niezależnie od definicji kapsuły. Obiekty Secret i ConfigMap mają swoje
wady i zalety, które omawiamy szczegółowo w rozdziale 19.
Krótko o wartościach domyślnych
Domyślne wartości upraszczają nasze życie, ponieważ nie musimy własnoręcznie
wybierać wartości każdego parametru konfiguracji, którego istnienia możemy nawet nie
być świadomi. Odgrywają one istotną rolę w paradygmacie „konwencja ponad
konfiguracją”. Jednak użycie wartości domyślnych nie zawsze jest dobrym pomysłem.
Czasami mogą one stać się antywzorcem dla rozwijającej się aplikacji.
Wynika to z faktu, że zmiana wartości domyślnych po jakimś czasie staje się trudnym
zadaniem. Po pierwsze, zmiana wartości domyślnej oznacza zmianę jej wartości w
kodzie, co wymaga ponownego przebudowania aplikacji. Po drugie, ludzie korzystający z
wartości domyślnych (w wyniku stosowania konwencji lub nieświadomie) zawsze będą
zaskoczeni, gdy ulegnie ona zmianie. Każda zmiana musi być precyzyjnie
zakomunikowana, a użytkownicy takiej aplikacji będą musieli wprowadzić modyfikacje
także w swoim kodzie.
Zmiana wartości domyślnych często ma sens, ponieważ trudno jest ustalić właściwą
wartość domyślną od samego początku. Niezwykle istotne jest, by być świadomym, że
zmiana wartości domyślnej jest istotną zmianą i tak właśnie należy ją traktować. Jeśli
stosujemy wersjonowanie semantyczne, taka modyfikacja powinna podnieść główny
numer wersji (ang. major version). Jeśli domyślna wartość nie jest właściwa, lepiej
usunąć ją w całości i generować błąd w przypadku braku określenia jej w konfiguracji. W
ten sposób aplikacja wygeneruje błąd, ale zdecydowanie wcześniej, dzięki czemu
unikniemy problemów powstających jakiś czas po wdrożeniu.
Biorąc pod uwagę wszystkie kwestie, najlepszym rozwiązaniem jest unikanie wartości
domyślnych, jeśli nie mamy pewności (na minimum 90%), że rozsądnie dobrana wartość
domyślna nie ulegnie zmianie. Hasła lub parametry połączeń do bazy danych stanowią
dobre przykłady parametrów, które nie powinny mieć wartości domyślnych, ponieważ
zależą one w dużej mierze od środowiska i nie mogą być stabilnie przewidywalne. Jeśli
nie chcesz korzystać z wartości domyślnych, informacje nt. konfiguracji muszą być
dostarczone jawnie, co samo w sobie stanowi formę dokumentacji.
Użyta w poprzednim przykładzie zmienna SEED pochodzi z zasobu Secret. Choć taki przykład
użycia jest jak najbardziej poprawny, należy pamiętać także o tym, że zmienne środowiskowe
nie są bezpieczne. Umieszczanie wrażliwych informacji w czytelnym formacie w zmiennych
środowiskowych sprawia, że można uzyskać do nich łatwy dostęp, a nawet mogą one pojawić
się w logach.
Dyskusja
Zmienne środowiskowe są łatwe w użyciu i każdy je zna. Można z nich łatwo korzystać w
obrębie kontenerów, ponadto obsługuje je każda platforma. Z drugiej strony, zmienne te nie są
bezpieczne i można z nich wygodnie korzystać tylko w przypadku niewielkiej liczby wartości.
Gdy liczba parametrów zaczyna rosnąć, zarządzanie zmiennymi środowiskowymi staje się
niepraktyczne.
Kolejnym problemem zmiennych środowiskowych jest to, że ustawienie ich wartości jest
możliwe tylko przed startem aplikacji — nie możemy ich zmienić później. Z jednej strony, brak
możliwości zmiany konfiguracji „na gorąco”, w czasie działania aplikacji, stanowi duży problem.
Z drugiej strony, wiele osób traktuje ten problem jako zaletę, ponieważ promuje to
niezmienność aplikacji, także w odniesieniu do konfiguracji. Niezmienność oznacza, że
kontener z problematyczną konfiguracją zostaje usunięty, a nowa kopia, z dobrą konfiguracją,
jest uruchamiana od zera, z reguły z właściwie przygotowaną strategią wdrażania, np. w
postaci ciągłej aktualizacji. Dzięki temu masz pewność, że stan konfiguracji jest właściwie
zdefiniowany i dobrze znany.
Więcej informacji
Przykład Konfiguracji EnvVar: http://bit.ly/2YcUtJC
Aplikacja dwunastu czynników: https://12factor.net/config
Niezmienny serwer: https://martinfowler.com/bliki/ImmutableServer.html
Zastosowanie zbiorów wartości konfiguracyjnych na przykładzie profilów Spring Boot:
http://bit.ly/2YcSKUE
Rozdział 19. Zasób Konfiguracji
Kubernetes dostarcza natywne zasoby konfiguracji w celu obsługi zarówno zwykłych, jak i
poufnych danych. Dzięki temu jesteśmy w stanie zmniejszyć powiązania pomiędzy cyklem życia
konfiguracji i cyklem życia aplikacji. Wzorzec Zasób Konfiguracji objaśnia zasady działania
zasobów ConfigMap i Secret, pokazuje, w jaki sposób możemy z nich skorzystać, a także jakie
są ich ograniczenia.
Problem
Niezwykle istotną wadą wzorca Konfiguracja EnvVar jest fakt, że stanowi on dobre rozwiązanie
tylko w przypadku posiadania niewielkiego zbioru zmiennych, co wiąże się ze stosunkowo
prostą konfiguracją. Ponadto zmienne środowiskowe mogą być definiowane w różnych
miejscach, dlatego czasami trudno jest znaleźć definicję szukanej zmiennej. Nawet jeśli ją
znajdziesz, nie możesz być w pełni pewien, czy nie zostanie ona przesłonięta w innym miejscu.
Na przykład zmienne środowiskowe zdefiniowane w obrazie Dockera mogą być podmienione w
czasie wykonania w ramach zasobu Deployment.
Często znacznie lepiej jest przechowywać dane konfiguracyjne w jednym miejscu, zamiast
umieszczać je w różnych plikach z definicjami. Z drugiej strony, nie ma sensu umieszczać
zawartości całego pliku konfiguracyjnego w zmiennej środowiskowej. Zwiększenie stopnia
pośredniości (dodatkowych warstw konfiguracji) pozwoliłoby na większą elastyczność, co
zapewnia właśnie wzorzec Zasoby Konfiguracji w Kubernetesie.
Rozwiązanie
Kubernetes dostarcza dedykowane zasoby konfiguracji, które są znacznie bardziej elastyczne
niż zwykłe zmienne środowiskowe. Są to obiekty ConfigMap i Secret, które służą do
przechowywania — odpowiednio — danych zwykłych i wrażliwych (poufnych).
Z obu mechanizmów możemy korzystać w ten sam sposób, ponieważ oba dostarczają pamięć i
metody zarządzania parami klucz-wartość. Zasady działania obiektu ConfigMap możemy
zastosować w większości przypadków do obiektów Secret. Poza zastosowanym kodowaniem
danych (w przypadku obiektów Secret jest to Base64), nie ma żadnej technicznej różnicy
pomiędzy obiektami ConfigMap i Secret.
Po utworzeniu obiektu ConfigMap i przypisaniu do niego danych, możemy wykorzystać klucze
na dwa sposoby:
Plik zamontowany w wolumenie ConfigMap jest aktualizowany, gdy obiekt ConfigMap jest
aktualizowany za pomocą API Kubernetesa. Jeśli więc aplikacja obsługuje przeładowanie na
żywo (ang. hot reload) plików konfiguracyjnych, możemy od razu skorzystać z takiej
aktualizacji. W przypadku, gdy wpisy ConfigMap są używane jako zmienne środowiskowe,
aktualizacje nie są odnotowywane, ponieważ zmienne środowiskowe nie mogą ulec zmianie po
uruchomieniu procesu.
kind: ConfigMap
metadata:
name: random-generator-config
data:
server.port=7070
EXTRA_OPTIONS: “high-secure,native”
SEED: “432576345”
Obiekty ConfigMap mogą być używane jako zmienne środowiskowe i jako montowany plik.
Zalecamy stosowanie kluczy pisanych wielkimi literami w obiektach ConfigMap, aby wskazać
zastosowanie zmiennych środowiskowych i właściwych nazw plików, gdy są one używane jako
pliki podmontowane.
W tym przykładzie widzimy, że obiekt ConfigMap może zawierać kompletne pliki konfiguracji,
takie jak application.properties z technologii Spring Boot w tym przykładzie. Możesz sobie
wyobrazić, że w bardziej skomplikowanym kodzie ta sekcja mogłaby być całkiem duża.
Zamiast ręcznie tworzyć pełny deskryptor pliku, możemy skorzystać z narzędzia kubectl, aby
utworzyć także obiekty ConfigMap lub Secret. Listing 19.2 zawiera polecenie kubectl
równoważne instrukcjom wykonywanym w poprzednim przykładzie:
Listing 19.2. Tworzenie obiektu ConfigMap z pliku
--from-literal=JAVA_OPTIONS=-Djava.security.egd=file:/dev/urandom \
--from-file=application.properties
Ten obiekt ConfigMap można odczytać w różnych miejscach — wszędzie tam, gdzie są
definiowane zmienne środowiskowe (jak na listingu 19.3).
Listing 19.3. Zmienne środowiskowe ustawiane w obiekcie ConfigMap
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- env:
- name: PATTERN
valueFrom:
configMapKeyRef:
name: random-generator-config
key: PATTERN
….
Jeśli obiekt ConfigMap ma wiele wpisów, z których chcesz korzystać jak ze zmiennych
środowiskowych, zastosowanie określonej składni zaoszczędzi Ci sporo pisania. Zamiast
definiować każdy wpis osobno (co jest przedstawione w poprzednim przykładzie w sekcji env:),
możesz zastosować sekcję envFrom:, pozwalającą na udostępnienie wszystkich wpisów z
obiektu ConfigMap mających klucz, który może być użyty jako prawidłowa zmienna
środowiskowa. Możemy poprzedzić ją prefiksem, jak na listingu 19.4.
Listing 19.4. Zmienna środowiskowa ustawiona w obiekcie ConfigMap
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
envFrom:
- configMapRef:
name: random-generator-config
prefix: CONFIG_
Wybierz wszystkie klucze z wpisu random-generator-config, które mogą być użyte jako
nazwy zmiennych środowiskowych.
Obiekty Secret, podobnie jak w przypadku obiektów ConfigMap, mogą być używane jako
zmienne środowiskowe, zarówno pojedynczo, jak i wszystkie naraz. Aby uzyskać dostęp do
obiektu Secret zamiast ConfigMap, zamień klucz configMapKeyRef na secretKeyRef.
Gdy korzystamy z obiektu jak z wolumenu, pełny obiekt ConfigMap jest odwzorowywany w
wolumenie, przy czym klucze stają się nazwami plików (listing 19.5).
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
volumeMounts:
- name: config-volume
mountPath: /config
volumes:
- name: config-volume
configMap:
name: random-generator-config
Wolumen oparty na obiekcie ConfigMap zawiera tyle plików, ile jest wpisów w obiekcie.
Klucze stają się nazwami plików, a wartości — ich zawartością.
Konfiguracja z listingu 19.1, podmontowana jako wolumen, spowoduje powstanie dwóch plików
w katalogu /config. Będą to application.properties z treścią zdefiniowaną w obiekcie ConfigMap
i PATTERN, zawierający jeden wiersz.
Kolejną zaletą stosowania obiektów ConfigMap i Secret jest fakt, że są to integralne funkcje
platformy. Nie trzeba korzystać z dodatkowych konstrukcji, co jest konieczne w rozdziale 20.
Mimo swoich zalet, Zasoby Konfiguracji mają swoje ograniczenia: maksymalny rozmiar
obiektów Secret wynosi 1 MB, dlatego nie możemy przechowywać w nich dowolnie dużych
danych. Nie są one dobrze dopasowane do przechowywania danych aplikacji niebędących
danymi konfiguracyjnymi. W obiektach Secret możemy przechowywać dane binarne, jednak z
uwagi na narzut kodowania Base64, zmniejszamy maksymalny rozmiar danych do 700 kB.
Więcej informacji
Przykład Zasobu Konfiguracji: http://bit.ly/2YeGymi
Dokumentacja obiektu ConfigMap: http://bit.ly/2Cs59uQ
Dokumentacja obiektów Secret: https://kubernetes.io/docs/concepts/configuration/secret/
Szyfrowanie danych poufnych: http://bit.ly/2ORsavt
Rozpowszechniaj dane logowania bezpiecznie za pomocą obiektów Secret:
http://bit.ly/2FfcvCn
Wolumeny gitRepo: http://bit.ly/2HxuGqO
Ograniczenia rozmiaru obiektu ConfigMap: http://bit.ly/2UkHRRy
Rozdział 20. Niezmienna
Konfiguracja
Wzorzec Niezmienna Konfiguracja opakowuje dane konfiguracyjne do postaci niezmiennego
obrazu kontenera i wiąże kontener konfiguracji z aplikacją w czasie wykonania. Dzięki temu
wzorcowi jesteśmy w stanie nie tylko skorzystać z niezmiennej i wersjonowanej formy danych
konfiguracyjnych, ale także poradzić sobie z ograniczeniem rozmiaru danych konfiguracyjnych
przechowywanych w zmiennych środowiskowych i obiektach ConfigMap.
Problem
Jak widzieliśmy w rozdziale 18., zmienne środowiskowe pozwalają na łatwą konfigurację
aplikacji kontenerowych. Choć są one proste w użyciu i można z nich korzystać wszędzie,
stosowanie ich w większej liczbie jest niezwykle trudne.
Rozwiązanie
Aby rozwiązać wyżej wymienione problemy, możemy umieścić wszystkie dane konfiguracyjne
związane ze środowiskiem w pojedynczy, pasywny obraz danych, który będziemy
rozpowszechniać jako zwykły obraz kontenera. W czasie wykonania, aplikacja i obraz danych
zostaną powiązane, dzięki czemu aplikacja będzie w stanie pobrać konfigurację z obrazu
danych. Dzięki takiemu podejściu możemy przygotować różne obrazy danych konfiguracyjnych
dla różnych środowisk. Obrazy te łączą różne dane konfiguracji dla różnych środowisk i mogą
być wersjonowane, tak jak inne obrazy kontenerów.
Tworzenie obrazu danych jest trywialne, ponieważ jest to zwykły obraz kontenera, który
zawiera jedynie dane. Wyzwaniem jest powiązanie go z aplikacją podczas jej startu. Możemy
skorzystać z różnych podejść, w zależności od używanej platformy.
Wolumeny Dockera
Zanim przyjrzymy się Kubernetesowi, cofnijmy się o krok i przeanalizujmy zwykły kontener
Dockera. W Dockerze możemy udostępnić wolumen z danymi z poziomu kontenera. Dzięki
dyrektywie VOLUME zawartej w pliku Dockerfile możemy określić katalog, który zostanie
udostępniony. Podczas startu aplikacji, zawartość tego katalogu z kontenera zostanie
skopiowana do katalogu współdzielonego. Jak widać na rysunku 20.1, wolumen powiązany
stanowi świetną metodę współdzielenia danych konfiguracyjnych pomiędzy dedykowanym
kontenerem konfiguracji oraz innym kontenerem aplikacji.
FROM scratch
VOLUME /config
Teraz utworzymy obraz i kontener Dockera, za pomocą pokazanego na listingu 20.2 polecenia
narzędzia Docker CLI.
Obraz aplikacji oczekuje, że pliki konfiguracji zostaną umieszczone w katalogu /config, czyli
wolumenie udostępnionym przez kontener konfiguracji. Przemieszczenie aplikacji ze
środowiska deweloperskiego do produkcyjnego będzie wymagać jedynie zmiany polecenia
uruchamiającego. Nie trzeba zmieniać samego obrazu aplikacji. Musimy jedynie powiązać
wolumenem kontener aplikacji z produkcyjnym kontenerem konfiguracji (listing 20.4).
Listing 20.4. Zastosowanie innej konfiguracji w środowisku produkcyjnym
Kontenery potrafią więc współdzielić całe wolumeny (zewnętrzne), ale nie potrafią udostępniać
katalogów zlokalizowanych w ramach kontenerów. Aby skorzystać z kontenerów Niezmiennej
Konfiguracji w Kubernetesie, możemy skorzystać z Kontenerów Inicjalizacji z rozdziału 14.,
które inicjalizują puste wolumeny współdzielone w momencie startu.
FROM busybox
Główne różnice pomiędzy zwykłym przykładem użycia Dockera (listing 20.1) a powyższym
kodem polegają na konieczności zastosowania innego obrazu bazowego, a także dodania
atrybutu ENTRYPOINT, który kopiuje plik właściwości do katalogu przekazanego jako argument
w momencie startu obrazu Dockera. Do tego obrazu możemy się odwoływać w kontenerze
inicjalizacji w ramach pola .template.spec obiektu Deployment (listing 20.6).
initContainers:
- image: k8spatterns/config-dev:1
name: init
args:
- “/config”
volumeMounts:
- mountPath: “/config”
name: config-directory
containers:
- image: k8spatterns/demo:1
name: demo
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath: “/config”
name: config-directory
volumes:
- name: config-directory
emptyDir: {}
Rysunek 20.2 przedstawia jak kontener aplikacji korzysta z danych konfiguracji utworzonych
przez kontener inicjalizacji we współdzielonym wolumenie.
Rysunek 20.2. Niezmienna konfiguracja w kontenerze inicjalizacji
Szablony OpenShift
Szablony to zwykłe deskryptory zasobów, które można parametryzować. Jak widać na listingu
20.7, możemy ustawić obraz konfiguracji jako parametr.
apiVersion: v1
kind: Template
metadata:
name: demo
parameters:
- name: CONFIG_IMAGE
objects:
- apiVersion: v1
kind: DeploymentConfig
// ....
spec:
template:
metadata:
// ....
spec:
initContainers:
- name: init
image: ${CONFIG_IMAGE}
args: [ “/config” ]
volumeMounts:
- mountPath: /config
name: config-directory
containers:
- image: k8spatterns/demo:1
// ...
volumeMounts:
- mountPath: /config
name: config-directory
volumes:
- name: config-directory
emptyDir: {}
Pokazujemy jedynie fragment pełnego deskryptora; widać na nim jednak wyraźnie parametr
CONFIG_IMAGE, do którego odnosimy się w deklaracji kontenera inicjalizacji. Jeśli utworzymy
ten szablon jako klaster OpenShift, możemy uzyskać instancję przez wywołanie polecenia oc
(listing 20.8).
Listing 20.8. Zastosowanie szablonu OpenShift w celu utworzenia nowej aplikacji
Szczegółowe instrukcje na temat uruchomienia tego przykładu (podobnie jak pełna wersja
deskryptorów wdrożenia) znajdują się w naszym repozytorium Git.
Dyskusja
Zastosowanie kontenerów danych we wzorcu Niezmienna Konfiguracja z pewnością jest nieco
skomplikowane. Z drugiej strony, takie podejście daje nam dużo korzyści:
Jego obsługa wymaga więcej pracy, ponieważ konieczne jest zbudowanie dodatkowych
obrazów i opublikowanie ich w rejestrach.
Wzorzec nie rozwiązuje żadnych problemów bezpieczeństwa związanych z danymi
wrażliwymi.
W Kubernetesie konieczne jest dodatkowe przetwarzanie kontenerów inicjalizacji, dlatego
musimy zarządzać różnymi obiektami wdrożeń dla różnych środowisk.
Musimy zatem starannie przeanalizować naszą sytuację, aby wybrać najlepsze rozwiązanie.
Jeśli niezmienność nie jest kluczowa, być może lepszym wyborem będzie obiekt ConfigMap,
opisany w rozdziale 19.
Innym podejściem do obsługi dużych plików konfiguracyjnych, które różnią się tylko
nieznacznie pomiędzy różnymi środowiskami, jest wzorzec Szablon Konfiguracji, opisany w
kolejnym rozdziale.
Więcej informacji
Przykład Niezmiennej Konfiguracji: http://bit.ly/2HL95dp
Jak uzyskać efekt parametru --volumes-from w Kubernetesie: http://bit.ly/2YbRhhy
Żądanie nowej funkcji — wolumeny obrazów w Kubernetesie: http://bit.ly/2Wf0pjt
docker-flexvol — sterownik Kubernetesa, który obsługuje wolumeny Dockera:
https://github.com/dims/docker-flexvol
Szablony OpenShift: https://red.ht/2Ohh7vO
Rozdział 21. Szablon
Konfiguracji
Wzorzec Szablonu Konfiguracji pozwala na tworzenie i przetwarzanie dużych zbiorów
konfiguracji w trakcie startu aplikacji. Wygenerowana konfiguracja jest powiązana z
konkretnym środowiskiem docelowym, co jest odzwierciedlone w zbiorze parametrów
przetwarzanych w szablonie konfiguracji.
Problem
W rozdziale 19. zapoznaliśmy się ze sposobem użycia natywnych dla Kubernetesa zasobów
ConfigMap i Secret w celu skonfigurowania aplikacji. Czasami pliki konfiguracyjne potrafią
rozrosnąć się do ogromnych rozmiarów. Umieszczenie ich bezpośrednio w obiektach ConfigMap
może być problematyczne, ponieważ muszą one być właściwie osadzane w definicji zasobu.
Musimy być niezwykle ostrożni i unikać znaków specjalnych (takich jak cudzysłowy) czy uważać
na składnię zasobów Kubernetesa. Rozmiar konfiguracji stanowi kolejny problem, ponieważ
istnieje górny limit sumy rozmiarów wszystkich wartości przechowywanych w obiektach
ConfigMap lub Secret, wynoszący 1 MB (limit ten wynika wprost z będącego fundamentem
tych zasobów magazynu Etcd).
Duże pliki konfiguracyjne z reguły różnią się dość nieznacznie pomiędzy różnymi środowiskami.
W związku z tym, użycie obiektów ConfigMap prowadzi do dużej redundancji, ponieważ każde
środowisko ma z grubsza te same dane. Na szczęście szablon Wzorzec Konfiguracji rozwiązuje
wszystkie wymienione problemy.
Rozwiązanie
Aby ograniczyć duplikację danych, najbardziej sensowne wydaje się przechowywanie jedynie
różnic w wartościach konfiguracji, takich jak parametry połączenia do bazy danych w obiekcie
ConfigMap czy bezpośrednio w zmiennych środowiskowych. Podczas startu kontenera, te
wartości są przetwarzane za pomocą Szablonów Konfiguracji w celu utworzenia pełnego pliku
konfiguracji (takiego jak standalone.xml w JBoss WildFly). Istnieje wiele dodatkowych narzędzi,
takich jak Tiller (w Rubym) czy Gomplate (w Go) do przetwarzania szablonów podczas
inicjalizacji aplikacji. Rysunek 21.1 stanowi przykład Szablonu Konfiguracji wypełnionego
danymi pochodzącymi ze zmiennych środowiskowych lub zamontowanego wolumenu,
wspartego przez obiekt ConfigMap.
Rysunek 21.1. Szablon konfiguracji
Pełny przykład wraz z kompletnymi instrukcjami instalacji znajduje się w naszym repozytorium
w serwisie GitHub1 (tutaj pokazujemy tylko główny koncept — więcej szczegółów znajdziesz w
repozytorium).
Wzorzec logów z listingu 21.1 jest przechowywany w pliku standalone.xml, który
parametryzujemy korzystając ze składni szablonu Go.
....
<formatter name=”COLOR-PATTERN”>
....
Dysponując tym szablonem, możemy utworzyć obraz Dockera dla kontenera inicjalizacji. Plik
Dockerfile dla obrazu k8spatterns/example-configuration-template-init jest bardzo prosty
(listing 21.2).
FROM k8spatterns/gomplate
COPY in /in
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
example: cm-template
replicas: 1
template:
metadata:
labels:
example: cm-template
spec:
initContainers:
- image: k8spatterns/example-config-cm-template-init
name: init
volumeMounts:
- mountPath: “/params”
name: wildfly-parameters
- mountPath: “/out”
name: wildfly-config
containers:
- image: jboss/wildfly:10.1.0.Final
name: server
command:
- “/opt/jboss/wildfly/bin/standalone.sh”
- “-Djboss.server.config.dir=/config”
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath: “/config”
name: wildfly-config
volumes:
- name: wildfly-parameters
configMap:
name: wildfly-parameters
- name: wildfly-config
emptyDir: {}
Deklaracja wolumenu dla parametrów obiektu ConfigMap. Pusty katalog jest używany do
współdzielenia przetworzonej konfiguracji.
Należy pamiętać, że nie musimy zmieniać deskryptorów zasobu Deployment, gdy zmieniamy
środowisko z deweloperskiego na produkcyjne. Tylko obiekt ConfigMap z parametrami szablonu
ulega zmianie.
Dzięki zastosowaniu tego rozwiązania, możemy stworzyć konfigurację zgodną z zasadą DRY
(Don’t Repeat Yourself — „nie powtarzaj się”), bez kopiowania dużej liczby plików konfiguracji i
zarządzania nimi. Gdy konfiguracja serwera WildFly zmieni się we wszystkich środowiskach,
musimy zaktualizować tylko jeden plik szablonu w kontenerze inicjalizacji. To podejście
znacząco ułatwia utrzymanie, ponieważ nie ma zagrożenia desynchronizacji poszczególnych
wersji konfiguracji.
Dyskusja
Wzorzec Szablon Konfiguracji bazuje na wzorcu Zasób Konfiguracji i jest on dostosowany do
sytuacji, w której musimy radzić sobie z aplikacjami w różnych środowiskach, ale z podobnymi
konfiguracjami. Ustawienie Szablonu Konfiguracji jest jednak dość trudne i o wiele więcej
rzeczy może pójść nie tak. Korzystaj z niego tylko wtedy, gdy aplikacja naprawdę wymaga
ogromnej ilości danych konfiguracyjnych. Takie aplikacje często wymagają podania bardzo
dużej ilości danych, z których tylko niewielka część zależy od środowiska. Nawet jeśli kopiujesz
całą konfigurację bezpośrednio do obiektu ConfigMap dla danego środowiska i początkowo
takie podejście wydaje się sensowne, powstaje w ten sposób problem utrzymania spójności
takiej konfiguracji z pozostałymi obiektami, co wraz z upływem czasu staje się coraz
trudniejsze. W takiej sytuacji zdecydowanie warto skorzystać z podejścia opartego na
szablonach.
Więcej informacji
Przykład Szablonu Konfiguracji: http://bit.ly/2TKUHZY
Silnik szablonów Tiller: https://github.com/markround/tiller
Gomplate: https://github.com/hairyhenderson/gomplate
Składnia szablonów Go: https://golang.org/pkg/html/template
1 https://github.com/k8spatterns/examples/tree/master/configuration/ConfigurationTemplate
Część V. Wzorce zaawansowane
Wzorce z tej kategorii poruszają bardziej zaawansowane zagadnienia, które nie pasują do
żadnej z wcześniejszych kategorii. Niektóre wzorce, takie jak Kontroler, są ponadczasowe i na
ich podstawie jest zbudowany sam Kubernetes. Niektóre z implementacji wzorców są w czasie
powstawania tej książki wciąż stosunkowo nowe (np. Knative, używany do budowania obrazów
kontenera i skalowania do zera usług), w związku z czym mogą one zauważalnie zmienić się do
momentu, gdy będziesz czytać te słowa. Aby być na bieżąco, zapoznaj się z naszymi
przykładami w internecie — dzięki temu żadna nowinka nie umknie Twojej uwadze.
W kolejnych rozdziałach omawiamy następujące wzorce:
Rozdział 22., „Kontroler”, jest kluczowy dla funkcjonowania Kubernetesa jako takiego.
Wzorzec pokazuje, jak tworzyć własne kontrolery, aby rozszerzać możliwości platformy.
Rozdział 23., „Operator”, łączy możliwości Kontrolera z własnymi zasobami
charakterystycznymi dla danej dziedziny w celu przekształcenia wiedzy operacyjnej do
postaci zautomatyzowanej.
Rozdział 24., „Elastyczne skalowanie”, opisuje w jaki sposób Kubernetes jest w stanie
poradzić sobie z dynamicznie zmieniającym się obciążeniem, umożliwiając skalowanie w
różnych wymiarach.
Rozdział 25., „Budowniczy obrazów”, przesuwa kwestię budowania obrazów aplikacji do
samego klastra.
Rozdział 22. Kontroler
Kontroler aktywnie monitoruje zbiór zasobów Kubernetesa i zarządza nim w celu osiągnięcia i
utrzymania pewnego pożądanego stanu. Rdzeń Kubernetesa stanowi zbiór kontrolerów, które
stale obserwują i modyfikują stan aplikacji, aby był on zgodny z pożądanym. W tym rozdziale
dowiesz się jak skorzystać z tego kluczowego konceptu, aby dostosować możliwości platformy
do swoich potrzeb.
Problem
Widzieliśmy już, że Kubernetes jest wyszukaną i wszechstronną platformą, która oferuje cały
szereg narzędzi dostępnych w standardzie. Mimo to musimy pamiętać, że ta platforma do
orkiestracji jest narzędziem ogólnego przeznaczenia, które nie jest w stanie ująć przypadków
użycia wszystkich aplikacji. Na szczęście otrzymujemy możliwość rozszerzenia funkcji
platformy, aby obsłużyć nasze specyficzne wymagania, dokładając dodatkową warstwę na
szczycie standardowych mechanizmów dostarczanych przez Kubernetesa.
Podstawowe wyzwanie polega na takim rozszerzeniu możliwości Kubernetesa, które nie zmieni
ani nie zepsuje standardowych funkcji.
Z założenia, fundamentem Kubernetesa jest deklaratywne API, skoncentrowane na obsłudze
zasobów. Co właściwie oznacza określenie deklaratywny? W przeciwieństwie do podejścia
imperatywnego, podejście deklaratywne nie instruuje Kubernetesa jak należy wykonać dane
operacje — interesuje nas tylko stan docelowy. Gdy skalujemy zasób wdrożenia, nie zajmujemy
się bezpośrednio tworzeniem kapsuł — nie wydajemy polecenia typu „Utwórz nową kapsułę”.
Zamiast tego, zmieniamy właściwość replicas w zasobie wdrożenia za pomocą API
Kubernetesa, aby osiągnąć pożądaną liczbę.
Jak wiec są faktycznie tworzone nowe kapsuły? Dzieje się to za kulisami, przy użyciu
kontrolerów. Każda zmiana stanu wśród zasobów (np. zmiana właściwości replicas zasobu
Deployment) powoduje utworzenie zdarzenia przez Kubernetesa i przekazanie go do wszystkich
zainteresowanych słuchaczy. Słuchacze mogą reagować modyfikując, usuwając lub tworząc
nowe zasoby, co w rezultacie prowadzi do utworzenia innych zdarzeń (np. zdarzenia utworzenia
kapsuły). Zdarzenia te mogą być wykrywane ponownie przez inne kontrolery, które wtedy
wykonują swoje działania.
Cały proces znany jest pod nazwą uzgadniania (rekoncyliacji) stanu. Dochodzi do niego, gdy
stan docelowy (pożądana liczba replik) różni się od stanu obecnego (liczby aktualnie
uruchomionych instancji). W takiej sytuacji kontroler musi podjąć działania związane z
doprowadzeniem do stanu docelowego. Patrząc na proces z tej perspektywy, Kubernetes jest
swego rodzaju zarządcą rozproszonego stanu. Przekazujesz do niego pożądany stan instancji
komponentu, a on próbuje utrzymać instancję w tym stanie w razie jakichkolwiek zmian.
Teraz możemy zająć się omówieniem szczegółów włączenia się w proces uzgadniania stanu bez
potrzeby modyfikacji kodu Kubernetesa — utworzymy kontroler dopasowany do naszych
potrzeb.
Rozwiązanie
Kubernetes zawiera kolekcję wbudowanych kontrolerów, które zarządzają standardowymi
zasobami Kubernetesa, takimi jak ReplicaSet, DaemonSet, StatefulSet, Deployment czy
usługi. Kontrolery są uruchamiane jako część menedżera kontrolerów, który jest wdrażany (w
formie samodzielnego procesu lub kapsuły) na węźle głównym. Kontrolery nie mają wiedzy na
temat innych kontrolerów. Są one uruchomione w nieskończonej pętli uzgadniania, aby
monitorować zasoby pod kątem aktualnego i pożądanego stanu, a w razie czego działać w taki
sposób, aby zbliżyć stan aktualny do pożądanego.
Poza kontrolerami standardowymi, architektura Kubernetesa sterowana zdarzeniami pozwala
na natywne podłączanie innych, własnych kontrolerów. Własne kontrolery mogą zawierać
dodatkowe funkcje, powiązane ze zdarzeniami zmiany stanu, na tej samej zasadzie, co
kontrolery wewnętrzne. Typową cechą kontrolerów jest ich reaktywność — reagują one na
zdarzenia w systemie i w związku z tym wykonują określone działania. Omawiając temat dość
ogólnie, można powiedzieć, że proces uzgadniania składa się z następujących kroków:
Obserwacja
Analiza
Określ różnice pomiędzy stanem bieżącym a docelowym.
Działanie
Wykonaj operacje, które doprowadzą zasób do stanu docelowego.
Rysunek 22.1 przedstawia sposób rejestracji kontrolera jako słuchacza zdarzeń w celu
wykrywania zmian w zarządzanych przez niego zasobach. Kontroler obserwuje bieżący stan i
zmienia go, wywołując serwer API, i zbliżając się w ten sposób do stanu docelowego (jeśli w
danej chwili jest taka potrzeba).
Kontrolery stanowią część płaszczyzny kontroli Kubernetesa. Bardzo szybko okazało się, że
pozwolą one także na rozszerzenie platformy o własne zachowania. Co więcej, kontrolery stały
się standardowym mechanizmem do rozszerzania możliwości platformy, umożliwiając
zaawansowane zarządzanie cyklem życia aplikacji. Rezultatem tego rozwoju stało się nowe
pokolenie wyszukanych kontrolerów, nazywanych Operatorami. Z punktu widzenia ewolucji i
złożoności tych zagadnień, aktywne komponenty uzgadniania możemy podzielić na dwie grupy:
Kontrolery
Kontroler to prosty proces uzgadniania, który monitoruje i działa ze standardowymi
zasobami Kubernetesa. Znacznie częściej kontrolery rozszerzają zachowania platformy i
dodają do niej nowe funkcje.
Operatory
Jak stwierdziliśmy wcześniej, ten podział pozwala na stopniowe wprowadzanie nowych pojęć. W
tym rozdziale skupiamy się na prostszych Kontrolerach, a w kolejnym przejdziemy do obiektów
CRD i omówimy wzorzec Operator.
Aby uniknąć operowania przez wiele kontrolerów na tych samych zasobach w tym samym
czasie, kontrolery korzystają z wzorca SingletonService, omówionego w rozdziale 10.
Większość kontrolerów jest wdrażana jako obiekty Deployment, ale tylko z jedną repliką.
Wynika to z faktu, że Kubernetes korzysta z optymistycznych blokad na poziomie zasobów, aby
zapobiec problemom z dostępem współbieżnym w momencie zmiany zasobów. Kontroler jest
przecież po prostu aplikacją działającą stale w tle.
Kubernetes został napisany w języku Go, podobnie jak kompletna biblioteka kliencka, która
daje do niego dostęp. W związku z tym, wiele kontrolerów również jest pisanych w tym języku.
Kontrolery można też tworzyć w innych językach programowania — wystarczy mieć możliwość
wysyłania żądań do serwera API Kubernetesa. W kodzie z listingu 22.1 zapoznamy się z
kontrolerem napisanym w formie skryptu powłoki.
Etykiety
Etykiety stanowią element metadanych zasobu, który może być obserwowany przez
dowolny kontroler. Są one indeksowane w bazie danych po stronie backendu i mogą być
wydajnie przeszukiwane za pomocą zapytań. Z etykiety warto korzystać, gdy konieczne jest
uzyskanie mechanizmu przypominającego selektory (np. dopasowania kapsuł usługi lub
wdrożenia). Ograniczeniem etykiety jest możliwość stosowania jedynie alfanumerycznych
nazw i wartości. Więcej informacji na temat składni i dopuszczalnych znaków znajdziesz w
dokumentacji Kubernetesa.
Adnotacje
Adnotacje stanowią doskonałą alternatywę dla etykiet. Trzeba z nich skorzystać, jeśli
pożądane przez nas wartości nie są zgodne z ograniczeniami etykiet. Adnotacje nie są
indeksowane, dlatego korzystamy z nich w przypadku informacji niebędących
identyfikatorami ani kluczami w zapytaniach kontrolerów. Stosowanie adnotacji zamiast
etykiet w odniesieniu do dowolnych metadanych ma tę zaletę, że nie wpływa ono
negatywnie na wewnętrzną wydajność Kubernetesa.
Obiekty ConfigMap
Kontrolery potrzebują niekiedy dodatkowych informacji, które nie pasują dobrze do zasad
działania etykiet i adnotacji. W takiej sytuacji obiekty ConfigMap mogą być używane do
przechowywania definicji stanu docelowego. Obiekty te są następnie obserwowane i
odczytywane przez kontrolery. Do projektowania własnych specyfikacji stanów docelowych
znacznie lepiej skorzystać z obiektów CRD. Rejestracja tychże wymaga jednak wyższych
uprawnień na poziomie klastra. Jeśli ich nie posiadasz, najprościej pozostać przy obiektach
ConfigMap. Obiekty CRD zostaną objaśnione w rozdziale 23.
Oto kilka prostych przykładów kontrolerów, które możesz potraktować jako przykładowe
implementacje wzorca:
jenkins-x/exposecontroller
Ten kontroler1 obserwuje definicje usług i jeśli dojdzie do wykrycia adnotacji o nazwie
expose w metadanych, kontroler automatycznie udostępni obiekt Ingress, dając
zewnętrzny dostęp do usługi. Kontroler dba także o automatyczne usunięcie obiektu
Ingress w przypadku usunięcia usługi.
fabric8/configmapcontroller
Ten kontroler2 obserwuje obiekty ConfigMap pod kątem zmian i wykonuje ciągłą
aktualizację powiązanych wdrożeń. Możemy skorzystać z tego kontrolera w aplikacjach,
które nie są w stanie obserwować obiektu ConfigMap i dynamicznie aktualizować swojego
stanu na podstawie konfiguracji. Jest to istotne zwłaszcza wtedy, gdy kapsuła korzysta z
tego obiektu ConfigMap zadeklarowanego w postaci zmiennych środowiskowych lub gdy
aplikacja nie może szybko i stabilnie zaktualizować siebie w locie bez restartu. W kodzie z
listingu 22.2 implementujemy kontroler za pomocą zwykłego skryptu powłoki.
Ten kontroler3 restartuje węzeł Kubernetesa, gdy na węźle zostanie wykryta odpowiednia
adnotacja.
Teraz przeanalizujmy praktyczny przykład: kontroler, który składa się z jednego skryptu
powłoki i obserwuje API Kubernetesa pod kątem zmian w zasobach ConfigMap. Jeśli oznaczymy
taki obiekt ConfigMap za pomocą adnotacji k8spatterns.io/podDeleteSelector, wszystkie
kapsuły z daną wartością adnotacji zostaną usunięte w momencie zaistnienia zmian w obiekcie
ConfigMap. Zakładając, że kapsuły zostaną wsparte za pomocą zasobu wyższego rzędu, takiego
jak Deployment czy ReplicaSet, zostaną one zrestartowane i wykryją zmienioną konfigurację.
Utworzony w ramach przykładu poniższy obiekt ConfigMap będzie kontrolowany przez nasz
kontroler. W przypadku zaistnienia zmian nastąpi restart wszystkich kapsuł, których etykieta
app ma wartość webapp. Obiekt ConfigMap w kodzie z listingu 22.1 jest używany w aplikacji
webowej, aby dostarczyć wiadomość powitalną.
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
annotations:
k8spatterns.io/podDeleteSelector: “app=webapp”
data:
Adnotacja używana jako selektor dla kontrolera z listingu 22.2, dzięki której możemy
znaleźć kapsuły wymagające restartu.
Nasz skrypt powłoki kontrolera dokona ewaluacji obiektu ConfigMap z listingu 22.1. Źródła w
kompletnej wersji znajdziesz w naszym repozytorium Gita. Mówiąc w skrócie, kontroler
rozpoczyna „wiszące” żądanie HTTP GET, dzięki któremu otwieramy niekończący się strumień
odpowiedzi HTTP. Za jego pomocą otrzymujemy zdarzenia cyklu życia wypychane z serwera API
do nas. Te zdarzenia mają postać zwykłych obiektów w formacie JSON, które analizujemy w
celu wykrycia adnotacji w zmienionych obiektach ConfigMap. Po otrzymaniu zdarzeń działamy,
usuwając wszystkie kapsuły pasujące do selektora dostarczonego jako wartość adnotacji.
Przyjmijmy się bliżej zasadzie działania kontrolera.
Główną jego część stanowi pętla uzgadniania, która nasłuchuje zdarzeń cyklu życia obiektu
ConfigMap (listing 22.2).
namespace=${WATCH_NAMESPACE:-default}
base=http://localhost:8001
ns=namespaces/$namespace
curl -N -s $base/api/v1/${ns}/configmaps?watch=true | \
do
# ...
done
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
Korzystając z tej przestrzeni nazw, skrypt kontrolera utworzy adres URL do końcówki API
Kubernetesa, aby obserwować obiekty ConfigMap.
Jak widać, nasz kontroler kontaktuje się z serwerem API Kubernetesa za pośrednictwem hosta
localhost. Nie wdrożymy tego skryptu bezpośrednio na węźle master API Kubernetesa — jednak
w jakiś sposób musimy skorzystać w skrypcie z hosta lokalnego. W tym miejscu posłużymy się
innym wzorcem. Skrypt wdrożymy w kapsule z wykorzystaniem kontenera Ambasador, który
udostępnia port 8001 na hoście lokalnym i przekierowuje go do faktycznej usługi Kubernetesa.
Więcej informacji na ten temat znajdziesz w rozdziale 17. Definicję kapsuły — wraz z
Ambasadorem — znajdziesz w dalszej części rozdziału.
Obserwowanie zdarzeń w ten sposób nie jest, rzecz jasna, zbyt wyszukane. Połączenie może
ulec zatrzymaniu w dowolnym momencie, dlatego powinna istnieć metoda do restartowania
pętli. Może też zaistnieć sytuacja, w której niektóre zdarzenia zostaną pominięte, dlatego
produkcyjne kontrolery powinny nie tylko obserwować zdarzenia, ale od czasu do czasu
odpytywać serwer API o aktualny stan i traktować go jako nową podstawę. Dla celów naszego
przykładu taki mechanizm jednak wystarczy.
curl -N -s $base/api/v1/${ns}/configmaps?watch=true | \
do
selector=$(echo $annotations | \
jq -r “\
to_entries |\
.[] |\
select(.key == \”k8spatterns.io/podDeleteSelector\”) |\
.value |\
@uri \
“)
fi
pods=$(curl -s $base/api/v1/${ns}/pods?labelSelector=$selector |\
jq -r .items[].metadata.name)
done
Jeśli zdarzenie wskazuje, że doszło do zmiany obiektu ConfigMap i nasza adnotacja jest
dołączona, znajdź wszystkie kapsuły pasujące do tego selektora etykiety.
Nasz kontroler, będący w istocie po prostu skryptem powłoki, z pewnością nie kwalifikuje się do
narzędzi produkcyjnych (pętla zdarzeń może się w każdym momencie zatrzymać), jednak w
elegancki sposób ujawnia on podstawowe koncepty bez potrzeby pisania zbyt dużej ilości kodu,
ograniczając się do rzeczywiście niezbędnego i podstawowego.
Teraz musimy tylko utworzyć obiekty zasobów i obrazy kontenerów. Skrypt kontrolera jest
przechowywany w obiekcie ConfigMap o nazwie config-watcher-controller i w razie czego
możemy poddać go edycji w późniejszym czasie.
to_entries |\
.[] |\
select(.key == \”k8spatterns.io/podDeleteSelector\”) |\
.value |\
@uri \
“)
{
“k8spatterns.io/pattern”: “Controller”,
“k8spatterns.io/podDeleteSelector”: “app=webapp”
}
na selektor app%3Dwebapp.
spec:
template:
# ...
spec:
serviceAccountName: config-watcher-controller
containers:
- name: kubeapi-proxy
image: k8spatterns/kubeapi-proxy
- name: config-watcher
image: k8spatterns/curl-jq
# ...
command:
- “sh”
- “/watcher/config-watcher-controller.sh”
volumeMounts:
- mountPath: “/watcher”
name: config-watcher-controller
volumes:
- name: config-watcher-controller
configMap:
name: config-watcher-controller
Serwer HTTP wdrażamy wraz z obiektami ConfigMap i Deployment, co widać na listingu 22.6.
Listing 22.6. Przykładowa aplikacja webowa z obiektami Deployment i ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
annotations:
k8spatterns.io/podDeleteSelector: “app=webapp”
data:
kind: Deployment
# ...
spec:
# ...
template:
spec:
containers:
- name: app
image: k8spatterns/mini-http-server
ports:
- containerPort: 8080
env:
- name: MESSAGE
valueFrom:
configMapKeyRef:
name: webapp-config
key: message
Dyskusja
Podsumowując, wzorzec Kontroler stanowi aktywny proces uzgadniania, który stale obserwuje
wybrane obiekty, porównując ich stan bieżący z pożądanym. Jeżeli stany te nie są identyczne,
kontroler podejmuje działania, aby stan aktualny był identyczny z pożądanym. Kubernetes
stosuje ten mechanizm dla swoich wewnętrznych kontrolerów, ale można także skorzystać z
niego dla kontrolerów własnych. Pokazaliśmy co wiąże się z tworzeniem własnego kontrolera i
jak możemy rozszerzyć możliwości Kubernetesa jako platformy.
Istnienie kontrolerów wynika z wysoce modułowego i zdarzeniowego charakteru architektury
Kubernetesa. Zachęca ona do tworzenia luźno powiązanych i asynchronicznych mechanizmów,
przy czym kontrolery stanowią naturalne miejsca, w których rozszerzenia mogą być tworzone.
Istotną zaletą tego podejścia jest precyzyjna, techniczna granica pomiędzy samym
Kubernetesem a rozszerzeniami. Niestety, asynchroniczny charakter kontrolerów utrudnia
debugowanie, ponieważ przepływ zdarzeń nie zawsze jest łatwy do przeanalizowania. W
związku z tym trudno ustawić punkty przerwania w swoim kontrolerze, aby zatrzymać aplikację
i zdiagnozować konkretny problem.
W rozdziale 23. poznasz jeszcze jeden wzorzec — Operator — który jest zbudowany na bazie
wzorca Kontroler, ale dostarcza bardziej elastyczne metody konfiguracji naszych działań.
Więcej informacji
Przykład wzorca Kontroler: http://bit.ly/2TWw6AW
Pisanie kontrolerów: http://bit.ly/2HKlIWc
Pisanie własnego kontrolera w Pythonie: https://red.ht/2HxC85a
Skok na głęboką wodę z kontrolerami Kubernetesa: http://bit.ly/2ULdC3t
Kontroler udostępniania: https://github.com/jenkins-x/exposecontroller
Kontroler ConfigMap: https://github.com/fabric8io/configmapcontroller
Pisanie własnego kontrolera: http://bit.ly/2TYgo9b
Pisanie własnych kontrolerów w Kubernetesie: http://bit.ly/2Cs1rS4
Kontroler Contour Ingress: https://github.com/heptio/contour
AppController: https://github.com/Mirantis/k8s-AppController
Znaki dopuszczalne w etykietach: http://bit.ly/2Q0td0M
Kubectl-proxy: http://bit.ly/2FgearB
1 http://bit.ly/2Ushlpy
2 http://bit.ly/2uJ2FnI
3 http://bit.ly/2uFcNgX
4 http://bit.ly/2FnPVsw
Rozdział 23. Operator
Operator to kontroler, który korzysta z CRD, aby enkapsulować wiedzę operacyjną danej
aplikacji w zautomatyzowany lub zalgorytmizowany sposób. Wzorzec Operator pozwala na
rozszerzenie wzorca Kontroler z poprzedniego rozdziału, dając większą elastyczność i
możliwości zastosowania.
Problem
W rozdziale 22. opisaliśmy, jak rozszerzyć Kubernetesa w prosty i elastyczny sposób. W bardziej
skomplikowanych sytuacjach zwykłe kontrolery nie są wystarczająco funkcjonalne, z uwagi na
fakt, że są one w stanie jedynie obserwować wewnętrzne zasoby Kubernetesa i zarządzać nimi.
Czasami chcemy dodać nowe koncepty do Kubernetesa, które wymagają uwzględnienia
dodatkowych obiektów domenowych. Załóżmy, że naszym rozwiązaniem w zakresie
monitorowania jest Prometheus. W związku z tym chcielibyśmy dodać go jako narzędzie do
monitorowania w Kubernetesie, zgodnie z dobrymi praktykami. Byłoby świetnie, gdybyśmy
mogli dodać zasób Prometheusa, opisujący naszą konfigurację monitorowania i wszelkie
szczegóły związane z wdrożeniem, na tej samej zasadzie, co w przypadku innych zasobów
Kubernetesa, prawda? A gdyby dodać do tego możliwość deklarowania w zasobach, które
usługi chcemy monitorować (np. za pomocą selektora etykiet)?
Wszystkie te życzenia są możliwe do spełnienia dzięki obiektom CustomResourceDefinition
(CRD). Pozwalają one na rozszerzanie API Kubernetesa przez dodawanie własnych zasobów do
klastra Kubernetesa i używanie ich w taki sposób, jakbyśmy mieli do czynienia z natywnymi
zasobami. Własne zasoby, wraz z Kontrolerem działającym na tych zasobach, tworzą wzorzec
Operatora.
Poniższe słowa Jimmy’ego Zelinskiego najlepiej oddają zasadę działania Operatora:
Operator to kontroler Kubernetesa, który funkcjonuje w dwóch dziedzinach: Kubernetesa i
dodatkowej. Łącząc wiedzę z obu obszarów, operator potrafi zautomatyzować zadania,
które z reguły wymagają udziału człowieka, poruszającego się w obu dziedzinach.
Rozwiązanie
W rozdziale 22. pokazaliśmy, że jesteśmy w stanie efektywnie reagować na zmiany stanu w
domyślnych zasobach Kubernetesa. Skoro masz już wiedzę na temat jednej części wzorca
Operator, przyjrzyjmy się drugiej części — reprezentującej własne zasoby w Kubernetesie przy
użyciu zasobów CRD.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: prometheuses.monitoring.coreos.com
spec:
group: monitoring.coreos.com
names:
kind: Prometheus
plural: prometheuses
scope: Namespaced
version: v1
validation:
openAPIV3Schema: ....
Nazwa.
Reguła nazewnictwa użyta do stworzenia liczby mnogiej. Stosowana do określenia listy tego
rodzaju obiektów.
Zasięg — określa czy zasób może być stworzony w zakresie klastra, czy jest powiązany z
konkretną przestrzenią nazw.
Wersja CRD.
Dodatkowo, Kubernetes pozwala na określenie dwóch podzasobów dla CRD, korzystając z pola
subresources:
scale
Dzięki tej właściwości CRD może określić, w jaki sposób zarządzać liczbą replik. To pole
może być użyte do określenia ścieżki w formacie JSON, w której określamy pożądaną liczbę
replik własnego zasobu: ścieżka właściwości przechowuje liczbę uruchomionych replik, a
opcjonalna ścieżka do selektora etykiet może być zastosowana do znalezienia kopii
instancji własnych zasobów. Selektor etykiet jest opcjonalny, ale może być wymagany, jeśli
chcesz zastosować własny zasób z obiektem HorizontalPodAutoscaler, objaśnionym w
rozdziale 24.
status
Po ustawieniu tej właściwości możemy skorzystać z nowego wywołania API, które pozwala
jedynie na zmianę statusu. To wywołanie API będzie zabezpieczane indywidualnie,
pozwalając na zmianę statusu spoza kontrolera. Z drugiej strony, jeśli zostanie zmieniony w
całości własny zasób, sekcja status będzie ignorowana w przypadku standardowych
zasobów Kubernetesa.
Listing 23.2 przedstawia możliwe ścieżki podzasobów, używane w przypadku zwykłych kapsuł.
kind: CustomResourceDefinition
# ...
spec:
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
labelSelectorPath: .status.labelSelector
Ścieżka JSON do liczby zadeklarowanych replik.
Ścieżka JSON do selektora etykiet odpytywanego w celu uzyskania liczby aktywnych replik.
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus
spec:
serviceMonitorSelector:
matchLabels:
team: frontend
resources:
requests:
memory: 400Mi
Sekcja metadata: ma ten sam format i reguły walidacji, co inne zasoby Kubernetesa. Sekcja
spec: zawiera treść dostosowaną do formatu CRD. Kubernetes waliduje treści, odnosząc się do
reguł walidacji z CRD.
Własne zasoby same w sobie nie mają zastosowania bez aktywnego komponentu, który z nich
korzysta. Aby nadać sens ich istnieniu, musimy skorzystać ze znanego nam Kontrolera, który
obserwuje cykl życia zasobów i działa zgodnie z deklaracjami znalezionymi w obrębie zasobów.
CRD instalacyjne
CRD aplikacji
Obiekty te są używane do reprezentowania konceptów związanych z dziedziną aplikacji.
Ten rodzaj CRD pozwala na głęboką integrację z Kubernetesem, co wiąże się z łączeniem
Kubernetesa z zachowaniem specyficznym dla dziedziny aplikacji. Na przykład, CRD
obiektu ServiceMonitor jest używany przez operator Prometheusa do rejestracji usług
charakterystycznych dla Kubernetesa, analizowanych przez serwer Prometheusa. Operator
Prometheusa zajmuje się właściwym dostosowaniem konfiguracji serwera Prometheusa.
Zwróć uwagę na to, że Operator może działać z różnymi CRD, tak jak w
przypadku operatora Prometheusa. Zakres pomiędzy tymi dwoma kategoriami
CRD jest dość płynny.
Jednym z takich przykładów jest kontroler, który korzysta z obiektu ConfigMap jako zastępstwa
za obiekt CRD. To podejście ma sens w sytuacji, gdy domyślne zasoby Kubernetesa nie
wystarczają, ale tworzenie CRD również nie ma sensu. W takiej sytuacji obiekt ConfigMap
stanowi doskonałe rozwiązanie pośrednie, pozwalając na enkapsulację logiki domenowej w
ramach zawartości obiektu ConfigMap. Zaletą zastosowania zwykłego obiektu ConfigMap jest
brak konieczności posiadania uprawnień administracyjnych na poziomie klastra do
zarejestrowania CRD (np. przy uruchamianiu publicznych klastrów, takich jak OpenShift
Online).
Mimo to, koncepcja Obserwacja-Analiza-Działanie ma duży sens, gdy zamieniasz CRD na zwykły
obiekt ConfigMap do obsługi domenowej konfiguracji. Problem w tym, że w przypadku użycia
obiektów CRD nie możesz skorzystać z podstawowych narzędzi, takich jak kubectl get. Nie
dysponujesz też walidacją na poziomie serwera API, a także nie możesz skorzystać z
wersjonowania API. Nie masz ponadto wpływu na przeniesienie pola status: do obiektu
ConfigMap, podczas gdy w obiekcie CRD możesz w dowolny odpowiadający Ci sposób
zdefiniować model stanu.
Kolejną zaletą zastosowania CRD jest precyzyjne modelowanie uprawnień, bazujące na innym
rodzaju CRD, który możesz dostroić do swoich potrzeb. Tego rodzaju bezpieczeństwo w modelu
RBAC nie jest możliwe, gdy cała konfiguracja dziedziny jest osadzona w obiektach ConfigMap,
ponieważ wszystkie obiekty tego typu w tej samej przestrzeni nazw współdzielą tę samą
konfigurację uprawnień.
Aby powiązać usługę z nazwą custom-api-server obsługiwaną przez kapsułę z naszą usługą,
możemy skorzystać z zasobu z listingu 23.4.
metadata:
name: v1alpha1.sample-api.k8spatterns.io
spec:
group: sample-api.k8spattterns.io
service:
name: custom-api-server
version: v1alpha1
Dzięki własnemu serwerowi API uzyskujemy zupełnie nowy poziom wolności, który wykracza
daleko poza obserwowanie zdarzeń cyklu życia zasobów. Z drugiej strony, musimy
implementować dodatkową logikę. W typowych przypadkach użycia operator obsługujący
zwykłe obiekty CRD jest dla nas wystarczający.
Szczegółowe omówienie możliwości serwera API wykracza poza zasięg tego rozdziału. Więcej
informacji zawiera oficjalna dokumentacja2; warto również sięgnąć po kompletny przykład
sample-apiserver3. Możesz także skorzystać z biblioteki apiserver-builder4, która pomaga
w implementowaniu agregacji serwera API.
Teraz zobaczmy, w jaki sposób możemy utworzyć i wdrożyć nasze operatory za pomocą
obiektów CRD.
Framework operatorów
Framework operatorów (ang. Operator Framework) wspiera tworzenie operatorów w języku
Golang. Framework dostarcza kilka podkomponentów:
Operator SDK dostarcza wysokopoziomowe API, które daje dostęp do klastra Kubernetesa
i umożliwia stworzenie szkieletu projektu operatora.
Menedżer cyklu życia operatora zarządza publikacjami i aktualizacjami operatorów, a
także ich obiektów CRD. Możesz traktować go jako „operator operatorów”.
Operator Metering udostępnia raportowanie dla operatorów.
Nie będziemy w tym miejscu szczegółowo analizować SDK operatorów (który wciąż się
zmienia), warto jednak wspomnieć, że menedżer cyklu życia operatorów (OLM) dostarcza
wyjątkowo cenną pomoc w użyciu operatorów. Jednym z problemów związanych z CRD jest
konieczność rejestracji zasobów w całym klastrze, co wymaga uprawnień administracyjnych na
poziomie całego klastra5. Zwykli użytkownicy Kubernetesa mogą z reguły zarządzać wszystkimi
aspektami przestrzeni nazw, do których mają dostęp, nie są jednak w stanie korzystać z
operatorów bez kontaktu z administratorem klastra.
Aby uprościć tę interakcję, OLM pomyślano jako usługę klastra działającą w tle w ramach konta
usługi z uprawnieniami do instalacji CRD. Dedykowany CRD o nazwie ClusterServiceVersion
(CSV) jest rejestrowany wraz z OLM i pozwala na określenie wdrożenia operatora wraz z
odniesieniem do definicji CRD skojarzonej z tym operatorem. Po utworzeniu CSV, część OLM
będzie oczekiwać na rejestrację danego CRD i wszystkich jego zależności. W takiej sytuacji
OLM wdraża operatora określonego w CSV. Pozostała część OLM może być użyta do rejestracji
CRD w imieniu nieuprzywilejowanego użytkownika. Takie podejście stanowi elegancką metodę
pozwalającą na dopuszczenie zwykłych użytkowników klastra do instalacji swoich operatorów.
Kubebuilder
Kubebuilder to projekt opracowany przez SIG API Machinery. Poświęcona mu jest obszerna
dokumentacja6. Podobnie jak w przypadku SDK operatorów, Kubebuilder obsługuje tworzenie
podstawowego szkieletu projektów Golang i umożliwia zarządzanie wieloma CRD z poziomu
jednego projektu.
Metakontroler
Metakontroler to mechanizm zauważalnie inny od dwóch omówionych do tej pory
frameworków. Rozszerza on możliwości Kubernetesa przy użyciu API, które enkapsulują
wspólne części własnych kontrolerów. Obiekt ten funkcjonuje na podobnej zasadzie, co
Menedżer Kontrolerów w Kubernetesie, uruchamiając wiele kontrolerów, które nie są zapisane
na sztywno, tylko zdefiniowane dynamicznie za pomocą obiektów CRD zależnych od
metakontrolera. Innymi słowy, to delegujący kontroler wywołuje usługę, która dostarcza logikę
Kontrolera.
Metakontroler można też opisać za pomocą zachowania deklaratywnego. Choć obiekt CRD
pozwala na przechowywanie nowych typów w API Kubernetesa, metakontroler umożliwia łatwe
definiowanie deklaratywnych zachowań dla standardowych lub własnych zasobów.
Ta delegacja pozwala na pisanie funkcji w dowolnym języku, który potrafi obsłużyć protokół
HTTP i format JSON, ponieważ nie występuje żadna forma zależności od API Kubernetesa lub
jego bibliotek klienckich. Funkcje mogą być przechowywane w Kubernetesie lub zewnętrznie,
w ramach dostawcy funkcji-jako-usługi (ang. Function-as-a-Service).
Nie możemy w tym miejscu omówić zbyt wielu szczegółów, jeśli jednak uznasz, że niezbędne Ci
jest rozszerzanie i dostosowanie Kubernetesa za pomocą prostych narzędzi do automatyzacji i
orkiestracji, a nie potrzebujesz żadnych dodatkowych funkcji, powinieneś przyjrzeć się
metakontrolerowi, zwłaszcza gdy chcesz zaimplementować własną logikę biznesową w innym
niż Go języku. Niektóre przykłady kontrolerów pokazują, jak zaimplementować obiekty
StatefulSet, wdrożenia niebiesko-zielone, indeksowane zadania czy usługi w ramach kapsuły,
tylko za pomocą metakontrolera.
Przykład
Przyjrzyjmy się przykładowi użycia operatora. Rozszerzymy kod z rozdziału 22., wprowadzając
obiekt CRD typu ConfigWatcher. Instancja tego CRD określa referencję do obiektu ConfigMap,
który ma być obserwowany. Oprócz tego, instancja ta wskazuje kapsuły wymagające restartu w
przypadku zmiany obiektu ConfigMap. Dzięki temu usuwamy zależność od obiektu ConfigMap w
kapsułach, ponieważ nie musimy modyfikować obiektu ConfigMap, aby dodać adnotacje
wyzwalające. Ponadto z uwagi na nasze proste, oparte na adnotacjach podejście, zastosowane
w przykładzie z kontrolerami, obiekt ConfigMap możemy połączyć tylko z pojedynczą aplikacją.
Jeśli korzystamy z CRD, dopuszczalne są dowolne połączenia obiektów ConfigMap i kapsuł.
Własny zasób ConfigWatcher jest przedstawiony w kodzie z listingu 23.5.
apiVersion: k8spatterns.io/v1
metadata:
name: webapp-config-watcher
spec:
configMap: webapp-config
podSelector:
app: webapp
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: configwatchers.k8spatterns.io
spec:
scope: Namespaced
group: k8spatterns.io
version: v1
names:
kind: ConfigWatcher
singular: configwatcher
plural: configwatchers
validation:
openAPIV3Schema:
properties:
spec:
properties:
configMap:
type: string
description: “Nazwa obiektu ConfigMap”
podSelector:
type: object
description: “Selektor etykiet dla kapsuł”
additionalProperties:
type: string
Początkowa wersja.
name: config-watcher-crd
rules:
- apiGroups:
- k8spatterns.io
resources:
- configwatchers
- configwatchers/finalizers
do
type=$(echo “$event” | jq -r ‘.type’)
label_selector=$(extract_label_selector $watcher)
delete_pods_with_selector “$label_selector”
done
fi
done
Uruchom strumień obserwacji, aby śledzić zmiany obiektu ConfigMap dla danej przestrzeni
nazw.
Pobierz z listy wszystkie elementy ConfigWatcher, które odwołują się do danego obiektu
ConfigMap.
Choć nasz operator ma swoje zastosowanie, widać wyraźnie, że operatory oparte na skryptach
są dość proste i nie poruszają przykładów brzegowych ani błędów. Znacznie więcej przykładów
działających na poziomie produkcyjnym znajdziesz w rzeczywistych aplikacjach.
Jeśli szukasz operatora napisanego w języku Java, powinieneś zainteresować się operatorem
Strimzi. Stanowi on doskonały przykład operatora, który zarządza skomplikowanym systemem
komunikacji, takim jak Apache Kafka, w ramach Kubernetesa. Dobrym punktem startowym dla
operatorów bazujących na języku Java jest JVM Operator Toolkit, który dostarcza podstawy do
tworzenia operatorów w języku Java, a także w językach bazujących na maszynie wirtualnej
Javy, takich jak Groovy czy Kotlin. Narzędzie to zawiera także szeroki zbiór przykładów.
Dyskusja
Choć wiesz już, jak rozszerzać możliwości Kubernetesa, musisz mieć świadomość, że operatory
nie są uniwersalnym lekarstwem na wszelkie nasze bolączki. Zanim skorzystasz z operatora,
warto dokładnie przeanalizować przypadek użycia, aby określić, czy dobrze pasuje on do
paradygmatu Kubernetesa.
Jeśli Twój przypadek użycia pasuje do tych kryteriów, ale potrzebujesz większej elastyczności w
zakresie implementacji i utrwalania własnych zasobów, rozważ użycie własnego serwera API.
Nie należy traktować możliwości rozszerzania Kubernetesa jak złotego środka na wszelkie
bolączki.
Jeśli Twój przypadek użycia nie jest deklaratywny, a zarządzane dane nie pasują do modelu
zasobów Kubernetesa, lub po prostu nie potrzebujesz ścisłej integracji z platformą, znacznie
lepiej rozważyć napisanie własnego, samodzielnego API i udostępnienie go za pomocą zwykłej
usługi lub obiektu Ingress.
Więcej informacji
Przykład wzorca Operator: http://bit.ly/2HvfIkV
Framework operatorów: http://bit.ly/2CKLYN1
OpenAPI V3: http://bit.ly/2Tluk82
Kubebuilder: http://bit.ly/2I8w9mz
Biblioteki klienckie Kubernetesa: http://bit.ly/2Sh1XYk
Metacontroller: https://metacontroller.app/
Zestaw narzędzi dla operatorów w JVM: https://github.com/jvm-operators
Rozszerz API Kubernetesa za pomocą obiektów CustomResourceDefinitions:
http://bit.ly/2uk6Iq5
Niesamowite operatory w naturalnym środowisku: http://bit.ly/2Ucjs0J
Własne zasoby a agregacje serwera API: http://bit.ly/2FrfR6I
Porównanie Kubebuildera, frameworku operatorów i metakontrolera:
http://bit.ly/2FpO4Ug
TPR nie żyje! Kubernetes 1.7 opiera się na obiektach CRD: http://bit.ly/2FQnCSA
Generowanie kodu dla własnych zasobów: https://red.ht/2HIS9Er
Prosty operator w Go: http://bit.ly/2UppsTN
Operator Prometheusa: http://bit.ly/2HICRjT
Operator Etcd: http://bit.ly/2JTz8SK
Operator Memhog: https://github.com/secat/memhog-operator
1 Relacja jest (ang. is-a) podkreśla dziedziczenie, jakie zachodzi pomiędzy Operatorem a
Kontrolerem. Operator ma wszystkie cechy Kontrolera plus kilka dodatkowych.
2 http://bit.ly/2uk7TWM
3 http://bit.ly/2HJULSy
4 http://bit.ly/2JIhHEl
5 To ograniczenie może zostać zniesione w przyszłości, ponieważ są plany, aby umożliwić
rejestrację w zakresie przestrzeni nazw dla CRD.
6 SIG (ang. Special Interest Group — specjalna grupa zainteresowania) pozwala na grupowanie
obszarów zainteresowań w społeczności Kubernetesa. Listę obecnych SIG-ów możesz znaleźć w
repozytorium Kubernetesa: https://github.com/kubernetes-sigs.
7 http://bit.ly/2Ucjs0J
8 http://bit.ly/2FrfyJ6
Rozdział 24. Elastyczne
Skalowanie
Wzorzec Elastyczne Skalowanie porusza temat skalowania w różnych wymiarach — skalowania
horyzontalnego, polegającego na dostosowywaniu liczby replik danej kapsuły, wertykalnego,
którego celem jest dostosowanie wymagań zasobów kapsuł, a także skalowania samego klastra
przez zmianę liczby węzłów. Wszystkie te operacje można wykonać — rzecz jasna — ręcznie, ale
w tym rozdziale dowiesz się jak Kubernetes poradzi sobie z tym sam, bazując na aktualnym
obciążeniu.
Problem
Kubernetes automatyzuje procesy orkiestracji i zarządzania aplikacjami rozproszonymi,
złożonymi z dużej liczby niezmiennych kontenerów, zarządzając pożądanym stanem określonym
w deklaratywny sposób. Z uwagi na zmienny w czasie charakter obciążenia, czasami trudno
określić, czym właściwie jest dla nas pożądany stan. Dokładne zidentyfikowanie ilości zasobów
wymaganych do działania kontenera, a także liczby replik koniecznych do spełnienia wymagań
określonych w umowie typu SLA (ang. service-level agreement) zajmuje sporo czasu i wysiłku.
Na szczęście Kubernetes pozwala w łatwy sposób zmieniać zasoby kontenera, pożądaną liczbę
replik usługi czy liczbę węzłów w klastrze. Takie zmiany mogą być wykonywane ręcznie lub, po
określeniu pewnych reguł, automatycznie.
Kubernetes nie tylko potrafi utrzymać określoną, niezmienną kapsułę i konfigurację klastra, ale
może również monitorować obciążenie zewnętrzne i zdarzenia związane z pojemnością,
analizować aktualny stan i skalować się w celu osiągnięcia pożądanej wydajności. Dzięki
umiejętności obserwacji, Kubernetes jest w stanie dostosowywać się i funkcjonować na bazie
aktualnych wskaźników życia, a nie parametrów określonych z góry na sztywno.
Przeanalizujemy różne sposoby na osiągnięcie takich efektów, zobaczymy także, jak połączyć
różne metody skalowania w celu uzyskania jeszcze lepszego efektu.
Rozwiązanie
Istnieją dwa główne podejścia do skalowania aplikacji: horyzontalne i wertykalne. Horyzontalne
skalowanie oznacza tworzenie coraz większej liczby replik. Wertykalne skalowanie powoduje
przekazanie większej ilości zasobów kontenerom uruchomionym przez kapsuły. Choć pozornie
wydaje się to dość proste, utworzenie konfiguracji aplikacji do automatycznego skalowania we
współdzielonej platformie chmurowej bez wpływu na inne usługi i klaster sam w sobie wymaga
wykonania wielu prób i testów. Jak zawsze, Kubernetes dostarcza różne funkcje i mechanizmy,
które pozwolą nam znaleźć najlepszą konfigurację dla naszych aplikacji. Omówimy je już za
chwilę.
Skalowanie imperatywne
Kontroler (np. ReplicaSet) odpowiada za zapewnienie odpowiedniej liczby uruchomionych
instancji. W związku z tym skalowanie kapsuły polega na zmianie liczby pożądanych replik.
Załóżmy, że nasze wdrożenie ma nazwę random-generator. Wyskalowanie go do czterech
instancji można osiągnąć za pomocą polecenia z listingu 24.1.
Po wprowadzeniu zmiany obiekt ReplicaSet może utworzyć dodatkowe kapsuły lub, jeśli jest
ich więcej niż potrzeba — usunąć je, aby wyskalować kapsuły w dół.
Skalowanie deklaratywne
Choć użycie polecenia do skalowania jest trywialnie proste i dobre w nagłych problemach, nie
utrwali ono konfiguracji poza klastrem. Zazwyczaj wszystkie definicje zasobów w aplikacjach
kubernetesowych są przechowywane w systemie kontroli wersji — dotyczy to także informacji o
liczbie replik. Utworzenie obiektu ReplicaSet z oryginalnej definicji spowoduje przywrócenie
liczby replik do początkowej wartości. Aby uniknąć takich zmian i wprowadzić procedury
pozwalające na utrwalanie tymczasowych zmian w oryginalnych plikach (ang. backporting),
znacznie lepiej jest wprowadzić żądaną liczbę replik deklaratywnie w obiekcie ReplicaSet lub
w innej definicji, a następnie zastosować zmiany w Kubernetesie (listing 24.2).
Kolejnym zasobem kubernetesowym, który możemy skalować, ale podlega on innej konwencji
nazewniczej, jest zadanie, opisane w rozdziale 7. Zadanie można skalować w celu wykonywania
wielu instancji tej samej kapsuły w tym samym czasie przez zmianę pola .spec.parallelism
zamiast .spec.replicas. Mimo to efekt pozostaje taki sam: zwiększona pojemność z większą
liczbą jednostek przetwarzania, funkcjonujących jak pojedyncza jednostka logiczna.
HPA dla wdrożenia random-generator tworzymy w kodzie z listingu 24.3. Aby HPA odniósł
jakikolwiek efekt, konieczne jest zadeklarowanie limitu procesora za pomocą pola
.spec.resources.requests (por. rozdział 2.). Ponadto musimy włączyć serwer wskaźników,
będący agregatorem zużycia zasobów, działającym na poziomie całego klastra.
kind: HorizontalPodAutoscaler
metadata:
minReplicas: 1
maxReplicas: 5
scaleTargetRef:
apiVersion: extensions/v1beta1
kind: Deployment
name: random-generator
metrics:
- resource:
name: cpu
target:
averageUtilization: 50
type: Utilization
type: Resource
Pożądane użycie procesora jako procent żądanego zasobu procesora dla kapsuł. Jeśli
wartość .spec.resources.requests.cpu jest równa 200 m, skalowanie nastąpi, gdy użyte
zostanie średnio więcej niż 100 m CPU (= 50%).
Teraz zobaczymy jak HPA potrafi zastąpić człowieka w zakresie skalowania. Mówiąc ogólnie,
kontroler HPA wykonuje w sposób ciągły następujące operacje:
pożądanaLiczbaReplik = sufit(aktualnaLiczbaReplik ·
(bieżącaWartośćWskaźnika/pożądanaWartośćWskaźnika))
Jeśli dysponujemy pojedynczą kapsułą o wartości użycia CPU na poziomie 90% żądanej
wartości zasobu CPU1, a pożądana wartość to 50%, liczba replik zostanie podwojona, ponieważ
sufit (1 × 90/50) to 2. Faktyczna implementacja tej zależności jest bardziej skomplikowana,
ponieważ musi ona uwzględniać wiele kapsuł uruchomionych jednocześnie, różne rodzaje
wskaźników, a także liczne przypadki brzegowe i zmieniające się wartości. Na przykład, jeśli
określono wiele wskaźników, HPA oblicza każdy z nich niezależnie i proponuje wartość będącą
największą spośród nich. Po wykonaniu obliczeń, finalnym wynikiem jest pojedyncza liczba
całkowita, określająca pożądaną liczbę replik, które utrzymują mierzoną wartość poniżej
pożądanej wartości granicznej.
Wskaźniki standardowe
Własne wskaźniki
W tej kategorii znajdują się wskaźniki, które opisują zasoby niebędące częściami klastra
Kubernetesa. Załóżmy, że dysponujesz kapsułą, która konsumuje komunikaty z chmurowej
usługi kolejkowania. W takich sytuacjach może zaistnieć konieczność wyskalowania liczby
kapsuł konsumujących na podstawie rozmiaru kolejki. Taki wskaźnik będzie uzupełniany za
pomocą zewnętrznej wtyczki do wskaźników, podobnej do własnych wskaźników.
Wybór wskaźnika
Zapobieganie migotaniu
Opóźniona reakcja
Udostępnianie w Knative
Udostępnianie w Knative (omówione w sekcji „Budowanie w Knative” w rozdziale 25.)
pozwala na zastosowanie jeszcze bardziej zaawansowanych technik skalowania
horyzontalnego. Do tych zaawansowanych funkcji możemy zaliczyć skalowanie do zera,
w którym zbiór kapsuł wspierających usługę może zostać wyskalowany w dół do zera, a
w górę tylko w momencie zajścia określonego zdarzenia (np. żądania przychodzącego).
Takie rozwiązanie działa, ponieważ mechanizm Knative jest zbudowany na bazie siatki
usług (ang. service mesh) Istio, która zapewnia przezroczyste, wewnętrzne usługi proxy
dla kapsuł. Udostępnianie w Knative daje podstawę do stworzenia frameworku typu
headless, w celu osiągnięcia jeszcze bardziej elastycznego i szybkiego skalowania
horyzontalnego, które wykracza poza standardowe mechanizmy Kubernetesa.
Szczegółowe omówienie mechanizmu udostępniania w Knative wykracza poza ramy tej
książki, ponieważ wciąż jest to dość młody projekt i z pewnością zasługuje na osobną
publikację. Więcej linków do zasobów Knative znajdziesz w sekcji „Więcej informacji” na
końcu tego rozdziału.
Jak widzieliśmy w rozdziale 2., każdy kontener w kapsule może określić swoje parametry
requests i limits dotyczące procesora i pamięci, które będą miały wpływ na miejsce
przypisania kapsuł. W pewnym sensie, ustawienia requests i limits kapsuły tworzą kontrakt
pomiędzy kapsułą a planistą — pewna ilość zasobów jest gwarantowana — inaczej kapsuła nie
zostanie rozplanowana. Ustawienie parametru requests pamięci na zbyt niską wartość
spowoduje ciaśniejsze upakowanie kapsuł, co w konsekwencji może doprowadzić do błędów, a
nawet wywłaszczenia zadań z uwagi na brak pamięci. Jeśli parametr limits procesora jest
ustawiony zbyt nisko, zadania nie będą wykonywane wydajnie. Z drugiej strony, określenie zbyt
wysokiego parametru requests spowoduje marnotrawstwo zasobów. Niezwykle ważne jest, aby
określić parametr requests tak dokładnie, jak to możliwe, ponieważ ma on wpływ na
wykorzystanie klastra i efektywność skalowania horyzontalnego. Zobaczmy, jak wspomaga nas
w tym VPA.
metadata:
name: random-generator-vpa
spec:
selector:
matchLabels:
app: random-generator
updatePolicy:
updateMode: “Off”
Określa sposób, w jaki VPA wdraża zmiany. Tryb Initial pozwala na przypisanie żądań
zasobów tylko podczas czasu tworzenia kapsuły — nie później. Domyślny tryb Auto pozwala
na przypisanie zasobów do kapsuł w czasie tworzenia, ale dodatkowo pozwala aktualizować
kapsuły w czasie ich istnienia, wywłaszczając i rozplanowując je ponownie. Wartość Off
wyłącza automatyczne aktualizacje w kapsułach, pozwalając na sugerowanie wartości
zasobów. Jest to rodzaj „jazdy próbnej”, która pozwala na określenie właściwego rozmiaru
kontenera, ale bez stosowania go bezpośrednio.
Definicja VPA może obejmować strategię wykorzystania zasobów, która wpływa na sposób
obliczania zasobów rekomendowanych (np. ustawiając dolne i górne ograniczenia zasobów dla
każdego kontenera).
updateMode: Initial
W tym trybie VPA idzie o krok dalej. Poza działaniami wykonywanymi przez komponent
polecający, zostaje włączany także moduł dopuszczający, który wdraża rekomendacje
jedynie do nowo tworzonych kapsuł. Na przykład, jeśli kapsuła jest skalowana ręcznie za
pomocą HPA lub aktualizowana przez obiekt Deployment, tudzież wywłaszczana albo
restartowana z dowolnej przyczyny, wartości żądania zasobu kapsuły są aktualizowane
przez kontroler dopuszczający VPA.
Kontroler ten jest modyfikującą wtyczką dopuszczającą, która przesłania żądania nowych
kapsuł, pasujących do selektora etykiety VPA. Ten tryb nie doprowadzi do restartu
działającej kapsuły, ale wciąż może spowodować zakłócenia, ponieważ dochodzi do zmiany
żądań zasobów dla nowo tworzonych kapsuł. Efektem tego jest możliwość zmiany miejsca
rozplanowania kapsuły. Co więcej, istnieje możliwość, że po zastosowaniu zalecanych żądań
zasobów kapsuła zostanie umieszczona na innym węźle, co może doprowadzić do
nieoczekiwanych konsekwencji. To nie wszystko — kapsuła w ogóle może nie zostać
rozplanowana, jeśli klaster nie będzie miał dostatecznie dużej pojemności.
updateMode: Auto
Nie będziemy teraz wchodzić w szczegóły tego zagadnienia, ponieważ VPA jest wciąż w fazie
rozwoju (beta) i może się jeszcze zmienić. Z pewnością jest to funkcja, która ma potencjał, aby
istotnie usprawnić zużycie zasobów.
Autoskalowanie klastra
Wzorce przedstawione w tej książce dotyczą głównie prymitywów i zasobów Kubernetesa.
Wzorce te są przydatne zwłaszcza dla programistów korzystających z klastrów Kubernetesa,
które istnieją i są już skonfigurowane, co z reguły jest zadaniem o charakterze operacyjnym.
Skoro w tym rozdziale zajmujemy się elastycznością i skalowaniem obsługi obciążenia, w tym
miejscu krótko zajmiemy się mechanizmem autoskalowania klastra Kubernetesa (ang. Cluster
Autoscaler).
Jedną z zasad pracy w chmurach jest korzystanie z zasobów na zasadzie „płać za tyle, ile
zużyjesz” (ang. pay-as-you-go). Z usług chmurowych możemy korzystać wtedy, gdy jest to
potrzebne, i tylko w takim zakresie, jaki jest faktycznie niezbędny. CA może wchodzić w
interakcję z dostawcami chmurowymi w trakcie działania Kubernetesa, żądając dostarczenia
dodatkowych węzłów w szczycie ruchu, a także wyłączając niezagospodarowane węzły, gdy
ruchu nie ma, redukując w ten sposób koszt infrastruktury. O ile HPA i VPA wykonują
skalowanie na poziomie kapsuły, zapewniając elastyczność w ramach klastra, CA zapewnia
skalowalność na poziomie węzła, dając nam elastyczną pojemność klastra.
API klastra
Wszyscy istotni dostawcy chmurowi wspierają CA Kubernetesa. Taki efekt udało się
osiągnąć dzięki wtyczkom dostarczonym przez dostawców chmurowych, co niestety
doprowadziło do blokady dostawcy i niespójności w zakresie wsparcia CA. Na szczęście
został stworzony projekt API klastra Kubernetesa, którego celem jest dostarczenie API do
tworzenia, konfiguracji i zarządzania klastrem. Wszyscy główni dostawcy publicznych i
prywatnych chmur, takich jak AWS, Azure, GCE, vSphere i OpenStack, wspierają tę
inicjatywę. Dzięki temu CA może być uruchamiany na instalacjach typu on-premise.
Sercem API klastra jest kontroler uruchomiony w tle, dla którego istnieje kilka
niezależnych implementacji, takich jak kontroler Kubermatic czy operator machine-api
autorstwa OpenShift. Warto śledzić rozwój API klastra, ponieważ w przyszłości może ono
stać się podstawą autoskalowania klastrów.
CA wykonuje dwie główne operacje: dodaje nowe węzły do klastra lub usuwa je z klastra.
Przeanalizujmy proces realizowania tych operacji.
Dodawanie nowego węzła (skalowanie w górę)
Jeśli Twoja aplikacja ma zmienne obciążenie (jest mocno zajęta w ciągu dnia, w weekendy
lub w sezonie świątecznym, ale znacznie mniej obciążona przez resztę czasu), musisz
zapewnić różnego rodzaju pojemność w różnym czasie. Oczywiście zawsze można kupić
stałą, dostatecznie dużą pojemność u dostawcy chmurowego, aby zapewnić pełne wsparcie
dla szczytowego obciążenia, ale opłacanie pełnej pojemności w czasie małego obciążenia
zmniejsza korzyści ze stosowania dostawców chmurowych. To właśnie wtedy CA staje się
naprawdę przydatny.
Gdy kapsuła jest skalowana horyzontalnie lub wertykalnie, zarówno ręcznie, jak i za
pomocą mechanizmów HPA/VPA, repliki są przypisywane do węzłów o pojemności na tyle
dużej, aby spełnić oczekiwane żądania dotyczące CPU i pamięci. Jeśli w klastrze nie ma
węzła o pojemności, która spełni wszystkie wymagania kapsuły, zostanie ona oznaczona
mianem nieplanowalnej (ang. unschedulable), pozostając w stanie oczekiwania, dopóki
węzeł nie zostanie znaleziony. CA obserwuje takie kapsuły, aby sprawdzać, czy dodanie
nowego węzła nie spełniłoby żądań kapsuł. Jeśli odpowiedź jest twierdząca, dochodzi do
zmiany rozmiaru klastra i rozmieszczenia oczekujących kapsuł.
CA nie potrafi rozszerzyć klastra o dowolny węzeł — musi on należeć do jednej z
dostępnych grup węzłów, na których klaster jest uruchomiony. W związku z tym
przyjmujemy założenie, że wszystkie maszyny dostępne w tej samej grupie węzłów mają te
same pojemność i etykiety, a także uruchamiają te same kapsuły, określone w lokalnych
plikach manifestów lub obiektach DaemonSet. To założenie jest konieczne dla CA, aby móc
określić, ile dodatkowej pojemności dla kapsuł wniesie nowy węzeł do klastra.
Jeśli wiele grup węzłów spełnia potrzeby oczekujących kapsuł, można skonfigurować CA
tak, aby wybór grupy węzłów odbywał się zgodnie z jedną ze strategii, nazywanych
ekspanderami. Ekspander może rozszerzyć grupę węzłów o dodatkowy węzeł, porządkując
je według najmniejszego kosztu, najmniejszej straty zasobów, największej liczby
rozlokowany kapsuł lub po prostu losowo. Po zakończeniu skutecznego procesu selekcji
węzła, nowa maszyna zostanie dostarczona przez dostawcę w przeciągu kilku minut, po
czym nastąpi jej rejestracja w serwerze API jako nowego węzła Kubernetesa, gotowego do
hostowania oczekujących kapsuł.
Usuwanie węzła (skalowanie w dół)
Skalowanie w dół węzłów lub kapsuł bez zakłócania usługi zawsze wymaga więcej uwagi i
wykonania bardziej starannego procesu kontroli. CA wykonuje proces skalowania w dół,
jeśli nie ma potrzeby wykonania skalowania w górę, a węzeł jest oznaczony jako
niepotrzebny. Węzeł jest kwalifikowany do skalowania w dół, jeśli spełnia następujące
warunki:
Więcej niż połowa pojemności jest nieużyta, tzn. suma całego żądanego CPU i pamięci
wszystkich kapsuł na węźle zajmuje mniej niż 50% alokowalnej pojemności zasobu.
Wszystkie przenaszalne kapsuły na węźle (kapsuły, które nie są uruchomione lokalnie za
pomocą plików manifestów lub kapsuły utworzone przez obiekty DaemonSet) mogą być
umieszczone w innych węzłach. Aby to umożliwić, CA wykonuje symulację planowania,
identyfikując przyszłe lokalizacje wszystkich kapsuł, które zostaną wywłaszczone.
Ostateczne położenie kapsuł zostanie ustalone przez planistę i może się różnić od
zaplanowanego, niemniej symulacja zapewni, że dla kapsuł znajdzie się wolne miejsce.
Nie ma żadnych powodów, aby powstrzymać usuwanie węzłów (np. gdy węzeł jest
wykluczany ze skalowania w dół za pomocą adnotacji).
Nie ma kapsuł, które nie mogą być przemieszczone (np. kapsuły z obiektem
PodDisruptionBudget, którego nie da się spełnić, kapsuły z pamięcią lokalną, z
adnotacjami zapobiegającymi wywłaszczeniu, tworzone bez kontrolera czy wreszcie
kapsuły systemowe).
Wszystkie te warunki są sprawdzane, aby mieć pewność, że nie zostanie żadna kapsuła,
która nie może być uruchomiona na innym węźle. Jeśli wszystkie powyższe warunki są
spełnione przez jakiś czas (domyślnie 10 minut), węzeł jest kwalifikowany do usunięcia.
Węzeł jest usuwany przez oznaczenie go jako nieplanowalnego i przeniesienie wszystkich
kapsuł do innych węzłów.
Rysunek 24.3 podsumowuje sposób, w jaki CA wchodzi w interakcję z dostawcą chmury i
Kubernetesem w celu skalowania węzłów klastra.
Jak pewnie zauważyłeś, skalowanie kapsuł i węzłów to procesy odrębne, ale komplementarne.
HPA i VPA potrafią analizować wskaźniki użycia i zdarzenia, a także skalować kapsuły. Jeśli
pojemność klastra jest zbyt mała, CA pozwoli na jej zwiększenie. CA jest przydatny także wtedy,
gdy w klastrze pojawiają się nieregularności w jego obciążeniu, z uwagi na zadania wsadowe,
powtarzające się, testy ciągłej integracji lub inne zadania, które wymagają tymczasowego
zwiększenia pojemności. W ten sposób możemy zwiększać i zmniejszać pojemność, uzyskując
istotne oszczędności w kosztach infrastruktury chmurowej.
Poziomy skalowania
W tym rozdziale omówiliśmy różne sposoby skalowania wdrożonych rozwiązań w celu
spełnienia ich potrzeb dotyczących zasobów. Choć większość z przedstawionych operacji może
wykonać człowiek, takie podejście nie jest zgodne z „chmurowym” sposobem myślenia.
Zarządzanie rozbudowanymi i rozproszonymi systemami wymaga automatyzacji powtarzalnych
czynności. Preferowane podejście opiera się na automatyzacji skalowania i pozostawieniu
ludziom zadań, z którymi Operator Kubernetesa sobie jeszcze nie poradzi.
Dostrajanie aplikacji
Na poziomie najbardziej szczegółowym możemy skorzystać z mechanizmu dostrajania aplikacji.
Nie omawialiśmy go w tym rozdziale, ponieważ nie jest związany bezpośrednio z
Kubernetesem. Mimo to warto pamiętać, że dostrojenie działania aplikacji w kontenerze
pozwala na najlepsze użycie zasobów. Ta aktywność nie jest wykonywana przy każdym
skalowaniu usługi, ale za to musi być wykonana zanim aplikacja zostanie umieszczona w
środowisku produkcyjnym. W przypadku środowisku uruchomieniowego Java oznacza to
właściwe dopasowanie puli wątków w celu najlepszego użycia dostępnego czasu procesora, a
następnie dostrojenie różnych obszarów pamięci, takich jak sterta, czy rozmiaru stosu wątków.
Dostosowanie tych wartości jest wykonywane za pomocą zmian konfiguracji, a nie zmian w
kodzie aplikacji.
Natywne aplikacje kontenerowe korzystają ze skryptów startowych, które potrafią obliczyć
dobre wartości domyślne np. dla liczby wątków czy rozmiaru pamięci aplikacji, bazując na
zaalokowanych zasobach kontenera, a nie na pełnej pojemności klastra. Zastosowanie takich
skryptów stanowi doskonały pierwszy krok. Kolejny krok polega na wykorzystaniu technik i
bibliotek takich jak Adaptive Concurrency Limits (adaptacyjne limity współbieżności) Netfliksa.
Dzięki tej bibliotece aplikacja potrafi dynamicznie obliczyć limity współbieżności, dokonując
samoprofilowania i adaptacji. Jest to swego rodzaju wewnątrzaplikacyjne autoskalowanie, które
ogranicza konieczność ręcznego dostrajania usług.
Dostrajanie aplikacji potrafi doprowadzić do regresji, podobnych do tych powstających w
wyniku modyfikacji kodu. Z tego względu musi po nim nastąpić faza testów. Na przykład zmiana
rozmiaru sterty aplikacji może doprowadzić do powstawania błędu OutOfMemory, z którym
horyzontalne skalowanie sobie nie poradzi. Z drugiej strony, skalowanie kapsuł wertykalne lub
horyzontalne, tudzież dostarczenie większej liczby węzłów nie będzie tak efektywne, jeśli Twoja
aplikacja nie skorzysta z zasobów przypisanych do kontenera we właściwy sposób. W związku z
tym dostrajanie na tym poziomie ma wpływ na wszystkie inne metody i jednocześnie może też
generować zakłócenia. Bez względu na wszystko, dostrajanie powinno się wykonać co najmniej
raz, aby uzyskać optymalne zachowanie aplikacji.
Autoskalowanie klastra
Techniki skalowania opisane przy okazji HPA i VPA dostarczają elastyczność w zakresie
pojemności klastra. Możesz stosować je wtedy, gdy jest wystarczająco dużo miejsca w ramach
klastra Kubernetesa. CA wprowadza elastyczność także na poziomie pojemności klastra. CA
uzupełnia pozostałe metody skalowania, ale mechanizm ten może być także stosowany
całkowicie niezależnie. Nie ma znaczenia, skąd pojawia się zapotrzebowanie na dodatkową
pojemność, a także dlaczego może pojawić się pojemność nieużywana. Nie obchodzi go to, czy
ma do czynienia z operatorem-człowiekiem, czy też z autoskalowaniem, które zmienia profile
robocze. Mechanizm może rozszerzyć klaster, aby zapewnić żądaną pojemność, lub skurczyć
klaster, aby oszczędzić zasoby.
Dyskusja
Elastyczność i różne techniki skalowania stanowią obszar, w którym Kubernetes wciąż
intensywnie się rozwija. W HPA dodano ostatnio właściwą obsługę wskaźników, a VPA w całości
znajduje się w fazie eksperymentalnej. Ponadto, wraz z popularyzacją modelu programowania
typu serverless, skalowanie do zera i szybkie skalowanie stały się niezwykle ważne. Mechanizm
udostępniania w Knative stanowi dodatek do Kubernetesa, który zapewnia podstawę
rozwiązania problemu skalowania do zera. Pokrótce opisaliśmy go w punkcie „Udostępnianie w
Knative” w tym rozdziale, a także opisujemy go w rozdziale kolejnym, w punkcie „Budowanie w
Knative”. Knative i stojące za nim siatki usług zmieniają się szybko, wprowadzając niezwykle
ekscytujące, nowe, natywne prymitywy chmurowe. Obserwujemy tę przestrzeń z niezwykłą
uwagą — również i Tobie zalecamy uważną obserwację projektu Knative.
Dysponując specyfikacją pożądanego stanu systemu rozproszonego, Kubernetes potrafi
utworzyć go i zarządzać nim po utworzeniu. Potrafi także dbać o jego stabilność i uodparniać
na błędy, stale monitorując i przywracając go do pełnej sprawności w razie problemów,
zapewniając, że aktualny stan pasuje do idealnego (pożądanego). Choć stabilny i odporny
system jest wystarczająco dobry dla wielu obecnie istniejących aplikacji, Kubernetes idzie o
krok dalej. Mały, ale właściwie skonfigurowany system oparty na Kubernetesie nie polegnie pod
wpływem ogromnego obciążenia — zamiast tego zacznie skalować kapsuły i węzły. W świetle
czynników zewnętrznych, system sam będzie stawał się coraz większy i silniejszy, a nie
mniejszy i słabszy, co świetnie świadczy o właściwościach Kubernetesa.
Więcej informacji
Przykład Elastycznego Skalowania: http://bit.ly/2HwQa6V
Dopasuj rozmiar swoich kapsuł przy użyciu wertykalnego autoskalowania kapsuł:
http://bit.ly/2WInN9l
Autoskalowanie w Kubernetesie — lekcja 1.: http://bit.ly/2U0XoGa
Horyzontalne autoskalowanie kapsuł: http://bit.ly/2r08Row
Algorytm HPA: http://bit.ly/2Fh35Xb
Horyzontalne autoskalowanie kapsuł — poradnik: http://bit.ly/2FlUSRH
API wskaźników Kubernetesa i klienci: https://github.com/kubernetes/metrics/
Wertykalne autoskalowanie kapsuł: http://bit.ly/2Fixzbn
Konfiguracja wertykalnego autoskalowania kapsuł: http://bit.ly/2HyI0eb
Propozycja wertykalnego autoskalowania kapsuł: http://bit.ly/2OfAOnW
Repozytorium GitHub wertykalnego autoskalowania kapsuł: http://bit.ly/2BDnAMZ
Autoskalowanie klastra w Kubernetesie: http://bit.ly/2TkNQl9
Adaptacyjne limity współbieżności: http://bit.ly/2JuXxxx
FAQ autoskalowania klastrów: http://bit.ly/2Cum0NH
API klastrów: http://bit.ly/2D133T9
Kontroler maszyny Kubermatic: http://bit.ly/2VeTqae
Operator API maszyn OpenShift: https://github.com/openshift/machine-api-operator/
Knative: https://cloud.google.com/knative/
Knative — obsługa Twoich usług typu serverless: https://red.ht/2HvenKZ
Poradnik Knative: https://github.com/redhat-developer-demos/knative-tutorial
Problem
Wszystkie dotychczas omówione wzorce dotyczyły funkcjonowania aplikacji w Kubernetesie.
Dowiedzieliśmy się, jak tworzyć i przygotowywać aplikacje, aby były dostosowane do
natywnych chmur. Co jednak począć z samym procesem budowania aplikacji? Typowe podejście
polega na budowaniu obrazów kontenerów poza klastrem, umieszczaniu ich w rejestrze i
odwoływaniu się do nich w ramach deskryptorów wdrożeń Kubernetesa. Budowanie w samym
klastrze ma jednak sporo zalet.
Jeśli pozwala na to polityka firmy, dysponowanie jednym klastrem do wszystkiego przynosi
wiele korzyści. Budowanie i uruchamianie aplikacji w jednym miejscu zauważalnie zmniejsza
koszty utrzymania. Ułatwia także planowanie pojemności i zmniejsza narzut na zasoby
platformy.
Z reguły systemy ciągłej integracji (CI — ang. Continuous Integration), takie jak Jenkins,
pomagają w budowaniu obrazów. Budowanie z zastosowaniem systemu CI stanowi problem
planowania, związany z koniecznością efektywnego znalezienia wolnych zasobów dla zadań
budowania. Sercem Kubernetesa jest wysoce skomplikowany planista, który doskonale
sprawdza się w przypadku tego rodzaju wyzwania.
Przechodząc dalej do ciągłego dostarczania (CD — ang. Continuous Delivery), gdzie
koncentrujemy się nie na budowaniu, a uruchamianiu kontenerów, wykonanie tej operacji w
tym samym klastrze znacząco ułatwia przejście. Załóżmy, że w podstawowym obrazie,
używanym we wszystkich aplikacjach, znaleziono nową podatność na zagrożenia związane z
bezpieczeństwem. Tuż po naprawieniu błędu przez Twój zespół, musisz przebudować wszystkie
obrazy aplikacji, które zależą od tego obrazu, a następnie zaktualizować aplikacje za pomocą
nowego obrazu. W przypadku zaimplementowania wzorca Budowniczy Obrazów klaster potrafi
wykonać obie operacje — zbudowanie obrazu i wdrożenie go. Dzięki temu ponowne wdrożenie
może być wykonane automatycznie, zaraz po zmianie obrazu bazowego. W podrozdziale
„Budowanie w OpenShift” zobaczysz, jak zautomatyzować ten proces w OpenShift.
Rozwiązanie
Jednym z najstarszych i najbardziej dojrzałych sposobów budowania obrazów w klastrze
Kubernetesa jest wykorzystanie podsystemu budowania OpenShift. Podsystem ten udostępnia
wiele metod budowania obrazów. Jedną z nich jest Source-to-Image (S2I), opiniowana metoda
tworzenia obrazów za pomocą tzw. budowniczych. Więcej na jej temat znajdziesz w
podrozdziale „Budowanie w OpenShift”.
Budowanie w OpenShift
Red Hat OpenShift to korporacyjna dystrybucja Kubernetesa. Poza wszystkim, co jest zawarte
w standardowej wersji Kubernetesa, OpenShift dodaje kilka funkcji korporacyjnych, takich jak
zintegrowany rejestr obrazów kontenerów, wsparcie dla pojedynczego logowania (ang. single
sign-on), nowy interfejs użytkownika, a także natywny mechanizm budowania obrazów. OKD2
(dawniej znany pod nazwą OpenShift Origin) stanowi otwartą wersję społecznościową, która
zawiera wszystkie funkcje OpenShift.
Source-to-Image (S2I)
Metoda przyjmuje kod źródłowy aplikacji i tworzy uruchamialny artefakt za pomocą
budowniczego obrazów dostosowanego do języka, po czym umieszcza obrazy w
zintegrowanym rejestrze.
Budowanie obrazów Dockera
Metoda korzysta z pliku Dockerfile, a także z katalogu kontekstu, tworząc obraz na takiej
samej zasadzie, co demon Dockera.
Budowanie potokowe
Własne wersje
Metoda daje pełną kontrolę nad sposobem tworzenia obrazu. Dzięki własnym wersjom
możesz utworzyć obraz w ramach kontenera budowania i umieścić go w rejestrze.
Źródła informacji wejściowych dla procesu budowania mogą być następujące:
Git
Repozytorium określone za pomocą zdalnego adresu URL, z którego pobierany jest kod
źródłowy.
Dockerfile
Plik Dockerfile, przechowywany bezpośrednio jako część zasobu konfiguracji wersji.
Obraz
Inny obraz kontenera, z którego pliki są przenoszone do obecnej wersji. Ten rodzaj źródła
pozwala na tworzenie wersji łańcuchowych (listing 25.2).
Secret
Zanim przyjrzymy się obiektowi BuildConfig, musimy zrozumieć dwa zasadnicze pojęcia
związane z OpenShift.
ImageStream to zasób OpenShift, który odnosi się do co najmniej jednego obrazu kontenera.
Zasób ten przypomina repozytorium Dockera, które również może zawierać wiele obrazów z
różnymi oznaczeniami. OpenShift wiąże faktyczny, oznaczony obraz z zasobem
ImageStreamTag, dzięki czemu ImageStream (repozytorium) ma listę odwołań do obiektów
ImageStreamTag (oznaczonych obrazów). Po co wprowadzono tę dodatkową abstrakcję? Dzięki
temu OpenShift może emitować zdarzenia w momencie aktualizacji obrazu dla danego obiektu
ImageStreamTag w rejestrze. Obrazy są tworzone podczas budowania lub gdy obraz jest
umieszczany w wewnętrznym rejestrze OpenShift. Dzięki temu procesy budowania i wdrożenia
mogą nasłuchiwać te zdarzenia i wyzwalać tworzenie nowych wersji lub rozpoczęcie wdrożenia.
Kolejnym ważnym pojęciem jest wyzwalacz (ang. trigger), który może być potraktowany jak
swego rodzaju słuchacz zdarzeń. Jednym z wyzwalaczy jest imageChange, który reaguje na
zdarzenie opublikowane z powodu zmiany obiektu ImageStreamTag. W odpowiedzi taki
wyzwalacz może np. uruchomić przebudowanie innego obrazu lub ponowne wdrożenie kapsuł z
wykorzystaniem tego obrazu. Więcej na temat wyzwalaczy i różnych dostępnych rodzajów (poza
wyzwalaczem imageChange) znajdziesz w dokumentacji OpenShift3.
Source-to-Image
Przeanalizujmy pokrótce, jak jest zbudowany budowniczy obrazów S2I. Nie będziemy zajmować
się szczegółami, ale powinniśmy nadmienić, że obraz budowniczego S2I stanowi standardowy
obraz kontenera, który zawiera skrypty S2I, wraz z dwoma koniecznymi poleceniami, które
musimy dostarczyć.
assemble
Ten skrypt jest wywoływany w momencie startu procesu budowania. Jego zadaniem jest
pobranie źródła jednego ze skonfigurowanych wejść, skompilowanie go w razie potrzeby i
skopiowanie powstałych artefaktów do właściwych lokalizacji.
run
Używany jako punkt wejściowy dla tego obrazu. OpenShift wywołuje ten skrypt w
momencie wdrożenia obrazu. Ten skrypt korzysta z wygenerowanych artefaktów, aby
dostarczyć usługi aplikacji.
Listing 25.1 przedstawia prostą wersję S2I Javy z obrazem S2I Javy. Ta wersja przyjmuje źródło
i obraz budowniczego, generując obraz wyjściowy, który jest wysyłany do obiektu
ImageStreamTag. Można uruchomić go za pomocą polecenia oc start-build lub
automatycznie, w momencie zmiany obrazów budowniczego.
apiVersion: v1
kind: BuildConfig
metadata:
name: random-generator-build
spec:
source:
git:
uri: https://github.com/k8spatterns/random-generator
strategy:
sourceStrategy:
from:
kind: DockerImage
name: fabric8/s2i-java
output:
to:
kind: ImageStreamTag
name: random-generator-build:latest
triggers:
- type: ImageChange
Inną metodą zmniejszania czasu budowania jest stosowanie wersji inkrementalnych za pomocą
S2I. Polega to na ponownym użyciu artefaktów stworzonych lub pobranych dla poprzednich
wersji S2I. Z drugiej strony, z reguły całkiem sporo danych jest kopiowanych z poprzednio
wygenerowanych obrazów do bieżących wersji, tak więc korzyści wydajnościowe nie są dużo
większe niż przy stosowaniu lokalnego pośrednika w klastrze, przechowującego zależności.
Kolejną wadą S2I jest fakt, że wygenerowany obraz zawiera całe środowisko do budowania. Nie
tylko zwiększa to rozmiar samego obrazu, ale także zmniejsza bezpieczeństwo, ponieważ
podatności w narzędziach budowniczego mogą również narazić cały obraz.
Aby pozbyć się niepotrzebnych narzędzi budowania, takich jak Maven, OpenShift udostępnia
wersje łańcuchowe, które na wejściu przyjmują wersję zbudowaną przez S2I, generując na
wyjściu wyszczuplony obraz uruchomieniowy. Więcej na ten temat znajdziesz w podrozdziale
„Wersje łańcuchowe”.
Wersje dockerowe
OpenShift obsługuje także wersje dockerowe bezpośrednio w ramach klastra. Wersje
dockerowe funkcjonują po zamontowaniu gniazda demona Dockera bezpośrednio w
budowanym kontenerze, który jest używany w trakcie polecenia Dockera build. Źródłem wersji
Dockera jest plik Dockerfile i katalog przechowujący kontekst. Możesz także skorzystać ze
źródła Image, które odwołuje się do dowolnego obrazu i z którego pliki mogą być skopiowane
do katalogu kontekstu wersji Dockera. Jak wspominamy w kolejnym podrozdziale, ten
mechanizm może być używany — wraz z wyzwalaczami — do wersji łańcuchowych.
Wersje łańcuchowe
Mechanizm wersji łańcuchowej jest przedstawiony na rysunku 25.2. Wersja łańcuchowa składa
się z początkowej wersji S2I, która tworzy artefakt uruchomieniowy, np. binarny plik
wykonywalny. Artefakt jest następnie wyłuskiwany z obrazu uruchomieniowego przez drugą
wersję, zazwyczaj dockerową.
Rysunek 25.2. Wersja łańcuchowa z obsługą S2I do kompilacji i z wersją dockerową przeznaczoną
dla obrazów aplikacji
Listing 25.2 przedstawia konfigurację tej drugiej wersji, która korzysta z pliku JAR
wygenerowanego na listingu 25.1. Obraz, który ostatecznie trafia do obiektu ImageStream o
nazwie random-generator-runtime, może być używany w obiekcie DeploymentConfig do
uruchamiania aplikacji.
metadata:
name: runtime
spec:
source:
images:
- from:
kind: ImageStreamTag
name: random-generator-build:latest
paths:
- sourcePath: /deployments/.
destinationDir: “.”
dockerfile: |-
FROM openjdk:8-alpine
COPY *.jar /
CMD java -jar /∗.jar
strategy:
type: Docker
output:
to:
kind: ImageStreamTag
name: random-generator:latest
triggers:
- imageChange:
automatic: true
from:
kind: ImageStreamTag
name: random-generator-build:latest
type: ImageChange
Źródło obrazu odwołuje się do obiektu ImageStream, który zawiera wynik uruchomienia
wersji S2I, wybierając katalog wewnątrz obrazu, który zawiera skompilowane archiwum JAR.
Źródło pliku Dockerfile dla wersji dockerowej, która kopiuje archiwum JAR z obrazu
ImageStream wygenerowanego przez wersję S2I.
Przebuduj automatycznie, gdy wynikowy obiekt ImageStream dla wersji S2I się zmienia —
po skutecznym wykonaniu S2I w celu skompilowania archiwum JAR.
Zarejestruj słuchacza do aktualizacji obrazów. Wykonuj ponowne wdrożenie, gdy nowy obraz
trafia do obiektu ImageStream.
Budowanie w Knative
Google rozpoczęło projekt Knative w 2018 roku, mając na celu włączenie zaawansowanych
funkcji związanych z aplikacjami do Kubernetesa.
Podstawą działania Knative jest siatka usług (ang. service mesh), taka jak Istio6, która
standardowo dostarcza usługi infrastrukturalne do zarządzania ruchem, prowadzenia
obserwacji i zapewnienia bezpieczeństwa. Siatki usług korzystają z wzorca Przyczepka do
instrumentacji aplikacji i funkcji związanych z infrastrukturą.
Na podbudowie siatki usług Knative dostarcza dodatkowe usługi, przeznaczone głównie dla
programistów aplikacji:
Udostępnianie w Knative
Budowanie w Knative
Jest używane do kompilowania kodu źródłowego aplikacji do postaci obrazów kontenerów
w ramach klastra Kubernetesa. Dodatkowym projektem jest Tekton Pipelines, który
docelowo zastąpi projekt budowania w Knative.
Zarówno Istio, jak i Knative, są zaimplementowane za pomocą wzorca Operator i wykorzystują
obiekty CRD do deklarowania zarządzanych zasobów domen.
Proste wersje
Build CRD stanowi centralny element procesu budowania w Knative. Definiuje konkretne kroki,
które operator budowania w Knative musi wykonać, aby uzyskać zbudowaną wersję. Listing
25.3 przedstawia jego główne składniki.
Atrybut source wskazuje lokalizację kodu źródłowego aplikacji. Źródło może być
repozytorium Gita (jak w tym przykładzie) lub inną zdalną lokalizacją, taką jak Google
Cloud Storage, a nawet dowolnym kontenerem, z którego operator budowania może
wyekstrahować źródło.
Atrybut steps jest konieczny do przekształcenia kodu źródłowego w uruchamialny obraz
kontenera. Każdy krok odwołuje się do obrazu budowniczego, który jest używany do
wykonania kroku. Każdy krok ma dostęp do zamontowanego w katalogu /workspace
wolumenu zawierającego kod źródłowy, używanego także do współdzielenia danych
pomiędzy krokami.
Listing 25.3. Knative obsługuje aplikację w Javie z wykorzystaniem Mavena i Jib
apiVersion: build.knative.dev/v1alpha1
kind: Build
metadata:
name: random-generator-build-jib
spec:
source:
git:
url: https://github.com/k8spatterns/random-generator.git
revision: master
steps:
- name: build-and-push
image: gcr.io/cloud-builders/mvn
args:
- compile
- com.google.cloud.tools:jib-maven-plugin:build
- -Djib.to.image=registry/k8spatterns/random-generator
workingDir: /workspace
Szablony budowania
Listing 25.3 zawiera tylko jeden krok budowania, ale z reguły wersje składają się z wielu
kroków. Można skorzystać z własnego zasobu BuildTemplate, aby skorzystać ponownie z tych
samych kroków w podobnych procesach budowania.
apiVersion: build.knative.dev/v1alpha1
kind: BuildTemplate
metadata:
name: maven-kaniko
spec:
parameters:
- name: IMAGE
description: Nazwa obrazu do utworzenia i wysłania
steps:
- name: maven-build
image: gcr.io/cloud-builders/mvn
args:
- package
workingDir: /workspace
- name: prepare-docker-context
image: alpine
command: [ .... ]
- name: image-build-and-push
image: gcr.io/kaniko-project/executor
args:
- --context=/workspace
- --destination=${IMAGE}
metadata:
name: random-generator-build-chained
spec:
source:
git:
url: https://github.com/k8spatterns/random-generator.git
revision: master
template:
name: maven-kaniko
arguments:
- name: IMAGE
value: registry:80/k8spatterns/random-generator
Dyskusja
W tym rozdziale pokazaliśmy dwa sposoby budowania obrazów kontenerów w ramach klastra.
System budowania OpenShift oferuje jedną z głównych zalet budowania i uruchamiania
aplikacji w tym samym klastrze. Dzięki wyzwalaczom obiektu ImageStream możemy nie tylko
połączyć wiele wersji, ale także wdrożyć ponownie aplikację, jeśli proces budowania
zaktualizuje obraz kontenera. Jest to niezwykle użyteczne zwłaszcza w tych środowiskach, w
których etap budowania poprzedza etap wdrożenia. Lepsza integracja pomiędzy procesami
budowania i wdrożenia jest krokiem naprzód w kierunku świętego Graala ciągłego wdrażania
(CD). Wersje zbudowane za pomocą S2I są uznanym standardem, jednak S2I można obecnie
użyć tylko w ramach dystrybucji OpenShift Kubernetesa.
Budowanie w Knative to kolejna implementacja wzorca Budowniczy Obrazów. Głównym celem
budowania w Knative jest przekształcenie kodu źródłowego w wykonywalny obraz kontenera i
wysłanie go do rejestru, dzięki czemu może być używany przez obiekty Deployment. Kroki te są
wykonywane przez budowniczych obrazów, dostarczanych dla różnych technologii. Budowanie
w Knative nie jest zorientowane na konkretne kroki procesu budowania. W centrum uwagi jest
natomiast cykl życia procesu budowania i sposób jego planowania.
Knative to wciąż (w roku 2019) projekt bardzo młody, który dostarcza elementy konstrukcyjne
dla procesu budowania. Nie ma on większego znaczenia dla użytkownika końcowego, ale jest
zdecydowanie bardziej przydatny dla twórców narzędzi. Możemy spodziewać się opracowania
nowych i modyfikacji istniejących narzędzi, które będą wspierać budowanie w Knative, a także
powstawania projektów, które mogą go zastąpić, zatem praktycznie pewne jest pojawienie się
kolejnych implementacji wzorca Budowniczy Obrazów.
Więcej informacji
Przykłady obiektów ImageBuilder: http://bit.ly/2FpCkkL
Jib: https://github.com/GoogleContainerTools/jib
Img: https://github.com/genuinetools/img
Buildah: https://github.com/projectatomic/buildah
Kaniko: https://github.com/GoogleContainerTools/kaniko
Dokument projektowy budowania w OpenShift: http://bit.ly/2HILD0E
Wieloetapowy plik Dockerfile: http://bit.ly/2YfUY63
Łańcuchowanie wersji S2I: https://red.ht/2Jqzlw9
Wyzwalacze budowania: https://red.ht/2FrDIDj
Specyfikacja Source-to-Image: https://github.com/openshift/source-to-image
Inkrementalne wersje S2I: https://red.ht/2TSGxp9
Knative: https://cloud.google.com/knative/
Budowanie obrazów kontenerów w klastrze Kubernetesa za pomocą Knative:
http://bit.ly/2YJuZUC
Budowanie w Knative: https://github.com/knative/build
Tekton Pipelines: https://github.com/knative/build-pipeline
Knative — budowanie usług typu serverless: https://red.ht/2Oew8Pj
Wprowadzenie do Knctl — prostszy sposób na pracę z Knative: https://ibm.co/2Hwnmvw
Interaktywny tutorial budowania w Knative: http://bit.ly/2OewLZb
Szablony budowania w Knative: https://github.com/knative/build-templates
1 https://cloud.google.com/knative/
2 https://www.okd.io
3 https://red.ht/2FrDIDj
4 http://bit.ly/2CxnnuX
5 http://bit.ly/2CxnnuX
6 https://istio.io/
7 http://bit.ly/2CxnnuX
8 https://github.com/GoogleContainerTools/jib
Posłowie
Wszechobecna platforma
Kubernetes jest obecnie najpopularniejszą platformą do orkiestracji kontenerów. Jest ona
rozwijana i wspierana wspólnie przez wszystkie najważniejsze firmy odpowiedzialne za
tworzenie oprogramowania. Znajdziesz ją w ofercie wszystkich istotnych dostawców rozwiązań
chmurowych. Kubernetes jest obsługiwany w systemach operacyjnych Linux i Windows, a także
we wszystkich istotnych językach programowania. Potrafi orkiestrować i automatyzować
aplikacje stanowe i bezstanowe, zadania wsadowe i okresowe, a także aplikacje serverless.
Kubernetes to nowa warstwa, zapewniająca przenośność aplikacji, będąca wspólnym
mianownikiem wszystkich osób pracujących w chmurze. Jeśli jesteś programistą, który tworzy
aplikacje chmurowe, najprawdopodobniej prędzej czy później Kubernetes stanie się częścią
Twojego życia.
Mieszanka odpowiedzialności
W ostatnich latach coraz więcej niefunkcjonalnych wymagań aplikacji dostarcza się w ramach
natywnych platform chmurowych. Wymagania aplikacji rozproszonych, takie jak alokacja
zasobów, wdrożenie, wykrywanie usług, zarządzanie konfiguracją i zadaniami, izolacja zasobów,
a także weryfikacja kondycji aplikacji, są implementowane przez Kubernetesa. Wraz z
popularyzacją architektury mikrousług, implementacja nawet stosunkowo prostej usługi
wymaga dobrego zrozumienia stosów technologii rozproszonych i podstaw orkiestracji
kontenerów. W rezultacie programista musi bardzo dobrze radzić sobie z nowoczesnymi
językami programowania, aby implementować logikę biznesową, ale powinien równie dobrze
odnajdywać się w natywnych technologiach chmurowych, w celu spełnienia wymagań
niefunkcjonalnych.
Wzorce podstawowe reprezentują reguły, które muszą być spełnione przez aplikacje
konteneryzowane, aby móc korzystać z korzyści oferowanych przez Kubernetesa.
Niezależnie od samej aplikacji i narzuconych zewnętrznych wytycznych, warto stosować
się do tych zasad — w ten sposób zapewnisz, że aplikacje są dostosowane do
automatyzacji z wykorzystaniem Kubernetesa.
Wzorce zachowań opisują mechanizmy komunikacji i interakcji pomiędzy kapsułami a
platformą zarządzającą. Zależnie od rodzaju operacji do wykonania, kapsuła może być
wykonywana jako zadanie wsadowe lub cyklicznie. Może też działać jako usługa demona
lub singleton. Wybór odpowiedniego prymitywu do zarządzania pozwoli Ci uruchamiać
kapsułę z pożądaną skutecznością.
Wzorce strukturalne koncentrują się na utworzeniu struktury i zorganizowaniu kapsuł
dostosowanym do różnych przypadków użycia. Dysponowanie dobrze skonfigurowanymi
kontenerami to nie wszystko — ponowne użycie kontenerów i połączenie ich w kapsuły w
celu osiągnięcia oczekiwanego efektu stanowi kolejny ważny krok do skutecznej
automatyzacji.
Wzorce konfiguracji pozwalają na dostosowanie aplikacji do różnych potrzeb
konfiguracyjnych w chmurze. Każda aplikacja musi być dobrze skonfigurowana i nigdy
jedna konfiguracja nie działa świetnie we wszystkich przypadkach. Analizujemy różne
wzorce — od najpopularniejszych do najbardziej wyspecjalizowanych.
Wzorce zaawansowane zawierają opis zagadnień zaawansowanych, które nie należą do
żadnej z wcześniejszych kategorii. Niektóre z nich, jak Kontroler, są dojrzałe —
Kubernetes jest zbudowany na jego bazie. Inne są wciąż dość nowe i mogą zmienić się do
czasu, gdy zapoznasz się z tą książką. Nie zmienia to faktu, że te wzorce omawiają
kwestie zasadnicze dla każdego programisty natywnych aplikacji chmurowych.
Słowo końcowe
Wszystko, co dobre, ma swój koniec — nie inaczej jest z tą książką. Mamy nadzieję, że jej
lektura stanowiła dla Ciebie przyjemność i udało nam się zmienić sposób, w jaki myślisz o
Kubernetesie. Naprawdę szczerze wierzymy, że Kubernetes i związane z nim pojęcia staną się
kluczowe, jak choćby koncepty programowania obiektowego. Ta książka stanowi naszą próbę
odtworzenia książki Wzorce projektowe autorstwa Gangu Czterech w zakresie orkiestracji
kontenerów. Mamy nadzieję, że to nie koniec, a dopiero początek Twojej przygody z
Kubernetesem.
Miłego kubectlowania!
O autorach
Bilgin Ibryam (@bibryam) jest starszym architektem w firmie Red Hat, członkiem organizacji
Apache Software Foundation, a także aktywnym członkiem wielu projektów otwartego
oprogramowania. Bilgin regularnie bloguje, jest ewangelistą niosącym dobrą nowinę o idei
otwartego oprogramowania. Pasjonuje go blockchain. Jest wytrawnym mówcą. Napisał książkę
Camel Design Patterns. Ma ponad dziesięcioletnie doświadczenie w budowaniu i projektowaniu
skalowalnych i odpornych systemów rozproszonych.
W swojej codziennej pracy Bilgin lubi być mentorem, programować i pełnić rolę lidera w
dużych firmach, tworząc skuteczne rozwiązania oparte na idei otwartego oprogramowania.
Bilgin aktualnie pracuje nad projektami integracyjnymi, zastosowaniem blockchaina w
korporacjach, projektami systemów rozproszonych, mikrousługami i natywnymi aplikacjami
chmurowymi.
Dr Roland Huß (@ro14nd) jest starszym inżynierem oprogramowania w firmie Red Hat.
Wcześniej pracował jako lider techniczny w firmie Fuse Online, ostatnio był członkiem zespołu
serverless pracującego nad Knative. Roland ma ponad 20 lat doświadczenia w tworzeniu
aplikacji w Javie, a ostatnio zakochał się w języku Golang. Nigdy jednak nie zapomniał o swoich
korzeniach, gdy był administratorem systemów. Roland jest aktywnym kontrybutorem
środowiska open source, głównym programistą mostu JMX-HTTP Jolokia, a także niektórych
popularnych narzędzi do budowania aplikacji w Javie, używanych do tworzenia obrazów
kontenerów i wdrażania ich w Kubernetesie i OpenShift. Poza programowaniem, Roland lubi
pisać i występować na konferencjach, opowiadając o swojej pracy.
Kolofon
Zwierzę przedstawione na okładce tej książki to hełmiatka zwyczajna (łac. Netta rufina). Słowo
rufina po łacinie oznacza „czerwonowłosa”. Inną stosowaną nazwą jest „kaczka hełmiasta”.
Tego ptaka można często spotkać na europejskich mokradłach, jak również w Azji Centralnej.
Stopniowo populacje tego gatunku pojawiają się również na północy Afryki i na mokradłach Azji
Południowej.
Dieta kaczki hełmiastej składa się głównie z korzeni, ziaren i roślin wodnych. Kaczki te budują
gniazda w pobliżu bagien i jezior, składając jaja wiosną i latem. Typowy wylęg składa się z 8 –
12 kaczątek. Kaczki hełmiaste są najlepiej słyszalne w okresie godów. Dźwięki wydawane przez
osobniki męskie przypominają bardziej świst niż gwizd, podczas gdy osobniki żeńskie wydają
dźwięki podobne do „wra, wra, wra”.
Autorką ilustracji na okładce jest Karen Montgomery. Ilustracja powstała na podstawie czarno-
białej ryciny z książki British Birds.
Spis treści
1. Przedmowa
2. Wstęp
1. Kubernetes
2. Wzorce projektowe
3. Jak podzieliliśmy tę książkę
4. Dla kogo jest ta książka
5. Czego się nauczysz z tej książki
6. Konwencje formatowania tekstu
7. Zastosowanie przykładowych kodów
8. Podziękowania
3. Rozdział 1. Wprowadzenie
1. Droga do natywnej chmury
2. Rozproszone prymitywy
1. Kontenery
2. Kapsuły
3. Usługi
4. Etykiety
5. Adnotacje
6. Przestrzenie nazw
3. Dyskusja
4. Więcej informacji
4. Część I. Wzorce podstawowe
5. Rozdział 2. Przewidywalne Wymagania
1. Problem
2. Rozwiązanie
1. Zależności uruchomieniowe
2. Profile zasobów
3. Priorytety kapsuł
4. Zasoby projektowe
5. Planowanie pojemności
3. Dyskusja
4. Więcej informacji
6. Rozdział 3. Deklaratywne Wdrażanie
1. Problem
2. Rozwiązanie
1. Ciągłe wdrażanie
2. Stałe wdrażanie
3. Wydanie niebiesko-zielone
4. Wydanie kanarkowe
3. Dyskusja
4. Więcej informacji
7. Rozdział 4. Sonda Kondycji
1. Problem
2. Rozwiązanie
1. Kontrola działania procesu
2. Sonda żywotności
3. Sondy gotowości
3. Dyskusja
4. Więcej informacji
8. Rozdział 5.Zarządzany Cykl Życia
1. Problem
2. Rozwiązanie
1. Sygnał SIGTERM
2. Sygnał SIGKILL
3. Hak postartowy
4. Hak przed zatrzymaniem
5. Inne mechanizmy kontroli cyklu życia
3. Dyskusja
4. Więcej informacji
9. Rozdział 6. Automatyczne Rozmieszczanie
1. Problem
2. Rozwiązanie
1. Dostępne węzły zasobów
2. Oczekiwania zasobów wobec kontenera
3. Zasady rozmieszczenia
4. Proces rozplanowania
5. Przypisanie węzła
6. Przypisanie i rozdzielność kapsuł
7. Skazy i tolerancje
3. Dyskusja
4. Więcej informacji
10. Część II. Wzorce zachowań
11. Rozdział 7. Zadanie Wsadowe
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
12. Rozdział 8. Zadanie Okresowe
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
13. Rozdział 9. Usługa Demona
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
14. Rozdział 10. Usługa Singleton
1. Problem
2. Rozwiązanie
1. Blokada pozaaplikacyjna
2. Blokada wewnątrzaplikacyjna
3. Budżet zakłóceń kapsuły
3. Dyskusja
4. Więcej informacji
15. Rozdział 11. Usługa Stanowa
1. Problem
1. Pamięć trwała
2. Sieć
3. Tożsamość
4. Uporządkowanie
5. Inne wymagania
2. Rozwiązanie
1. Pamięć trwała
2. Sieć
3. Tożsamość
4. Uporządkowanie
5. Inne funkcje
3. Dyskusja
4. Więcej informacji
16. Rozdział 12. Wykrywanie Usług
1. Problem
2. Rozwiązanie
1. Wykrywanie usług wewnętrznych
2. Ręczne wykrywanie usług
3. Wykrywanie usług spoza klastra
4. Wykrywanie usług w warstwie aplikacji
3. Dyskusja
4. Więcej informacji
17. Rozdział 13. Samoświadomość
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
18. Część III. Wzorce strukturalne
19. Rozdział 14. Kontener Inicjalizacji
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
20. Rozdział 15. Przyczepka
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
21. Rozdział 16. Adapter
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
22. Rozdział 17. Ambasador
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
23. Część IV. Wzorce konfiguracyjne
24. Rozdział 18. Konfiguracja EnvVar
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
25. Rozdział 19. Zasób Konfiguracji
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
26. Rozdział 20. Niezmienna Konfiguracja
1. Problem
2. Rozwiązanie
1. Wolumeny Dockera
2. Kontenery inicjalizacji Kubernetesa
3. Szablony OpenShift
3. Dyskusja
4. Więcej informacji
27. Rozdział 21. Szablon Konfiguracji
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
28. Część V. Wzorce zaawansowane
29. Rozdział 22. Kontroler
1. Problem
2. Rozwiązanie
3. Dyskusja
4. Więcej informacji
30. Rozdział 23. Operator
1. Problem
2. Rozwiązanie
1. Definicje własnych zasobów
2. Klasyfikacja kontrolerów i operatorów
3. Tworzenie i wdrażanie operatorów
1. Framework operatorów
2. Kubebuilder
3. Metakontroler
4. Przykład
3. Dyskusja
4. Więcej informacji
31. Rozdział 24. Elastyczne Skalowanie
1. Problem
2. Rozwiązanie
1. Ręczne skalowanie horyzontalne
1. Skalowanie imperatywne
2. Skalowanie deklaratywne
2. Horyzontalne autoskalowanie kapsuł
3. Wertykalne autoskalowanie kapsuł
4. Autoskalowanie klastra
5. Poziomy skalowania
1. Dostrajanie aplikacji
2. Wertykalne autoskalowanie kapsuł
3. Horyzontalne autoskalowanie kapsuł
4. Autoskalowanie klastra
3. Dyskusja
4. Więcej informacji
32. Rozdział 25. Budowniczy Obrazów
1. Problem
2. Rozwiązanie
1. Budowanie w OpenShift
1. Source-to-Image
2. Wersje dockerowe
3. Wersje łańcuchowe
2. Budowanie w Knative
1. Proste wersje
2. Szablony budowania
3. Dyskusja
4. Więcej informacji
33. Posłowie
1. Wszechobecna platforma
2. Mieszanka odpowiedzialności
3. Co omówiliśmy w tej książce
4. Słowo końcowe
5. O autorach
6. Kolofon