Download as pdf or txt
Download as pdf or txt
You are on page 1of 228

Bilgin Ibryam, Roland Huß

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.

— Brendan Burns, współtwórca Kubernetesa


Wstęp
Wraz z rozwojem mikrousług i kontenerów w ostatnich kilku latach zmienił się sposób
projektowania, tworzenia i uruchamiania oprogramowania. Dzisiejsze aplikacje są
optymalizowane pod kątem skalowalności, elastyczności, odporności na błędy i możliwości
wprowadzania w nich zmian. Nowe zasady wymusiły zastosowanie w nowo opracowywanych
architekturach rozmaitych wzorców i praktyk. Naszym celem w tej książce jest wsparcie
programistów w tworzeniu aplikacji natywnych dla środowisk chmurowych, z wykorzystaniem
Kubernetesa jako platformy uruchomieniowej. Zacznijmy od krótkiego zapoznania Czytelnika z
dwoma głównymi tematami poruszanymi w tej książce — Kubernetesem i wzorcami
projektowymi.

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.

W niniejszej publikacji zakładamy, że masz podstawową wiedzę na temat Kubernetesa. W


rozdziale 1. omawiamy podstawowe pojęcia związane z tą technologią, a także kładziemy
podwaliny pod omówienie wzorców w kolejnych rozdziałach.

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.

Jak podzieliliśmy tę książkę


W niniejszej publikacji stosujemy prosty format wzorców. Nie korzystamy z konkretnego języka
ich opisu. Dla każdego wzorca stosujemy następującą strukturę:

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

W tym miejscu omawiamy rozwiązanie problemu za pomocą Kubernetesa. Tutaj podajemy


także odwołania do innych wzorców — stanowiących część danego wzorca lub z nim
powiązanych.

Dyskusja

Tutaj będziemy omawiać wady i zalety rozwiązania w zależności od kontekstu.

Więcej informacji
Na zakończenie przedstawimy dodatkowe źródła nt. danego wzorca, z którymi warto się
zapoznać.

Wzorce przedstawione w tej książce zostały ułożone w następujący sposób:

Część I, „Wzorce podstawowe”, zawiera opis podstawowych pojęć związanych z


Kubernetesem. Znajdziesz tutaj opis podstawowych praktyk i reguł związanych z
tworzeniem aplikacji chmurowych opartych na kontenerach.
Część II, „Wzorce zachowań”, prezentuje wzorce, które korzystają z wzorców
podstawowych, dodając bardziej precyzyjne i szczegółowe techniki związane z
zarządzaniem różnego rodzaju interakcjami pomiędzy kontenerami a platformami.
Część III, „Wzorce strukturalne”, opisuje wzorce związane z organizowaniem kontenerów
w ramach kapsuły (ang. Pod), stanowiącej podstawową jednostkę organizacyjną na
platformie Kubernetes.
Część IV, „Wzorce konfiguracyjne”, przedstawia różne metody konfiguracji aplikacji w
ramach Kubernetesa. Wzorce te są niezwykle granularne (precyzyjne), dlatego znajdziesz
wśród nich dokładne przepisy na powiązanie konfiguracji ze swoimi aplikacjami.
Część V, „Wzorce zaawansowane”, wprowadza szereg zaawansowanych tematów, takich
jak sposoby na rozszerzenie samej platformy czy też tworzenie obrazów kontenerów
bezpośrednio z poziomu klastra.

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.

Dla kogo jest ta książka


Ta książka jest przeznaczona dla programistów, którzy chcą projektować i rozwijać aplikacje
chmurowe na platformie Kubernetes. Naszym zdaniem, jest ona najlepiej dopasowana do tych
Czytelników, którzy mają podstawowe pojęcie na temat kontenerów oraz Kubernetesa i chcą
rozwinąć swoją wiedzę i umiejętności. Do zrozumienia wzorców i przypadków użycia nie jest
jednak wymagana szczegółowa wiedza na temat tajników funkcjonowania Kubernetesa.
Architekci, konsultanci techniczni czy programiści z pewnością zyskają wiele, poznając opisane
w tej książce powtarzalne wzorce.

Ta książka zawiera konkretne opisy przypadków użycia, a także doświadczeń zdobytych w


rzeczywistych projektach. Chcemy pomóc Ci tworzyć lepsze aplikacje chmurowe — a nie
wynajdywać koło na nowo!
Czego się nauczysz z tej książki
W tej książce zawartych jest wiele przydatnych informacji. Niektóre wzorce, na pierwszy rzut
oka, mogą wyglądać jak fragmenty podręcznika do Kubernetesa. Po uważniejszym zapoznaniu
się z treścią książki zauważysz z pewnością, że wzorce są prezentowane pod kątem
konceptualnym, którego próżno szukać w innych publikacjach dotyczących Kubernetesa.
Pozostałe wzorce omawiamy w inny sposób, wprowadzając szczegółowe wytyczne związane z
bardzo konkretnymi problemami, np. w części IV, „Wzorce konfiguracyjne”.
Niezależnie od poziomu szczegółów, na jakim jest opisywany każdy wzorzec, dowiesz się
wszystkiego, co Kubernetes jest w stanie zapewnić w związku z wykorzystaniem tego wzorca.
Przedstawiamy też wiele przykładów ilustrujących różne sytuacje. Wszystkie przykłady zostały
przetestowane, a powiązane z nimi kody źródłowe możesz pobrać zgodnie z instrukcją podaną
w podrozdziale „Zastosowanie kodów źródłowych”.

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.

Konwencje formatowania tekstu


Jak już wspomnieliśmy, wzorce wprowadzają prosty język, w którym występuje wiele odwołań
między poszczególnymi elementami. Aby odwzorować tę sieć wzorców, każdy wzorzec jest
zapisywany za pomocą kursywy (np. Wózek Boczny). Wprowadzając wzorzec o nazwie zgodnej z
pojęciami wprowadzanymi przez Kubernetesa (np. Kontener Inicjalizacji lub Kontroler),
stosujemy formatowanie tylko odwołując się bezpośrednio do wzorca. Tam, gdzie ma to sens,
odwołujemy się także do innych rozdziałów, aby ułatwić poruszanie się po książce.

Stosujemy także następujące zasady formatowania:

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.

Wiele przykładów korzysta z usługi REST o nazwie random-generator, która zwraca


pseudolosowe liczby w momencie wywołania. Została ona specjalnie opracowana na potrzeby
przykładów z tej książki. Również jej kod źródłowy jest zawarty w repozytorium na GitHub, a
obraz kontenera k8spatterns/random-generator jest przechowywany w serwisie Docker Hub.

Dla wygody czytelnika archiwum z kodami źródłowymi do wszystkich przykładów zamieściliśmy


także na serwerze FTP wydawnictwa Helion, pod adresem
ftp://ftp.helion.pl/przyklady/kubewp.zip. Po pobraniu pliku archiwum wystarczy rozpakować go
do wybranego katalogu na swoim dysku.

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.

Wszystkie przykłady są rozpowszechniane na licencji Creative Commons Attribution 4.0 (CC BY


4.0). Kod można wykorzystywać za darmo, także w przypadku udostępniania go i
dostosowywania do własnych projektów, zarówno komercyjnych, jak i niekomercyjnych.
Pamiętaj o odwołaniu się do niniejszej książki, jeśli kopiujesz lub rozpowszechniasz zawarty w
niej kod.

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.

1 Język wzorców, Gdańskie Wydawnictwo Psychologiczne, Gdańsk 2008 — przyp. tłum.

2 Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion,


Gliwice 2010 — przyp. red.

3 Christopher Alexander i jego zespół w swojej pracy zdefiniowali to pojęcie w kontekście


architektury w następujący sposób: „Każdy wzorzec opisuje problem, który pojawia się w
naszym otoczeniu raz za razem. Następnie załączamy kluczowy fragment rozwiązania, który
choćby go zastosować milion razy, nigdy nie zostanie wdrożony dwa razy tak samo”. (Język
wzorców, Christopher Alexander et al., 1977). Naszym zdaniem, ta definicja całkiem dobrze
opisuje wzorce, które przedstawiamy w niniejszej książce, z tą różnicą, że w naszych
rozwiązaniach nie będzie możliwa aż taka zmienność.
4 https://www.martinfowler.com/articles/writingPatterns.html
Rozdział 1. Wprowadzenie
W tym początkowym rozdziale przygotujemy warunki do pracy realizowanej przez resztę
książki, wprowadzając kilka kluczowych kwestii związanych z Kubernetesem, stosowanych przy
projektowaniu i implementowaniu kontenerowych natywnych aplikacji chmurowych (ang.
cloud-native applications). Zrozumienie najważniejszych pojęć, a także związanych z nimi reguł
i wzorców opisanych w tej książce, stanowi klucz do tworzenia rozproszonych,
zautomatyzowanych aplikacji chmurowych.
Lektura tego rozdziału nie jest konieczna do zrozumienia dalej omawianych wzorców.
Czytelnicy zaznajomieni z pojęciami charakterystycznymi dla Kubernetesa mogą pominąć ten
rozdział i przejść do wybranej grupy wzorców.

Droga do natywnej chmury


Najpopularniejszą architekturą aplikacji stosowaną w natywnych platformach chmurowych,
takich jak Kubernetes, jest architektura mikrousług. W ramach tego podejścia złożoność
oprogramowania jest ograniczana przez zastosowanie modularyzacji poszczególnych funkcji
biznesowych. Redukcja złożoności oprogramowania powoduje zwiększenie złożoności
operacyjnej.
Tworzenie aplikacji mikrousługowej zasadniczo odbywa się na dwa różne sposoby — albo w
wyniku tworzenia takiej aplikacji od podstaw, albo przez podział aplikacji monolitycznej na
mikrousługi. Większość dobrych praktyk z tym związanych jest opisana w książce Domain-
Driven Design Erica Evansa, wynika zaś z zasad ograniczonych kontekstów (ang. bounded
contexts) i agregatów (ang. aggregates). Ograniczone konteksty pozwalają na podział dużych
modeli na różne komponenty, a agregaty pomagają grupować różne konteksty ograniczone w
moduły, z dobrze określonymi granicami transakcji. Poza rozważaniami związanymi z domeną
biznesową, każdy system rozproszony — niezależnie od tego, czy spełnia założenia mikrousług,
czy też nie — musi poradzić sobie z rozmaitymi kwestiami technicznymi, takimi jak organizacja,
struktura czy zachowanie w czasie życia systemu.

Kontenery i orkiestratory kontenerów (takie jak Kubernetes) dostarczają wiele nowych


prymitywów i abstrakcji w celu rozwiązania problemów typowych dla aplikacji rozproszonych.
W tym miejscu rozważymy różne możliwości umiejscowienia rozproszonego systemu w ramach
Kubernetesa.

W tej książce poddajemy analizie kontenery i interakcje w ramach platformy, traktując


kontenery jak czarne skrzynki. W tym rozdziale chcieliśmy jednak podkreślić jak ważne jest to,
co trafia do kontenerów. Kontenery i natywne platformy chmurowe mogą dać ogromne korzyści
tworzonym przez Ciebie aplikacjom rozproszonym, ale jeśli do kontenerów trafi byle co, w
rezultacie byle co zostanie wyskalowane. Rysunek 1.1 przedstawia zestawienie umiejętności
wymaganych do stworzenia dobrej jakości natywnych aplikacji chmurowych.
Rysunek 1.1. Droga do natywnej chmury

Natywne aplikacje chmurowe wymagają rozważenia różnych reguł i zasad na różnych


poziomach abstrakcji:

Na poziomie kodu — najniższym z możliwych — każda zmienna, którą definiujesz, każda


metoda, którą tworzysz, i każda klasa, której obiekty powołujesz do życia, odgrywa pewną
rolę w utrzymywaniu aplikacji przy życiu przez dłuższy czas. Niezależnie od zastosowanej
technologii kontenerowania czy też platformy do orkiestracji, to zespół deweloperski i
utworzone przez niego elementy systemu będą miały największy wpływ na działanie
systemu. Programiści powinni pisać czysty kod, zawierający odpowiednią ilość testów
automatycznych. Refaktoryzacja kodu powinna być na stałe wpisana w proces jego
tworzenia.
Projektowanie sterowane modelem (DDD) — to podejście do projektowania
oprogramowania nastawione na utrzymanie architektury odwzorowującej świat
rzeczywisty tak blisko, jak to tylko możliwe. To podejście sprawdza się najlepiej w
przypadku języków zorientowanych obiektowo, niemniej osiągnięcie celu stawianego
przez DDD jest możliwe również innymi drogami. Model aplikacji zawierający właściwe
ograniczenia biznesowe i transakcyjne, łatwe do użycia interfejsy i bogate API, stanowi
podstawę sukcesu podczas późniejszej konteneryzacji i automatyzacji.
Architektura mikrousług w krótkim czasie stała się standardem, dostarczając wartościowe
reguły i praktyki przydatne w projektowaniu ciągle zmieniających się, rozproszonych
aplikacji. Zastosowanie tych reguł ułatwi Ci tworzenie aplikacji zoptymalizowanych pod
kątem wprowadzania częstych zmian, skalowania i odporności na awarie — wszystkie te
kwestie są niezwykle ważne w tworzeniu oprogramowania w dzisiejszych czasach.
Kontenery również niezwykle szybko stały się standardowym sposobem na opakowanie i
uruchamianie rozproszonych aplikacji. Tworzenie modularnych, reużywalnych
kontenerów jest niezbędne do uzyskania właściwie przygotowanych natywnych aplikacji
chmurowych. Wraz z rosnącą liczbą kontenerów wzrasta konieczność zarządzania nimi za
pomocą efektywnych narzędzi i technik. Często używane w tej książce określenie
natywnej platformy chmurowej jest związane ze zbiorem reguł, wzorców i narzędzi, które
pozwalają automatyzować konteneryzowane mikrousługi. Pojęcie to będziemy często
stosować odwołując się po prostu do Kubernetesa, będącego najpopularniejszą dostępną
obecnie otwartą, natywną platformą chmurową.

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.

Oczywiście wytworzenie rozproszonej aplikacji będzie wymagać również zastosowania


zwykłych obiektów, niemniej prymitywy Kubernetesa pozwalają na rozwiązanie wielu
rozmaitych problemów spotykanych podczas tworzenia aplikacji. Tabela 1.1 przedstawia, jak
rozmaite sytuacje można zaadresować za pomocą lokalnych i rozproszonych prymitywów.

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.

Tabela 1.1. Lokalne i rozproszone prymitywy

Pojęcie Prymityw lokalny Prymityw rozproszony

Enkapsulacja zachowania Klasa Obraz kontenera

Instancja zachowania Obiekt Kontener

Jednostka ponownego użycia Plik JAR Obraz kontenera

Wzorzec przyczepy (ang.


Kompozycja Klasa A zawiera klasę B
Sidecar)

Klasa A dziedziczy po klasie Obraz rodzica (FROM)


Dziedziczenie
B kontenera

Jednostka wdrożenia Plik JAR/WAR/EAR Kapsuła

Izolacja czasu budowania/czasu Przestrzeń nazw, kapsuła,


Moduł, pakiet, klasa
wykonania kontener

Inicjalizacja Konstruktor Kontener inicjalizacji


Metoda inicjalizacji (np.
Wyzwalacz po inicjalizacji postStart
init)

Metoda niszcząca (np.


Wyzwalacz przed zniszczeniem preStop
destroy)

Metoda finalize(), hak


Mechanizm czyszczenia
zamknięcia Opóźniony kontener1

Wykonanie asynchroniczne i ThreadPoolExecutor,


Job (zadanie)
równoległe ForkJoinPool

Timer,
Zadanie okresowe CronJob (zadanie Crona)
ScheduledExecutorService

Zadanie w tle Wątek demona DaemonSet (zbiór demonów)

System.getenv(), ConfigMap (słownik


Zarządzanie konfiguracją
Properties konfiguracji), Secret

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.

Kontenery inicjalizacji (ang. init containers) stanowią odpowiednik konstruktorów obiektów.


Obiekty DaemonSet przypominają wątki demonów, działających w tle (np. odśmiecacz w Javie).
Kapsuła jest podobna do znanego z frameworku Spring kontekstu odwrócenia kontroli (ang.
Inversion of Control — IoC), w którym wiele obiektów współdzieli zarządzany cykl życia i ma
dostęp do siebie nawzajem.

Podobnych porównań nie będziemy ciągnąć w nieskończoność — warto zapamiętać, że


kontenery odgrywają kluczową rolę w Kubernetesie, a tworzenie modularnych i reużywalnych
obrazów kontenerów skoncentrowanych na realizacji jednego celu jest kluczowe do
długoterminowego sukcesu każdego projektu, a nawet całego ekosystemu kontenerów.
Zastanówmy się teraz — pomijając kwestie techniczne związane z obrazem kontenera, takie jak
umożliwienie odpowiedniej separacji i tworzenia paczek — co właściwie reprezentuje
pojedynczy kontener i jaki jest jego cel w kontekście aplikacji rozproszonej? Oto kilka
możliwych odpowiedzi:

Obraz kontenera stanowi pojedynczy fragment funkcjonalny systemu, który odpowiada na


jeden zadany problem.
Obraz kontenera należy do jednego zespołu i podlega cyklowi publikacji (wydawniczemu).
Obraz kontenera jest samowystarczalny — definiuje i zarządza zależnościami czasu
wykonania.
Obraz kontenera jest niezmienny — po zbudowaniu nie ulega zmianom, może za to
podlegać konfiguracji.
Obraz kontenera zawiera listę zależności czasu wykonania i wymagań dotyczących
zasobów.
Obraz kontenera ma dobrze zdefiniowane API, za pomocą którego udostępnia swoje
funkcje.
Kontener działa z reguły w ramach pojedynczego procesu uniksowego.
Kontener można bezpiecznie usuwać, a także skalować w górę lub w dół w dowolnym
momencie.

Poza wymienionymi aspektami, zgodny ze standardami obraz kontenera powinien być


modularny. Jego zachowanie powinno podlegać konfiguracji za pomocą parametrów, powinna
także istnieć możliwość użycia go w różnych środowiskach. Niewielkie, modularne i reużywalne
obrazy kontenerów prowadzą do powstania rozbudowanych i niezwykle użytecznych zbiorów,
podobnie jak w przypadku bibliotek w świecie programistycznym.

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.

Kapsuła stanowi niepodzielną jednostkę w Kubernetesie, w ramach której „żyje” Twoja


aplikacja. Z kapsuł nie korzysta się jednak bezpośrednio — do tego celu stosuje się usługi.

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

Oto kilka przykładów, w których etykiety mogą okazać się użyteczne:

Etykiety są używane w obiektach ReplicaSet do utrzymania niektórych kapsuł w


działaniu. Oznacza to, że każda definicja kapsuły musi mieć unikatową kombinację etykiet
do planowania.
Etykiety są używane przez planistę. Planista stosuje je do kolokowania lub
rozpowszechniania kapsuł tylko w węzłach, które spełniają wymagania tych kapsuł.
Etykieta może wyodrębnić logiczną grupę kapsuł i nadać im tożsamość w ramach
aplikacji.
Poza typowymi przypadkami użycia, opisanymi powyżej, etykiety mogą być używane do
przechowywania metadanych. Czasami trudno jest przewidzieć, do czego przyda się dana
etykieta, jednak z reguły warto jest mieć ich wystarczająco dużo, aby móc wyczerpująco
opisać wszystkie aspekty danej grupy kapsuł. Do takich aspektów można zaliczać
przynależność do logicznej grupy w obrębie aplikacji, cechy biznesowe, krytyczność, a
także zależności uruchomieniowe platformy, np. architekturę sprzętową lub preferencje
lokalizacji.
Na późniejszym etapie, wszystkie te etykiety mogą być użyte przez planistę do bardziej
wyrafinowanego planowania lub do zarządzania kapsułami z poziomu wiersza poleceń. Z
drugiej strony, we wszystkim trzeba zachować umiar, nie należy więc tworzyć zbędnych
etykiet. Zawsze można też dodać je na dalszym etapie konfigurowania aplikacji. Usuwanie
etykiet jest zawsze bardziej ryzykowne, ponieważ nie ma łatwej i jasnej metody
weryfikacji tego, do czego dana etykieta jest używana, a także jakie niezamierzone efekty
może przynieść jej usunięcie.

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:

Przestrzeń nazw jest zarządzana jako zasób Kubernetesa.


Przestrzeń nazw udostępnia zasięg dla zasobów takich jak kontenery, kapsuły, usługi i
obiekty ReplicaSet. Nazwy zasobów muszą być unikalne w obrębie przestrzeni nazw, ale
już nie pomiędzy nimi.
Domyślnie, przestrzenie nazw udostępniają zasięg dla zasobów, jednak nic nie
powstrzymuje ich od uzyskiwania dostępu do siebie nawzajem. Na przykład, kapsuła w
przestrzeni nazw deweloperskiej może uzyskać dostęp do kapsuły z przestrzeni
produkcyjnej, o ile tylko jest znany adres IP kapsuły produkcyjnej. Problem ten można
rozwiązać dzięki wtyczkom Kubernetesa zapewniającym izolację na poziomie sieciowym;
uzyskujemy w ten sposób prawdziwą wielopodmiotowość pomiędzy przestrzeniami nazw.
Niektóre zasoby, takie jak same przestrzenie nazw, węzły czy obiekty PersistentVolume,
nie należą do żadnej przestrzeni nazw. Ich nazwy powinny być unikatowe w skali całego
klastra.
Każda usługa Kubernetesa należy do przestrzeni nazw i uzyskuje powiązany adres DNS,
którego przestrzeń nazw ma postać <nazwa_usługi>.
<nazwa_przestrzeni>.svc.cluster.local. W związku z tym nazwa przestrzeni nazw
pojawia się w adresie URI każdej usługi należącej do danej przestrzeni. Należy więc
uważać przy dobieraniu nazw przestrzeni.
Obiekty ResourceQuota dostarczają ograniczenia, które zmniejszają zużycie zasobów w
danej przestrzeni nazw. Dzięki tym obiektom administrator klastra może kontrolować
liczbę obiektów danego typu, które są dopuszczone w przestrzeni nazw. Na przykład,
przestrzeń deweloperska może dopuszczać tylko 5 obiektów ConfigMap, 5 obiektów
Secret, 5 usług, 5 obiektów ReplicaSet, 5 obiektów PersistentVolumeClaim i 10 kapsuł.
Obiekty ResourceQuota mogą ograniczać łączną ilość zasobów obliczeniowych w danej
przestrzeni nazw. W klastrze o parametrach 32 GB RAM i 16 rdzeni możemy zaalokować
połowę zasobów — 16 GB RAM i 8 rdzeni — dla przestrzeni produkcyjnej, 8 GB RAM i 4
rdzenie dla środowiska typu staging, a 4 GB RAM i 2 rdzenie dla środowiska
deweloperskiego i testowego. Możliwość określania ograniczeń zasobów dla grupy
obiektów za pomocą przestrzeni nazw i obiektów ResourceQuota jest bezcenna.

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.

Rysunek 1.4. Mechanizmy Kubernetesa przydatne dla programistów


Wraz z upływem czasu, nowe prymitywy pozwalają na wymyślanie nowych rozwiązań
rozmaitych problemów, a powtarzalne rozwiązania stają się wzorcami. W tej książce, zamiast
opisywać szczegółowo konkretne rodzaje zasobów Kubernetesa, skupimy się na tych jego
aspektach, które sprawdzają się jako wzorce.

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:

Rozdział 2., „Przewidywalne Wymagania”, uzasadnia obecność profilu zasobów w


deklaracji kontenera, a także wyjaśnia, dlaczego trzeba dostosowywać swój kontener do
określonych wymagań.
Rozdział 3., „Deklaratywne Wdrażanie”, przedstawia różne strategie wdrażania aplikacji,
które można zrealizować w sposób deklaratywny.
Rozdział 4., „Sondy Kondycji”, uzasadnia konieczność implementowania w kontenerach
specjalnych API, które pozwolą platformie na obserwowanie aplikacji i zarządzanie nią w
najlepszy możliwy dla jej stanu sposób.
W rozdziale 5., „Zarządzany Cykl Życia”, dowiesz się, czemu kontener powinien mieć
możliwość odczytywania zdarzeń nadchodzących z platformy, a także w jaki sposób
reagować na te zdarzenia.
W rozdziale 6., „Automatyczne Rozmieszczanie”, wprowadzamy wzorzec przydatny w
rozmieszczaniu kontenerów w wielowęzłowym klastrze Kubernetesa.
Rozdział 2. Przewidywalne
Wymagania
Podstawą skutecznego zarządzania i wdrażania aplikacji, a także możliwości jej współistnienia
w ramach współdzielonego środowiska chmurowego, jest zdolność określenia i zadeklarowania
wymagań aplikacji dotyczących zasobów i zależności uruchomieniowych. Wzorzec
Przewidywalne Wymagania określa metodę deklarowania wymagań aplikacji, niezależnie od
tego, czy są to niezbędne zależności uruchomieniowe, czy też wymagania dotyczące zasobów.
Deklarowanie wymagań jest konieczne, aby Kubernetes był w stanie znaleźć właściwe miejsce
dla Twojej aplikacji w ramach klastra.

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.

Zobaczmy zatem, jak zadeklarować zależności uruchomieniowe, zanim przejdziemy do profilów


zasobów.

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).

Listing 2.1. Zależność typu PersistentVolume


apiVersion: v1

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

D:\_____Helion\Wicked Cool Ruby Scripts\01.tif

D:\_____Helion\Wicked Cool Ruby Scripts\01.tif Zależność PVC będzie obecna i powiązana


Planista ustali wymagany przez kapsułę rodzaj wolumenu, a następnie — w zależności od
uzyskanej informacji — podejmie decyzję o umiejscowieniu kapsuły. Jeśli kapsuła potrzebuje
wolumenu, który nie jest dostępny w żadnym węźle klastra, nie zostanie ona w ogóle
rozplanowana. Wolumeny stanowią jeden z przykładów zależności uruchomieniowych, które
mają istotny wpływ na infrastrukturę, na której są uruchamiane kapsuły, a nawet na sam fakt
ich rozplanowania!

Podobna sytuacja ma miejsce, gdy poprosisz Kubernetesa o udostępnienie portu kontenera na


konkretnym porcie hosta, korzystając z własności hostPort. Zastosowanie tej własności
stworzy kolejną zależność uruchomieniową w ramach węzła i ograniczy możliwość
rozplanowania kapsuły. Własność hostPort rezerwuje port na każdym węźle w klastrze i
ogranicza maksymalną liczbę kapsuł w ramach pojedynczego węzła do jednej. Z powodu
konfliktu portów, możliwość skalowania takiej kapsuły zostaje ograniczona do liczby węzłów w
klastrze Kubernetesa.

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.

Listing 2.2. Zależność od obiektu ConfigMap

apiVersion: v1

kind: Pod
metadata:

name: random-generator

spec:

containers:

- image: k8spatterns/random-generator:1.0
name: random-generator

env:

- name: PATTERN

valueFrom:

configMapKeyRef: D:\_____Helion\Wicked Cool Ruby Scripts\01.tif

name: random-generator-config

key: pattern

Obraz2390.PNG Zależność obiektu ConfigMap, która musi być obecna

Obiekty Secret przypominają obiekty ConfigMap, gwarantując przy tym


większe
bezpieczeństwo rozpowszechniania konfiguracji uzależnionych od środowiska w kontenerze1.
Sposób użycia obiektu Secret jest taki sam, jak w przypadku obiektu ConfigMap — w obu
przypadkach tworzymy taki sam rodzaj zależności od kontenera do przestrzeni nazw.

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.

Bazując na sposobie, w jaki zaimplementowałeś swoją aplikację, konieczne jest zdefiniowanie


minimalnej, niezbędnej ilości zasobów, za pomocą własności requests (żądania), a także
maksymalnej dopuszczalnej ilości zasobów, za pomocą własności limits (limity). Każda
definicja kontenera może zawierać ilość pamięci i czasu procesora, określoną jako minimum i
maksimum. Patrząc na ten problem wysokopoziomowo, pojęcia żądań i limitów można
porównać do ograniczeń miękkich i twardych. W przypadku Javy, rozmiar sterty określamy za
pomocą parametrów wiersza poleceń -Xms i -Xmx.

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.

Listing 2.3. Limity zasobów

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

Obraz2413.PNG Wymagania minimalne dla procesora i pamięci

Obraz2422.PNG Górny limit, którego osiągnięcie dopuszczamy w naszej aplikacji

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).

Best-Effort (największe staranie)

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)

Jest to kapsuła o zdefiniowanych parametrach żądań i limitów, w której nie są one


identyczne (co w oczywisty sposób oznacza, że limity są większe od żądań). Taka kapsuła
ma zagwarantowaną pewną pulę zasobów, ale jeżeli jest to możliwe, otrzyma dodatkowe
zasoby, jednak nie większe niż te określone własnością limits. Gdy węzłowi zacznie
brakować zasobów niekompresowalnych, kapsuły Burstable zaczną być wyłączane dopiero
po wyłączeniu wszystkich kapsuł Best-Effort.

Guaranteed (gwarantowane)

Kapsuła tego rodzaju przyjmuje te same wartości parametrów requests i limits. Są to


kapsuły o najwyższym priorytecie i podlegają one wyłączeniu w ostateczności, jeżeli nie ma
już kapsuł typu Best-Effort i Burstable.
Jak widać, szczegóły definicji zasobów mają istotny wpływ na jakość usługi i jednocześnie
kolejkują kapsuły według priorytetów w sytuacji braku zasobów. Definiując wymagania zasobów
w kapsułach, musisz o tym pamiętać.

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.

Listing 2.4. Priorytet kapsuł


apiVersion: scheduling.k8s.io/v1beta1

kind: PriorityClass

metadata:

name: high-priority Obraz2430.PNG

value: 1000 Obraz2437.PNG

globalDefault: false

description: Ta kapsuła ma bardzo wysoki priorytet

---

apiVersion: v1

kind: Pod

metadata:
name: random-generator

labels:

env: random-generator

spec:

containers:

- image: k8spatterns/random-generator:1.0

name: random-generator

priorityClassName: high-priority Obraz2446.PNG

Obraz2454.PNG Nazwa obiektu klasy priorytetu

Obraz2462.PNG Wartość priorytetu obiektu

Obraz2469.PNG Klasa priorytetu użyta w tej kapsule, zdefiniowana zgodnie z zasobem

PriorityClass

W tym przykładzie stworzyliśmy obiekt PriorityClass — obiekt nieprzypisany do żadnej


przestrzeni nazw, który służy do definiowania priorytetu w formie liczby całkowitej.
Zastosowana przez nas konkretna wartość to high-priority, o wartości liczbowej równej
1000. Teraz możemy przypisać priorytet do kapsuł, korzystając z własności
priorityClassName: high-priority. PriorityClass to mechanizm do określania wagi kapsuł
względem siebie; wyższa wartość liczbowa oznacza ważniejsze kapsuły.

Skorzystanie z funkcji priorytetów kapsuł spowoduje zmianę sposobu rozmieszczania kapsuł


przez planistę w węzłach. Przede wszystkim, kontroler dopuszczający kapsuły korzysta z pola
priorityClassName, aby określić wartość priorytetu w przypadku nowych kapsuł. Gdy do
rozmieszczenia jest wiele kapsuł, planista porządkuje kolejkę oczekujących, począwszy od
najwyższego priorytetu. Oczekująca kapsuła o wyższym priorytecie zostanie zawsze
rozplanowana przed tą o niższym priorytecie. Jeżeli nie ma żadnych ograniczeń, które
powstrzymają kapsułę przed rozplanowaniem — zostanie ona poddana rozplanowaniu.

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

Kontener CPU 500m 2 500m 250m 4

Kontener Pamięć 250Mi 2Gi 500Mi 250Mi 4

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

A 500m 500m 500Mi 500Mi 4

B 250m 500m 250Mi 1000Mi 2

C 500m 1000m 1000Mi 2000Mi 2

D 500m 500m 500Mi 500Mi 1

Łącznie 4000m 5500m 5000Mi 8500Mi 9

Oczywiście w rzeczywistych scenariuszach wziętych z życia, bardziej prawdopodobną


przyczyną zastosowania platformy takiej jak Kubernetes, jest duża liczba usług do zarządzania.
Niektóre z nich mogą niedługo zostać wycofane, a inne znajdować się dopiero w fazie
projektowania i rozwoju. Nawet jeśli nasz cel stale się zmienia, bazując na podejściu podobnym
do zaprezentowanego powyżej, możemy obliczyć łączną ilość zasobów wymaganych dla
wszystkich usług dla każdego środowiska.
Pamiętaj, że w różnych środowiskach może występować różna liczba kontenerów. Konieczne
może okazać się miejsce dla mechanizmów autoskalowania, zadań związanych z budowaniem
aplikacji, kontenerami infrastrukturalnymi itd. Dysponując zestawem takich informacji, a także
wiedzą nt. dostawcy infrastruktury, możesz wybrać instancje obliczeniowe optymalne kosztowo,
które zapewnią wymaganą przez Ciebie ilość zasobów.

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.

Co ważne, dzięki profilom zasobów aplikacja może komunikować się z Kubernetesem,


wspierając go w rozplanowywaniu kontenerów i zarządzaniu nimi. Jeśli aplikacja nie dostarcza
parametrów requests ani limits, Kubernetes potraktuje Twoje kontenery jak czarne skrzynki,
których trzeba się pozbyć w momencie zapełnienia klastra. Znacznie lepszym podejściem jest
zdefiniowanie deklaracji zasobów i uniknięcie tego rodzaju sytuacji.
Skoro wiesz już, w jaki sposób podejść do kwestii planowania zasobów, w rozdziale 3. poznasz
kilka sposobów na instalowanie i aktualizowanie aplikacji w Kubernetesie.

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

1 Więcej informacji na temat bezpieczeństwa i obiektów Secret znajdziesz w rozdziale 19.


Rozdział 3. Deklaratywne
Wdrażanie
Sercem wzorca Deklaratywne Wdrażanie jest zasób Deployment Kubernetesa. Ta abstrakcja
enkapsuluje procesy aktualizacji i wycofania (odrzucenia) grupy kontenerów, dzięki czemu ich
wykonanie stanowi powtarzalną i w pełni zautomatyzowaną czynność.

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).

W rozdziale 2. zaobserwowaliśmy, że w celu efektywnego wykonania swojej pracy planista


wymaga dostarczenia wystarczających zasobów przez system hosta, określenia reguł
rozmieszczania, a także kontenerów z dobrze zdefiniowanymi profilami zasobów. Analogicznie,
aby odnieść największe korzyści ze stosowania Wdrożeń, kontenery muszą być zgodne z
zasadami tworzenia natywnych aplikacji chmurowych. Kluczową cechą każdego Wdrożenia jest
możliwość uruchomienia i zatrzymania zbioru kapsuł w przewidywalny sposób. Aby osiągnąć
ten efekt, kontenery muszą nasłuchiwać i respektować zdarzenia cyklu życia (np. SIGTERM —
por. rozdział 5.), a także udostępniać końcówki do kontroli swojej kondycji, co opisujemy w
rozdziale 4.
Jeśli kontener we właściwy sposób zajmie się tymi dwoma zagadnieniami, platforma może
bezpiecznie wyłączyć stare kontenery i zastąpić je zaktualizowanymi instancjami. W takiej
sytuacji, wszystkie pozostałe kwestie związane z procesem aktualizacji można określić w
sposób deklaratywny i wykonać jako jedną, niepodzielną operację z predefiniowanymi krokami i
oczekiwanym wynikiem. Zobaczmy więc, jakie mamy możliwości w zakresie definiowania
aktualizacji kontenerów.

Imperatywne, ciągłe aktualizacje za pomocą kubectl są przestarzałe


Kubernetes obsługuje aktualizacje ciągłe od samego początku istnienia. Pierwsza
implementacja była z założenia imperatywna — klient kubectl instruował serwer o tym,
co ma być zrealizowane w każdym kroku procesu aktualizacji.
Choć polecenie kubectl rolling-update jest wciąż dostępne, jego użycie jest wysoce
niezalecane z następujących przyczyn:
Zamiast opisywać pożądany stan docelowy, polecenie kubectl rolling-update wydaje
polecenia, aby osiągnąć ów stan.
Cała logika orkiestracji używana do podmiany kontenerów i kontrolerów replikacji jest
wykonywana przez polecenie kubectl, które monitoruje API serwera i kontaktuje się z
nim w trakcie wykonywania procesu aktualizacji, przenosząc odpowiedzialność za
ewidentne zadania serwerowe na klienta.
Przeniesienie systemu w pożądany stan może wymagać wykonania więcej niż jednego
polecenia. Wszystkie polecenia muszą być zautomatyzowane i powtarzalne w obrębie
różnych środowisk.
Ktoś inny może przesłonić Twoje zmiany po jakimś czasie.
Proces aktualizacji musi być dobrze udokumentowany i odświeżany w miarę rozwoju
usługi.
Jedynym sposobem na sprawdzenie stanu tego, co zostało wdrożone, jest ręczna
weryfikacja stanu systemu. Czasami bieżący stan systemu nie jest zgodny z
oczekiwaniami, co wymaga interwencji programisty, podjętej na podstawie
dokumentacji wdrożeniowej.
Aby uniknąć tych problemów, wprowadzono obiekt Wdrożenia (Deployment), który
obsługuje aktualizacje deklaratywne, w pełni zarządzane przez backend Kubernetesa.
Deklaratywne aktualizacje mają tak wiele zalet, że wsparcie dla aktualizacji
imperatywnych zostanie ostatecznie wycofane; dlatego w tym wzorcu skupiamy się
jedynie na aktualizacjach deklaratywnych.

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.

Listing 3.1. Wdrożenie ciągłej aktualizacji


apiVersion: apps/v1

kind: Deployment

metadata:

name: random-generator
spec:

replicas: 3 D:\_____Helion\Wicked Cool Ruby Scripts\01.tif


strategy:
type: RollingUpdate

rollingUpdate:

maxSurge: 1 D:\_____Helion\Wicked Cool Ruby Scripts\02.tif

maxUnavailable: 1 D:\_____Helion\Wicked Cool Ruby Scripts\03.tif

selector:

matchLabels:
app: random-generator

template:

metadata:

labels:
app: random-generator

spec:

containers:

- image: k8spatterns/random-generator:1.0

name: random-generator

readinessProbe: D:\_____Helion\Wicked Cool Ruby Scripts\04.tif

exec:

command: [ “stat”, “/random-generator-ready” ]

D:\_____Helion\Wicked Cool Ruby Scripts\01.tif Deklaracja trzech replik. Aby operacja ciągłej

aktualizacji miała sens, konieczne są minimum dwie repliki.

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.

D:\_____Helion\Wicked Cool Ruby Scripts\04.tif Sondy gotowości są niezwykle ważne dla

ciągłego wdrażania, aby zapewnić brak przestojów — nie zapominaj o nich (por. rozdział 4.)!

Strategia RollingUpdate zapewnia, że w trakcie procesu aktualizacji nie będzie przestoju. Za


kulisami, obiekt Deployment wykona podobne operacje, tworząc obiekty ReplicaSet i
zamieniając stare kontenery na nowe. Jednym z usprawnień jest możliwość kontroli tempa
wydania nowych kontenerów. Obiekt Deployment pozwala na sterowanie zakresem dostępnych i
nadmiarowych kapsuł za pomocą pól maxSurge i maxUnavailable. Rysunek 3.1 przedstawia
proces ciągłej aktualizacji.

03_01

Rysunek 3.1. Ciągłe wdrożenie

Aby wyzwolić deklaratywną aktualizację, należy wykonać jedną z trzech operacji:

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

Rysunek 3.2. Stałe wdrażanie z wykorzystaniem strategii Recreate

Zastosowanie strategii Recreate skutkuje ustawieniem parametru maxUnavailable na liczbę


zadeklarowanych replik. Oznacza to, że najpierw zostaną zlikwidowane wszystkie kontenery z
bieżącej wersji, a wszystkie nowe kontenery zostaną uruchomione po wywłaszczeniu starych. W
rezultacie, będziemy mieli do czynienia z pewnym czasem przestoju — po zatrzymaniu
wszystkich kontenerów ze starej wersji, a przed przejściem nowych kontenerów w stan
gotowości do obsługi żądań. Z drugiej strony, unikniemy sytuacji, w której kontenery o różnych
wersjach są uruchomione w tym samym czasie, upraszczając tym samym życie konsumentów
usług, którzy muszą być w stanie korzystać tylko z jednej wersji w danej chwili.

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

Rysunek 3.3. Wydanie niebiesko-zielone

Korzyścią płynącą z zastosowania podejścia niebiesko-zielonego jest obecność tylko jednej


wersji aplikacji obsługującej żądania, co likwiduje konieczność równoległej obsługi obu wersji
przez konsumentów usługi. Wadą tego podejścia jest konieczność posiadania dwukrotnie
większej pojemności, ponieważ przez pewien czas muszą być uruchomione zarówno pojemniki
niebieskie, jak i zielone. Ponadto podczas wykonywania przejścia mogą zaistnieć rozmaite
komplikacje związane z długo działającymi procesami, a także stanem bazy danych.

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

Rysunek 3.4. Wydanie kanarkowe


W Kubernetesie takie rozwiązanie można osiągnąć tworząc nowy obiekt ReplicaSet dla nowej
wersji kontenera (najlepiej z wykorzystaniem obiektu Deployment) i określając niewielką liczbę
replik, które mogą być traktowane jako instancje kanarkowe. W tym momencie usługa powinna
przekierowywać część konsumentów na zaktualizowane instancje kapsuły. Po upewnieniu się,
że wszystko działa jak należy, możemy zastąpić stare obiekty ReplicaSet nowymi. Można
powiedzieć, że wykonujemy kontrolowany i oparty na testach użytkowników, przyrostowy
proces publikacji.

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

Rysunek 3.5. Strategie publikacji i wdrożenia

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.

W czasie, kiedy powstawała ta książka, w środowisku Kubernetesa padła propozycja, aby


dopuścić haki (ang. hooks) w procesie wdrażania. Haki wstępne i końcowe pozwoliłyby na
uruchomienie dowolnych poleceń przed i po wykonaniu strategii wdrażania. Takie polecenia
mogą wykonywać dodatkowe działania w trakcie trwania wdrażania i mogłyby również
doprowadzić do przerwania, ponowienia lub kontynuowania procesu wdrożenia. Te polecenia
stanowią dobry krok w stronę automatyzacji wdrożeń i strategii publikacji. Póki co, pozostaje
nam oskryptowanie procesu aktualizacji na wyższym poziomie w celu zarządzania procesem
aktualizacji usług i zależności za pomocą obiektu Deployment i innych prymitywów omawianych
w tej książce.

Niezależnie od zastosowanej strategii wdrażania, Kubernetes musi wiedzieć, kiedy Twoje


kapsuły są w pełni uruchomione i działają — inaczej nie będzie w stanie wykonywać sekwencji
kroków wymaganej do osiągnięcia docelowego stanu wdrożenia. Kolejny wzorzec omawiany w
rozdziale 4. — Sonda Kondycji — opisuje, w jaki sposób aplikacja może poinformować
Kubernetesa o swoim stanie.

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.

Kontrola działania procesu


Kontrola działania procesu (ang. process health check) stanowi najprostszy przykład
weryfikacji, które Kubelet wykonuje wobec procesów kontenerów. Jeśli procesy kontenerów nie
są uruchomione, mechanizm kontrolny zostanie zrestartowany. Nawet jeśli nie zastosujemy
żadnego dodatkowego mechanizmu, zyskujemy w ten sposób podstawową metodę kontroli
kondycji. Jeśli aplikacja jest w stanie wykrywać wewnętrznie jakiekolwiek problemy i wyłączać
się sama w razie ich wystąpienia, kontrola działania procesu jest jedynym mechanizmem,
którego potrzebujesz. W większości przypadków będą jednak potrzebne dodatkowe
mechanizmy.

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.

Przykład użycia sondy kondycji HTTP jest przedstawiony na listingu 4.1.

Listing 4.1. Kontener z sondą żywotności


apiVersion: v1

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

Sonda HTTP weryfikująca stan końcówki

Odczekaj 30 sekund przed wykonaniem pierwszego testu

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.

Listing 4.2. Kontener z sondą gotowości

apiVersion: v1

kind: Pod
metadata:

name: pod-with-readiness-check

spec:

containers:

- image: k8spatterns/random-generator:1.0
name: random-generator

readinessProbe:
exec:

command: [ “stat”, “/var/run/random-generator-ready” ]

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.

Sondy żywotności i gotowości są nierozłącznie związane z procesem automatyzacji natywnych


aplikacji chmurowych. Frameworki aplikacyjne, takie jak Spring Actuator, sondy WildFly
Swarm, Karaf czy też specyfikacja MicroProfile w Javie, oferują różne implementacje sond
kondycji.

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.

Poza rejestrowaniem informacji w standardowych strumieniach, zawsze dobrze jest rejestrować


zakończenie działania kontenera w pliku /dev/termination-log. To właśnie w tym pliku kontener
może odnotować swoje „ostatnie słowa” przed ostatecznym wyłączeniem. Rysunek 4.1
przedstawia możliwości komunikacji kontenera ze środowiskiem uruchomieniowym.
Rysunek 4.1. Możliwości obserwowania kontenera

Kontenery dostarczają jednolity sposób opakowywania i uruchamiania aplikacji, dzięki czemu


można traktować je jak czarne skrzynki. Z drugiej strony, dowolny kontener, który chce być
„praworządnym obywatelem” natywnej platformy chmurowej, musi dostarczać API do
środowiska uruchomieniowego, za pomocą którego możliwa jest obserwacja jego stanu i
podjęcie odpowiednich działań z nim związanych. Obsługa tego rodzaju możliwości stanowi
kluczowy warunek skutecznej automatyzacji procesów aktualizacji i cyklu życia kontenera, co w
konsekwencji prowadzi do zwiększenia odporności na błędy i poprawy doświadczenia
użytkownika. W praktyce oznacza to, że Twoja skonteneryzowana aplikacja powinna
udostępniać API do weryfikowania stanu aplikacji (żywotności i gotowości).

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ą.

Kolejny wzorzec — Zarządzany Cykl Życia — również dotyczy komunikacji pomiędzy


aplikacjami a warstwą zarządzania Kubernetesa, jednak pod nieco innym kątem. Dowiesz się,
jak aplikacja może zostać poinformowana o ważnych zdarzeniach cyklu życia kapsuły.

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.

Listing 5.1. Kontener z hakiem postartowym

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

- sleep 30 && echo “Obudź się!” > /tmp/postStart_done

Polecenie postStart czeka 30 sekund. Polecenie sleep służy jedynie do zasymulowania


dowolnej operacji wydłużającej start aplikacji. Korzystamy też z pliku wyzwalacza, aby pozostać
w synchronizacji z główną aplikacją, która jest uruchamiana równolegle.

Polecenie postStart jest wykonywane po utworzeniu kontenera, asynchronicznie w odniesieniu


do głównego procesu kontenera. Nawet jeżeli sporą część logiki związanej ze startem i
inicjalizacją aplikacji można zaimplementować w ramach kroków startowych kontenera,
postStart wciąż ma swoje zastosowania. Akcja postStart stanowi wywołanie blokujące, a stan
kontenera ma wartość Waiting (oczekujący) dopóki mechanizm haka postStart nie zostanie
zakończony — wtedy cała kapsuła otrzymuje stan Pending (w oczekiwaniu). W ten sposób, hak
postStart może opóźnić start kontenera, dając czas na inicjalizację jego głównego procesu.

Kolejnym przykładem użycia haka postStart jest uniemożliwienie uruchomienia kontenera,


gdy kapsuła nie spełnia pewnych wymagań. Jeśli hak postStart wykryje błąd i zwróci
niezerową wartość kodu odpowiedzi, proces kontenera zostanie ubity przez Kubernetesa.

Mechanizmy wykonania haków postStart i preStop są podobne do tych opisanych w rozdziale


4. i obsługują następujące operacje:

exec — wykonuje polecenie bezpośrednio w kontenerze,

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.

Hak przed zatrzymaniem


Hak preStop stanowi blokujące wywołanie, wysyłane do kontenera przed jego zakończeniem.
Zasada działania jest taka, jak w przypadku sygnału SIGTERM — hak powinien być używany do
przeprowadzenia bezpiecznego wyłączenia kontenera, gdy reakcja na sygnał SIGTERM nie jest
możliwa. Akcja preStop na listingu 5.2 musi zostać zakończona przed wywołaniem operacji
usunięcia kontenera, wysłanej do środowiska uruchomieniowego kontenera, co spowoduje
wysłanie powiadomienia SIGTERM.

Listing 5.2. Kontener z hakiem preStop

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

Wywołanie końcówki /shutdown działającej w ramach aplikacji.


Choć wywołanie preStop jest blokujące, wstrzymywanie go lub zwrócenie kodu świadczącego o
problemach nie powstrzyma przed usunięciem kontenera i ubiciem procesu. Hak preStop
stanowi wyłącznie wygodną alternatywę dla sygnału SIGTERM, służącą do bezpiecznego
zamykania aplikacji. Hak oferuje te same metody obsługi i gwarancje, co omówiony przed
chwilą hak postStart.

Inne mechanizmy kontroli cyklu życia


W tym rozdziale omówiliśmy haki, które pozwalają na wykonywanie poleceń w momencie
zaistnienia zdarzenia cyklu życia kontenera. Istnieje jeszcze jeden mechanizm, który operuje na
poziomie kapsuły, pozwalając na wykonywanie instrukcji inicjalizacyjnych.

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

Aspekt Haki cyklu życia 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

Polecenie postStart jest Wszystkie kontenery inicjalizacji muszą


Gwarancje wykonywane w tym samym zostać zakończone prawidłowo, zanim
czasowe czasie, co ENTRYPOINT którykolwiek z kontenerów aplikacji może
kontenera zostać uruchomiony

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.

Dostępne węzły zasobów


Klaster Kubernetesa potrzebuje przede wszystkim węzłów o określonej pojemności zasobów,
aby uruchamiać nowe kapsuły. Każdy węzeł ma określoną pojemność przeznaczoną do
uruchamiania kapsuł, a planista zapewnia, że suma zasobów zażądanych przez kapsułę jest
mniejsza niż dostępna do zaalokowania przestrzeń węzła. Podczas zakładania węzła używanego
w całości przez Kubernetesa, jego pojemność można obliczyć za pomocą wzoru z listingu 6.1.

Listing 6.1. Pojemność węzła


Alokowalne zasoby [pojemność dla kapsuł aplikacji] =

Pojemność węzła [pojemność dostępna w ramach węzła]

- Kube-Reserved [demony Kubernetesa, takie jak kubelet, środowisko


uruchomieniowe kontenera]
- System-Reserved [demony system operacyjnego, np. sshd, udev]

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.

Oczekiwania zasobów wobec kontenera


Kolejnym ważnym wymaganiem związanym z efektywnym rozmieszczaniem kapsuł jest
określenie przez kontenery swoich zależności uruchomieniowych i oczekiwań wobec zasobów.
To zagadnienie omówiliśmy szczegółowo w rozdziale 2. Sprowadza się do tego, że kontenery
określają profile zasobów (minimalne i maksymalne), a także zależności środowiskowe
związane z przestrzenią dyskową i portami. To działanie jest konieczne, aby kapsuły były
rozmieszczone w rozsądny sposób w węzłach i mogły działać bez wzajemnych kolizji w
szczytowym obciążeniu.

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.

Zarówno zasady planisty, jak i własne narzędzia planistów, może definiować


tylko administrator w ramach konfiguracji klastra. Jako zwykły użytkownik
możesz odwoływać się tylko do predefiniowanych klastrów.

Listing 6.2. Przykład zasad planisty

“kind” : “Policy”,

“apiVersion” : “v1”,

“predicates” : [

{“name” : “PodFitsHostPorts”},

{“name” : “PodFitsResources”},
{“name” : “NoDiskConflict”},

{“name” : “NoVolumeZoneConflict”},

{“name” : “MatchNodeSelector”},

{“name” : “HostName”}

],

“priorities” : [

{“name” : “LeastRequestedPriority”, “weight” : 2},

{“name” : “BalancedResourceAllocation”, “weight” : 1},


{“name” : “ServiceSpreadingPriority”, “weight” : 2},

{“name” : “EqualPriority”, “weight” : 1}

Predykaty to reguły, które odfiltrowują węzły niespełniające wymagań. Predykat


PodFitsHostPorts rozplanowuje kapsuły, które oczekują przydzielenia odpowiednich portów,
dlatego trafiają one do węzłów, w których te porty są dostępne.

Priorytety to reguły, które sortują dostępne węzły w zależności od preferencji. Na przykład


priorytet LeastRequestedPriority przekazuje wyższy priorytet węzłom o mniejszej ilości
żądanych zasobów.

Pamiętaj, że poza konfiguracją zasad domyślnego planisty, istnieje możliwość uruchomienia


wielu narzędzi tego typu i dania kapsułom wyboru co do tego, które z nich ma je rozmieścić.
Możesz uruchomić kolejną instancję planisty, która jest skonfigurowana inaczej, nadając jej
inną nazwę. W momencie definiowania kapsuły dodaj do jej specyfikacji pole
.spec.schedulerName, zawierające nazwę własnego planisty. Dzięki temu kapsuła zostanie
rozmieszczona tylko za pomocą własnego planisty.
Proces rozplanowania
Kapsuły są przypisywane do węzłów o określonych pojemnościach na podstawie zasad
rozmieszczenia. Dla kompletności wywodu na rysunku 6.1 przedstawiamy — w ogólny sposób —
jak elementy te łączą się ze sobą, a także jakie są główne kroki związane z planowaniem
kapsuły.

Rysunek 6.1. Proces przypisania kapsuły do węzła

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łę.

Listing 6.3. Selektor węzła wymuszający obecność dysku SSD

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.

Listing 6.4. Kapsuła z zadeklarowanym przypisaniem 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 twarde, które wymusza obecność minimum trzech rdzeni (określonych za


pomocą etykiety węzła) w trakcie procesu planowania. Reguła nie jest weryfikowana ponownie
w trakcie działania, nawet jeżeli sytuacja w węźle ulegnie zmianie.

Dopasowanie według etykiet.

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.

Przypisanie i rozdzielność kapsuł


Przypisanie węzła stanowi niezwykle ważny mechanizm planowania i powinien on stanowić
domyślny wybór, jeśli nodeSelector okazuje się niewystarczający. Mechanizm ten pozwala na
ograniczeni węzłów, na których można uruchomić kapsułę, na podstawie dopasowania etykiet
lub pól. Nie jesteśmy jednak w stanie wyrazić zależności między kapsułami, aby określić, gdzie
dana kapsuła powinna być umiejscowiona względem innych. Aby określić sposób
rozmieszczenia kapsuł zwiększający dostępność lub usprawniający latencję, możemy skorzystać
z przypisania i rozdzielności kapsuł.
Przypisanie węzła działa na poziomie węzłów, zaś przypisanie kapsuł nie jest do nich
ograniczone — możemy wyrażać reguły, które obejmują wiele różnych poziomów topologii.
Korzystając z pola topologyKey i dopasowania etykiet, możemy tworzyć niezwykle
wyrafinowane reguły, łączące dziedziny takie jak węzły, szafy (ang. rack), strefy dostawców
platform chmurowych i regiony (listing 6.5).
Listing 6.5. Kapsuła z przypisaniem

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.

Selektor etykiet określający kapsuły, z którymi dana kapsuła ma być kolokowana.

Węzły, na których są uruchomione kapsuły z etykietami confidential=high, powinny


zawierać etykietę security-zone. Kapsuła zdefiniowana w tym miejscu jest rozplanowana do
węzła z taką samą etykietą i wartością.

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.

Listing 6.6. Skażony węzeł


apiVersion: v1

kind: Node
metadata:
name: master
spec:

taints:
- effect: NoSchedule

key: node-role.kubernetes.io/master

Skaza w specyfikacji węzła oznacza go jako niedostępny do rozplanowania z wyjątkiem


sytuacji, gdy kapsuła toleruje daną skazę.
Listing 6.7. Kapsuła, która toleruje skazy węzła

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.

Rysunek 6.2. Rozplanowanie procesów w węzłach a opuszczone zasoby

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

Ta strategia jest używana do wywłaszczania kapsuł, które naruszają reguły przypisania


węzłów.

Niezależnie od wdrożonej polityki, deplanista unika wywłaszczania:

kapsuł krytycznych, oznaczonych adnotacją scheduler.alpha.kubernetes.io/critical-


pod,
kapsuł niezarządzanych przez obiekty ReplicaSet, Deployment lub Job,
kapsuł zarządzanych przez obiekt DaemonSet,
kapsuł, które mają pamięć lokalną,
kapsuł z określonym PodDisruptionBudget, w których przypadku wywłaszczenie
naruszyłoby ich reguły,
kapsuły deplanowania (ang. Deschedule Pod) — w tym celu trzeba jednak oznaczyć taką
kapsułę jako krytyczną.

Oczywiście wszystkie wywłaszczenia uwzględniają poziomy jakości usługi (QoS) kapsuł,


najpierw wybierając kapsuły typu Best-Effort, potem Burstable, a na końcu Guaranteed. Więcej
na ten temat dowiesz się w rozdziale 2.

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

Domyślny planista jest odpowiedzialny za rozmieszczanie nowych kapsuł w węzłach w


ramach klastra i radzi sobie z tym całkiem nieźle. Czasami może zaistnieć konieczność
zmiany listy, kolejności i wag zasad filtrowania, a także priorytetów tego planisty.

Przypisanie i rozdzielność kapsuł


Te reguły pozwalają na określanie zależności od innych kapsuł, np. z uwagi na wymagania
dotyczące latencji, wysokiej dostępności, bezpieczeństwa itd.

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:

Rozdział 7., „Zadanie Wsadowe”, opisuje niezależną, niepodzielną jednostkę pracy,


uruchomioną aż do zakończenia swojego działania.
Rozdział 8., „Zadanie Cykliczne”, zawiera informacje pozwalające na wykonywanie
jednostki pracy na podstawie zdarzenia czasowego.
Rozdział 9., „Usługa Demona”, prezentuje sposób uruchamiania kapsuł skoncentrowanych
na infrastrukturze na określonych węzłach, przed rozmieszczeniem kapsuł aplikacji.
Rozdział 10., „Usługa Singleton”, pokazuje jak zapewnić obecność tylko jednej instancji
usługi w danej chwili, a także zagwarantować jej wysoką dostępność.
Rozdział 11., „Usługa Stanowa”, pozwala na tworzenie rozproszonych aplikacji stanowych
w Kubernetesie i zarządzanie nimi.
Rozdział 12., „Wykrywanie Usług”, objaśnia jak klienci mogą odkrywać instancje
zapewniające dostęp do usług aplikacji.
Rozdział 13., „Samoświadomość”, opisuje mechanizmy używane do introspekcji i
wstrzykiwania do aplikacji zależności.
Rozdział 7. Zadanie Wsadowe
Wzorzec Zadanie Wsadowe jest używany do zarządzania odrębnymi, niepodzielnymi
jednostkami pracy. Bazuje na abstrakcji zadania (Job), która uruchamia krótkotrwałe kapsuły w
stabilny sposób, kontrolując je aż do zakończenia działania w rozproszonym środowisku.

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

command: [ “java”, “-cp”, “/”, “RandomRunner”, “/numbers.txt”,


“10000” ]

Zadanie powinno uruchomić pięć kapsuł i wszystkie powinny zakończyć swe działanie z
sukcesem.

Dwie kapsuły mogą być uruchomione równolegle.

Określenie atrybutu restartPolicy jest obowiązkowe dla zadania.

Podstawową różnicą pomiędzy obiektami Job i ReplicaSet jest definicja atrybutu


.spec.template.spec.restartPolicy. Domyślna wartość dla obiektu ReplicaSet to Always,
która ma sens w przypadku procesów długotrwałych. Wartość Always nie jest dozwolona dla
obiektu Job; dwie dopuszczalne w tym przypadku możliwości to OnFailure lub Never.

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.

Dwa pola, które odgrywają znaczącą rolę w zachowaniu zadania, to:

.spec.completions

Określa ile kapsuł zostanie uruchomionych w celu zakończenia zadania.

.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.

Rysunek 7.1. Zrównoleglone Zadanie Wsadowe z ustaloną liczbą wykonań

Na podstawie tych dwóch parametrów możemy wyróżnić następujące rodzaje zadań:

Zadanie dla pojedynczej kapsuły

Ten rodzaj zadania jest używany, gdy pominiesz atrybuty .spec.completions i


.spec.parallelism lub ustawisz je na wartości domyślne równe 1. Takie zadanie zostanie
uruchomione tylko na jednej kapsule i zostanie zakończone, gdy kapsuła zakończy swe
działanie (z kodem wyjścia równym 0).

Zadanie z ustaloną liczbą wykonań


Jeżeli ustawisz parametr .spec.completions na wartość większą niż 1, konieczne jest
skuteczne wykonanie takiej właśnie liczby kapsuł. Możesz również ustawić wartość
atrybutu .spec.parallelism lub pozostawić ją z wartością domyślną 1. Takie zadanie
zostanie zakończone, gdy zostaną skutecznie wykonane kapsuły w liczbie równej wartości
atrybutu .spec.completions. Listing 7.1 przedstawia ten właśnie tryb w praktyce. Jest to
najlepszy wybór, gdy wiesz, ile elementów należy przetworzyć, a koszt przetworzenia
pojedynczego elementu uzasadnia użycie dedykowanej kapsuły.

Zadanie kolejki roboczej

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.

Jeśli dysponujesz nieograniczonym strumieniem elementów do przetworzenia, zdecydowanie


lepszym wyborem będą obiekty takie jak ReplicaSet.

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 spowoduje utworzenie większej liczby zadań Kubernetesa, co zaowocuje większym


zużyciem zasobów. Jest ona jednak użyteczna, gdy element do przetworzenia stanowi
skomplikowaną operację, którą trzeba rejestrować, śledzić lub skalować niezależnie.

Jedno zadanie — wszystkie elementy 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.

Prymityw Job dostarcza jedynie podstawowe mechanizmy do planowania elementów do


przetworzenia. Dowolna bardziej skomplikowana implementacja musi połączyć prymityw Job z
wsadowym frameworkiem aplikacji (np. w ekosystemie Javy mamy do dyspozycji Spring Batch i
JBeret), aby osiągnąć pożądany efekt.

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

command: [ “java”, “-cp”, “/”, “RandomRunner”, “/numbers.txt”,


“10000” ]
restartPolicy: OnFailure

Specyfikacja Crona, która spowoduje wykonywanie zadania co 3 minuty.

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

Wpis Crontaba do określania planu zadania (np. 0 * * * * w celu wykonywania zadania co


godzinę).

.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

To pole wstrzymuje wszystkie następne wykonania, bez wpływu na wykonania już


uruchomione.

.spec.successfulJobsHistoryLimit i .spec.failedJobsHistoryLimit

Te pola określają — odpowiednio — liczbę zakończonych prawidłowo i nieprawidłowo


zadań, które powinny być przechowywane do późniejszej analizy.
Obiekt CronJob jest bardzo specjalistycznym prymitywem i stosuje się go tylko wtedy, gdy
operacja do wykonania ma wymiar czasowy. Nawet jeśli CronJob nie jest prymitywem ogólnego
przeznaczenia, stanowi doskonały przykład możliwości Kubernetesa w zakresie tworzenia
nowych rozwiązań na bazie już istniejących, a zarazem oferuje wsparcie również dla
niechmurowych przypadków użycia.

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.

Listing 9.1. Zasób DaemonSet


apiVersion: extensions/v1beta1

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;

sleep 30; done”

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.

Atrybut hostPath uzyskuje dostęp bezpośrednio do katalogów węzła.

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:

Domyślnie, obiekt DaemonSet umieszcza jedną kapsułę w każdym węźle. Zachowanie to


możemy ograniczyć, określając podzbiór węzłów za pomocą pola nodeSelector.
Kapsuła utworzona przez obiekt DaemonSet ma określony atrybut nodeName. Dzięki temu
obiekt DaemonSet nie wymaga obecności planisty Kubernetesa do uruchamiania
kontenerów. W ten sposób można skorzystać z obiektu DaemonSet do uruchamiania
komponentów Kubernetesa i zarządzania nimi.
Kapsuły utworzone przez obiekt DaemonSet mogą być uruchomione przed startem
planisty, co pozwala im na bycie uruchomionymi przed wszystkimi innymi kapsułami
umieszczonymi w ramach węzła.
Skoro planista nie jest używany, pole unschedulable węzła nie jest respektowane przez
kontroler DaemonSet.
Kapsuły zarządzane przez obiekt DaemonSet z założenia powinny być uruchamiane tylko
na docelowych węzłach. W związku z tym otrzymują wyższy priorytet i są traktowane
inaczej przez wiele kontrolerów. Na przykład deplanista unika wywłaszczania takich
kapsuł, mechanizm autoskalujący klastra zarządza nimi osobno itd.

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ł.

NodeIP z atrybutem hostPort

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.

Rysunek 10.1 przedstawia sposób osiągnięcia blokady pozaaplikacyjnej za pomocą obiektu


StatefulSet lub kontrolera ReplicaSet z jedną repliką.

Rysunek 10.1. Mechanizm blokady pozaaplikacyjnej

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.

Uruchamiane w kapsułach aplikacje typu singleton z reguły otwierają połączenia do brokerów


komunikatów, relacyjnych baz danych, serwerów plików, czy też innych systemów
uruchomionych w innych kapsułach lub w zewnętrznych systemach. Czasami Twoja kapsuła-
singleton będzie również akceptować połączenia — w tym celu będzie konieczne skorzystanie z
zasobu usługi.

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

Wiele istniejących frameworków rozproszonych korzysta z tego mechanizmu do osiągnięcia


wysokiej dostępności i odporności. Na przykład broker komunikatów Apache ActiveMQ może
być uruchomiony w wysoce dostępnej topologii aktywny-pasywny, w której to źródło danych
dostarcza współdzieloną blokadę. Pierwsza instancja brokera, która zostaje uruchomiona,
pozyskuje blokadę i staje się aktywna. Wszystkie następne uruchomione instancje stają się
pasywne i czekają na zwolnienie blokady. Ta strategia zapewnia, że będzie tylko jedna aktywna
instancja brokera, a ponadto będzie ona odporna na niepowodzenia.

Tę strategię możemy porównać do klasycznego singletona, znanego ze świata obiektowego.


Singleton stanowi instancję obiektu przechowywaną w statycznej zmiennej klasy. W tym
przypadku, klasa jest świadoma bycia singletonem — jest napisana w taki sposób, by
niemożliwe było tworzenie wielu instancji tego samego procesu. W rozproszonych systemach
oznaczałoby to konieczność napisania aplikacji konteneryzowanej tak, aby nie pozwolić na
posiadanie więcej niż jednej instancji w danej chwili, niezależnie od liczby uruchomionych
kapsuł. Aby osiągnąć ten efekt w środowisku rozproszonym, najpierw musimy skorzystać z
implementacji rozproszonej blokady, np. Apache ZooKeeper, HashiCorp Consul, Redisa czy
Etcd.
Typowa implementacja przy użyciu ZooKeepera korzysta z węzłów efemerycznych, które
istnieją w czasie sesji klienta i są usuwane zaraz po jej zakończeniu. Pierwsza instancja usługi,
która zostaje uruchomiona, inicjuje sesję na serwerze ZooKeeper, tworząc węzeł efemeryczny,
który staje się aktywny. Wszystkie pozostałe instancje usługi z tego samego klastra stają się
pasywne i muszą poczekać, aż węzeł efemeryczny zostanie zwolniony. W ten sposób
implementacja oparta na ZooKeeperze upewnia się, że istnieje tylko jedna instancja usługi w
całym klastrze, zapewniając zabezpieczenie pomiędzy węzłami aktywnym i pasywnymi.
W świecie Kubernetesa, zamiast zarządzać klastrem ZooKeepera tylko w celu użycia funkcji
blokady, znacznie lepiej skorzystać z możliwości narzędzia Etcd udostępnianych za pomocą API
Kubernetesa i uruchamianych na węzłach master. Etcd to rozproszony magazyn typu klucz-
wartość, który korzysta z protokołu Raft, aby zarządzać swym replikowanym stanem. Co ważne,
Etcd dostarcza mechanizmy niezbędne do implementacji procesu wyboru lidera, a kilka
bibliotek klienckich ma już zaimplementowaną tę funkcję. W bibliotece Apache Camel jest
dostępne złącze do Kubernetesa, które także udostępnia proces wyboru lidera i możliwości
singletona. Złącze pozwala na skorzystanie z API Kubernetesa w celu wykorzystania obiektów
ConfigMap jako blokady rozproszonej (zamiast bezpośredniego dostępu do API Etcd). W tym
celu złącze polega na mechanizmie optymistycznej blokady Kubernetesa w celu edycji zasobów
takich jak obiekty ConfigMap — tylko jedna kapsuła może modyfikować taki obiekt w danej
chwili.

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.

Implementacja z wykorzystaniem ZooKeepera, Etcd czy innego mechanizmu rozproszonej


blokady będzie podobna do tej opisanej: tylko jedna instancja aplikacji zostanie liderem i
aktywuje się — wszystkie inne są pasywne i oczekują na blokadę. W ten sposób nawet
utworzenie wielu zdrowych i działających kapsuł nie spowoduje aktywacji więcej niż jednej z
nich. Wszystkie pozostałe kapsuły będą oczekiwać na zwolnienie blokady w przypadku, gdy
węzeł master zostanie wyłączony lub zawiedzie w inny sposób.

Budżet zakłóceń kapsuły


O ile Usługa Singleton i mechanizm wyboru lidera starają się ograniczyć maksymalną liczbę
działających w danej chwili instancji usługi, funkcja PodDisruptionBudget działa niejako
pomocniczo i odwrotnie — ograniczając liczbę instancji wyłączonych w danej chwili w celu
utrzymania systemu.

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.

Listing 10.1. PodDisruptionBudget

apiVersion: policy/v1beta1

kind: PodDisruptionBudget

metadata:
name: random-generator-pdb

spec:

selector:

matchLabels:

app: random-generator
minAvailable:

Selektor do zliczania dostępnych kapsuł.

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.

Poza własnością .spec.minAvailable, możemy skorzystać także z opcji


.spec.maxUnavailable, określającą liczbę kapsuł z tego zbioru, które mogą być niedostępne po
wywłaszczeniu. Nie możesz jednak określić obu pól, a PodDisruptionBudget jest stosowany z
reguły do kapsuł zarządzanych przez kontroler. W przypadku kapsuł niezarządzanych przez
kontroler (nazywanych nagimi kapsułami) należy opracować inne ograniczenia.

Ta funkcja jest użyteczna w przypadku aplikacji bazujących na zasadzie kworum, które


wymagają dostępności minimalnej liczby replik działających cały czas w celu zapewnienia
kworum. Przydaje się też, gdy konieczne jest obsłużenie krytycznie dużego ruchu, który nigdy
nie powinien zejść poniżej pewnego progu procentowego łącznej liczby instancji. Jest to kolejny
prymityw, który kontroluje zarządzanie instancjami w czasie działania systemu i wpływa na nie,
co sprawia, że jest wart wspomnienia w tym rozdziale.

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.

Pewnym obejściem tego problemu byłoby zastosowanie współdzielonej pamięci trwałej i


obsługa przez mechanizm wewnątrz aplikacji, który podzieli pamięć na podkatalogi i pozwoli na
jej użycie bez powstawania konfliktów. Choć jest to możliwe, takie podejście powoduje
powstanie pojedynczego punktu podatności na awarię (ang. single point of a failure). Takie
rozwiązanie jest także podatne na błędy, ponieważ podczas zmiany liczby kapsuł w trakcie
skalowania może dojść do wielu problemów związanych z próbami zapobiegnięcia utracie
danych.
Kolejnym obejściem byłoby zastosowanie odrębnego obiektu ReplicaSet (zakładając, że
replicas=1) dla każdej instancji rozproszonej aplikacji stanowej. W tym przypadku każdy
obiekt ReplicaSet otrzymuje swój PVC i dedykowaną pamięć trwałą. Niedogodnością związaną
z takim rozwiązaniem jest duży nakład pracy do wykonania — skalowanie w górę wymaga
utworzenia nowego zbioru definicji obiektów ReplicaSet, PVC lub usług. To podejście nie
pozwala na zastosowanie pojedynczej abstrakcji do zarządzania wszystkimi instancjami
aplikacji stanowej jak jedną.

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.

Przed chwilą omówiliśmy niektóre z typowych wyzwań zarządzania rozproszonymi aplikacjami


stanowymi, a także dość przeciętne sposoby na ich obejście. Teraz przejdziemy do natywnego
mechanizmu obsługi tych wymagań za pomocą prymitywu StatefulSet.

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.

Listing 11.1. Usługa używana do uzyskiwania dostępu do StatefulSet

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.

Odwołanie do obowiązkowej usługi zdefiniowanej na listingu 11.2.


Dwa składniki kapsuł w obiekcie StatefulSet noszą nazwy ng-0 i ng-1.

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.

Do tej pory powiedzieliśmy, że obiekty PVC są tworzone i kojarzone z kapsułami, ale


pominęliśmy w ogóle kwestię obiektów PV. Wynika to z faktu, że obiekty StatefulSet nie
zarządzają w żaden sposób obiektami PV. Pamięć dla kapsuł musi być zapewniona z góry przez
administratora lub dostarczona na żądanie przez dostawcę obiektów PV na podstawie żądanej
klasy pamięci trwałej. Taka pamięć trwała musi być przygotowana na wykorzystanie przez
kapsuły stanowe.

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

Deklaruje usługę typu headless.

Kapsuły bezstanowe utworzone za pomocą obiektu ReplicaSet są traktowane jako identyczne i


nie ma znaczenia, do której z nich trafi żądanie (stąd można skorzystać z równoważenia
obciążenia za pomocą regularnej usługi). W przypadku kapsuł stanowych, różniących się od
siebie, skorzystanie z danej kapsuły może wymagać użycia jej koordynatów.

Usługa typu headless z ustawionym selektorem (.selector.app == random-generator) działa


dokładnie w ten sposób. Tego rodzaju usługa tworzy rekordy Endpoint w serwerze API i wpisy
w usłudze DNS, odpowiedzialne za zwracanie rekordów A (adresów) wskazujących
bezpośrednio na kapsuły obsługujące usługę. Mówiąc krótko, każda kapsuła otrzymuje wpis
DNS, dzięki któremu klienci mogą bezpośrednio uzyskać do niej dostęp w przewidywalny
sposób. Jeśli usługa random-generator należy do przestrzeni nazw default, możesz skorzystać
z kapsuły rg-0 za pomocą w pełni kwalifikowanej nazwy domenowej: rg-0.random-
generator.default.svc.cluster.local, przy czym nazwa kapsuły jest wstawiana na
początku nazwy usługi. To powiązanie pozwala innym składnikom aplikacji klastrowanej (lub
innym klientom) uzyskać w miarę potrzeby dostęp do wybranych kapsuł.
Możemy także wykonać wyszukiwanie wpisu DNS dla rekordów SRV (np. za pomocą polecenia
dig SRV random-generator.default.svc.cluster.local) i wykrywać wszystkie uruchomione
kapsuły, zarejestrowane za pomocą usługi zarządzającej obiektem StatefulSet. Mechanizm
ten pozwala na dynamiczne wykrywanie członków klastra, jeśli oczekuje tego aplikacja
kliencka. Skojarzenie pomiędzy usługą typu headless i obiektem StatefulSet nie jest oparte
jedynie na selektorach — StatefulSet powinien wiązać się z usługą, określając jej nazwę (np.
serviceName: „random-generator”).

Zdefiniowanie pamięci trwałej osiągnięte za pomocą atrybutu volumeClaimTemplates nie jest


obowiązkowe, ale już powiązanie z usługą za pomocą atrybutu serviceName — tak. Usługa
zarządzająca musi istnieć przed utworzeniem obiektu StatefulSet. Jest ona odpowiedzialna za
określenie tożsamości sieciowej zbioru. Jeśli chcesz dodatkowo zrównoważyć obciążenie
pomiędzy swoimi kapsułami stanowymi, możesz utworzyć inne rodzaje usług.

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.

Rysunek 11.1. Rozproszona aplikacja stanowa w Kubernetesie

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).

Jeśli utworzymy obiekt ReplicaSet z wieloma replikami, kapsuły są rozplanowywane i


uruchamiane razem, bez oczekiwania na skuteczne uruchomienie pierwszej (statusy
uruchomienia i gotowości są opisane w rozdziale 4.). Kolejność uruchamiania i gotowości
kapsuł nie jest gwarantowana. Sytuacja wygląda tak samo podczas skalowania w dół obiektu
ReplicaSet (niezależnie od tego, czy wynika to ze zmiany wartości atrybutu replicas czy z
faktu usunięcia). Wszystkie kapsuły należące do obiektu ReplicaSet rozpoczynają proces
zamykania równocześnie, bez zachowania kolejności i zależności między nimi. Takie
zachowanie pozwala szybciej przeprowadzić cały proces, ale w przypadku aplikacji
niestanowych nie zawsze będzie ono pożądane, zwłaszcza jeśli wśród instancji dochodzi do
podziału i rozpowszechniania danych.

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

Przed chwilą opisaliśmy gwarancje sekwencyjnego porządku, zachowanego podczas


skalowania obiektu StatefulSet. W przypadku aktualizacji już działającej aplikacji (np.
przez zmianę elementu .spec.template), obiekty StatefulSet pozwalają na wykonanie
wdrożenia (rolloutu) etapami (np. przy użyciu wydania kanarkowego), co gwarantuje
pewną liczbę działających, niezmienionych instancji w czasie, gdy aktualizacje są wdrażane
na pozostałych instancjach.
Korzystając z domyślnej strategii ciągłej aktualizacji, możesz podzielić instancje
korzystając z atrybutu .spec.updateStrategy.rollingUpdate.partition. Parametr (o
domyślnej wartości 0) wskazuje indeks, pod którym obiekt StatefulSet powinien być
podzielony pod kątem procesu aktualizacji. Po określeniu parametru, wszystkie kapsuły z
indeksem większym lub równym wartości partition zostaną zaktualizowane, podczas gdy
kapsuły z indeksem mniejszym — nie. To stwierdzenie jest prawdziwe nawet gdy kapsuły są
usuwane — Kubernetes odtworzy je z poprzedniej wersji. Funkcja ta może pozwolić na
częściowe aktualizacje klastrowanych aplikacji stanowych (np. zapewniając zachowanie
kworum), a następnie wdrażając zmiany do reszty klastra przez ustawienie wartości
partition z powrotem na 0.
Równoległe wdrożenia

W przypadku ustawienia atrybutu .spec.podManagementPolicy na Parallel, obiekt


StatefulSet uruchamia lub wyłącza wszystkie kapsuły działające równolegle, nie czekając
na uruchomienie i zgłoszenie gotowości tudzież pełne wyłączenie przed przejściem do
kolejnej kapsuły. Jeśli sekwencyjne przetwarzanie nie jest dla Ciebie kluczowe, wybór tej
opcji może przyspieszyć działanie.
Gwarancja At-Most-One

Unikalność stanowi jeden z kluczowych atrybutów instancji aplikacji stanowych.


Kubernetes udostępnia takie gwarancje zapewniając, że żadne dwie kapsuły należące do
tego samego obiektu StatefulSet nie będą miały takiej samej tożsamości i nie będą
przypisane do tego samego PV. Dla porównania, ReplicaSet udostępnia atrybut At-Least-
X-Guarantee dla swoich instancji, który zapewnia minimalną liczbę działających instancji
— ustawienie go na wartość 2 zapewni, że zawsze będą działać minimum dwie instancje.
Nawet jeśli czasami liczba ta zwiększy się, głównym założeniem kontrolera jest
uniemożliwienie zmniejszenie jej poniżej określonego limitu. Liczba replik może za to
zwiększyć się, np. w przypadku podmiany replik lub gdy stara kapsuła nie została w pełni
zakończona. Liczba może zwiększyć się także wtedy, gdy węzeł Kubernetesa staje się
niedostępny, ze stanem NotReady, ale wciąż działają na nim kapsuły. W tym przypadku
kontroler ReplicaSet uruchomiłby nowe kapsuły na zdrowych węzłach, co mogłoby
doprowadzić do uruchomienia większej niż potrzebna liczby kapsuł. Wszystko to ma sens w
przypadku semantyki At-Least-X.
Z drugiej strony, kontroler StatefulSet zapewnia nas, że nie występują duplikaty kapsuł —
stąd gwarancja At-Most-One. Kapsuła nie zostanie uruchomiona ponownie, o ile stara
instancja nie potwierdzi kompletnego zakończenia działania. Gdy węzeł przestaje działać,
następuje rozplanowanie nowych kapsuł na innym węźle, chyba że Kubernetes potwierdzi,
iż kapsuły (a może i cały węzeł) zostały zamknięte. Semantyka At-Most-One wprowadza
takie właśnie reguły.
Oczywiście wciąż istnieje możliwość przełamania tych gwarancji i powstania duplikatów
kapsuł z obiektu StatefulSet, jednak wymaga to interwencji człowieka, na przykład w
przypadku usunięcia nieosiągalnego obiektu zasobu węzła z serwera API, podczas gdy
węzeł fizyczny wciąż działa. Takie działanie powinno być wykonywane tylko wtedy, gdy
węzeł nie działa lub został wyłączony i nie ma na nim uruchomionych procesów kapsuł.
Podobna sytuacja ma miejsce w przypadku siłowego usunięcia kapsuły za pomocą
polecenia kubectl delete pods _<pod>_ --grace-period=0 --force, co spowoduje brak
oczekiwania ze strony Kubeleta na potwierdzenie zatrzymania kapsuły. Takie działanie
automatycznie wyczyści kapsułę z serwera API, co spowoduje uruchomienie kolejnej
instancji kapsuły przez kontroler StatefulSet, co z kolei może doprowadzić do powstania
duplikatów.
Inne sposoby utworzenia singletonów omawiamy szczegółowo w rozdziale 10.

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).

Rysunek 12.1. Wykrywanie usług po stronie klienta

W czasach „po Kubernetesie” wiele spośród niefunkcjonalnych wymagań systemów


rozproszonych, takich jak rozmieszczanie, kontrola kondycji, proces przywracania sprawności
czy izolacja zasobów, wykonywanych jest na poziomie platformy — to samo dotyczy Wykrywania
Usług i równoważenia obciążenia. Jeśli odniesiemy się do definicji używanych w architekturze
zorientowanej na usługi (SOA — ang. Service-Oriented Architecture), instancja dostawcy usługi
musi zarejestrować się w rejestrze usług, równolegle dostarczając jej możliwości. Konsument
usługi ma dostęp do informacji w rejestrze, aby skorzystać z usługi.

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.

Rysunek 12.2. Wykrywanie usług po stronie serwera

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.

Wykrywanie usług wewnętrznych


Załóżmy, że dysponujesz aplikacją webową, którą chcesz uruchomić na Kubernetesie. Tuż po
utworzeniu wdrożenia z kilkoma replikami, planista rozmieści kapsuły w odpowiednich
węzłach, a każda z nich otrzyma adres IP klastra przed uruchomieniem. Jeśli inna usługa
kliencka, z innej kapsuły, zechce skorzystać z końcówek aplikacji webowej, nie będzie w stanie
z góry rozpoznać adresów IP kapsuł dostawcy usługi.

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).

Listing 12.1. Prosta usługa

apiVersion: v1

kind: Service
metadata:

name: random-generator

spec:

selector:
app: random-generator

ports:

- port: 80

targetPort: 8080

protocol: TCP

Selektor dopasowany do etykiet kapsuły

Port, na którym można skontaktować się z usługą

Port, na którym nasłuchują kapsuły


Definicja w tym przykładzie tworzy usługę o nazwie random-generator (nazwa jest istotna dla
późniejszego procesu wykrywania), a także ustawia atrybut type na wartość ClusterIP (jest to
wartość domyślna). Oprócz tego akceptujemy połączenia TCP na porcie 80 i przekierowujemy
na port 8080 dla wszystkich kapsuł pasujących do selektora app: random-generator. Nie ma
znaczenia, kiedy czy jak kapsuły są tworzone — dowolna pasująca kapsuła stanie się celem
trasowania (rysunek 12.3).

Rysunek 12.3. Wykrywanie wewnętrznych usług

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:

Wykrycie za pomocą zmiennych środowiskowych

Gdy Kubernetes uruchamia kapsułę, do zestawu jej zmiennych środowiskowych trafią


wszystkie zmienne należące do dotychczas uruchomionych usług. Nasza usługa random-
generator, która nasłuchuje na porcie 80, zostanie wstrzyknięta do nowo uruchamianej
kapsuły, co widać na przykładzie zmiennych środowiskowych z listingu 12.2. Aplikacja,
która jest uruchomiona w tej kapsule, będzie znać nazwę usługi, z której musi skorzystać —
wystarczy odczytać wskazane zmienne środowiskowe. Taki mechanizm może być
zaimplementowany w aplikacji napisanej w dowolnym języku programowania; łatwo go
emulować poza klastrem Kubernetesa, w trakcie tworzenia i testowania oprogramowania.
Główny problem związany z tym podejściem to kwestia czasowej zależności tworzenia
usługi. Skoro zmienne środowiskowe nie mogą być wstrzykiwane do już uruchomionych
kapsuł, koordynaty usług będą dostępne tylko dla kapsuł uruchomionych po starcie usługi.
To wymaga zdefiniowania usługi przed uruchomieniem kapsuł, które od niej zależą — lub
zrestartowania tychże kapsuł.

Listing 12.2. Związane z usługą zmienne środowiskowe ustawiane automatycznie w kapsule

RANDOM_GENERATOR_SERVICE_HOST=10.109.72.32

RANDOM_GENERATOR_SERVICE_PORT=8080

Wykrywanie za pomocą wyszukiwania w usłudze DNS

Kubernetes uruchamia serwer DNS, z którego wszystkie kapsuły są w stanie automatycznie


korzystać. Co więcej, w momencie tworzenia nowej usługi, automatycznie otrzymuje ona
wpis DNS, którego wszystkie kapsuły mogą natychmiast używać. Zakładając, że klient zna
nazwę usługi, z której chce skorzystać, może to uczynić podając pełną, jednoznaczną nazwę
domenową (FQDN), np. random-generator.default.svc.cluster.local. W tym
przypadku random-generator to nazwa usługi, default to nazwa przestrzeni nazw, svc
oznacza zasób usługi, a cluster.local jest sufiksem danego klastra. Możemy ominąć
sufiks klastra, jeśli chcemy, podobnie jak przestrzeń nazw (o ile korzystamy z usługi z tej
samej przestrzeni).
Mechanizm wykrywania DNS nie ma wad znanych z podejścia opartego na zmiennych
środowiskowych, ponieważ serwer DNS pozwala na wykrywanie wszystkich usług przez
wszystkie kapsuły tuż po zdefiniowaniu usługi. Mimo to wciąż może zaistnieć potrzeba
skorzystania ze zmiennych środowiskowych, aby sprawdzić numer portu, jeśli jest on
niestandardowy lub nieznany konsumentowi usługi.
Poniżej przedstawiamy wysokopoziomowe opisy usług z atrybutem type ustawionym na
wartość ClusterIP, z których korzystają inne typy:

Wiele portów

Pojedyncza definicja usługi może obsługiwać wiele portów źródłowych i docelowych. Na


przykład, jeśli Twoja kapsuła obsługuje zarówno protokół HTTP na porcie 8080, jak
również HTTPS na porcie 8443, nie ma potrzeby definiowania dwóch usług. Pojedyncza
usługa może na przykład udostępnić oba protokoły na portach 80 i 443.

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

W rozdziale 4. nauczyliśmy się definiować sondę gotowości (readinessProbe) dla


kontenera. Jeśli kapsuła ma zdefiniowane testy gotowości, które na niej nie przechodzą,
jest usuwana z listy końcówek usługi, nawet jeśli dopasował ją selektor etykiety.

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).

Wybór wartości ClusterIP

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.

Ręczne wykrywanie usług


Gdy tworzymy usługę za pomocą atrybutu selector, Kubernetes śledzi listę pasujących i
gotowych do działania kapsuł na liście zasobów końcówek. W przykładzie z listingu 12.1 możesz
sprawdzić wszystkie utworzone przez usługę końcówki, używając w tym celu polecenia kubectl
get endpoints random-generator. Zamiast przekierowania połączeń do kapsuł wewnątrz
klastra, możemy przekierować je do zewnętrznych adresów IP i portów. Możemy to osiągnąć
pomijając definicję atrybutu selector usługi i ręcznie tworząc zasoby końcówek (listing 12.3).

Listing 12.3. Usługa bez selektora

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).

Listing 12.4. Końcówki zewnętrznej usługi


apiVersion: v1

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).

Rysunek 12.4. Ręczne wykrywanie usług

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.

Do grupy usług wymagających ręcznej konfiguracji destynacji (celu) możemy zaliczyć tę z


listingu 12.5.

Listing 12.5. Usługa z zewnętrzną destynacją (celem)


apiVersion: v1
kind: Service

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.

Wykrywanie usług spoza klastra


Omówione do tej pory mechanizmy wykrywania usług korzystają z wirtualnego adresu IP, który
wskazuje na kapsuły lub zewnętrzne końcówki. Wirtualny adres IP jest jednak dostępny jedynie
z wnętrza klastra Kubernetesa. Klaster nie jest całym naszym światem; poza połączeniami
wychodzącymi z kapsuł do zasobów zewnętrznych, bardzo często konieczne jest dopuszczenie
operacji odwrotnej — zewnętrzne aplikacje chcą uzyskać dostęp do końcówek dostarczonych
przez kapsuły. Zobaczmy, jak można udostępnić kapsuły klientom funkcjonującym poza
klastrem.
Pierwszym ze sposobów na udostępnienie usługi na zewnątrz klastra jest ustawienie atrybutu
type na wartość NodePort.
Definicja z listingu 12.6 tworzy usługę podobnie jak poprzednio, czyli obsługując kapsuły
pasujące do selektora app: random-generator. Usługa akceptuje połączenia na porcie 80 i
wirtualnym adresie IP, przekierowując żądania na port 8080 wybranej kapsuły. Dodatkowo
rezerwujemy także port 30036 we wszystkich kapsułach, przekierowując połączenia
przychodzące do usługi. Taka operacja sprawia, że usługa jest dostępna wewnętrznie za
pomocą wirtualnego adresu IP, a także zewnętrznie, dzięki dedykowanemu portowi dostępnemu
w każdym węźle.
Listing 12.6. Usługa typu NodePort

apiVersion: v1
kind: Service

metadata:
name: random-generator
spec:

type: NodePort

selector:
app: random-generator

ports:
- port: 80

targetPort: 8080

nodePort: 30036

protocol: TCP

Otwórz port we wszystkich węzłach.


Określ konkretny port (musi być dostępny) lub pomiń ten atrybut, aby port został określony
losowo.
Ta metoda udostępniania usług, przedstawiona na rysunku 12.5, może wydawać się dobrym
podejściem, jednak ma ona swoje wady. Przeanalizujmy jej istotne cechy:
Numer portu
Zamiast określać konkretny numer portu (nodePort: 30036), możesz pozwolić
Kubernetesowi na wybranie dowolnego dostępnego portu.
Reguły zapory sieciowej

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.

Listing 12.7. Usługa typu LoadBalancer


apiVersion: v1

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.

Pole status jest zarządzane przez Kubernetesa i dodaje adres IP Ingressa.

Dysponując taką definicją, zewnętrzna aplikacja kliencka może nawiązać połączenie z


równoważnikiem obciążenia, który wybierze węzeł i zlokalizuje kapsułę. Szczegóły procesu
równoważenia obciążenia i wykrywania usług zmieniają się w zależności od dostawcy chmury.
Niektórzy dostawcy pozwalają na definiowanie adresu równoważnika, a inni nie. Jedni oferują
mechanizmy do zachowywania adresów źródłowych, a drudzy podmieniają je na adres
równoważnika. Szczegóły implementacji będą z pewnością opisane w dokumentacji Twojego
dostawcy chmury.

Można skorzystać z jeszcze jednego rodzaju usług — typu headless


(bezgłowych), dla których nie musimy prosić o dedykowany adres IP. Usługę
typu headless tworzy się ustawiając atrybut clusterIP na wartość None
wewnątrz sekcji spec usługi. W przypadku usług typu headless kapsuły
wspierające są dodawane do wewnętrznego serwera DNS i jako takie są one
najbardziej użyteczne w implementacji usług dla obiektów StatefulSet
(opisanych w rozdziale 11.).

Wykrywanie usług w warstwie aplikacji


W przeciwieństwie do omówionych do tej pory mechanizmów, Ingress nie jest rodzajem usługi,
a osobnym zasobem Kubernetesa, który funkcjonuje na przedzie usług, działając jako
inteligentny router i punkt wejściowy dla całego klastra. Ingress z reguły daje dostęp do usług
oparty na protokole HTTP, za pomocą dostępnego z zewnątrz adresu URL. Oprócz tego
zapewnia równoważenie obciążenia, terminację SSL i wirtualny hosting na bazie nazw. Można
jednak spotkać się też z innymi, specjalistycznymi implementacji Ingressa.
Do działania Ingressa konieczne jest dodanie do klastra minimum jednego kontrolera Ingress.
Prosty Ingress, który udostępnia pojedynczą usługę, przedstawia listing 12.8.
Listing 12.8. Prosta definicja Ingressa

apiVersion: extensions/v1beta1
kind: Ingress

metadata:
name: random-generator
spec:
backend:

serviceName: random-generator
servicePort: 8080

W zależności od infrastruktury, w której funkcjonuje Kubernetes, a także implementacji


kontrolera Ingress, definicja zarezerwuje dostępny na zewnątrz adres IP i udostępni usługę
random-generator na porcie 80. Ta sytuacja nie różni się zbytnio od usługi z atrybutem type:
LoadBalancer, która wymaga podania zewnętrznego adresu IP dla każdej definicji usługi.
Prawdziwa siła Ingressa tkwi w możliwości użycia pojedynczego, zewnętrznego równoważnika
obciążenia i adresu IP do obsługi wielu usług i zmniejszenia kosztów infrastruktury.
Prosta konfiguracja, za pomocą której wiążemy jeden adres IP z wieloma usługami na
podstawie ścieżek adresu HTTP URI, jest przedstawiona na listingu 12.9.
Listing 12.9. Definicja Ingressa z powiązaniami
apiVersion: extensions/v1beta1

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

Dedykowane reguły dla kontrolera Ingress, które pozwalają na rozdzielenie żądań na


podstawie ścieżki żądania.
Przekierowanie wszystkich żądań do usługi random-generator…

…poza ścieżką /cluster-status, która kieruje do innej usługi.


Każda implementacja kontrolera Ingress jest inna, dlatego poza standardową definicją,
kontroler może wymagać podania dodatkowych parametrów konfiguracji, przekazywanych za
pomocą adnotacji. Zakładając, że Ingress jest skonfigurowany prawidłowo, poprzednia definicja
dostarczy równoważnik obciążenia i pozyska zewnętrzny adres IP, który obsługuje dwie różne
usługi pod dwoma różnymi ścieżkami (rysunek 12.7).

Rysunek 12.7. Wykrywanie usług w warstwie aplikacji

Ingress jest najpotężniejszym, a zarazem najbardziej złożonym mechanizmem Wykrywania


Usług w Kubernetesie. Użyteczność tego rozwiązania wynika z możliwości udostępniania wielu
usług pod tym samym adresem IP, gdy wszystkie usługi korzystają z tego samego protokołu
warstwy aplikacji (z reguły jest to HTTP).

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 Najczęściej używany mechanizm


ClusterIP Wewnętrzny
.spec.selector wykrywania wewnętrznego

type: ClusterIP
Ręczny IP Wewnętrzny Wykrywanie zewnętrznego IP
kind: Endpoints

Ręczna pełna type: ExternalName Wykrywanie zewnętrznej nazwy


nazwa Wewnętrzny
.spec.externalName domenowej
domenowa

type: ClusterIP
Usługa typu Wykrywanie na podstawie usługi
.spec.clusterIP: Wewnętrzny
headless DNS bez wirtualnego adresu IP
None

Preferowane dla ruchu innego niż


NodePort type: NodePort Zewnętrzny
HTTP

Wymaga obsługi po stronie


LoadBalancer type: LoadBalancer Zewnętrzny
infrastruktury chmurowej

Ingress kind: Ingress Zewnętrzny Inteligentny mechanizm trasowania


na podstawie protokołu warstwy
aplikacji (z reguły HTTP).

W tym rozdziale w szczegółowy sposób omówiliśmy kluczowe koncepty związane z dostępem do


usług i ich wykrywaniem. Nasza podróż nie kończy się jednak w tym miejscu. Dzięki projektowi
Knative wprowadzono nowe prymitywy bazujące na Kubernetesie, które pomagają twórcom
aplikacji w zaawansowanych aspektach obsługi klientów, budowania aplikacji i obsługi
komunikacji.
W kontekście Wykrywania Usług szczególne znaczenie ma podprojekt Knative serving,
ponieważ wprowadza on nowy zasób usługi, tego samego rodzaju co przedstawione w tym
rozdziale (ale z inną grupą API). Knative serving dostarcza obsługę dla różnych rewizji
aplikacji, umożliwiając bardzo elastyczne skalowanie usług za mechanizmem równoważenia
obciążenia. Nieco więcej na temat tego projektu znajdziesz w punktach „Udostępnianie w
Knative” w rozdziale 24. i „Budowanie w Knative” w rozdziale 25., jednak szczegółowa dyskusja
na jego temat wykracza poza zakres tej książki. W sekcji „Więcej informacji” w rozdziale 24.
znajdziesz linki, dzięki którym dowiesz się więcej na temat Knative.

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

Przede wszystkim należy zwrócić uwagę na fakt, że metadane są wstrzykiwane do kapsuły i


udostępniane lokalnie. Aplikacja nie musi korzystać z klienta i wchodzić w interakcje z API
Kubernetesa. Zobaczmy jak łatwo jest uzyskać dostęp do metadanych za pomocą zmiennych
środowiskowych (listing 13.1).
Listing 13.1. Zmienne środowiskowe z Downward API

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.

Zmienna środowiskowa MEMORY_LIMIT jest ustawiana na podstawie limitu pamięci


kontenera. W tym miejscu nie podajemy faktycznego limitu.

W tym przykładzie korzystamy z pola fieldRef, aby uzyskać dostęp do metadanych na


poziomie kapsuły. Kluczowe informacje, przedstawione w tabeli 13.1, są dostępne dla obiektu
fieldRef.fieldPath zarówno jako zmienne środowiskowe, jak i wolumeny downwardAPI.

Tabela 13.1. Informacje z Downward API dostępne w obiekcie fieldRef.fieldPath

Nazwa Opis

spec.nodeName Nazwa węzła, na którym znajduje się kapsuła

status.hostIP Adres IP węzła, na którym znajduje się kapsuła

metadane.name Nazwa kapsuły

metadata.namespace Przestrzeń nazw, w której kapsuła jest uruchomiona

status.podIP Adres IP kapsuły

spec.serviceAccountName Obiekt ServiceAccount używany dla tej kapsuły

metadata.uid Unikatowy ID kapsuły

metadata.labels[‚klucz’] Wartość etykiety kapsuły o podanym kluczu

metadata.annotations[‚klucz’] Wartość adnotacji kapsuły o podanym kluczu

Podobnie jak w przypadku obiektu fieldRef, możemy skorzystać z obiektu resourceFieldRef,


aby uzyskać dostęp do metadanych kontenera należącego do kapsuły. Te metadane są
ograniczone do kontenera, który jest określony za pomocą atrybutu
resourceFieldRef.container. Jeśli korzystasz z tego mechanizmu jako zmiennej
środowiskowej, domyślnie stosowany jest bieżący kontener. Możliwe wartości kluczy dla
obiektu resourceFieldRef.resource są przedstawione w tabeli 13.2.

Tabela 13.2. Informacje dostępne w obiekcie resourceFieldRef.resource w Downward API

Nazwa Opis

requests.cpu Wartość żądana dla CPU kontenera

limits.cpu Wartość limitów dla CPU kontenera

limits.memory Wartość żądana dla pamięci kontenera

requests.memory Wartość limitów dla pamięci kontenera


Użytkownik może zmienić niektóre metadane, takie jak etykiety czy adnotacje, w trakcie
działania kapsuły. O ile kapsuła nie zostanie zrestartowana, zmienne środowiskowe nie
odnotują takiej zmiany. Z drugiej strony, wolumeny downwardAPI potrafią zauważyć zmiany
wprowadzone do etykiet i adnotacji. Poza pojedynczymi polami opisanymi wcześniej, wolumeny
downwardAPI mogą przechwycić wszystkie etykiety i adnotacje kapsuł do plików za pomocą
odwołań metadata.labels i metadata.annotations. Listing 13.2 przedstawia jak korzystać z
takich wolumenów.
Listing 13.2. Downward API przy użyciu wolumenów

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

Wartości z Downward API mogą być montowane w kapsule jako pliki.

Plik labels zawiera wszystkie etykiety, podane wiersz po wierszu, w formacie


nazwa=wartość. Plik jest aktualizowany w miarę zmian wprowadzanych w etykietach.
Plik annotations przechowuje wszystkie adnotacje w tym samym formacie, co etykiety.

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.

W różnych językach programowania w celu interakcji z API serwera Kubernetesa można


korzystać z licznych bibliotek klienckich, dzięki którym możliwe jest uzyskanie większej ilości
informacji, wykraczających poza zakres udostępniany przez Downward API.

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.

Z jednej strony, kontenery inicjalizacji mają te same możliwości, co kontenery aplikacji:


wszystkie kontenery są częścią tej samej kapsuły, w związku z czym współdzielą limity zasobów,
wolumeny i ustawienia bezpieczeństwa, trafiając do tego samego węzła. Z drugiej strony, nieco
inaczej działają mechanizmy sprawdzania kondycji i obsługi zasobów. Nie występuje mechanizm
sprawdzania gotowości dla kontenerów inicjalizacji, ponieważ wszystkie kontenery inicjalizacji
muszą zakończyć się prawidłowo, zanim proces uruchamiania kapsuły będzie kontynuowany z
kontenerami aplikacji.
Ponadto kontenery inicjalizacji nieco inaczej obsługują obliczanie wymagań dotyczących
zasobów w przypadku planowania, autoskalowania i zarządzania kwotą. Biorąc pod uwagę
kolejność uruchamiania wszystkich kontenerów w kapsule (najpierw sekwencyjnie
uruchamiane są kontenery inicjalizacji, a następnie równolegle wszystkie kontenery aplikacji),
wartości żądań i limitów dla całej kapsuły są ustawiane na najwyższe wartości z obu
następujących grup:

najwyższa wartość żądań/limitów spośród wszystkich kontenerów inicjalizacji,


suma wartości żądań/limitów dla wszystkich kontenerów aplikacji.

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ć.

Kolejnym istotnym aspektem jest możliwość zastosowania separacji zagadnień i stworzenia


kontenerów ograniczonych do osiągnięcia jednego celu. Kontener aplikacji może być utworzony
przez programistę, koncentrującego się wyłącznie na realizacji logiki aplikacji. Wdrożeniowiec
może za to stworzyć kontener inicjalizacji, którego celem jest przygotowanie zadań
konfiguracyjnych i inicjalizacyjnych. W kodzie z listingu 14.1 przedstawiamy jeden kontener
aplikacyjny udostępniający pliki, zbudowany na bazie serwera HTTP.
Kontener to zwykły serwer HTTP, który nie czyni żadnych założeń co do źródeł pochodzenia
plików. W tej samej kapsule uwzględniamy kontener inicjalizacji, który dostarcza funkcję
klienta Git i jego jedynym celem jest sklonowanie repozytorium Gita. Skoro oba kontenery
stanowią część tej samej kapsuły, mogą korzystać z tego samego wolumenu, aby współdzielić
dane. Korzystamy z tego samego mechanizmu do współdzielenia sklonowanych plików
pomiędzy kontenerem inicjalizacji i kontenerem aplikacji.

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

Sklonuj zewnętrzne repozytorium Gita do podmontowanego katalogu.

Współdzielone wolumeny używane zarówno przez kontener inicjalizacji, jak i kontener


aplikacji.

Pusty katalog używany na węźle do współdzielenia danych.

Ten sam efekt moglibyśmy osiągnąć korzystając z obiektów ConfigMap i PersistentVolume,


jednak chcieliśmy pokazać, jak działają kontenery inicjalizacji. Ten przykład ilustruje typowy
sposób użycia kontenera inicjalizacji, w którym współdzielimy wolumen z głównym kontenerem.

Pozostaw kapsułę uruchomioną

Aby zdebugować wynik działania kontenerów inicjalizacji, niezwykle pomocne


jest tymczasowe zastąpienie polecenia kontenera aplikacji zwykłym
poleceniem sleep, dzięki czemu możesz przeanalizować efekt działania
kontenera inicjalizacji. Jest to pomocne zwłaszcza wtedy, gdy kontener
inicjalizacji nie uruchamia się, przez co nie działa też cała aplikacja — np. z
powodu brakującej lub nieprawidłowej konfiguracji. Poniższe polecenie użyte w
deklaracji kapsuły da Ci godzinę na zdebugowanie wolumenów
zamontowanych w kapsule za pomocą innego polecenia (kubectl exec -it
<kapsuła> sh):

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.

Jeszcze więcej technik inicjalizacji


Jak widać, kontener inicjalizacji to konstrukcja funkcjonująca na poziomie kapsuły,
aktywowana zaraz po jej starcie. Warto pamiętać o kilku innych powiązanych technikach,
używanych do inicjalizacji zasobów Kubernetesa:
Kontrolery dopuszczające (ang. admission controllers)

Istnieje zbiór wtyczek, które przechwytują wszystkie żądania do serwera API


Kubernetesa, jeszcze przed utrwaleniem obiektu. Wtyczki mogą zweryfikować
(zwalidować) obiekt, a także zmienić jego zawartość. Możemy zastosować kontrolery do
wprowadzania rozmaitych testów, wymuszania limitów czy ustawiania wartości
domyślnych. Niezależnie od zastosowania, wszystkie wtyczki są kompilowane do
binariów kube-apiserver i konfigurowane przez administratora klastra w momencie
startu serwera API. System wtyczek nie jest jednak elastyczny, dlatego do Kubernetesa
zostały dodane haki dopuszczające.

Haki dopuszczające (ang. admission webhooks)

Są to zewnętrzne kontrolery dopuszczające, które wykonują wywołania zwrotne HTTP


dla pasujących żądań. Istnieją dwa rodzaje haków dopuszczających: hak modyfikujący
(który może zmieniać zasoby w celu wymuszenia własnych wartości domyślnych) i hak
walidujący (który może odrzucić zasoby niespełniające własnych reguł dopuszczenia).
Takie podejście do kontrolerów zewnętrznych pozwala na tworzenie haków
dopuszczających tworzonych poza Kubernetesem i konfigurowanych w czasie wykonania
aplikacji.

Inicjalizatory (ang. initializers)

Inicjalizatory są używane przez administratorów do wymuszania rozmaitych reguł lub do


wstrzykiwania wartości domyślnych przez określanie listy oczekujących zadań
preinicjalizacyjnych, przechowywanych w metadanych każdego obiektu. W takiej sytuacji
własne kontrolery inicjalizacji, których nazwy odpowiadają nazwom zadań, wykonują
zadania. Dopiero po zakończeniu zadań inicjalizacji obiekt API staje się widoczny dla
zwykłych kontrolerów.

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.

Inicjalizacja zasobów Kubernetesa może być wykonywana na wiele sposób. Wszystkie te


techniki różnią się od haków dopuszczających, ponieważ dochodzi do walidacji i
modyfikacji zasobów w czasie tworzenia. Możesz skorzystać z tych mechanizmów do
wstrzyknięcia kontenera inicjalizacji do dowolnej kapsuły, która jeszcze go nie zawiera.
Dla porównania, wzorzec Kontenera Inicjalizacji, omówiony w tym rozdziale, aktywuje i
wykonuje swoje obowiązki podczas startu kapsuły. Koniec końców, najistotniejszą różnicę
stanowi grupa docelowa — kontenery inicjalizacji są używane przez programistów
wdrażających swoje oprogramowanie na Kubernetesie, a techniki omówione w tej sekcji
pomagają administratorom kontrolować proces inicjalizacji kontenerów i zarządzać nim.

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.

Typowym przykładem zastosowania tego wzorca jest współpraca serwera HTTP i


synchronizatora Gita. Kontener serwera HTTP skupia się jedynie na udostępnianiu plików za
pomocą protokołu HTTP, nie wiedząc skąd pochodzą pliki i jak są dostarczane. Na podobnej
zasadzie, kontener synchronizatora dba jedynie o synchronizację danych pomiędzy serwerem
Git i lokalnym systemem plików. Nie interesuje go, co dzieje się z plikami po synchronizacji —
jego jedynym zmartwieniem jest utrzymanie lokalnego folderu w synchronizacji ze zdalnym
serwerem Git. Listing 15.1 przedstawia definicję kapsuły z dwoma kontenerami
skonfigurowanymi do używania wolumenu do wymiany plików.
Listing 15.1. Kapsuła z przyczepką

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”

- “git clone $(GIT_REPO) . && watch -n 600 git pull”

workingDir: /var/lib/data
volumes:

- emptyDir: {}

name: git

Główny kontener aplikacji, odpowiedzialny za udostępnianie plików za pomocą protokołu


HTTP.

Kontener przyczepki uruchomiony równolegle i pobierający dane z serwera Git.

Współdzielona lokalizacja do wymiany danych pomiędzy przyczepką a głównym kontenerem


aplikacji.

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).

Ten prosty wzorzec, przedstawiony na rysunku 15.1, pozwala na współpracę kontenerów w


czasie wykonania aplikacji. Jednocześnie dzięki niemu możemy separować zagadnienia
związane z obydwoma kontenerami, które mogą być zarządzane przez odrębne zespoły, za
pomocą różnych języków programowania, z różnymi cyklami wydawniczymi itd. To rozwiązanie
promuje wymienialność i reużywalność kontenerów, takich jak serwer HTTP czy synchronizator
Gita. Z powodzeniem można skorzystać z nich w innych aplikacjach z inną konfiguracją,
zarówno w formie pojedynczych kontenerów w kapsułach, jak i w modelu współpracy z innymi
kontenerami.
Rysunek 15.1. Wzorzec Przyczepka

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ć.

W tym przypadku Adapter jest doskonałym rozwiązaniem: kontener Przyczepki uruchamia


niewielki serwer HTTP i przy każdym żądaniu odczytuje własny plik logów, po czym
przekształca go do postaci akceptowalnej dla Prometheusa. Listing 16.1 przedstawia obiekt
Deployment z zastosowanym Adapterem. Ta konfiguracja pozwala na uzyskanie luźno
powiązanego z naszą usługą Prometheusa. Co więcej, główna aplikacja nie wie nawet, że
jesteśmy zintegrowani z tą usługą! Pełny przykład, wraz z instalacją Prometheusa, jest
dołączony do naszego repozytorium w serwisie GitHub.
Listing 16.1. Adapter udostępniający dane zgodne z formatem Prometheusa

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.

Katalog współdzielony z kontenerem adaptera Prometheusa.

Obraz eksportera Prometheusa, eksportujący na porcie 9889.

Ścieżka do pliku, do którego główna aplikacja zapisuje logi.

Współdzielony wolumen jest montowany w kontenerze Adaptera.

Pliki są współdzielone za pomocą wolumenu emptyDir z systemu plików węzła.

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.

W kolejnym rozdziale poznasz kolejny wariant Przyczepki: wzorzec Ambasador, który


funkcjonuje jak pośrednik (proxy) prowadzący do świata zewnętrznego.
Więcej informacji
Przykład Adaptera: http://bit.ly/2HvFF3Y
Zestaw narzędzi dla systemów rozproszonych — wzorce kontenerów używane do
projektowania modularnych systemów rozproszonych: https://bit.ly/2U2iWD9
Rozdział 17. Ambasador
Wzorzec Ambasador stanowi wyspecjalizowaną wersję przyczepki, odpowiedzialną za ukrycie
złożoności aplikacji i dostarczenie jednolitego interfejsu, pozwalającego na dostęp do usługi
spoza kapsuły. W tym rozdziale zobaczymy jak wzorzec Ambasador funkcjonuje jako pośrednik
(proxy), ograniczając dostęp głównej kapsuły do zewnętrznych zależności.

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.

Listing 17.1. Ambasador, który przetwarza logi aplikacji

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.

Adres URL połączenia, używany do komunikacji z ambasadorem za pośrednictwem hosta


lokalnego.

Ambasador uruchomiony równolegle do kontenera głównego, nasłuchujący na porcie 9009,


który nie jest udostępniony poza kapsułą.

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.

W przypadku obrazów Dockera, zmienne środowiskowe można definiować bezpośrednio w


plikach Dockerfile, korzystając z dyrektywy ENV. Możesz zdefiniować ją wiersz po wierszu,
możesz też ustawić wszystkie zmienne w jednym wierszu (listing 18.1).
Listing 18.1. Przykład pliku Dockerfile ze zmiennymi środowiskowymi

FROM openjdk:11
ENV PATTERN “Konfiguracja EnvVar”

ENV LOG_FILE “/tmp/random.log”


ENV SEED “1349093094”
# Alternatywnie:

ENV PATTERN=”EnvVar Configuration” LOG_FILE=/tmp/random.log SEED=1349093094


...

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).

Listing 18.2. Odczyt zmiennych środowiskowych w Javie


public Random initRandom() {

long seed = Long.parseLong(System.getenv(“SEED”));

return new Random(seed);

Inicjalizuje generator liczb pseudolosowych za pomocą ziarna uzyskanego ze zmiennej


środowiskowej.
Bezpośrednie uruchomienie takiego obrazu spowoduje użycie domyślnych, zapisanych na
sztywno wartości. W większości przypadków będziesz zapewne przesłaniać te parametry
wartościami spoza obrazu.
Uruchomienie takiego obrazu z poziomu Dockera da nam możliwość ustawienia zmiennych
środowiskowych z poziomu wiersza poleceń w momencie wywołania Dockera (listing 18.3).

Listing 18.3. Ustawienie zmiennych środowiskowych przy starcie kontenera Dockera


docker run -e PATTERN=”EnvVarConfiguration” \

-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).

Listing 18.4. Wdrożenie z ustawionymi zmiennymi środowiskowymi

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 zawierający wartość dosłowną.

EnvVar pochodzący z obiektu ConfigMap.

Nazwa obiektu ConfigMap.

Pochodzący z obiektu ConfigMap klucz, za pomocą którego pobieramy wartość zmiennej


środowiskowej.

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.

W takich sytuacjach wiele osób korzysta z dodatkowej warstwy pośredników, umieszczając


ustawienia np. w plikach konfiguracyjnych, po jednym dla każdego ze środowisk. Następnie
pojedyncza zmienna środowiskowa odpowiada za wybór jednego z tych plików. Profile używane
w Spring Boot stanowią przykład tego podejścia. Skoro pliki profilów konfiguracji są z reguły
przechowywane w samej aplikacji (zawartej w kontenerze), dochodzi do ścisłego powiązania ich
z aplikacją. Takie zachowanie często prowadzi do umieszczania ustawień deweloperskich i
produkcyjnych w tym samym obrazie Dockera, co wymaga przebudowania przy każdej zmianie
środowiska. Wszystko to pokazuje, że stosowanie zmiennych środowiskowych jest rozsądne
tylko w przypadku niewielkich zbiorów konfiguracji.
Wzorce Zasób Konfiguracji, Niezmienna Konfiguracja i Szablon Konfiguracji, opisane w
kolejnych rozdziałach, stanowią dobre alternatywy, po które możemy sięgnąć, gdy musimy
opracować bardziej skomplikowane przykłady konfiguracji.

Zmienne środowiskowe można stosować zawsze, dlatego możemy ustawiać je na różnych


poziomach. Ta możliwość prowadzi do rozproszenia definicji konfiguracji, przez co trudno jest
niekiedy stwierdzić, skąd pochodzi dana zmienna środowiskowa. Jeżeli nie mamy jednego,
centralnego repozytorium wszystkich zmiennych środowiskowych, debugowanie problemów z
konfiguracją staje się trudniejsze.

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.

Zmienne środowiskowe są łatwe w użyciu, ale w praktyce stosuje się je w prostych


przypadkach, ponieważ ich ograniczenia utrudniają zastosowanie ich w bardziej złożonych
systemach. Przy omawianiu kolejnych wzorców dowiemy się, jak poradzić sobie z tymi
ograniczeniami.

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:

jako odwołanie do zmiennych środowiskowych, w którym klucz zawiera nazwę zmiennej


środowiskowej,
jako pliki powiązane z wolumenem, podmontowanym do kapsuły. Klucz zawiera nazwę
pliku.

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.

Poza obiektami ConfigMap i Secret, możemy też przechowywać konfigurację w zewnętrznych


wolumenach, które są następnie montowane.
W poniższych przykładach koncentrujemy się na obiektach ConfigMap, jednak można też
skorzystać z nich w przypadku obiektów Secret. W takiej sytuacji należy pamiętać o jednej
ważnej różnicy — wartości przechowywane w obiektach Secret muszą być zakodowane za
pomocą kodowania Base64.
Zasób ConfigMap zawiera pary klucz-wartość w sekcji data (listing 19.1).

Listing 19.1. Zasób ConfigMap


apiVersion: v1

kind: ConfigMap

metadata:
name: random-generator-config

data:

PATTERN: Zasób Konfiguracji


application.properties: |

# Konfiguracja generatora liczb losowych


log.file=/tmp/generator.log

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

kubectl create cm spring-boot-config \

--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.

Dołącz prefiks CONFIG_ do wszystkich kluczy obiektu ConfigMap. Dysponując obiektem


ConfigMap zdefiniowanym na listingu 19.1, otrzymujemy trzy udostępnione zmienne
środowiskowe: CONFIG_PATTERN, CONFIG_EXTRA_OPTIONS i CONFIG_SEED.

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).

Listing 19.5. Montowanie obiektu ConfigMap jako wolumenu

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.

Odwzorowanie danych konfiguracyjnych można dostroić w znacznie bardziej dokładny sposób,


dodając właściwości do deklaracji wolumenu. Zamiast odwzorowywania wszystkich wpisów
jako plików, możesz wybrać pojedyncze klucze, które powinny być udostępniane, a także nazwy,
pod którymi powinny być dostępne. Więcej informacji na ten temat znajdziesz w dokumentacji
obiektu ConfigMap.

Kolejnym sposobem na przechowywanie konfiguracji z pomocą Kubernetesa jest zastosowanie


wolumenów gitRepo. Ten rodzaj wolumenu podmontowuje pusty katalog w kapsule i klonuje do
niego repozytorium Gita. Zaletą przechowywania konfiguracji w serwisie Git jest wbudowane
wersjonowanie i audyt zawartości. Wolumeny gitRepo wymagają jednak zewnętrznego dostępu
do repozytorium Gita, który nie jest zasobem Kubernetesa i najprawdopodobniej jest
zlokalizowany poza klastrem, przez co musi być monitorowany i zarządzany odrębnie.
Klonowanie i montowanie odbywają się podczas startu kapsuły, a sklonowane, lokalne
repozytorium nie jest automatycznie aktualizowane o nowe zmiany. Wolumen działa podobnie
do mechanizmu opisanego w rozdziale 20., „Niezmienna konfiguracja”, przez zastosowanie
Kontenerów Inicjalizacji w celu skopiowania konfiguracji do współdzielonego wolumenu
lokalnego.

W praktyce wolumeny gitRepo są obecnie uważane za przestarzałe — zaleca się stosowanie


rozwiązań opartych na Kontenerach Inicjalizacji, ponieważ to podejście jest bardziej
uniwersalne i obsługuje inne rodzaje źródeł danych — nie tylko Gita. Można więc skorzystać z
tego samego podejścia do pobrania danych konfiguracyjnych z zewnętrznych systemów i
przechować je w wolumenie. Zamiast jednak stosować predefiniowany wolumen gitRepo,
skorzystaj z elastycznej metody kontenera inicjalizacji. Szczegółowo omawiamy ten mechanizm
w rozdziale 20., w podrozdziale „Kontenery inicjalizacji Kubernetesa”.

Jak bezpieczne są obiekty Secret?


Obiekty Secret przechowują dane zakodowane przy użyciu formatu Base64. Są one
dekodowane przed przekazaniem do kapsuły, niezależnie od formy, jaką posiadają —
zmiennej środowiskowej czy montowanego wolumenu. Bardzo często ten mechanizm
jest traktowany jako mechanizm bezpieczeństwa — jest to błąd! Kodowanie Base64 nie
jest szyfrowaniem — z punktu widzenia bezpieczeństwa jest to format tak samo
(nie)bezpieczny jak zwykły tekst. Kodowanie Base64 w obiektach Secret pozwala na
przechowywanie danych binarnych — dlaczego zatem Secret jest uważany za bardziej
bezpieczny od obiektu ConfigMap? Istnieje kilka szczegółów implementacyjnych
obiektów Secret, które czynią je bezpiecznymi. W tym obszarze stale dochodzi do
usprawnień. Obecnie główne zalety są następujące:
Secret jest udostępniany tylko węzłom, na których znajdują się kapsuły wymagające
dostępu do danego obiektu Secret.
W ramach węzłów, Secret jest przechowywany w pamięci w lokalizacji tmpfs — nigdy
nie jest on zapisywany do pamięci trwałej. Secret jest usuwany z pamięci w momencie
usunięcia kapsuły.
W Etcd wszystkie obiekty Secret są przechowywane w formie zaszyfrowanej.
Niezależnie od wszystkich zabezpieczeń, dostęp do obiektów Secret jest możliwy z
poziomu użytkownika root lub po prostu w wyniku utworzenia kapsuły i podmontowania
obiektu Secret. Istnieje możliwość zdefiniowania kontroli dostępu do obiektów Secret (a
także innych zasobów) na bazie ról (RBAC — ang. role-based access control), dzięki
czemu tylko kapsuły z określonych kont usług mogą z tych zasobów korzystać. Mimo to
użytkownicy, którzy mogą tworzyć kapsuły w danej przestrzeni nazw, mogą podwyższyć
swoje uprawnienia w ramach tej przestrzeni przez utworzenie kapsuł. Mogą oni
uruchomić kapsułę na koncie usługi o większych przywilejach, co da im dostęp do
obiektów Secret. Użytkownik lub kontroler z możliwością tworzenia kapsuł w danej
przestrzeni nazw może podszyć się pod dowolne konto usług i w ten sposób uzyskać
dostęp do obiektów Secret i ConfigMap w danej przestrzeni nazw. W związku z tym,
dodatkowe szyfrowanie wrażliwych danych jest wykonywane również na poziomie
aplikacji.
Dyskusja
Obiekty ConfigMap i Secret pozwalają na przechowywanie informacji nt. konfiguracji w
dedykowanych obiektach zasobów, którymi można łatwo zarządzać za pomocą API Kubernetesa.
Najistotniejszą zaletą obiektów ConfigMap i Secret jest rozluźnienie powiązania między
definicją danych konfiguracyjnych a miejscem ich użycia. Brak tego powiązania pozwala na
zarządzanie obiektami wykorzystującymi konfiguracje niezależnie od samych konfiguracji.

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.

W prawdziwych klastrach Kubernetesa możemy spotkać się z indywidualnymi ograniczeniami


(kwotami) nakładanymi na liczbę obiektów ConfigMap, które mogą być użyte w ramach
przestrzeni nazw lub projektu, dlatego obiekt ConfigMap nie jest złotym środkiem na wszystkie
problemy.

W kolejnych dwóch rozdziałach omówimy mechanizmy obsługi dużych ilości danych


konfiguracyjnych, korzystając ze wzorców Niezmienna Konfiguracja i Szablony Konfiguracji.

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.

Złożoność danych konfiguracyjnych do pewnego stopnia można pokonać korzystając z Zasobów


Konfiguracji. Niestety, wzorzec ten nie wymusza niezmienności samych danych
konfiguracyjnych. Niezmienność oznacza, że nie możemy zmienić danych konfiguracyjnych po
starcie aplikacji, co pozwala na zapewnienie dobrze zdefiniowanego stanu danych konfiguracji.
Ponadto wzorzec Niezmiennej Konfiguracji pozwala na stosowanie systemu kontroli wersji i
śledzenie wprowadzonych zmian.

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.

Rysunek 20.1. Niezmienna konfiguracja z wolumenem Dockera

Przeanalizujmy przykład. W środowisku deweloperskim tworzymy obraz Dockera, który


przechowuje konfigurację deweloperską i tworzy wolumen /config. Taki obraz możemy
utworzyć za pomocą pliku Dockerfile-config z listingu 20.1.
Listing 20.1. Plik Dockerfile z obrazem konfiguracji

FROM scratch

ADD app-dev.properties /config/app.properties

VOLUME /config

Dodaj określoną właściwość.

Utwórz wolumen i skopiuj do niego właściwość.

Teraz utworzymy obraz i kontener Dockera, za pomocą pokazanego na listingu 20.2 polecenia
narzędzia Docker CLI.

Listing 20.2. Budowanie obrazu Dockera związanego z konfiguracją

docker build -t k8spatterns/config-dev-image:1.0.1 -f Dockerfile-config


docker create --name config-dev k8spatterns/config-dev-image:1.0.1 .

Ostatnim krokiem jest uruchomienie kontenera aplikacji i połączenie go z kontenerem


konfiguracji (listing 20.3).

Listing 20.3. Uruchomienie kontenera aplikacji z powiązanym kontenerem aplikacji

docker run --volumes-from config-dev k8spatterns/welcome-servlet:1.0

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

docker build -t k8spatterns/config-prod-image:1.0.1 -f Dockerfile-config

docker create --name config-prod k8spatterns/config-prod-image:1.0.1 .

docker run --volumes-from config-prod k8spatterns/welcome-servlet:1.0

Kontenery inicjalizacji Kubernetesa


W Kubernetesie wolumen współdzielony w ramach kapsuły jest dopasowany do wiązania
kontenerów aplikacji i konfiguracji. Jeśli jednak chcesz przenieść mechanizm wiązania
wolumenów Dockera do Kubernetesa, zauważysz, że w Kubernetesie nie ma możliwości
wiązania wolumenów kontenera. Biorąc pod uwagę czas trwania dyskusji i jej niezwykłe
rozbudowanie, trudno przypuszczać, że ta funkcja zostanie uwzględniona w Kubernetesie w
najbliższym czasie,

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.

W przykładzie z Dockerem opieramy swoją konfigurację obrazu Dockera na obrazie scratch —


pustym obrazie Dockera bez żadnych plików systemu operacyjnego. Nie potrzebujemy nic
więcej, ponieważ wszystkie dane konfiguracyjne są załączane za pomocą wolumenów Dockera.
W przypadku kontenerów inicjalizacji Kubernetesa musimy skorzystać z pomocy obrazu
bazowego, aby skopiować dane konfiguracyjne do wolumenu współdzielonego kapsuły. Obraz
busybox jest dobrym wyborem jako obraz bazowy, ponieważ jest on niewielki; możemy też
skorzystać ze zwykłego uniksowego narzędzia cp do osiągnięcia pożądanego efektu.
Jak w szczegółach działa inicjalizacja współdzielonych wolumenów z konfiguracją?
Przeanalizujmy przykład. Najpierw utwórzmy obraz konfiguracji z plikiem Dockerfile (listing
20.5).

Listing 20.5. Obraz konfiguracji deweloperskiej

FROM busybox

ADD dev.properties /config-src/demo.properties

ENTRYPOINT [ “sh”, “-c”, “cp /config-src/∗ $1”, “--” ]

Stosujemy powłokę w celu rozwiązania znaku wieloznacznego.

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).

Listing 20.6. Wdrożenie, które kopiuje konfigurację do miejsca docelowego w kontenerze


inicjalizacji

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: {}

Specyfikacja szablonu kapsuły wdrożenia zawiera pojedynczy wolumen i dwa kontenery:

Wolumen config-directory typu emptyDir, co powoduje, że po utworzeniu jest to pusty


katalog w obrębie węzła, na którym znajduje się ta kapsuła.
Kontener inicjalizacji, wywoływany przez Kubernetesa w momencie startu, jest budowany
z obrazu, który przed chwilą utworzyliśmy. Ustawiamy pojedynczy argument /config,
wykorzystywany w ramach atrybutu ENTRYPOINT obrazu. Dzięki temu kontener
inicjalizacji skopiuje swoją zawartość do określonego katalogu. Katalog /config jest
montowany z wolumenu config-directory.
Kontener aplikacji podmontowuje wolumen config-directory w celu uzyskania dostępu do
konfiguracji, która została skopiowana przez kontener inicjalizacji.

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

Aby zmienić konfigurację z deweloperskiej na produkcyjną, musimy jedynie wymienić obraz


kontenera inicjalizacji. Możemy to osiągnąć, zmieniając definicję YAML lub za pomocą
polecenia kubectl. Edycja deskryptora zasobu dla każdego środowiska nie jest rozwiązaniem
idealnym. Jeśli korzystasz z oprogramowania OpenShift firmy Red Hat (korporacyjnej wersji
Kubernetesa), możesz skorzystać z narzędzia OpenShift Templates. Narzędzie to pozwala
tworzyć różne deskryptory zasobów dla różnych środowisk z jednego szablonu.

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.

Listing 20.7. Szablon OpenShift z parametryzowanym obrazem konfiguracji

apiVersion: v1
kind: Template

metadata:

name: demo

parameters:

- name: CONFIG_IMAGE

description: Nazwa obrazu konfiguracji


value: k8spatterns/config-dev:1

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: {}

Deklaracja parametru CONFIG_IMAGE szablonu.

Skorzystaj z parametru szablonu.

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

oc new-app demo -p CONFIG_IMAGE=k8spatterns/config-prod:1

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:

Konfiguracja zależna od środowiska jest zaszyta wewnątrz kontenera. W związku z tym


można wersjonować ją tak, jak inne obrazy kontenerów.
Konfiguracja utworzona w ten sposób może być rozproszona w rejestrze kontenerów.
Można zapoznać się z nią nawet bez dostępu do klastra.
Konfiguracja jest niezmienna, tak jak obraz kontenera, w którym jest zawarta: zmiana w
konfiguracji wymaga zmiany wersji i stworzenia nowego obrazu kontenera.
Obrazy danych konfiguracji są użyteczne, gdy dane konfiguracyjne są zbyt
skomplikowane, aby umieścić je w zmiennych środowiskowych lub obiektach ConfigMap.
Dla odmiany w obrazach możemy przechowywać dowolnie duże zbiory danych
konfiguracyjnych.

Jak można było się spodziewać, wzorzec ma pewne wady:

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

Przed startem aplikacji umieszczamy w pełni przetworzony plik konfiguracyjny w miejscu, w


którym można skorzystać z niego bezpośrednio, tak jak z każdego innego pliku
konfiguracyjnego.

Przetwarzanie konfiguracji na żywo można zrealizować na dwa sposoby:

Możemy dodać procesor szablonu w ramach atrybutu ENTRYPOINT w pliku Dockerfile.


Dzięki temu przetwarzanie szablonów stanie się bezpośrednio częścią obrazu kontenera.
Punkt wejściowy to z reguły skrypt, który najpierw wykonuje przetwarzanie szablonów, a
następnie uruchamia aplikację. Parametry szablonu pochodzą ze zmiennych
środowiskowych.
W Kubernetesie znacznie lepiej jest wykonywać inicjalizację w ramach Kontenera
Inicjalizacji. W ramach tego kontenera zostaje uruchomiony proces przetwarzania
szablonów, tworząc konfigurację dla pozostałych kontenerów aplikacji. Kontenery
Inicjalizacji są opisane szczegółowo w rozdziale 14.

Podejście związane z kontenerami inicjalizacji jest z pewnością najrozsądniejsze, ponieważ


możemy skorzystać bezpośrednio z obiektów ConfigMap dla parametrów szablonu. Diagram z
rysunku 21.1 ilustruje sposób działania wzorca.
Definicja kapsuły aplikacji składa się z minimum dwóch kontenerów: jeden służy do inicjalizacji
przetwarzania szablonu, a drugim jest kontener aplikacji. Kontener inicjalizacji zawiera nie
tylko procesor szablonu, ale także same szablony konfiguracji. Poza kontenerami, kapsuła
definiuje także dwa wolumeny: jeden do obsługi parametrów szablonu (wspierany przez obiekt
ConfigMap), a drugi — wolumen emptyDir — używany jest do współdzielenia przetwarzanych
szablonów pomiędzy kontenerami inicjalizacji i aplikacji.
Dysponując taką konfiguracją, w momencie uruchomienia kapsuły możemy wykonać
następujące kroki:

1. Kontener inicjalizacji zostaje uruchomiony i wywołuje procesor szablonu. Proces pobiera


szablony z obrazu, a parametry szablonu — z podmontowanego wolumenu ConfigMap —
zaś wynik jest przechowywany w wolumenie emptyDir.
2. Po zakończeniu działania kontenera inicjalizacji, uruchomiony zostaje kontener aplikacji,
który wczytuje pliki konfiguracji z wolumenu emptyDir.

Poniższy przykład korzysta z kontenera inicjalizacji do zarządzania pełnym zbiorem plików


konfiguracji WildFly dla dwóch środowisk: deweloperskiego i produkcyjnego. Oba są bardzo
podobne do siebie i różnią się nieznacznie. W naszym przykładzie, jedyna różnica polega
praktycznie na sposobie rejestrowania logów: każdy wiersz jest poprzedzany ciągiem
DEVELOPMENT: lub PRODUCTION:.

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.

Listing 21.1. Szablon konfiguracji logów

....

<formatter name=”COLOR-PATTERN”>

<pattern-formatter pattern=”{{(datasource “config”).logFormat}}”/>


</formatter>

....

W tym miejscu korzystamy z Gotemplate jako procesora szablonu. Gotemplate wprowadza


pojęcie źródła danych (ang. data source), dzięki czemu możemy odnosić się do parametrów
szablonu, które muszą zostać wypełnione. W naszym przypadku, źródło danych pochodzi z
wolumenu wspieranego przez obiekt ConfigMap, podmontowanego do kontenera inicjalizacji.
Obiekt ConfigMap zawiera jeden wpis z kluczem logFormat, z którego uzyskiwany jest format
logów.

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).

Listing 21.2. Prosty plik Dockerfile dla obrazu szablonu

FROM k8spatterns/gomplate
COPY in /in

Podstawowy obraz k8spatterns/gomplate zawiera procesor szablonu i skrypt punktu


wejściowego, który korzysta domyślnie z następujących katalogów:

/in przechowuje szablony konfiguracji WildFly, w tym parametryzowany plik


standalone.xml. Pliki te są dodawane bezpośrednio do obrazu.
/params jest używany do wyszukiwania źródeł danych Gomplate, będących plikami YAML.
Ten katalog jest montowany z wolumenu kapsuły wspieranego przez obiekt ConfigMap.
/out to katalog, do którego są zapisywane przetworzone pliki. Ten katalog jest montowany
w kontenerze aplikacji WildFly i jest używany do konfiguracji.
Drugim składnikiem naszego przykładu jest obiekt ConfigMap przechowujący parametry. Na
listingu 21.3 korzystamy z prostego pliku z parami klucz-wartość.

Listing 21.3. Wartości dla szablonu konfiguracji logów


logFormat: “DEVELOPMENT: %-5p %s%e%n”

Obiekt ConfigMap o nazwie wildfly-parameters zawiera dane w formacie YAML, do których


odwołujemy się za pomocą klucza config.yml. Jest on rozpoznawany przez kontener inicjalizacji.
Na zakończenie musimy utworzyć zasób Deployment dla serwera WildFly (listing 21.4).

Listing 21.4. Wdrożenie procesora szablonu jako kontenera inicjalizacji

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

labels:

example: cm-template

name: wildfly-cm-template spec:

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: {}

Obraz odpowiedzialny za przechowywanie szablonów konfiguracji.

Parametry są montowane z atrybutu wildfly-parameters obiektu ConfigMap.

Katalog docelowy do zapisu przetworzonych szablonów. Jest on montowany z pustego


katalogu.

Katalog odpowiedzialny za przechowanie pełnych, wygenerowanych plików konfiguracji.


Montowany jako /config.

Deklaracja wolumenu dla parametrów obiektu ConfigMap. Pusty katalog jest używany do
współdzielenia przetworzonej konfiguracji.

Ta deklaracja jest całkiem długa, dlatego przeanalizujmy ją po kolei: specyfikacja wdrożenia


zawiera kapsułę z naszym kontenerem inicjalizacji, kontenerem aplikacji i dwoma
wewnętrznymi wolumenami kapsuł:

Pierwszy wolumen, wildfly-parameters, zawiera nasz obiekt ConfigMap z tą samą nazwą


(tj. zawiera plik config.yml, w którym znajduje się wartość parametru).
Drugi wolumen to (początkowo) pusty katalog współdzielony pomiędzy kontenerem
inicjalizacji i kontenerem WildFly.
Po uruchomieniu obiektu Deployment wykonane zostaną następujące działania:

Kontener inicjalizacji zostaje utworzony a jego polecenie — wykonane. Ten kontener


przyjmuje plik config.yml z wolumenu ConfigMap, wypełnia szablony z katalogu /in w
kontenerze inicjalizacji, a przetworzone pliki przechowuje w katalogu /out. Katalog /out to
miejsce, w którym montowany jest wolumen wildfly-config.
Po zakończeniu inicjalizacji kontenera, serwer WildFly zostaje uruchomiony z właściwą
opcją, dzięki czemu poszukiwanie konfiguracji odbędzie się w katalogu /config.
Przypominamy, że katalog /config to współdzielony wolumen wildfly-config, zawierający
przetworzone pliki szablonów.

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.

Wskazówka dotycząca debugowania wolumenów


Praca z kapsułami i wolumenami według tego wzorca z pewnością nie ułatwia
debugowania w razie problemów. Jeśli chcesz przeanalizować przetworzone
szablony, zajrzyj do katalogu
/var/lib/kubelet/pods/{id_kapsuły}/volumes/kubernetes.io~empty-dir/ na
węźle, ponieważ to tam znajduje się zawartość katalogu emptyDir. Wystarczy
wykonać polecenie kubectl exec w kapsule w momencie jej uruchomienia, a
następnie przeanalizować katalog w poszukiwaniu utworzonych plików.

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

Obserwuj aktualny stan, śledząc zdarzenia wygenerowane przez Kubernetesa, gdy


dochodzi do zmiany obserwowanego zasobu.

Analiza
Określ różnice pomiędzy stanem bieżącym a docelowym.

Działanie
Wykonaj operacje, które doprowadzą zasób do stanu docelowego.

Na przykład, kontroler ReplicaSet obserwuje zmiany zasobów ReplicaSet, analizuje liczbę


kapsuł, które muszą być uruchomione, a także działa, wysyłając definicję kapsuł do serwera
API. Backend Kubernetesa jest odpowiedzialny za uruchomienie żądanej kapsuły na węźle.

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).

Rysunek 22.1. Cykl Obserwacja-Analiza-Działanie

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

Operator to wyszukany proces uzgadniania, który współpracuje z obiektem


CustomResourceDefinition (CRD), stanowiącym rdzeń wzorca Operator. Operatory z
reguły opakowują złożoną logikę dziedziny aplikacji, a także zarządzają pełnym cyklem
życia aplikacji. Operatory omawiamy szczegółowo w rozdziale 23.

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.

Najprostsze kontrolery rozszerzają standardowy sposób zarządzania zasobami w Kubernetesie.


Funkcjonują one na tych samych standardowych zasobach i wykonują te same zadania, co
wewnętrzne kontrolery Kubernetesa. Są one jednak niewidoczne dla użytkownika klastra.
Kontrolery ewaluują definicje zasobów i wykonują pewne działania w zależności od spełnienia
określonych warunków. Choć są one w stanie obserwować dowolne pole w definicji zasobu i
działać na podstawie niego, metadane i obiekty ConfigMap są najlepiej dostosowane do tego
celu. Poniżej przedstawiamy kilka uwag, o których trzeba pamiętać wybierając miejsce
przechowywania danych kontrolera.

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.

Operator aktualizacji kontenera linuksowego (ang. Container Linux Update)

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ą.

Listing 22.1. Obiekt ConfigMap używany w aplikacji webowej

apiVersion: v1

kind: ConfigMap

metadata:
name: webapp-config

annotations:
k8spatterns.io/podDeleteSelector: “app=webapp”

data:

message: “Witaj we Wzorcach Kubernetesa!”

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).

Listing 22.2. Skrypt kontrolera

namespace=${WATCH_NAMESPACE:-default}

base=http://localhost:8001

ns=namespaces/$namespace

curl -N -s $base/api/v1/${ns}/configmaps?watch=true | \

while read -r event

do

# ...

done

Przestrzeń nazw do obserwowania (w razie braku stosowana jest przestrzeń domyślna).

Dostęp do API Kubernetesa za pomocą Ambasadora, uruchomionego w tej samej kapsule.

Pętla, w której obserwujemy zdarzenia pochodzące z obiektów ConfigMap.

Zmienna środowiskowa WATCH_NAMESPACE określa przestrzeń nazw, w której kontroler powinien


obserwować aktualizacje obiektu ConfigMap. Możemy ustawić tę zmienną w deskryptorze
wdrożenia samego kontrolera. W naszym przykładzie korzystamy z Downward API, opisanego w
rozdziale 13., w celu monitorowania przestrzeni nazw, w której wdrożyliśmy kontroler
skonfigurowany w kodzie z listingu 22.3 jako część kontrolera wdrożenia.
Listing 22.3. Pozyskanie zmiennej WATCH_NAMESPACE z bieżącej przestrzeni nazw

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.

Zwróć uwagę na parametr zapytania watch=true w kodzie z listingu 22.2. Ten


parametr wskazuje, że serwer API nie powinien kończyć połączenia HTTP, tylko
używać go do przesyłania zdarzeń w ramach strumienia odpowiedzi tuż po ich
zaistnieniu (tego rodzaju technikę nazywa się wiszącymi żądaniami GET lub
Comet). Pętla odczytuje pojedynczo wszystkie zdarzenia tuż po ich odczytaniu,
pobierając je do dalszego przetworzenia.

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.

Wewnątrz pętli wykonujemy logikę przedstawioną w kodzie z listingu 22.4.

Listing 22.4. Pętla uzgadniania kontrolera

curl -N -s $base/api/v1/${ns}/configmaps?watch=true | \

while read -r event

do

type=$(echo “$event” | jq -r ‘.type’)

config_map=$(echo “$event” | jq -r ‘.object.metadata.name’)


annotations=$(echo “$event” | jq -r ‘.object.metadata.annotations’)

if [ “$annotations” != “null” ]; then

selector=$(echo $annotations | \
jq -r “\

to_entries |\

.[] |\

select(.key == \”k8spatterns.io/podDeleteSelector\”) |\

.value |\
@uri \

“)

fi

if [ $type = “MODIFIED” ] && [ -n “$selector” ]; then

pods=$(curl -s $base/api/v1/${ns}/pods?labelSelector=$selector |\

jq -r .items[].metadata.name)

for pod in $pods; do

curl -s -X DELETE $base/api/v1/${ns}/pods/$pod


done
fi

done

Pobierz nazwę i rodzaj obiektu ConfigMap ze zdarzenia.

Pobierz wszystkie adnotacje z obiektu ConfigMap dla klucza


k8spatterns.io/podDeleteSelector. Więcej informacji nt. wyrażenia jq znajdziesz w ramce
„Informacje nt. polecenia jq”.

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.

Usuń wszystkie kapsuły, które pasują do selektora.


Najpierw skrypt pobiera rodzaj zdarzenia, który określa, jaka akcja została wykonana na
obiekcie ConfigMap. Po wyekstrahowaniu obiektu ConfigMap uzyskujemy adnotacje za pomocą
jq. jq to świetne narzędzie do parsowania dokumentów JSON z wiersza poleceń. W skrypcie
założono, że narzędzie to jest dostępne w kontenerze, w którym uruchamiamy skrypt.
Jeśli obiekt ConfigMap ma adnotacje, sprawdzamy adnotację
k8spatterns.io/podDeleteSelector, korzystając z bardziej zaawansowanego zapytania jq.
Celem tego zapytania jest przekonwertowanie wartości adnotacji na selektor kapsuły, który
może być zastosowany w zapytaniu do API. Adnotacja
k8spatterns.io/podDeleteSelector:”app=webapp” jest przekształcana na ciąg
app%3Dwebapp, który jest używany jako selektor kapsuły. Ta konwersja jest wykonywana za
pomocą polecenia jq i jest szczegółowo omówiona w ramce „Informacje nt. polecenia jq”;
zajrzyj do niej, jeśli chcesz się dowiedzieć, jak przebiega proces ekstrakcji.

Jeśli skrypt może wyekstrahować selektor, możemy skorzystać z niego bezpośrednio do


pobrania kapsuł przeznaczonych do usunięcia. Najpierw wyszukujemy wszystkie kapsuły, które
pasują do danego selektora, a następnie usuwamy je jedna po drugiej za pomocą bezpośrednich
wywołań API.

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.

Informacje nt. polecenia jq


Uzyskanie wartości adnotacji k8spatterns.io/podDeleteSelector obiektu ConfigMap i
przekonwertowanie jej na selektor kapsuły jest wykonywane za pomocą polecenia jq.
Jest to świetne polecenie do obsługi formatu JSON, jednak niektóre związane z nim
koncepty mogą wydać się dość mylące. Przyjrzyjmy się bliżej wyrażeniu z naszego
listingu:
selector=$(echo $annotations | \
jq -r “\

to_entries |\
.[] |\

select(.key == \”k8spatterns.io/podDeleteSelector\”) |\
.value |\

@uri \
“)

Zmienna $annotations zawiera wszystkie adnotacje w formacie obiektu JSON, przy


czym nazwy adnotacji mają postać właściwości.
Dzięki poleceniu to_entries konwertujemy obiekty JSON z postaci { „a”: „b” } na
tablicę zawierającą wpisy postaci { „key”: „a”, „value”: „b” }. Więcej szczegółów
znajdziesz w dokumentacji polecenia jq4.
.[] pobiera tablicę wpisów, pozyskując każdy z nich pojedynczo.
Ze wszystkich wpisów pobieramy tylko te o pasującym kluczu. W rezultacie otrzymamy
maksymalnie jeden wpis.
Na zakończenie pobieramy wartość adnotacji i konwertujemy ją do postaci adresu URI,
dzięki czemu może być użyta jako fragment adresu URI.
To wyrażenie przekonwertuje następującą strukturę JSON:

{
“k8spatterns.io/pattern”: “Controller”,
“k8spatterns.io/podDeleteSelector”: “app=webapp”

}
na selektor app%3Dwebapp.

Do utworzenia kapsuły dla naszego kontrolera, zawierającej dwa kontenery, skorzystamy z


opisanego poniżej wdrożenia.

Pierwszy kontener, ambasador API Kubernetesa, będzie udostępniał API na hoście


lokalnym, na porcie 8001. Obraz k8spatterns/kubeapi-proxy to Alpine Linux z
zainstalowanym lokalnie narzędziem kubectl i uruchomionym kubectl proxy, z
podmontowanymi właściwym centrum certyfikacji (CA) i tokenem. Oryginalna wersja,
kubectl-proxy, została opracowana przez Marko Luksa, który wprowadził to proxy w
książce Kubernetes in Action.
Główny kontener, który wykonuje skrypt zawarty we właśnie utworzonym obiekcie
ConfigMap. Skorzystamy z bazowego obrazu Alpine, z zainstalowanymi narzędziami curl i
jq.

Pliki Dockerfile znajdziesz w obrazach k8spatterns/kubeapi-proxy i k8spatterns/curl-jq,


zawartych w repozytorium z przykładami.
Teraz, dysponując obrazami naszej kapsuły, możemy przejść do ostatniego kroku, jakim jest
wdrożenie kontrolera. Główne części wdrożenia znajdziemy w kodzie z listingu 22.5 (pełna
wersja znajduje się w repozytorium).
Listing 22.5. Kontroler wdrożenia

apiVersion: apps/v1 kind: Deployment


# ....

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

Obiekt ServiceAccount z właściwymi uprawnieniami do obserwowania zdarzeń i


restartowania kapsuł.

Kontener ambasadora do pośredniczenia pomiędzy hostem lokalnym a API Kubeservera.

Główny kontener zawierający wszystkie narzędzia i odpowiedzialny za zamontowanie


skryptu kontrolera.

Polecenie startowe wywołujące skrypt kontrolera.

Wolumen powiązany z obiektem ConfigMap, przechowującym nasz skrypt.

Podmontuj do głównej kapsuły wolumen wsparty przez obiekt ConfigMap.

Jak widać, podmontowujemy config-watcher-controller-script z obiektu ConfigMap, który


utworzyliśmy poprzednio, a następnie bezpośrednio korzystamy z niego jako polecenia
startowego w głównym kontenerze. Dla uproszczenia pomijamy sondy żywotności i gotowości,
podobnie jak deklaracje limitów zasobów. Obiekt ServiceAccount o nazwie config-watcher-
controller musi mieć możliwość obserwowania obiektów ConfigMap. W repozytorium z
przykładami znajdziesz pełną konfigurację bezpieczeństwa.
Teraz zajmiemy się działaniem kontrolera. W tym celu skorzystamy z prostego serwera
webowego, który udostępni wartość zmiennej środowiskowej jako jedyną dostępną treść. Obraz
bazowy korzysta ze zwykłego polecenia nc (netcat) do udostępniania treści. Plik Dockerfile dla
tego obrazu znajdziesz w przykładowym repozytorium.

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:

message: “Witamy we Wzorcach Kubernetesa!”


---
apiVersion: apps/v1

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

Obiekt ConfigMap, który przechowuje dane do udostępniania.

Adnotacja, który wyzwala restart kapsuły z aplikacją webową.


Komunikat używany w aplikacji webowej w odpowiedziach HTTP.

Wdrożenie aplikacji webowej.

Uproszczony obraz serwera HTTP z narzędziem netcat.

Zmienna środowiskowa używana w treści odpowiedzi HTTP, pobierana z obserwowanego


obiektu ConfigMap.
W ten sposób docieramy do końca naszego kontrolera dla obiektów ConfigMap,
zaimplementowanego w postaci zwykłego skryptu powłoki. Choć jest to prawdopodobnie
najbardziej skomplikowany przykład w tej książce, widać wyraźnie, że naprawdę nie potrzeba
wiele, aby napisać podstawowy kontroler.
Oczywiście w rzeczywistych aplikacjach, tego rodzaju kontroler powinien powstać w języku
programowania, który zapewnia lepszą obsługę błędów i inne zaawansowane funkcje.

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.

Definicje własnych zasobów


Dzięki CRD możemy rozszerzać Kubernetesa w celu zarządzania konceptami związanymi z
naszą dziedziną w ramach Kubernetesa. Własne zasoby są zarządzane tak, jak dowolne inne
zasoby, za pomocą API Kubernetesa. Są one przechowywane w magazynie Etcd. Historycznie
rzecz ujmując, poprzednikiem CRD były zasoby typu ThirdPartyResource.
Omówiony przed chwilą scenariusz można osiągnąć za pomocą nowych, własnych zasobów,
dostarczonych przez operator CoreOS Prometheus. Dzięki temu jesteśmy w stanie
bezproblemowo zintegrować Prometheusa z Kubernetesem. CRD Prometheusa jest
zdefiniowany w kodzie z listingu 23.1, który objaśnia większość dostępnych pól dla CRD.
Listing 23.1. Obiekt CustomResourceDefinition

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.

Grupa API, do której należy.

Rodzaj użyty do zidentyfikowania instancji tego zasobu.

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.

Schemat OpenAPI V3 do walidacji (pominięte w tym przykładzie).

Schemat OpenAPI V3 można określić w celu umożliwienia walidacji własnych zasobów. W


prostszych przypadkach można go pominąć, ale w produkcyjnych aplikacjach powinien być
dostarczony, aby błędy w konfiguracji były wcześnie wykrywane.

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ł.

Listing 23.2. Definicje podzasobów w obiekcie CustomResourceDefinition

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 liczby aktywnych replik.

Ścieżka JSON do selektora etykiet odpytywanego w celu uzyskania liczby aktywnych replik.

Po zdefiniowaniu CRD możemy z łatwością uzyskać taki zasób (listing 23.3).

Listing 23.3. Własny zasób Prometheusa

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.

Klasyfikacja kontrolerów i operatorów


Przed zagłębieniem się w szczegóły naszego operatora, przeanalizujmy kilka rodzajów
klasyfikacji Kontrolerów, Operatorów, a zwłaszcza obiektów CRD. Opierając się na działaniu
Operatorów, mamy do czynienia z następującymi działaniami:

CRD instalacyjne

Służą do instalowania i działania aplikacji na platformie Kubernetes. Do przykładów należy


CRD Prometheusa, który można zastosować do instalacji i zarządzania samym
Prometheusem.

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.

Zgodnie z naszym podziałem na Kontrolerów i Operatorów, Operator jest Kontrolerem, który


korzysta z obiektu CRD1. Nawet to rozróżnienie nie jest do końca precyzyjne, ponieważ istnieją
warianty pośrednie.

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ń.

Z punktu widzenia implementacji, duże znaczenie ma fakt, czy implementujemy kontroler


ograniczając jego użycie do standardowych obiektów Kubernetesa, czy też korzystamy z
własnych zasobów zarządzanych przez kontroler. W pierwszym przypadku dysponujemy typami
dostępnymi w wybranej przez nas bibliotece klienckiej Kubernetesa. W przypadku CRD nie
musimy dysponować od razu kompletem informacji — do zarządzania zasobami CRD możemy
skorzystać z podejścia bezschematowego lub zdefiniować własne typy, oparte na schemacie
OpenAPI, zawartym w definicji CRD. Wsparcie dla typów CRD jest zróżnicowane i zależy od
zastosowanych frameworków i bibliotek klienckich.

Rysunek 23.1 przedstawia podział na Kontrolery i Operatory, zaczynając od prostszych definicji


zasobów, aż do tych bardziej zaawansowanych, w których różnica pomiędzy Kontrolerami i
Operatorami tkwi w zastosowaniu własnych zasobów.
Rysunek 23.1. Spektrum kontrolerów i operatorów

W przypadku Operatorów można skorzystać z bardziej zaawansowanego rozszerzenia — haków.


Gdy CRD zarządzane przez Kubernetesa okazują się niewystarczające, aby określić problem
danej dziedziny, możemy rozszerzyć API Kubernetesa za pomocą własnej warstwy agregacji.
Możemy dodać własny, zaimplementowany zasób APIService jako nową ścieżkę URL do API
Kubernetesa.

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.

Listing 23.4. Agregacja API z własnym obiektem APIService

apiVersion: apiregistration.k8s.io/v1beta1 kind: APIService

metadata:

name: v1alpha1.sample-api.k8spatterns.io

spec:

group: sample-api.k8spattterns.io

service:
name: custom-api-server

version: v1alpha1

Poza utworzeniem implementacji usługi i kapsuły, musimy dodać konfigurację bezpieczeństwa,


aby ustawić obiekt ServiceAccount, za pomocą którego kapsuła będzie uruchamiana.
Po zakończeniu konfiguracji wszystkie żądania do serwera API zgodne z formatem:
https://<adres IP serwera>/apis/sample-
api.k8spatterns.io/v1alpha1/namespaces/<przestrzeń nazwa>/… będą przekierowane do
naszej, własnej implementacji usługi. To od implementacji usługi zależy sposób obsługi tych
żądań, włączając w to utrwalanie zasobów zarządzanych przez API. To podejście różni się od
wspomnianego wcześniej CRD, w którym Kubernetes samodzielnie zarządza własnymi
zasobami.

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.

Tworzenie i wdrażanie operatorów


W czasie pisania tych słów (w roku 2019) rozwój operatorów to ten obszar Kubernetesa, który
podlega ciągłej ewaluacji. Istnieje kilka zestawów narzędzi i frameworków do tworzenia
operatorów. Trzy główne projekty, które wspierają tworzenie operatorów, to:

Framework CoreOS Operator,


Kubebuilder, rozwijany przez SIG API Machinery of Kubernetes,
Metacontroller dostępny na platformie Google Cloud Platform.

Omówimy je pokrótce już za chwilę, ale miej świadomość, że wszystkie te projekty są


stosunkowo młode i mogą zmienić się lub zostać połączone z innymi w najbliższej przyszłości.

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.

Pomiędzy frameworkiem operatorów a Kubebuilderem zachodzi jedna ważna różnica —


Kubebuilder pracuje bezpośrednio z API Kubernetesa, podczas gdy SDK operatorów dodaje
abstrakcję ponad standardowymi API, co ułatwia jego użycie (ograniczając możliwość
skorzystania z pewnych interesujących funkcji).
Obsługa instalacji i zarządzania cyklem życia operatora nie jest tak wyszukana, jak w
przypadku OLM z frameworka operatorów. Oba projekty w dużej mierze się zazębiają, dlatego
w pewnym momencie może nastąpić ich połączenie.

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.

Tworząc kontroler za pomocą obiektu Metacontroller, możemy dostarczyć funkcję, która


zawiera jedynie logikę biznesową specyficzną dla naszego kontrolera. Metakontroler obsługuje
wszystkie interakcje z API Kubernetesa, uruchamiając pętlę uzgodnienia w naszym imieniu i
wywołując funkcję za pomocą haka webowego. Hak webowy jest wywołany z właściwie
zdefiniowanym ładunkiem, opisującym zdarzenie obiektu CRD. Funkcja zwraca wartość, a my
zwracamy definicję zasobów Kubernetesa, które powinny być utworzone (lub usunięte) w
imieniu naszej funkcji kontrolera.

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.

Listing 23.5. Prosty zasób ConfigWatcher


kind: ConfigWatcher

apiVersion: k8spatterns.io/v1
metadata:

name: webapp-config-watcher
spec:

configMap: webapp-config

podSelector:

app: webapp

Odniesienie do obserwowanego obiektu ConfigMap.

Selektor etykiety określający kapsuły, które muszą zostać zrestartowane.


Atrybut configMap odnosi się w tej definicji do nazwy obiektu ConfigMap, która ma być
obserwowana. Pole podSelector stanowi kolekcję etykiet i ich wartości, które określają kapsuły
do zrestartowania.
Na listingu 23.6 definiujemy rodzaj własnego zasobu za pomocą obiektu CRD.
Listing 23.6. CRD obiektu ConfigWatcher

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

Powiązanie z przestrzenią nazw.

Dedykowana grupa API.

Początkowa wersja.

Unikatowy rodzaj tego CRD.

Etykiety zasobu użyte w narzędziach takich jak kubectl.

Specyfikacja schematu OpenAPI V3 dla tego CRD.


Aby nasz operator mógł zarządzać własnymi zasobami tego typu, musimy powiązać konto
ServiceAccount z właściwymi uprawnieniami do obiektu wdrożenia naszego operatora. W tym
celu wprowadzamy dedykowaną rolę, która zostanie użyta w ramach obiektu RoleBinding w
celu powiązania jej z obiektem ServiceAccount; uzyskujemy to za pomocą kodu z listingu 23.7.
Listing 23.7. Definicja roli, która daje dostęp do własnego zasobu

apiVersion: rbac.authorization.k8s.io/v1 kind: Role


metadata:

name: config-watcher-crd
rules:

- apiGroups:
- k8spatterns.io

resources:
- configwatchers
- configwatchers/finalizers

verbs: [ get, list, create, update, delete, deletecollection, watch ]


Dzięki tym obiektom CRD możemy zdefiniować własne zasoby, jak w kodzie przedstawionym na
listingu 23.5.
Aby skorzystać z tych zasobów, musimy zaimplementować kontroler, który wyewaluuje te
zasoby i uruchomi restart kapsuły w momencie zmiany obiektu ConfigMap.

W tym miejscu rozszerzamy skrypt Kontolera z listingu 22.2 i dostosowujemy go do pętli


zdarzeń w naszym skrypcie kontrolera.
W przypadku zmiany obiektu ConfigMap nie sprawdzamy konkretnej adnotacji — zamiast tego
odpytujemy wszystkie zasoby typu ConfigWatcher i sprawdzamy, czy zmodyfikowany obiekt
ConfigMap jest dołączony jako wartość atrybutu configMap. Listing 23.8 przedstawia pętle
uzgodnienia. W naszym repozytorium w serwisie Git znajdziesz pełny przykład, który zawiera
także szczegółowe instrukcje związane z instalacją tego operatora.

Listing 23.8. Pętla uzgadniania kontrolera WatchConfig

curl -Ns $base/api/v1/${ns}/configmaps?watch=true | \


while read -r event

do
type=$(echo “$event” | jq -r ‘.type’)

if [ $type = “MODIFIED” ]; then


watch_url=”$base/apis/k8spatterns.io/v1/${ns}/configwatchers”

config_map=$(echo “$event” | jq -r ‘.object.metadata.name’)

watcher_list=$(curl -s $watch_url | jq -r ‘.items[]’)


watchers=$(echo $watcher_list | \
jq -r “select(.spec.configMap == \”$config_map\”) |
.metadata.name”)

for watcher in watchers; do

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.

Sprawdzaj jedynie zdarzenie MODIFIED.

Pobierz listę wszystkich zainstalowanych własnych zasobów obiektów ConfigWatcher.

Pobierz z listy wszystkie elementy ConfigWatcher, które odwołują się do danego obiektu
ConfigMap.

Dla każdego znalezionego obiektu ConfigWatcher usuń skonfigurowane kapsuły za pomocą


selektora. W tym miejscu dla większej czytelności pomijamy logikę odpowiedzialną za uzyskanie
selektora etykiety, a także za usunięcie kapsuł. W repozytorium w serwisie Git znajdziesz
przykładowe kody zawierające pełną implementację.

Podobnie jak w przykładzie z Kontrolerem, ten kontroler można przetestować za pomocą


aplikacji webowej dostarczonej w ramach wspomnianego repozytorium. Jedyna różnica polega
na tym, że korzystamy z nieoznaczonego obiektu ConfigMap do skonfigurowania aplikacji.

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.

Strona Awesome Operators7 zawiera listę rzeczywistych, praktycznych operatorów, które


opierają się na konceptach poruszonych w tym rozdziale. Zobaczyliśmy już, jak można
skorzystać z operatora Prometheusa w celu zarządzenia instalacji Prometheusa. Kolejnym
operatorem napisanym w języku Go jest operator Etcd, który zarządza magazynem klucz-
wartość i automatyzuje zadania operacyjne, takie jak tworzenie kopii zapasowych i
przywracanie bazy danych.

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.

W wielu przypadkach zwykły Kontroler, który korzysta ze standardowych zasobów, będzie


wystarczająco dobry. To podejście ma tę zaletę, że nie wymaga uprawnień administracyjnych na
poziomie klastra, aby zarejestrować obiekt CRD — z drugiej strony, ma ono pewne ograniczenia
w zakresie bezpieczeństwa i walidacji.
Operator jest dobrym wyborem do modelowania własnej logiki domenowej, która jest dobrze
dopasowana do deklaratywnego sposobu obsługi zasobów w ramach reaktywnych kontrolerów.

Zastosowanie operatora z CRD warto rozważyć w następujących przypadkach:

kiedy potrzebujesz ścisłej integracji z istniejącymi narzędziami Kubernetesa, takimi jak


kubectl,
kiedy chcesz zaprojektować aplikację zupełnie od podstaw,
kiedy chcesz skorzystać z konceptów Kubernetesa, takich jak ścieżki zasobów, grupy API,
wersjonowanie API, a zwłaszcza przestrzenie nazw,
kiedy chcesz dysponować dobrym wsparciem po stronie klienta w zakresie dostępu do API
za pomocą obserwatorów, uwierzytelnienia, autoryzacji w oparciu o role i selektorów dla
metadanych.

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.

Dokumentacja Kubernetesa8 zawiera rozdział, w którym znajdziesz sugestie dotyczące użycia


Kontrolerów, Operatorów, agregacji API czy własnych implementacji API.

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ę.

Ręczne skalowanie horyzontalne


Ręczne podejście do skalowania, zgodnie z nazwą, polega na wydawaniu poleceń
Kubernetesowi przez operatora będącego człowiekiem. To podejście może być stosowane przy
braku automatycznego skalowania lub w ramach procesu stopniowego wykrywania i
dostrajania optymalnej konfiguracji dla aplikacji, której obciążenie zmienia się w czasie w
sposób nieznaczny. Zaletą takiego podejścia jest możliwość przewidywania, a nie tylko reakcji
— znając zasady zmienności w czasie i oczekiwane obciążenie aplikacji, jesteśmy w stanie
wyskalować ją na zapas, a nie jedynie reagować na zwiększone obciążanie w ramach
autoskalowania. Ręczne skalowanie możemy wykonać na dwa sposoby.

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.

Listing 24.1. Skalowanie replik wdrożenia z poziomu wiersza poleceń


kubectl scale random-generator --replicas=4

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).

Listing 24.2. Zastosowanie wdrożenia do deklaratywnego ustawiania liczby replik

kubectl apply -f random-generator-deployment.yaml


Zasoby możemy skalować zarządzając wieloma kapsułami, takimi jak ReplicaSet, Deployment
czy StatefulSet. Zwróć uwagę na asymetrię zachowania przy skalowaniu obiektów
StatefulSet. Jak opisujemy w rozdziale 11., jeśli obiekt StatefulSet zawiera element
.spec.volumeClaimTemplates, w trakcie skalowania w górę zostaną utworzone obiekty PVC,
ale już przy skalowaniu w dół nie dojdzie do ich automatycznego usunięcia — celem jest
uniknięcie przypadkowego usunięcia danych.

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.

Do opisu pól zasobów korzystamy z notacji ścieżek JSON. Zapis


.spec.replicas wskazuje na pole replicas w sekcji spec zasobu.

Oba style skalowania ręcznego (imperatywne i deklaratywne) zakładają udział czynnika


ludzkiego w obserwacji i przewidywaniu zmian w obciążeniu aplikacji. To człowiek musi podjąć
decyzję co do skalowania i zastosowania go w klastrze. Efekt końcowy jest ten sam, jednak
takie podejście nie jest właściwe w przypadku dynamicznie i często zmieniającego się
obciążenia, zwłaszcza gdy wymaga ono stałej adaptacji w naszym klastrze. Zobaczmy, jak
zautomatyzować sam proces decyzyjny dotyczący skalowania.

Horyzontalne autoskalowanie kapsuł


Obciążenie potrafi zmieniać się w czasie dość dynamicznie, przez co trudno jest ustalić stałą
konfigurację skalowania. Natywne technologie chmurowe, takie jak Kubernetes, pozwalają na
tworzenie aplikacji, które dostosowują się do zmieniającego się obciążenia. Autoskalowanie w
Kubernetesie pozwala na definiowanie zmiennej pojemności dla aplikacji. Rzecz w tym, aby
zapewnić akurat taką pojemność, która pozwoli obsłużyć bieżące obciążenie. Najprostszym
sposobem na uzyskanie takiego zachowania jest zastosowanie obiektu
HorizontalPodAutoscaler (HPA), który pozwala na horyzontalne skalowanie liczby kapsuł.

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.

Listing 24.3. Tworzenie definicji HPA z wiersza poleceń

kubectl autoscale deployment random-generator --cpu-percent=50 --min=1 --


max=5

Powyższe polecenie utworzy obiekt HPA przedstawiony na listingu 24.4.

Listing 24.4. Definicja HPA


apiVersion: autoscaling/v2beta2

kind: HorizontalPodAutoscaler

metadata:

name: random-generator spec:

minReplicas: 1

maxReplicas: 5

scaleTargetRef:
apiVersion: extensions/v1beta1

kind: Deployment

name: random-generator

metrics:
- resource:

name: cpu

target:
averageUtilization: 50

type: Utilization

type: Resource

Minimalna liczba kapsuł, które zawsze powinny być uruchomione.

Maksymalna liczba kapsuł, do jakiej HPA może się wyskalować.

Odniesienie do obiektu, który powinien być skojarzony z tym HPA.

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%).

W kodzie z listingu 24.4 korzystamy z wersji API v2beta2 zasobu w celu


skonfigurowania HPA. Ta wersja jest aktywnie rozwijana i pod względem
funkcjonalnym stanowi nadzbiór HPA. Wersja v2 pozwala na użycie znacznie
większej liczby kryteriów niż samo użycie CPU — można skorzystać z pamięci
czy wskaźników powiązanych z aplikacją. Korzystając z polecenia kubectl get
hpa.v2beta2.autoscaling -o yaml, możesz przekonwertować zasób HPA
utworzony przez polecenie kubectl autoscale na zasób v2.

Ta definicja nakazuje kontrolerowi HPA utrzymać pomiędzy jedną a pięcioma instancjami


kapsuły, aby utrzymać średnie zużycie CPU na poziomie 50% limitu określonego w deklaracji
.spec.resources.requests. Choć jest możliwe zastosowanie takiego HPA do dowolnego
zasobu, który obsługuje podzasób scale (np. Deployment, ReplicaSet czy StatefulSet),
musisz uważać na efekty uboczne. Wdrożenia tworzą obiekty ReplicaSet podczas aktualizacji,
ale bez kopiowania definicji HPA. Jeśli zastosujesz HPA do obiektu ReplicaSet zarządzanego
przez wdrożenie, nie zostanie on (HPA) skopiowany do nowych obiektów ReplicaSet i w ten
sposób zostanie stracony. Znacznie lepszą metodą jest zastosowanie HPA do abstrakcji
wdrożenia wyższego rzędu, która zachowuje i stosuje HPA do nowych wersji obiektu
ReplicaSet.

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:

1. HPA pobiera wskaźniki kapsuł poddawanych skalowaniu zgodnie ze swoją definicją.


Wskaźniki nie są odczytywane bezpośrednio z kapsuł, ale z API wskaźników Kubernetesa,
które udostępnia w zagregowanej formie (włączając w to własne i zewnętrzne wskaźniki,
jeśli tego wymaga konfiguracja). Wskaźniki zasobów na poziomie kapsuł są pobierane z
API wskaźników, a wszystkie inne wskaźniki są uzyskiwane z API wskaźników własnych
Kubernetesa.
2. HPA oblicza wymaganą liczbę replik, opierając się na aktualnych wartościach wskaźników
i mając na uwadze pożądaną wartość każdego z nich. Oto uproszczony wzór:

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.

Pole replicas automatycznie skalowanego zasobu zostanie zaktualizowane przy użyciu


obliczonej wartości, a pozostałe kontrolery wykonają swoją część pracy, dostosowując się do
nowego, pożądanego stanu. Rysunek 24.1 przedstawia jak działa HPA: monitoruje wskaźniki i
zmienia zadeklarowaną liczbę replik.

Rysunek 24.1. Mechanizm horyzontalnego autoskalowania kapsuł

Autoskalowanie to jeden z tych obszarów Kubernetesa, w których wiele szczegółów


niskopoziomowych wciąż ewoluuje niezwykle dynamicznie, a jednocześnie każdy z tych
szczegółów ma duży wpływ na zachowanie autoskalowania. Nie sposób omówić w tej książce
wszystkich szczegółów, ale w ramce „Więcej informacji” na końcu tego rozdziału znajdziesz
najświeższe informacje w tym zakresie.

Mówiąc ogólnie, mamy do dyspozycji następujące rodzaje wskaźników:

Wskaźniki standardowe

Te wskaźniki są deklarowane, gdy pole .spec.metrics.resource[:].type przyjmuje


wartość Resource i reprezentuje wskaźniki użycia zasobów, takie jak CPU czy pamięć. Są
one ogólne i dostępne dla każdego kontenera, w dowolnym klastrze o tej samej nazwie.
Możesz określić je, korzystając z wartości procentowych, tak jak w poprzednim
przykładzie, lub z wartości bezwzględnej. W obu przypadkach wartości są określane na
bazie gwarantowanej ilości zasobów, określanej za pomocą atrybutu requests zasobu, a
nie limits. Takie rodzaje wskaźników, najłatwiejsze w użyciu, są dostarczane przez serwer
wskaźników lub komponent Heapster, można je też uruchomić jako dodatki do klastra.

Własne wskaźniki

Są to wskaźniki, których pole .spec.metrics.resource[&#x2a;].type jest równe Object


lub Pod. Wymagają one bardziej zaawansowanej konfiguracji monitorowania klastra, która
może różnić się pomiędzy klastrami. Własny wskaźnik z określonym rodzajem kapsuły, jak
sama nazwa wskazuje, opisuje wskaźnik charakterystyczny dla danej kapsuły, podczas gdy
typ Object może opisać dowolny inny obiekt. Własne wskaźniki są udostępniane w
zagregowanej formie w ramach serwera API pod ścieżką custom.metrics.k8s.io. Są one
dostarczane przez różne adaptery wskaźników, takie jak Prometheus, Datadog, Microsoft
Azure czy Google Stackdriver.
Wskaźniki zewnętrzne

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.

Prawidłowe skonfigurowanie autoskalowania nie jest łatwe i wymaga przeprowadzenia pewnej


liczby eksperymentów i dostrajania. Poniżej opisujemy kilka kluczowych obszarów do
rozważenia w momencie konfigurowania HPA.

Wybór wskaźnika

Jedna z najważniejszych decyzji, które trzeba podjąć w związku z autoskalowaniem,


dotyczy rodzaju wskaźnika. Aby HPA był użyteczny, musi istnieć bezpośrednia korelacja
między wartością wskaźnika a liczbą replik kapsuły. Na przykład, jeśli wskaźnikiem jest
liczba zapytań na sekundę (choćby zapytań HTTP), zwiększenie liczby kapsuł powinno
spowodować zmniejszenie liczby żądań, ponieważ łączna liczba żądań jest rozdzielana na
więcej kapsuł. To samo dotyczy sytuacji, gdy mierzymy użycie CPU, ponieważ istnieje
bezpośrednia korelacja między liczbą wykonywanych żądań a użyciem CPU (zwiększona
liczba żądań spowoduje wzrost użycia CPU). W przypadku innych wskaźników, takich jak
zużycie pamięci, ta zależność nie jest prawdziwa. Problem z pamięcią polega na tym, że w
przypadku zużycia pewnej ilości pamięci, uruchomienie dodatkowych kapsuł nie spowoduje
zmniejszenia ilości pamięci już zajętej. Takie zachowanie jest możliwe tylko wtedy, gdy
aplikacja podlega klastrowaniu, jest świadoma obecności innych instancji i dysponuje
mechanizmami do rozproszenia i zwolnienia pamięci. Jeśli pamięć nie zostanie zwolniona,
w związku z czym nie będzie czego odnotować we wskaźnikach, HPA będzie tworzyć coraz
więcej kapsuł, doprowadzając do osiągnięcia górnego limitu replik, co z reguły nie jest
pożądane. Zdecydowanie lepiej jest wybierać zatem wskaźniki bezpośrednio skorelowane z
liczbą kapsuł.

Zapobieganie migotaniu

HPA stosuje różne techniki w celu zapobiegania gwałtownemu wykonywaniu sprzecznych


ze sobą decyzji, które mogą doprowadzić do często zmieniającej się liczby replik w
przypadku niestabilnego obciążenia. Na przykład podczas skalowania w górę, HPA pomija
wysokie użycie procesora w trakcie inicjalizacji kapsuły, zapewniając spokojną reakcję na
wzrastające obciążenie. Aby uniknąć gwałtownego skalowania w dół w wyniku
krótkotrwałego spadku obciążenia, kontroler analizuje wszystkie rekomendacje związane
ze skalowaniem w czasie okna czasowego o ustalonej przez nas długości. Wszystko to
sprawia, że HPA staje się bardziej odporny na losowe, chwilowe zmiany wskaźników.

Opóźniona reakcja

Uruchomienie procesu skalowania na podstawie wartości wskaźnika to proces


wieloetapowy, związany z wieloma komponentami Kubernetesa. Najpierw agent cAdvisor
(doradca kontenerów) zbiera wskaźniki w regularnych odstępach dla narzędzia Kubelet.
Następnie serwer wskaźników pobiera informacje z Kubeleta, również w regularny sposób.
Pętla kontrolera HPA jest również wykonywana okresowo w celu analizy zebranych
wskaźników. Metoda skalowania HPA wprowadza opóźnioną reakcję w celu uniknięcia
fluktuacji i migotania (co opisaliśmy w poprzednim punkcie). Wszystkie te działania
powodują powstanie opóźnienia pomiędzy zaistniałą przyczyną wzrostu obciążenia, a
reakcją na nią. Zwiększenie opóźnienia sprawi, że HPA stanie się mniej responsywne, zaś
zmniejszenie opóźnienia zwiększy obciążenie platformy, a także migotanie. Właściwa
konfiguracja Kubernetesa w celu optymalnego zrównoważenia zasobów i wydajności
wymaga ciągłego procesu nauki.

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.

Wertykalne autoskalowanie kapsuł


Horyzontalne skalowanie jest przedkładane nad wertykalne, ponieważ mniej zaburza działanie
systemu, zwłaszcza w przypadku usług bezstanowych. Nie dotyczy to jednak usług stanowych,
w przypadku których preferowane jest skalowanie wertykalne (pionowe). Tego rodzaju
skalowanie przydaje się także, gdy konieczne jest dostrojenie potrzeb usługi w zakresie
zasobów na podstawie faktycznych wzorców obciążenia. Stwierdziliśmy już, dlaczego określenie
właściwej liczby replik kapsuły może być trudne, a nawet niemożliwe z powodu zmiany
obciążenia w czasie. Skalowanie wertykalne również stanowi swego rodzaju wyzwanie, ale
dotyczy to właściwego określenia parametrów requests i limits dla container. Wertykalne
autoskalowanie kapsuły (ang. Vertical Pod Autoscaler — VPA) pomaga w rozwiązaniu tego
problemu, automatyzując proces dostosowania i alokowania zasobów na podstawie informacji
zwrotnej dotyczącej zużycia, pozyskanych z rzeczywistych aplikacji.

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.

Aby zademonstrować wertykalne autoskalowanie kapsuł, możemy skorzystać z definicji VPA w


klastrze z VPA i zainstalowanym serwerem wskaźników (listing 24.5).

Listing 24.5. VPA


apiVersion: poc.autoscaling.k8s.io/v1alpha1
kind: VerticalPodAutoscaler

metadata:
name: random-generator-vpa
spec:
selector:

matchLabels:

app: random-generator
updatePolicy:

updateMode: “Off”

Selektor etykiet używany do identyfikowania zarządzanych kapsuł.

Reguła aktualizacji, która określa, jak VPA zastosuje zmiany.

Definicja VPA składa się z następujących głównych części:


Selektor etykiety
Określa elementy do skalowania, identyfikując etykiety, które powinny być przez niego
obsłużone.
Reguła aktualizacji

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).

W zależności od wartości atrybutu .spec.updatePolicy.updateMode, VPA zaangażuje różne


komponenty systemowe. Wszystkie trzy komponenty VPA — polecający, dopuszczający i
aktualizujący — są niezależne i mogą być podmieniane przy użyciu alternatywnych
implementacji. Moduł polecający generuje rekomendacje — inspiracja pochodzi z systemu Borg
firmy Google. Obecna implementacja analizuje aktualne użycie zasobów kontenera pod
określonym obciążeniem przez pewien określony przedział czasu (domyślnie 8 dni). Następnie
generowany jest histogram i wybierany jest górny procent z tego przedziału. Poza samym
wskaźnikiem, moduł analizuje zdarzenia związane z zasobami (a konkretnie z pamięcią kapsuł),
takie jak wywłaszczenia czy zdarzenia braku pamięci (OutOfMemory).
W naszym przykładzie pole .spec.updatePolicy.updateMode ma wartość Off, ale możemy też
skorzystać z jednej z dwóch pozostałych opcji. Każda z nich ma inny poziom możliwych
zakłóceń na skalowanych kapsułach. Przeanalizujmy jak owe wartości działają, zaczynając od
tej związanej z najmniejszymi zakłóceniami.
updateMode: Off
Moduł polecający VPA zbiera wskaźniki i zdarzenia kapsuły, a następnie generuje
rekomendacje, które są zawsze przechowywane w sekcji status zasobu VPA. Na tym
kończy się działanie trybu Off. W ramach tego trybu jest wykonywana analiza i
otrzymujemy rekomendacje, ale nie są one wdrażane w kapsułach. Ten tryb jest użyteczny
do uzyskiwania informacji na temat użycia zasobów w kapsułach bez wprowadzania
istotnych zmian i zakłóceń. To od nas zależy, czy takie podejście nas interesuje.

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

Poza tworzeniem rekomendacji i stosowaniem ich do nowo tworzonych kapsuł, w tym


trybie VPA dodatkowo aktywuje zaktualizowany komponent. Komponent ten wywłaszcza
uruchomione kapsuły pasujące do selektora etykiet. Po wywłaszczeniu, kapsuły są
tworzone ponownie przez wtyczkę dopuszczającą VPA, która aktualizuje żądania
poszczególnych zasobów. To podejście generuje największe zakłócenia, ponieważ wszystkie
kapsuły są restartowane w celu przymusowego wdrożenia wszystkich rekomendacji. Takie
działanie może doprowadzić do nieoczekiwanych problemów z planowaniem, o których
pisaliśmy wcześniej.
Kubernetes został zaprojektowany do zarządzania niemodyfikowalnymi kontenerami z
niezmiennymi definicjami kapsuł (spec), co widać na rysunku 24.2. Choć w ten sposób
upraszczamy skalowanie horyzontalne, jednocześnie komplikujemy kwestię skalowania
wertykalnego, wymuszając usunięcie i ponowne utworzenie kapsuły, co ma wpływ na proces
planowania i może doprowadzić do zakłóceń w działaniu usług. Jest to prawdziwe nawet wtedy,
gdy kapsuła jest skalowana w dół i chce zwolnić zasoby już zaalokowane bez żadnych zakłóceń.
Rysunek 24.2. Mechanizm wertykalnego autoskalowania kapsuł

Kolejny problem wynika ze współistnienia VPA i HPA, ponieważ oba mechanizmy


autoskalowania nie są aktualnie świadome siebie nawzajem. Może to doprowadzić do
niepożądanego zachowania. Na przykład, jeśli HPA korzysta ze wskaźników zasobów, takich jak
CPU czy pamięć, a VPA ma wpływ także na te wartości, może się okazać, że kapsuły skalowane
są zarówno horyzontalnie, jak i wertykalnie (będziemy mieli zatem do czynienia z podwójnym
skalowaniem).

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.

CA to dodatek do Kubernetesa, który musi być włączony i skonfigurowany z określeniem


minimalnej i maksymalnej liczby węzłów. Może on działać tylko wtedy, gdy klaster Kubernetesa
jest uruchomiony w ramach infrastruktury chmurowej, w której węzły są dostarczane i usuwane
na żądanie. Oczywiście konieczne jest zapewnienie wsparcia dla CA na poziomie dostawcy — na
szczęście otrzymujemy je w usługach AWS, Microsoft Azure, a także Google Compute Engine.

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.

Rysunek 24.3. Mechanizm autoskalowania 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.

Podsumujmy wszystkie sposoby skalowania, począwszy od najbardziej drobiazgowego (rysunek


24.4).
Rysunek 24.4. Poziomy skalowania aplikacji

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.

Wertykalne autoskalowanie kapsuł


Zakładając, że aplikacja korzysta z zasobów kontenera efektywnie, kolejnym krokiem jest
ustawianie właściwych limitów i żądań zasobów w ramach kontenerów. Wcześniej pokazaliśmy
jak VPA potrafi zautomatyzować proces wykrywania i dostosowania optymalnych wartości na
podstawie faktycznego zapotrzebowania. Istotny problem polega na tym, że Kubernetes
wymaga usunięcia kapsuł i utworzenia ich od nowa, co pozostawia miejsce na krótkie lub
nieoczekiwane okresy zakłócenia usługi. Zaalokowanie większej ilości zasobów dla
pozbawionego zasobów kontenera potrafi uczynić kapsułę nieplanowalną i zwiększyć
obciążenie innych instancji. Zwiększenie zasobów kontenera może wymagać dostrojenia
aplikacji w celu najlepszego użycia powiększonych zasobów.

Horyzontalne autoskalowanie kapsuł


Poprzednie dwie metody stanowią formę skalowania wertykalnego — staramy się uzyskać
lepszą wydajność w ramach istniejących kapsuł przez różne formy dostrojenia, jednak bez
zmiany ich liczby. Kolejne dwie techniki to już przykład skalowania horyzontalnego — nie
zmieniamy specyfikacji kapsuł, ale dostosowujemy liczbę węzłów i kapsuł. To podejście pozwala
na zmniejszenie szansy na wystąpienie zakłóceń i regresji, pozwalając na prostszą
automatyzację. HPA stanowi obecnie najpopularniejszą formę skalowania. Choć początkowo
mechanizm ten dostarczał bardzo prosty zakres funkcjonalny (obsługując wskaźniki procesora i
pamięci), teraz możemy korzystać ze wskaźników zewnętrznych i własnych, co pozwala na
obsługę jeszcze bardziej zaawansowanych mechanizmów skalowania.
Zakładając, że wypróbowałeś dwie poprzednie metody skalowania, aby określić właściwe
wartości konfiguracji Twojej aplikacji, a także zużycie zasobów kontenera, teraz możesz
włączyć HPA, aby aplikacja była w stanie dostosować się do zmieniających się potrzeb w
zakresie zasobów.

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

1 W przypadku uruchamiania wielu kapsuł jednocześnie, miara currentMetricValue oznacza


średnie użycie procesora.
Rozdział 25. Budowniczy
Obrazów
Kubernetes to silnik orkiestracji ogólnego przeznaczenia, odpowiedni nie tylko do uruchamiania
aplikacji, ale także do budowania obrazów kontenerów. Wzorzec Budowniczy Obrazów opisuje,
dlaczego niezwykle ważne jest tworzenie obrazów kontenerów wewnątrz klastra i z jakich
mechanizmów można skorzystać do tworzenia obrazów w Kubernetesie.

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.

Budowanie bez demonów


Gdy budujesz obrazy wewnątrz Kubernetesa, klaster ma pełną kontrolę nad procesem
budowania, ale wymaga przez to spełnienia wyższych standardów bezpieczeństwa,
ponieważ budowanie nie odbywa się w izolacji. Aby budować obrazy w klastrze, musi
odbywać się to bez uprawnień konta root. Na szczęście obecnie istnieje wiele sposobów
na budowanie obrazów bez demonów, co pozwala na pracę bez podwyższonych
uprawnień.
Docker odniósł ogromny sukces w upowszechnieniu technologii kontenerowych dzięki
swojemu niezrównanemu doświadczeniu użytkownika. Docker bazuje na architekturze
klient-serwer, przy czym demon Dockera jest uruchomiony w tle i przyjmuje polecenia od
klientów za pomocą REST-owego API. Demon ten wymaga uprawnień konta root głównie
z uwagi na obsługę sieci i zarządzanie wolumenami. Niestety, powoduje to problemy z
bezpieczeństwem, ponieważ niezaufane procesy mogą wydostać się spod kontroli
kontenera, a włamywacz może uzyskać kontrolę nad całym hostem. Problem ten dotyczy
nie tylko samego uruchamiania kontenerów, ale także ich budowania, ponieważ
budowanie odbywa się w ramach kontenera, gdy demon Dockera wykonuje dowolne
polecenia.
Utworzono wiele projektów, aby umożliwić budowanie obrazów Dockera bez konieczności
stosowania uprawnień konta root. Niektóre z nich nie pozwalają na wykonywanie poleceń
w czasie budowania (np. Jib), a pozostałe stosują inne techniki. W momencie pisania tych
słów, najważniejszymi narzędziami do budowania obrazów bez użycia demonów są img,
buildah i Kaniko. Ponadto warto przyjrzeć się systemowi S2I (ang. source-to-image —
źródło do obrazu), opisanego w podrozdziale „Source-to-Image”, który również buduje
obraz bez uprawnień konta root.

Skoro zapoznaliśmy się z korzyściami budowania obrazów na platformie, zastanówmy się, z


jakich technik możemy skorzystać do tworzenia obrazów w klastrze Kubernetesa.

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”.

Kolejnym mechanizmem do wewnątrzklastrowego budowania jest Budowanie w Knative.


Mechanizm ten działa na podstawie Kubernetesa i siatki usług Istio, będąc jedną z głównych
części platformy Knative1, używanej do budowania, wdrażania i zarządzania mechanizmami
typu serverless. W momencie pisania tych słów, Knative jest wciąż młodym i dynamicznie
rozwijającym się projektem. W podrozdziale „Budowanie w Knative” przedstawiamy omówienie
Knative i podajemy kilka przykładów związanych z budowaniem obrazów w klastrze
Kubernetesa z pomocą Knative.

Najpierw jednak przyjrzyjmy się technologii 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.

W OpenShift wprowadzono pierwszą zintegrowaną z klastrem metodę bezpośredniego


budowania obrazów zarządzaną przez Kubernetesa. Obsługuje ona wiele strategii budowania
obrazów:

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

Metoda wiąże wersje z zadaniami budowania zarządzanego wewnętrznie serwera Jenkins,


pozwalając użytkownikowi na skonfigurowanie potoku Jenkinsa.

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

Zasób dostarcza do wersji informacje poufne.


Binarny

Źródło wszystkich informacji wejściowych z zewnątrz. Te informacje muszą być


dostarczone, gdy dochodzi do uruchomienia procesu budowania.

Możliwość stosowania źródeł wejściowych w określony sposób zależy od strategii budowania.


Rodzaje Binarny i Git wzajemnie się wykluczają. Wszystkie inne rodzaje mogą być używane
łącznie lub samodzielnie. Szczegóły znajdziesz na listingu 25.1.

Wszystkie informacje dotyczące budowania są zdefiniowane w centralnym obiekcie zasobu o


nazwie BuildConfig. Możemy utworzyć ten zasób bezpośrednio, stosując go w klastrze, lub
korzystając z polecenia konsoli oc, co stanowi występujący w OpenShift równoważnik polecenia
kubectl. Polecenie oc obsługuje polecenia specyficzne dla wersji, związane z jej definiowaniem
i wyzwalaniem.

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.

Aby połączyć obiekt ImageStream z wdrożeniem, OpenShift korzysta z zasobu


DeploymentConfig zamiast zasobu Deployment, który potrafi skorzystać tylko
z obrazów powiązanych bezpośrednio. Mimo to wciąż możesz korzystać ze
zwykłych zasobów Deployment, o ile nie planujesz skorzystać z obiektów
ImageStream.

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.

Opcjonalnie możesz skorzystać ze skryptu, aby dostarczyć komunikat o sposobie użycia,


oszczędzając wygenerowane artefakty do tak zwanych wersji inkrementalnych, które są
dostępne za pomocą skryptu assemble przy kolejnych uruchomieniach procesu budowania. Za
pomocą skryptu możesz też dodać mechanizmy weryfikacyjne.
Przyjrzyjmy się bliżej wersji zbudowanej przez S2I, pokazanej na rysunku 25.1. Wersja z S2I ma
dwa główne składniki: obraz budowniczego i wejście źródłowe. Oba elementy są łączone razem
przez system budowania S2I w momencie startu procesu budowania — albo w momencie
otrzymania zdarzenia wyzwalacza, albo z powodu uruchomienia ręcznego. Gdy obraz
budowania zostaje ukończony, na przykład w wyniku kompilacji kodu źródłowego, kontener jest
umieszczany w obrazie i wysyłany do skonfigurowanego obiektu ImageStreamTag. Obraz ten
zawiera skompilowane i przygotowane artefakty, a skrypt run obrazu jest ustawiany jako punkt
wejściowy.
Rysunek 25.1. Wersja S2I ze źródłem Gita jako wejściem

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.

Listing 25.1. Wersja S2I zbudowana za pomocą obrazu budowniczego Javy

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

Odniesienie do kodu źródłowego, który należy pobrać — w tym przypadku pobieramy go z


GitHuba.

Atrybut sourceStrategy przełącza tryb na S2I. Obraz budowniczego jest pobierany


bezpośrednio z serwisu Docker Hub.

Obiekt ImageStreamTag zmodyfikowany za pomocą wygenerowanego obrazu. Kontener


budowniczego jest wysyłany po wykonaniu skryptu assemble.

Przebuduj automatycznie w momencie aktualizacji obrazu budowniczego.


S2I to wyszukany mechanizm do tworzenia obrazów aplikacji, bezpieczniejszy od zwykłych
wersji wygenerowanych przez Dockera, ponieważ proces budowania przebiega pod pełną
kontrolą zaufanych budowniczych obrazów. Niestety, to podejście ma pewne wady.
W przypadku złożonych aplikacji S2I potrafi działać wolno, zwłaszcza gdy zbudowana wersja
musi wczytać dużo zależności. Bez optymalizacji S2I wczytuje wszystkie zależności od nowa
podczas każdej operacji budowania. W przypadku aplikacji Java zbudowanej za pomocą Mavena
pamięć podręczna nie jest używana podczas tworzenia wersji lokalnych. Aby unikać pobierania
danych z internetu raz za razem, zdecydowanie zaleca się skonfigurować lokalne repozytorium
Mavena w ramach klastra; posłuży ono jako pamięć podręczna. Obraz budowniczego musi być
skonfigurowany tak, aby korzystać z tego wspólnego repozytorium zamiast pobierania
artefaktów ze zdalnych repozytoriów.

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.

Alternatywnie, możesz też skorzystać ze standardowego, wieloczęściowego pliku Dockerfile,


aby oddzielić części związane z budowaniem i uruchamianiem. Nasze przykładowe
repozytorium4 zawiera w pełni działającą dockerową wersję wieloczęściową, która daje taki
sam obraz, co wersja łańcuchowa opisana w kolejnym podrozdziale.

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.

Zwróć uwagę na to, że wyzwalacz zastosowany w kodzie z listingu 25.2


monitoruje wynik procesu budowania S2I. Wyzwalacz spowoduje
przebudowanie obrazu uruchomieniowego zawsze, gdy uruchomimy wersję
S2I, dzięki czemu oba obiekty ImageStream są zawsze zsynchronizowane.

Listing 25.2. Wersja dockerowa do zbudowania obrazu aplikacji


apiVersion: v1
kind: BuildConfig

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.

Atrybut strategy wybiera wersję dockerową.

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.

Pełny przykład, wraz z instrukcjami instalacji, znajdziesz w naszym repozytorium5.

Jak wspomnieliśmy, budowanie w OpenShift w połączeniu z trybem S2I stanowi jedną z


najstarszych i najdojrzalszych metod bezpiecznego budowania obrazów kontenerów w klastrze
Kubernetesa.

Teraz przyjrzymy się innej metodzie budowania obrazów kontenerów, z wykorzystaniem


zwykłego klastra Kubernetesa, bez żadnych dodatków.

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

Jest używane na przykład do wspierania mechanizmu skalowania do zera w usługach


aplikacji, wykorzystywanych w platformach typu funkcje-jako-usługa. Wraz z wzorcem
Elastycznego Skalowania, opisanym w rozdziale 24., i wsparciem bazowej siatki usług,
mechanizm udostępniania w Knative pozwala na skalowanie od zera do dowolnej liczby
replik.
Zdarzenia w Knative

Mechanizm dostarczania zdarzeń od źródeł (ang. sources) do odpływów (ang. sinks) za


pomocą kanałów (ang. channels). Zdarzenia mogą wyzwalać usługi używane jako odpływy
w celu ich wyskalowania od zera.

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.

W pozostałej części tego podrozdziału skupimy się na budowaniu w Knative, ponieważ to


właśnie ten aspekt stanowi implementację wzorca Budowniczy Obrazów w Knative.

Budowanie w Knative jest przeznaczone dla twórców narzędzi, dostarczając im


interfejs użytkownika i bezproblemowe doświadczenie budowania dla
użytkowników końcowych. W tym miejscu przedstawiamy ogólne omówienie
elementów procesu budowania w Knative. Projekt rozwija się niezwykle
dynamicznie i niedługo może zostać zastąpiony przez Tekton Pipelines, jednak
zasady działania pozostają bez zmian. Niezależnie od tego, pewne szczegóły
mogą zmienić się w przyszłości, dlatego zapoznaj się z przykładowymi
kodami7, które utrzymujemy na bieżąco, dostosowując je do najnowszych
wersji projektów Knative.

Knative zaprojektowano w celu dostarczenia elementów konstrukcyjnych do integracji z


istniejącymi rozwiązaniami z zakresu integracji i wdrażania ciągłego (CI/CD). Nie jest to
natomiast rozwiązanie CI/CD do samodzielnego budowania obrazów kontenerów. Uważamy, że
w miarę upływu czasu pojawi się coraz więcej tego rodzaju rozwiązań, ale póki co musimy
skupić się na elementach konstrukcyjnych.

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

Nazwa obiektu wersji.

Specyfikacja źródła zawierająca adres URL GitHuba.

Jeden lub więcej kroków budowania.

Obraz zawierający dystrybucje Javy i Mavena używane w tym kroku budowania.

Argumenty przekazane do kontenera budowniczego, które uruchomią Maven w celu


kompilacji, utworzenia i wysłania obrazu kontenera za pomocą wtyczki jib-maven-plugin.

Katalog /workspace jest współdzielony i montowany dla każdego kroku budowania.


W tym przykładzie źródłem jest ponownie nasz przykładowy projekt w Javie, umieszczony w
serwisie GitHub i zbudowany za pomocą Mavena. Obrazem budowniczego jest obraz kontenera,
który zawiera Javę i Mavena. Jib8 jest używany do zbudowania obrazu bez demona Dockera, a
także do wysłania go do rejestru.
Warto przyjrzeć się także pokrótce temu, co siedzi pod maską, aby dowiedzieć się, jak operator
budowania w Knative realizuje ten proces.

Rysunek 25.3 przedstawia sposób przekształcenia własnego zasobu na zwykłe zasoby


Kubernetesa. Zasób staje się kapsułą, a etapy budowania są tłumaczone na łańcuch kontenerów
inicjalizacji, wywoływanych jeden za drugim. Pierwsze kontenery inicjalizacji są tworzone
niejawnie. W naszym przykładzie jeden z kontenerów inicjalizuje dane uwierzytelniające
używane do interakcji z zewnętrznymi repozytoriami, a drugi pobiera źródło z serwisu GitHub.
Ostatni kontener inicjalizacji zawiera jedynie kroki z zadeklarowanymi obrazami budowniczego.
Po zakończeniu wszystkich kontenerów inicjalizacji, główny kontener nie robi nic, dlatego
kapsuła zatrzymuje się po zakończeniu kroków inicjalizacji.

Rysunek 25.3. Budowanie w Knative z wykorzystaniem kontenerów inicjalizacji

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.

Listing 25.4 przedstawia szablon budowania zawierający trzy kroki:

1. Utwórz plik JAR za pomocą polecenia mvn package.


2. Utwórz plik Dockerfile, który skopiuje plik JAR do obrazu kontenera, a następnie uruchom
go za pomocą polecenia java -jar.
3. Korzystając z Kaniko, utwórz i wyślij obraz kontenera zawierający budowniczego. Kaniko
to narzędzie utworzone przez Google do budowania obrazów kontenerów z pliku
Dockerfile wewnątrz kontenera, z lokalnym demonem Dockera uruchomionym w
przestrzeni użytkownika.
Listing 25.4. Szablon budowania w Knative z wykorzystaniem Mavena i Kaniko

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}

Lista parametrów do dostarczenia w momencie użycia szablonu.

Krok, w którym kompilujemy i pakujemy aplikację Javy za pomocą Mavena.

Krok, w którym tworzymy plik Dockerfile w celu skopiowania i uruchomienia


wygenerowanego pliku JAR. Pomijamy tutaj szczegóły, ale znajdziesz je w naszym repozytorium
w serwisie GitHub.
Krok, w którym wywołujemy Kaniko w celu zbudowania i wysłania obrazu Dockera.

Miejsce docelowe jest określone za pomocą dostarczonego parametru szablonu ${IMAGE}.


Szablon wygląda mniej więcej tak jak obiekt Build, z wyjątkiem tego, że obsługuje parametry
takie jak treści zastępcze, wypełniane w momencie użycia szablonu. W tym przykładzie IMAGE
to jedyny wymagany parametr, który określa docelowy obraz do utworzenia.
Ten szablon można wykorzystać w obiekcie Build, w którym określamy nazwę szablonu zamiast
listy kroków (listing 25.5). Jak widać, musisz podać jedynie nazwę obrazu kontenera aplikacji,
który chcesz utworzyć. W ten sposób możesz łatwo skorzystać wiele razy z tego samego
wieloetapowego procesu budowania, aby utworzyć różne aplikacje.
Listing 25.5. Budowanie w Knative z wykorzystaniem szablonu budowania
apiVersion: build.knative.dev/v1alpha1
kind: Build

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

Specyfikacja źródła, z którego pobieramy kod źródłowy.

Odniesienie do szablonu zdefiniowanego na listingu 25.4.

Specyfikacja obrazu wprowadzona do szablonu jako parametr.


Wiele predefiniowanych szablonów znajdziesz w repozytorium Knative o nazwie build-
templates.
Ten przykład kończy nasz krótki przegląd budowania w Knative. Jak wspomnieliśmy, projekt jest
wciąż bardzo młody i szczegóły działania mogą się z dużym prawdopodobieństwem zmienić,
jednak główny mechanizm powinien pozostać bez zmian.

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.

Co omówiliśmy w tej książce


W tej książce przedstawiliśmy 24 najpopularniejsze wzorce Kubernetesa, zgrupowane w
następujący sposób:

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.

Kaczka hełmiasta osiąga 45 – 60 cm wysokości i waży 0,8 – 1,5 kg (osobniki dorosłe).


Rozpiętość skrzydeł sięga 72 – 82 cm. Samice mają pióra o różnych odcieniach koloru
brązowego i jasne policzki. Są mniej kolorowe od samców. Samiec ma czerwony dziób,
rdzawopomarańczową głowę, białe boki, czarny ogon i pierś.

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

You might also like