Uploaded by Mirosław Stolarczyk

Najlepsze praktyki w Kubernetes. - Liz Rice, Brenda Burns, Eddie Villalba

advertisement
Brendan Burns, Eddie Villalba,
Dave Strebel, Lachlan Evenson
Najlepsze praktyki w
Kubernetes
Jak budować udane aplikacje
Tytuł oryginału: Kubernetes Best Practices: Blueprints for Building Successful Applications on
Kubernetes
Tłumaczenie: Robert Górczyński
ISBN: 978-83-283-7233-7
© 2021 Helion SA Authorized Polish translation of the English edition of Kubernetes Best
Practices ISBN 9781492056478 © 2020 Brendan Burns, Eddie Villalba, Dave Strebel, and
Lachlan Evenson
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.
Autorzy 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. Autorzy 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)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/naprak_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Pliki
z
przykładami
omawianymi
ftp://ftp.helion.pl/przyklady/naprak.zip
Poleć książkę
Kup w wersji papierowej
Oceń książkę
Księgarnia internetowa
Lubię to! » nasza społeczność
w
książce
można
znaleźć
pod
adresem:
Wprowadzenie
Dla kogo jest przeznaczona ta książka?
Kubernetes to faktycznie standard wdrożenia natywnej chmury. To narzędzie o potężnych
możliwościach, dzięki któremu Twoja następna aplikacja może być łatwiejsza do opracowania,
szybsza do wdrożenia i bardziej niezawodna w działaniu. Jednak żeby móc wykorzystać potężne
możliwości Kubernetes, trzeba używać go właściwie. Niniejsza książka jest przeznaczona dla
każdego, kto zajmuje się wdrażaniem rzeczywistych aplikacji w Kubernetes oraz jest
zainteresowany poznaniem wzorców i najlepszych praktyk, które mogą być zastosowane w
aplikacjach budowanych na podstawie Kubernetes.
Należy w tym miejscu wspomnieć, że ta książka nie zawiera informacji wprowadzających do
pracy z Kubernetes. Przyjęliśmy założenie, że znasz API i narzędzia Kubernetes, a także wiesz,
jak utworzyć klaster Kubernetes i z nim pracować. Jeżeli szukasz informacji pomagających w
rozpoczęciu pracy z Kubernetes, znajdziesz wiele dobrych pozycji na ten temat, np. Kubernetes.
Tworzenie niezawodnych systemów rozproszonych. Wydanie II (helion.pl/ksiazki/kuber2.htm).
Niniejsza książka została napisana dla czytelników, którzy chcą się zagłębić w temat wdrażania
określonych aplikacji i rozwiązań w Kubernetes. Powinna być użyteczna niezależnie od tego,
czy dopiero przystępujesz do wdrożenia pierwszej aplikacji w Kubernetes, czy też zajmujesz się
tym już od wielu lat.
Dlaczego napisaliśmy tę książkę?
Cała nasza czwórka ma duże doświadczenie w pomaganiu szerokiej gamie użytkowników we
wdrażaniu ich aplikacji w Kubernetes. Spotykaliśmy osoby zmagające się z wdrożeniem i
pomagaliśmy im znaleźć odpowiednie rozwiązania. W tej książce próbowaliśmy zawrzeć
wspomniane doświadczenie, aby jak najwięcej osób mogło uczyć się na naszych błędach, które
popełniliśmy w rzeczywistych wdrożeniach. Mamy nadzieję, że nasza wiedza zawarta w tej
książce pozwoli się skalować i że wykorzystasz ją do udanego wdrażania swoich aplikacji w
Kubernetes i zarządzania nimi.
Poruszanie się po książce
Wprawdzie tę książkę można przeczytać od deski do deski, ale tak naprawdę to nie będzie
właściwe podejście. Została zaprojektowana jako kolekcja oddzielnych rozdziałów. W każdym z
nich znajdziesz pełne omówienie określonego zadania, które być może będziesz chciał wykonać
za pomocą Kubernetes. Spodziewamy się, że zagłębisz się w lekturę, aby dokładnie poznać
interesujące Cię zagadnienie. Następnie odłożysz książkę na półkę i powrócisz do niej, gdy
pojawi się następny temat, który będziesz chciał poznać.
Mimo że przyjęliśmy podejście oparte na oddzielnych rozdziałach, pewne tematy przewijają się
w całej książce. Jest kilka rozdziałów poświęconych opracowywaniu aplikacji w Kubernetes. W
rozdziale 2. został omówiony sposób pracy programisty. W rozdziale 6. poruszamy temat ciągłej
integracji i testowania. W rozdziale 15. dowiesz się nieco o budowie platform wysokiego
poziomu na podstawie Kubernetes, a w rozdziale 16. zajmiemy się zarządzaniem informacjami o
stanie aplikacji. W kilku rozdziałach obok tematu opracowywania aplikacji zostały poruszone
zagadnienia związane z działaniem usług w Kubernetes. W rozdziale 1. przedstawiliśmy
konfigurację podstawowej usługi, a w rozdziale 3. — monitorowanie i sprawdzanie wskaźników.
W rozdziale 4. został poruszony temat zarządzania konfiguracją, rozdział 6. zaś dotyczy
wersjonowania i wydań. Z kolei z rozdziału 7. dowiesz się, jak można wdrożyć aplikację na
całym świecie.
W książce znalazło się kilka rozdziałów dotyczących zarządzania klastrem, m.in. rozdział 8.,
poświęcony zarządzaniu zasobami, rozdział 9., poświęcony sieci, rozdział 10., poświęcony
zapewnieniu bezpieczeństwa podom, rozdział 11., poświęcony polityce bezpieczeństwa i
zaleceniom, rozdział 12., poświęcony zarządzaniu wieloma klastrami, i rozdział 17., dotyczący
autoryzacji i sterowania dopuszczeniem do klastra. Ponadto mamy dwa prawdziwie niezależne
rozdziały. Pierwszy z nich (rozdział 14.) dotyczy uczenia maszynowego, a drugi (rozdział 13.) —
integracji z usługami zewnętrznymi.
Wprawdzie dobrym pomysłem może być przeczytanie wszystkich rozdziałów przed próbą
zajęcia się danym tematem w rzeczywistym projekcie, ale naszym celem było przygotowanie
książki, którą można traktować jako przewodnik. Ma ona pomóc w praktycznym zastosowaniu
omówionych tematów.
Konwencje zastosowane w książce
W tej książce zastosowano następujące konwencje typograficzne.
Kursywa
Wskazuje na nowe pojęcia, adresy URL i e-mail, nazwy plików, rozszerzenia plików itd.
Czcionka o stałej szerokości
Użyta w przykładowych fragmentach kodu, a także w samym tekście, w odwołaniach do
pewnych poleceń bądź innych elementów programistycznych, takich jak: nazwy zmiennych
lub funkcji, baz danych, typów danych, zmiennych środowiskowych, poleceń i słów
kluczowych.
Pogrubiona czcionka o stałej szerokości
Użyta w celu wyeksponowania poleceń bądź innego tekstu, który powinien być
wprowadzony przez czytelnika.
Pochylona czcionka o stałej szerokości
Wskazuje tekst, który powinien być zastąpiony wartościami podanymi przez użytkownika
bądź wynikającymi z kontekstu.
Taka ikona oznacza wskazówkę lub sugestię.
Taka ikona oznacza ogólną uwagę.
Taka ikona oznacza ostrzeżenie.
Użycie przykładowych kodów
Materiały dodatkowe (przykładowe fragmenty kodu, ćwiczenia itd.) zostały zamieszczone w
serwisie GitHub pod adresem https://github.com/brendandburns/kbp-sample.
Jeżeli masz pytania techniczne lub jakikolwiek problem związany z użyciem przykładowych
fragmentów kodu, możesz do nas napisać na adres bookquestions@oreilly.com.
Książka ta ma pomóc Ci w pracy. Ogólnie rzecz biorąc, można wykorzystywać zawarte w niej
przykłady w swoich programach i w dokumentacji. Nie trzeba kontaktować się z nami w celu
uzyskania zezwolenia, dopóki nie powiela się znaczących ilości kodu. Na przykład pisanie
programu, w którym znajdzie się kilka fragmentów kodu z tej książki, nie wymaga zezwolenia,
jednak sprzedawanie lub rozpowszechnianie płyty CD-ROM zawierającej przykłady z książki
wydawnictwa O’Reilly — już tak. Odpowiedź na pytanie przez cytowanie tej książki lub
przykładowego kodu nie wymaga zezwolenia, ale włączenie wielu przykładowych kodów z tej
książki do dokumentacji produktu czytelnika — już tak.
Jesteśmy wdzięczni za umieszczanie przypisów, ale nie wymagamy tego. Przypis zwykle zawiera
tytuł, autora, wydawcę i ISBN. Na przykład: Brendan Burns, Eddie Villalba, Dave Strebel i
Lachlan Evenson, Najlepsze praktyki w Kubernetes, ISBN 978-83-283-7232-0, Helion, Gliwice
2020.
Podziękowania
Brendan chciałby podziękować swojej wspaniałej rodzinie — Robin, Julii i Ethanowi — za miłość
i wsparcie, które otrzymuje na każdym kroku. Dziękuje także społeczności Kubernetes oraz
wspaniałym współautorom, bez których ta książka by nie powstała.
Dave chciałby podziękować swojej pięknej żonie Jen i trójce dzieci — Maxowi, Maddie i
Masonowi — za okazywane przez nich wsparcie. Dziękuje również społeczności Kubernetes za
wskazówki i pomoc, które otrzymał w ciągu wielu lat. Podziękowania składa także
współautorom, dzięki którym możliwe było powstanie tej książki.
Lachlan chciałby podziękować swojej żonie i trójce dzieci za miłość i wsparcie. Dziękuje
również każdemu członkowi społeczności Kubernetes, m.in. wspaniałym osobom, które przez
lata poświęciły swój czas, aby mu pomagać. Specjalne podziękowania kieruje do Josepha
Sandovala. Dziękuje także fantastycznym współautorom, dzięki którym możliwe było powstanie
tej książki.
Eddie chciałby podziękować swojej żonie Sandrze, za wsparcie i zgodę na to, aby znikał na całe
godziny, by pisać tę książkę w czasie, gdy ona była w ostatnim trymestrze ich pierwszej ciąży.
Chciałby też podziękować swojej córce Giavannie, za motywację do dalszego działania. Dziękuje
także społeczności Kubernetes oraz współautorom, którzy zawsze byli jego drogowskazami
podczas podróży do natywnej chmury.
Wszyscy chcielibyśmy podziękować Virginii Wilson za pracę nad tekstem oraz pomoc w
połączeniu wszystkich naszych pomysłów w jedną całość. Podziękowania składamy także innym
pracownikom wydawnictwa — Bridget Kromhout, Bilginowi Ibryamowi, Rolandowi Hußowi i
Justinowi Domingusowi — za ich zaangażowanie w dopracowanie szczegółów.
Rozdział 1. Konfiguracja
podstawowej usługi
W tym rozdziale zostaną przedstawione praktyki związane z konfiguracją wielowarstwowej
aplikacji w Kubernetes. Omawiane rozwiązanie składa się z prostej aplikacji internetowej i bazy
danych. Wprawdzie to nie jest zbyt skomplikowany przykład, ale doskonale nadaje się do
omówienia tematu zarządzania aplikacją w Kubernetes.
Ogólne omówienie aplikacji
Aplikacja, która zostanie użyta w omawianym przykładzie, nie zalicza się do szczególnie
skomplikowanych. To jest prosta usługa dziennika, przechowująca dane w backendzie opartym
na Redis. W aplikacji znajduje się oddzielny, statyczny plik serwera NGINX. W pojedynczym
adresie URL zostaną udostępnione dwie ścieżki internetowe. Jedna z nich jest przeznaczona dla
interfejsu API RESTful aplikacji (https://my-host.io/api), druga zaś to plik serwera dostępnego
pod głównym adresem URL (https://my-host.io/). Do zarządzania certyfikatami SSL (ang. secure
socket layer) została użyta usługa Let’s Encrypt (https://letsencrypt.org/). Omawiana aplikacja
została pokazana na rysunku 1.1. W rozdziale zajmiemy się jej budową: najpierw utworzymy
pliki konfiguracyjne YAML, a następnie pliki Helm w formacie chart1.
Rysunek 1.1. Wykres przedstawiający omawianą aplikację
Zarządzanie plikami konfiguracyjnymi
Zanim zagłębimy się w szczegóły związane z przygotowaniem tej aplikacji w Kubernetes, warto
zająć się tematem zarządzania plikami konfiguracyjnymi. W Kubernetes wszystko jest
przedstawiane w sposób deklaratywny. To oznacza możliwość zapisania oczekiwanego stanu
aplikacji w klastrze (ogólnie rzecz biorąc, te informacje umieszcza się w plikach typu YAML lub
JSON), a zadeklarowany stan będzie definiował wszystkie aspekty aplikacji. Takie deklaratywne
podejście jest znacznie chętniej stosowane niż podejście imperatywne, w którym aktualny stan
klastra jest wynikiem serii wprowadzonych w nim zmian. Jeżeli klaster jest definiowany
imperatywnie, wówczas bardzo trudno będzie replikować klaster do tego stanu. W takich
przypadkach niezwykle trudno jest dokładnie zrozumieć sposób funkcjonowania danego klastra
lub go naprawić po wystąpieniu problemów związanych z aplikacją.
Użytkownicy do deklarowania stanu aplikacji zwykle preferują pliki w formacie YAML lub JSON.
Kubernetes obsługuje oba wymienione typy. Warto wiedzieć, że format YAML jest zwięźlejszy i
łatwiejszy do edycji przez człowieka niż format JSON. Trzeba jednak w tym miejscu podkreślić,
że w formacie YAML wcięcia mają znaczenie. Część błędów w konfiguracjach Kubernetes
wynika z użycia niepoprawnych wcięć w pliku YAML. Jeżeli rozwiązanie nie działa zgodnie z
oczekiwaniami, wówczas procedurę debugowania najlepiej jest zacząć od sprawdzenia wcięć w
pliku konfiguracyjnym w formacie YAML.
Skoro deklaracyjny stan zapisany w plikach YAML działa w charakterze źródła danych o
aplikacji, to właściwe zarządzanie tymi informacjami o stanie ma krytyczne znaczenie dla
poprawności działania aplikacji. Podczas modyfikowania żądanego stanu aplikacji chcesz mieć
możliwość zarządzania zmianami, weryfikowania ich poprawności, sprawdzania, kto
wprowadził daną zmianę, i prawdopodobnie możliwość jej wycofania, gdy zmiana prowadzi do
niepoprawnego działania aplikacji. Na szczęście zostały opracowane narzędzia niezbędne do
zarządzania zmianami zapisanymi w postaci deklaratywnej oraz przeprowadzania audytu i
wycofywania zmian. Najlepsze praktyki w zakresie kontroli wersji i technik przeglądu kodu
(ang. code review) można bezpośrednio stosować podczas zarządzania deklaratywnym stanem
aplikacji.
Obecnie większość użytkowników przechowuje konfigurację Kubernetes w repozytoriach Git.
Wprawdzie szczegóły związane z systemem kontroli wersji nie mają znaczenia, ale wiele
narzędzi używanych w ekosystemie Kubernetes oczekuje plików znajdujących się w
repozytorium Git. Do przeglądu kodu używa się większej liczby narzędzi, choć GitHub jest
chętnie wykorzystywany także do tego celu. Do układania poszczególnych komponentów w
systemie plików warto stosować rozwiązanie oparte na katalogach systemu plików. Niezależnie
od sposobu implementacji technik przeglądu kodu w konfiguracji aplikacji do tego zadania
powinieneś podejść równie starannie i skoncentrować się nad tym, co dodajesz do kodu
źródłowego.
Do układania poszczególnych komponentów w systemie plików warto stosować rozwiązanie
oparte na katalogach systemu plików. Zwykle jeden katalog zawiera całą usługę aplikacji,
niezależnie od definicji usługi aplikacji stosowanej przez dany zespół. W tym katalogu znajdują
się podkatalogi zawierające podkomponenty aplikacji.
W przypadku omawianej aplikacji układ plików i katalogów wygląda tak:
journal/
frontend/
redis/
fileserver/
W poszczególnych katalogach znajdują się konkretne pliki YAML potrzebne do zdefiniowania
usługi. Jak zobaczysz w dalszej części rozdziału, gdy rozpoczniesz wdrażanie aplikacji w wielu
różnych regionach lub klastrach, ten układ plików i katalogów stanie się znacznie bardziej
skomplikowany.
Tworzenie usługi replikowanej za pomocą
wdrożeń
Aby opracować aplikację, rozpoczynamy od frontendu, a następnie zajmujemy się kolejnymi
komponentami. W omawianym przykładzie frontendem jest aplikacja Node.js, której kod został
utworzony w języku TypeScript. Pełna aplikacja (https://github.com/brendandburns/kbpsample) jest zbyt obszerna, aby jej kod w całości zamieścić w książce. Udostępnia ona na porcie
8080 usługę HTTP, która obsługuje żądania kierowane do ścieżki dostępu /api/∗, i używa
opartego na Redis backendu w celu dodania, usunięcia lub przekazania aktualnych wpisów
dziennika. Tę aplikację można umieścić w obrazie kontenera za pomocą dołączonego pliku
Dockerfile, a następnie przekazać ten obraz do własnego repozytorium obrazów. Jeżeli
zdecydujesz się na takie rozwiązanie, pamiętaj, aby w kolejnych przykładach plików
konfiguracyjnych YAML zamieniać użytą nazwę na nazwę utworzonego obrazu.
Najlepsze praktyki dotyczące zarządzania obrazami
kontenera
Choć tworzenie obrazów kontenera i zarządzanie nimi wykracza poza zakres tematyczny tej
książki, warto wspomnieć o kilku najlepszych podstawowych praktykach stosowanych podczas
tworzenia obrazów i nadawania im nazw. Ogólnie rzecz biorąc, proces tworzenia obrazu może
być podatny na ataki przeprowadzane na „łańcuch dostawców”. W trakcie takiego ataku
przeprowadzająca go osoba wstrzykuje kod lub pliki binarne do pewnej i pochodzącej z
zaufanego źródła zależności, która następnie zostaje wbudowana w aplikację. Z powodu
niebezpieczeństwa takich ataków krytyczne znaczenie podczas tworzenia obrazów ma
opieranie ich na doskonale znanych i w pełni zaufanych dostawcach obrazów. Ewentualnie
wszystko możesz zbudować od zera. To drugie rozwiązanie jest łatwe w przypadku niektórych
języków programowania (np. Go), które pozwalają na tworzenie statycznych plików binarnych.
Okazuje się jednak znacznie bardziej skomplikowane w przypadku języków interpretowanych,
takich jak Python, JavaScript i Ruby.
Następna najlepsza praktyka dotycząca obrazów jest związana z nadawaniem im nazw. Choć
wersja obrazu kontenera w rejestrze obrazów teoretycznie jest modyfikowalna, tag wersji
powinien być traktowany jako niemodyfikowalny. W szczególności dobrą praktyką w nadawaniu
nazw obrazom jest połączenie wersji semantycznej i wartości hash SHA operacji zatwierdzenia,
w trakcie której został utworzony obraz (np. v1.0.1-bfeda01f). Jeżeli nie podasz wersji obrazu,
domyślnie zostanie użyta wartość latest. Wprawdzie to może być wygodne rozwiązanie
podczas pracy nad rozwiązaniem, ale jest kiepskim pomysłem w środowisku produkcyjnym,
ponieważ wersja oznaczona jako latest niewątpliwie będzie modyfikowana w trakcie każdej
operacji tworzenia nowego obrazu.
Tworzenie replikowanej aplikacji
W omawianym przykładzie aplikacja frontendu jest bezstanowa i całkowicie opiera się na
backendzie Redis w zakresie informacji o stanie. Dlatego też można dowolnie ją replikować bez
wpływu na ruch sieciowy. Wprawdzie prawdopodobieństwo, że ta aplikacja nadaje się do użycia
na ogromną skalę, jest znikome, ale jest wystarczająco dobra do działania w co najmniej dwóch
replikach. W takim przypadku zarówno nieoczekiwana awaria aplikacji, jak i wdrożenie jej
nowej wersji nie muszą powodować przestoju w jej działaniu.
W Kubernetes ReplicaSet to zasób pozwalający na zarządzanie replikacją aplikacji
umieszczonej w kontenerze, więc bezpośrednie używanie tego zasobu nie jest najlepszą
praktyką. Zamiast tego należy skorzystać z zasobu o nazwie Deployment. Stanowi on rodzaj
połączenia oferowanych przez ReplicaSet możliwości replikacji z wersjonowaniem i zdolnością
do wprowadzania zmian etapami. Dzięki wykorzystaniu zasobu Deployment można użyć
wbudowanych w Kubernetes narzędzi do przejścia z jednej wersji aplikacji do drugiej.
Spójrz na kod zasobu Deployment w naszej aplikacji.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: my-repo/journal-server:v1-abcde
imagePullPolicy: IfNotPresent
name: frontend
resources:
request:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
Trzeba zwrócić uwagę na kilka kwestii dotyczących przedstawionego tutaj zasobu Deployment.
Przede wszystkim używamy etykiet do identyfikacji zasobów Deployment i ReplicaSet oraz
podów tworzonych przez zasób Deployment. Do wszystkich zasobów została dodana etykieta
layer: frontend, aby można było je analizować dla konkretnej warstwy w pojedynczym
żądaniu. W trakcie pracy przekonasz się, że podczas dodawania innych zasobów stosowana jest
dokładnie ta sama praktyka.
Dodatkowo w wielu miejscach pliku YAML dodaliśmy komentarze. Wprawdzie nie trafiają one
do zasobu Kubernetes przechowywanego w serwerze i są jedynie komentarzami do kodu, ale
mają pomóc osobom, które po raz pierwszy mają styczność z daną konfiguracją.
Powinieneś również zwrócić uwagę na to, że dla zasobu Deployment zostały zdefiniowane
żądania zasobów Request i Linit, a wartość Request jest równa wartości Limit. Podczas
działania aplikacji Request to miejsce zarezerwowane, którego wartością będzie nazwa hosta
zawierającego uruchomioną aplikację. Z kolei Limit określa maksymalną ilość zasobów, które
mogą być użyte przez dany kontener. Gdy uruchamiasz aplikację, przypisanie Request wartości
Limit zapewnia najbardziej przewidywalne zachowanie aplikacji. Ta przewidywalność odbywa
się za cenę większego użycia zasobów. Skoro przypisanie Request wartości Limit uniemożliwia
aplikacji nadmierne wykorzystanie dostępnych zasobów, nie będziesz miał możliwości użyć ich
w pełni aż do chwili, gdy niezwykle starannie dopasujesz wartości Request i Limit. Gdy
staniesz się bardziej zaawansowany w zakresie modelu zasobów Kubernetes, możesz rozważyć
niezależne modyfikowanie wartości Request i Limit w aplikacji. Jednak dla większości
użytkowników stabilizacja wynikająca z przewidywalności jest warta mniejszego poziomu
wykorzystania zasobów.
Po zdefiniowaniu zasobu Deployment można go przekazać do systemu kontroli wersji i wdrożyć
w Kubernetes.
$ git add frontend/deployment.yaml
$ git commit -m "Zdefiniowanie zasobu Deployment" frontend/deployment.yaml
$ kubectl apply -f frontend/deployment.yaml
Najlepszą praktyką jest również zagwarantowanie, że zawartość klastra dokładnie odpowiada
stanowi zdefiniowanemu w systemie kontroli wersji. Najlepszym wzorcem pozwalającym na
spełnienie tego warunku jest zaadaptowanie podejścia GitOps i wdrażanie do środowiska
produkcyjnego tylko kodu z określonych gałęzi systemu kontroli wersji z użyciem automatyzacji
w postaci ciągłej integracji (ang. continuous integration, CI) i ciągłego wdrażania (ang.
continuous delivery, CD). W ten sposób będziesz miał gwarancję zachowania spójności między
stanem aplikacji w środowisku produkcyjnym a jej stanem zdefiniowanym w systemie kontroli
wersji. Choć pełne rozwiązanie oparte na technikach CI/CD wydaje się przesadą w przypadku
tak prostej aplikacji, ale automatyzacja sama w sobie, niezależnie od niezawodności, jaką
zapewnia, zwykle jest warta wysiłku związanego z jej przygotowaniem. Warto w tym miejscu
dodać, że implementacja technik ciągłej integracji i ciągłego wdrażania dla już istniejącej i
wdrożonej aplikacji jest wyjątkowo trudnym zadaniem.
Pozostało jeszcze do omówienia kilka fragmentów tego pliku YAML opisującego aplikację (np.
zasoby ConfigMap, ukryte woluminy, a także kwestie związane z jakością usługi poda). Zrobimy
to dokładniej w dalszej części rozdziału.
Konfiguracja zewnętrznego
przychodzącego ruchu sieciowego HTTP
Kontener naszej przykładowej aplikacji został wdrożony, ale obecnie jeszcze nikt nie może
uzyskać do niej dostępu. Domyślnie zasoby klastra są dostępne jedynie dla użytkowników
danego klastra. Aby publicznie udostępnić aplikację, trzeba utworzyć usługę i mechanizm
równoważenia obciążenia w celu zdefiniowania zdalnego adresu IP, poprzez który kontener
będzie mógł otrzymywać ruch sieciowy. Udostępnianie kontenera na zewnątrz będzie się
odbywało za pomocą dwóch zasobów Kubernetes. Pierwszym jest usługa działająca w
charakterze mechanizmu równoważenia obciążenia dla ruchu sieciowego TCP (ang.
transmission control protocol) i UDP (ang. user datagram protocol). W omawianym przykładzie
jest wykorzystywany protokół TCP. Drugim jest zasób Ingress zapewniający mechanizm
równoważenia obciążenia HTTP(S), który stosuje sprytnie działający routing żądań na
podstawie ścieżek HTTP i nazw hostów. W przypadku tak prostej aplikacji jak omawiana być
może się zastanawiasz, dlaczego zdecydowaliśmy się na użycie tak złożonego zasobu Ingress.
Jak się przekonasz w dalszej części rozdziału, nawet ta prosta aplikacja będzie obsługiwała
żądania HTTP pochodzące z dwóch różnych usług. Co więcej, zdefiniowanie brzegowego zasobu
Ingress zapewnia elastyczność późniejszej rozbudowy usługi.
Zanim będzie można zdefiniować zasób Ingress, potrzebna jest Kubernetes usługa (zasób
Service), do której wymieniony zasób będzie prowadził. Etykiety wykorzystamy w celu
przekierowania usługi do podów utworzonych we wcześniejszej części rozdziału. Zasób Service
jest znacznie prostszy do zdefiniowania niż Deployment i przedstawia się następująco:
apiVersion: v1
kind: Service
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: frontend
type: ClusterIP
Po zdefiniowaniu zasobu Service można przystąpić do zdefiniowania zasobu Ingress. W
przeciwieństwie do zasobu Service zasób Ingress wymaga działającego w klastrze kontenera
kontrolera. Do dyspozycji masz wiele różnych implementacji, z których możesz wybierać: od
oferowanych przez dostawców chmury po implementacje serwerów typu open source. Jeżeli
zdecydujesz się na instalację zasobu Ingress na podstawie dostawcy oprogramowania typu
open source, dobrym rozwiązaniem będzie skorzystanie z menedżera pakietów Helm
(https://helm.sh/) do zainstalowania oprogramowania i zarządzania nim. Do popularnych
rozwiązań zaliczają się dostawcy Ingress o nazwach nginx i haproxy.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- http:
paths:
- path: /api
backend:
serviceName: frontend
servicePort: 8080
Konfigurowanie aplikacji za pomocą
zasobu ConfigMap
Każda aplikacja wymaga pewnej konfiguracji. W omawianym przykładzie to może być liczba
wpisów dziennika wyświetlanych na stronie, kolor określonego tła, definicja specjalnego
komunikatu wyświetlanego w okresie świątecznym lub dowolny inny rodzaj konfiguracji.
Zwykle oddzielenie takich informacji konfiguracyjnych od samej aplikacji to jedna z najlepszych
praktyk.
Jest kilka różnych powodów do stosowania wspomnianej separacji. Przede wszystkim być może
będziesz chciał skonfigurować ten sam plik binarny aplikacji, ale z odmienną konfiguracją, w
zależności od ustawień. Przykładowo w Europie być może będziesz chciał uczcić Wielkanoc,
podczas gdy w Chinach zechcesz przygotować coś specjalnego na chiński Nowy Rok. Poza taką
specjalizacją środowiskową mamy jeszcze wiele innych powodów do stosowania separacji. Pliki
binarne zwykle zawierają wiele różnych, nowych funkcjonalności. Jeżeli włączysz je w kodzie,
wówczas jedynym sposobem na modyfikację aktywnych funkcjonalności będzie skompilowanie i
utworzenie nowego pliku binarnego, co może być kosztownym i wolnym procesem.
Użycie konfiguracji do aktywacji zestawu funkcjonalności oznacza możliwość szybkiego (i
nawet dynamicznego) aktywowania oraz dezaktywowania funkcjonalności w odpowiedzi na
potrzeby użytkownika lub awarie kodu aplikacji. Funkcjonalności mogą być włączane i
wyłączane pojedynczo. Taka elastyczność gwarantuje nieustanny postęp w większości
funkcjonalności, nawet jeśli część z nich będzie musiała zostać wycofana w celu poprawy
wydajności działania lub usunięcia błędów.
W Kubernetes przykładem tego rodzaju konfiguracji jest zasób o nazwie ConfigMap. Zawiera on
wiele par klucz-wartość przedstawiających informacje konfiguracyjne lub plik. Te informacje
konfiguracyjne mogą być przekazane kontenerowi w podzie za pomocą plików lub zmiennych
środowiskowych. Wyobraź sobie, że chcesz skonfigurować aplikację dziennika internetowego w
taki sposób, aby wyświetlała możliwą do określenia liczbę wpisów na stronie. Aby osiągnąć ten
efekt, należy w pokazany tutaj sposób skonfigurować zasób ConfigMap.
$ kubectl create configmap frontend-config --from-literal=journalEntries=10
W celu skonfigurowania aplikacji informacje konfiguracyjne są przekazywane w postaci
zmiennej środowiskowej. W tym celu do zdefiniowanego wcześniej zasobu Deployment można
dodać przedstawiony tutaj zasób container.
...
# Tablica containers w PodTemplate w zasobie Deployment.
containers:
- name: frontend
...
env:
- name: JOURNAL_ENTRIES
valueFrom:
configMapKeyRef:
name: frontend-config
key: journalEntries
...
Wprawdzie ten przykład pokazuje, jak można używać zasobu ConfigMap do konfigurowania
aplikacji, ale w rzeczywistych wdrożeniach chcesz mieć możliwość regularnego wprowadzania
zmian w konfiguracji, np. co tydzień lub jeszcze częściej. Kusząca może być możliwość
wprowadzenia zmiany przez modyfikację samego zasobu ConfigMap, choć to nie jest najlepszą
praktyką. Powodów jest kilka. Jednym z nich jest to, że zmiana konfiguracji tak naprawdę nie
wywołuje uaktualnienia istniejących podów. Konfiguracja jest stosowana tylko podczas
ponownego uruchamiania poda. Dlatego też w takim przypadku wprowadzanie zmian nie
odbywa się na podstawie stanu poda i może nastąpić doraźnie lub przypadkowo.
Znacznie lepszym podejściem jest umieszczenie numeru wersji w nazwie zasobu ConfigMap.
Zamiast nazwy typu frontend-config można użyć frontend-config-v1. Gdy będziesz chciał
wprowadzić zmianę, to zamiast modyfikować istniejący zasób ConfigMap, powinieneś utworzyć
jego drugą wersję, a następnie uaktualnić zasób Deployment w celu użycia nowej wersji zasobu
ConfigMap. Gdy tak zrobisz, nastąpi automatyczne wywołanie wprowadzenia zmian zasobu
Deployment na podstawie odpowiedniej operacji sprawdzenia i zastosowanie pauzy między
zmianami. Co więcej, jeśli kiedykolwiek będziesz chciał powrócić do wcześniejszej wersji,
wiedz, że konfiguracja oznaczona jako v1 nadal pozostaje w klastrze, a wycofanie sprowadza się
do ponownego uaktualnienia zasobu Deployment.
Zarządzanie uwierzytelnianiem za
pomocą danych poufnych
Nawet nie zaczęliśmy omawiać usługi Redis, z którą jest połączony frontend aplikacji. Jednak w
każdej rzeczywistej aplikacji konieczne jest nawiązywanie bezpiecznych połączeń między
usługami. W tej części rozdziału zajmiemy się kwestiami bezpieczeństwa użytkowników i ich
danych. Ponadto bardzo duże znaczenie ma unikanie takich błędów jak nawiązanie połączenia
programistycznej wersji frontendu z produkcyjną wersją bazy danych.
Do uwierzytelniania bazy danych Redis jest używane zwykłe hasło. Za wygodne rozwiązanie
możesz uznać umieszczenie wspomnianego hasła w kodzie źródłowym aplikacji bądź też w pliku
znajdującym się w obrazie kontenera. Jednak oba te rozwiązania są naprawdę złe, i to z wielu
powodów. Przede wszystkim w ten sposób dane poufne (hasło) ujawniasz w środowisku, w
którym niekoniecznie masz kontrolę nad dostępem do danych. Jeżeli hasło zostanie
umieszczone w systemie kontroli wersji, w ten sposób każdemu, kto ma dostęp do kodu
źródłowego, zapewnisz również dostęp do wszystkich umieszczonych w nim danych poufnych.
To nie jest dobre rozwiązanie. Prawdopodobnie grono użytkowników z dostępem do kodu
źródłowego jest znacznie większe niż to, które powinno mieć dostęp do egzemplarza Redis.
Podobnie, jeśli użytkownik ma dostęp do obrazu kontenera, niekoniecznie powinien mieć dostęp
do produkcyjnej bazy danych.
Poza obawami związanymi z kontrolą dostępu powodem, dla którego lepiej unikać umieszczania
danych poufnych w kodzie źródłowym i/lub obrazach kontenera, jest parametryzacja. Zapewne
chcesz mieć możliwość wykorzystania tego samego kodu źródłowego i obrazów w różnych
środowiskach (np. programistycznym, kanarkowym i produkcyjnym). Jeżeli dane poufne będą
ściśle powiązane z kodem źródłowym lub obrazem, wówczas dla każdego z wymienionych
środowisk będzie potrzebny oddzielny obraz (lub kod źródłowy).
Skoro w poprzednim podrozdziale miałeś okazję zobaczyć zasób ConfigMap w akcji, być można
uważasz, że hasło można umieścić w konfiguracji, która następnie będzie przekazywana
aplikacji jako przygotowana specjalnie dla niej. Masz pełne prawo być przekonanym, że
odseparowanie konfiguracji od aplikacji jest tym samym, co oddzielenie od aplikacji danych
poufnych. Jednak trzeba w tym miejscu dodać, że dane poufne to koncepcja bardzo ważna sama
w sobie. Prawdopodobnie chcesz zająć się kontrolą dostępu do danych poufnych oraz ich
obsługą i uaktualnianiem nie poprzez konfigurację, ale w zdecydowanie inny sposób. Co
ważniejsze, chciałbyś skłonić programistów do innego sposobu myślenia podczas dostępu do
danych poufnych niż podczas dostępu do ustawień konfiguracyjnych. Dlatego też Kubernetes
ma wbudowany zasób o nazwie Secret, przeznaczony do zarządzania danymi poufnymi.
Utworzenie hasła dla bazy danych Redis może odbyć się tak:
$ kubectl create secret generic redis-passwd --from-literal=passwd=${RANDOM}
Oczywiście możesz zdecydować się na własne hasło zamiast losowo wybranej liczby. Ponadto
prawdopodobnie będziesz chciał używać usługi przeznaczonej do zarządzania hasłem,
oferowanej przez dostawcę chmury, np. Microsoft Azure Key Vault, lub w postaci projektu typu
open source, np. HashiCorp Vault. Gdy korzystasz z usługi zarządzania kluczami, zapewnia ona
ściślejszą integrację z zasobem Secrets w Kubernetes.
Dane poufne w Kubernetes są domyślnie przechowywane w postaci
niezaszyfrowanej. Jeżeli chcesz je przechowywać jako zaszyfrowane, możesz
zintegrować rozwiązanie z dostawcą kluczy, aby przekazać Kubernetes klucz
przeznaczony do odszyfrowania wszystkich danych poufnych znajdujących się w
klastrze. Zwróć uwagę na to, że to zabezpiecza klucze przed bezpośrednimi atakami
na bazę etcd; konieczne jest jeszcze właściwe zabezpieczenie dostępu za pomocą
API serwera Kubernetes.
Po umieszczeniu w Kubernetes hasła do bazy danych Redis konieczne jest dołączenie danych
poufnych do uruchomionej aplikacji po jej wdrożeniu w Kubernetes. W tym celu można
skorzystać z Kubernetes Volume, czyli pliku lub katalogu, który można zamontować w
działającym kontenerze, w miejscu wskazanym przez użytkownika. W przypadku danych
poufnych wolumin jest tworzony w pamięci RAM jako system plików tmpfs, a następnie
montowany w kontenerze. To gwarantuje, że nawet jeśli komputer zostanie fizycznie przejęty
(to w zasadzie niemożliwe w przypadku usług w chmurze, choć może się zdarzyć w fizycznie
istniejącym centrum danych), dane poufne nie będą łatwo dostępne dla osoby
przeprowadzającej atak.
Aby dodać wolumin z danymi poufnymi do zasobu Deployment, trzeba zdefiniować dwa nowe
polecenia w pliku YAML wymienionego zasobu. Pierwsze z nich to sekcja volumes dla poda
odpowiedzialnego za dodawanie woluminu do poda:
...
volumes:
- name: passwd-volume
secret:
secretName: redis-passwd
Gdy już wolumin znajduje się w podzie, następnym krokiem jest zamontowanie go w
określonym kontenerze. To się odbywa z użyciem właściwości zdefiniowanych w sekcji
volumeMounts w opisie kontenera.
...
volumeMounts:
- name: passwd-volume
readOnly: true
mountPath: "/etc/redis-passwd"
...
W ten sposób wolumin danych poufnych zostanie zamontowany w katalogu redis-passwd w celu
zapewnienia dostępu z poziomu kodu klienta. Po zebraniu wszystkiego w całość otrzymujemy
pełną konfigurację zasobu Deployment.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: my-repo/journal-server:v1-abcde
imagePullPolicy: IfNotPresent
name: frontend
volumeMounts:
- name: passwd-volume
readOnly: true
mountPath: "/etc/redis-passwd"
resources:
request:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
volumes:
- name: passwd-volume
secret:
secretName: redis-passwd
Na tym etapie mamy skonfigurowaną aplikację klienta, która otrzymuje dostęp do danych
poufnych pozwalających na przeprowadzenie uwierzytelnienia w usłudze Redis. Konfiguracja
Redis do użycia hasła odbywa się podobnie: należy zamontować wolumin w podzie Redis, a
następnie odczytać hasło z pliku.
Wdrożenie prostej bezstanowej bazy
danych
Wprawdzie pod względem koncepcji wdrożenie aplikacji zawierającej informacje o stanie
odbywa się podobnie do wdrożenia klienta, takiego jak nasz frontend, ale konieczność
przechowywania informacji o stanie wiąże się z pewnymi komplikacjami. Przede wszystkim pod
w Kubernetes może być ponownie przydzielony z wielu powodów, takich jak sprawdzenie stanu,
uaktualnienie i ponowne równoważenie obciążenia. W takiej sytuacji pod może trafić do innego
komputera. Jeżeli dane powiązane z egzemplarzem Redis znajdują się w określonym
komputerze lub wewnątrz kontenera, wówczas zostaną utracone po przeprowadzeniu migracji
kontenera lub po jego ponownym uruchomieniu. Aby tego uniknąć, trzeba stosować zdalne
trwałe woluminy (API PersistentVolumes), przeznaczone do zarządzania informacjami o stanie
powiązanymi z aplikacją.
Istnieje wiele różnych implementacji trwałych woluminów w Kubernetes, przy czym wszystkie
współdzielą cechy charakterystyczne. Podobnie jak w przypadku danych poufnych, omówionych
we wcześniejszej części rozdziału, trwałe woluminy są powiązane z podem i montowane w
kontenerze, w określonym położeniu. Jednak w przeciwieństwie do danych poufnych trwałe
woluminy są, ogólnie rzecz biorąc, zdalnymi magazynami danych zamontowanymi poprzez
protokoły sieciowe, takie jak NFS (ang. network file system), SMB (ang. server message block)
lub oparty na blokach (iSCSI, dysk oparty na chmurze itd.). W przypadku aplikacji takich jak
bazy danych preferowane są dyski oparte na blokach, ponieważ zapewniają one większą
wydajność działania. Jeśli zaś wydajność działania nie ma znaczenia krytycznego, dyski oparte
na plikach mogą czasami zapewnić większą elastyczność.
Zarządzanie stanem jest ogólnie dość skomplikowanym zadaniem i Kubernetes nie
jest tutaj wyjątkiem. Jeżeli operujesz w środowisku obsługującym usługi z
informacjami o stanie (MySQL jako usługa, Redis jako usługa itd.), wówczas używanie
tych usług jest dobrym rozwiązaniem. Początkowo koszt usługi typu SaaS (ang.
software as a service) zapewniającej obsługę informacji o stanie może wydawać się
wysoki. Jednak po uwzględnieniu wszystkich operacyjnych wymagań w zakresie
obsługi informacji o stanie (kopia zapasowa, lokalność danych, nadmiarowość danych
itd.) i tego, że istnienie w klastrze Kubernetes informacji o stanie utrudnia
przenoszenie aplikacji między klastrami, okazuje się, że w większości przypadków
warto jest ponieść koszt związany z SaaS. W środowiskach nieoferujących SaaS
przygotowanie osobnego zespołu dostarczającego pamięć masową jako usługę dla
całej organizacji to zdecydowanie lepsza praktyka niż umożliwienie każdemu
zespołowi opracowania takiego rozwiązania samodzielnie.
W celu wdrożenia usługi Redis wykorzystamy zasób StatefulSet. Dodany po początkowym
wydaniu Kubernetes jako uzupełnienie zasobu ReplicaSet, StatefulSet zapewnia większe
gwarancje np. w zakresie spójności nazw (żadnych losowych wartości hash) i zdefiniowanej
kolejności podczas skalowania w górę i skalowania w dół. Gdy wdrażasz wzorzec singleton, to
będzie miało mniejsze znaczenie. Natomiast jeśli chcesz wdrożyć replikowane informacje o
stanie, wymienione atrybuty są bardzo wygodne.
W celu przygotowania trwałego woluminu dla naszej bazy Redis wykorzystamy zasób
PersistentVolumeClaim. Słowo claim (z ang. oświadczenie) w nazwie oznacza tutaj „żądanie
zasobu”. Nasza baza danych Redis deklaruje pamięć masową o wielkości 50 GB, a klaster
Kubernetes określa, jak przygotować odpowiedni trwały wolumin. Tak się dzieje z dwóch
powodów. Pierwszy to możliwość utworzenia zasobu StatefulSet w sposób zapewniający
możliwość przeniesienia między różnymi chmurami, w których szczegóły związane z
implementacją dysku mogą być odmienne. Drugi to możliwość użycia oświadczenia woluminu
do przeprowadzenia operacji zapisu w szablonie, który może być replikowany do
poszczególnych podów z przypisanymi trwałymi woluminami. Trzeba w tym miejscu dodać, że
wiele typów trwałego woluminu można montować w pojedynczym podzie.
W kolejnym fragmencie kodu pokazaliśmy przykład zasobu StatefulSet w akcji.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis"
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:5-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
Ten kod spowoduje wdrożenie pojedynczego egzemplarza usługi Redis. Przyjmujemy założenie,
że chcemy replikować ten klaster Redis w celu skalowania operacji odczytu i zapewnienia
odporności na awarie. To oczywiście oznacza konieczność zwiększenia liczby replik do trzech, a
także zagwarantowania, że dwie nowe repliki nawiążą połączenie z serwerem głównym (ang.
master) dla Redis, aby przeprowadzać operacje zapisu.
Gdy definiujesz działającą w trybie headless usługę dla Redis zasobu StatefulSet, wówczas
następuje utworzenie wpisu DNS redis-0.redis. To jest adres IP pierwszej repliki. Tę wartość
można wykorzystać do utworzenia prostego skryptu, który będzie mógł zostać uruchomiony we
wszystkich kontenerach.
#!/bin/bash
PASSWORD=$(cat /etc/redis-passwd/passwd)
if [[ "${HOSTNAME}" == "redis-0" ]]; then
redis-server --requirepass ${PASSWORD}
else
redis-server --slaveof redis-0.redis 6379 --masterauth ${PASSWORD} -requirepass ${PASSWORD}
fi
Ten skrypt można utworzyć w postaci zasobu ConfigMap:
$ kubectl create configmap redis-config --from-file=launch.sh=launch.sh
Następnie można ten zasób ConfigMap dodać do zasobu StatefulSet i użyć go w charakterze
polecenia dla kontenera. Dodamy również hasło potrzebne podczas uwierzytelniania, które
zostało przygotowane nieco wcześniej w rozdziale.
Pełny kod zasobu dla trzech replik Redis jest następujący:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis"
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:5-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
- name: script
mountPath: /script/launch.sh
subPath: launch.sh
- name: passwd-volume
mountPath: /etc/redis-passwd
command:
- sh
- -c
- /script/launch.sh
volumes:
- name: script
configMap:
name: redis-config
defaultMode: 0777
- name: passwd-volume
secret:
secretName: redis-passwd
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
Utworzenie za pomocą usług mechanizmu
równoważenia obciążenia TCP
Po wdrożeniu usługi Redis zawierającej informacje o stanie konieczne jest jej udostępnienie
naszemu frontendowi. W tym celu trzeba będzie utworzyć dwie odmienne usługi Kubernetes.
Zadaniem pierwszej jest odczytywanie danych z Redis. Skoro Redis przeprowadza replikację
danych do wszystkich trzech zasobów StatefulSet, zupełnie nas nie interesuje to, do którego z
serwerów zostanie wykonane żądanie odczytu. W efekcie korzystamy z bardzo prostej usługi
odczytu danych z Redis.
apiVersion: v1
kind: Service
metadata:
labels:
app: redis
name: redis
namespace: default
spec:
ports:
- port: 6379
protocol: TCP
targetPort: 6379
selector:
app: redis
sessionAffinity: None
type: ClusterIP
Aby przeprowadzić operacje zapisu, trzeba wskazać serwer główny w Redis (replika nr 0). W
tym celu należy utworzyć usługę działającą w trybie headless. Taka usługa nie ma adresu IP
klastra. Zamiast tego programuje wpis DNS dla każdego poda w zasobie StatefulSet. To
oznacza możliwość uzyskania dostępu do serwera głównego Redis za pomocą nazwy DNS
redis-0.redis.
apiVersion: v1
kind: Service
metadata:
labels:
app: redis-write
name: redis-write
spec:
clusterIP: None
ports:
- port: 6379
selector:
app: redis
Dlatego gdy chcesz nawiązać połączenie z bazą Redis w celu przeprowadzenia operacji zapisu
lub transakcyjnej pary operacji odczytu i zapisu, wówczas możesz utworzyć oddzielnego klienta
nawiązującego połączenie z serwerem redis-0.redis.
Przekazanie przychodzącego ruchu
sieciowego do serwera pliku statycznego
Ostatnim komponentem aplikacji jest serwer pliku statycznego. Ten serwer jest odpowiedzialny
za udostępnianie plików typu HTML, CSS, JavaScript i obrazów. W omawianym przykładzie
znacznie efektywniejszym i czytelniejszym rozwiązaniem będzie dla nas oddzielenie serwera
pliku statycznego od API obsługującego omówiony wcześniej frontend. Bardzo łatwo można
wykorzystać charakteryzujący się wysoką wydajnością działania serwer pliku statycznego, taki
jak NGINX, do obsługi wymienionych rodzajów plików i pozwolić zespołowi programistów
skoncentrować się na kodzie niezbędnym do implementacji API.
Na szczęście zasób Ingress niezwykle ułatwia stosowanie architektury minimikrousług.
Podobnie jak w przypadku frontendu zasób Deployment można wykorzystać do opisania
replikowanego serwera NGINX. Zajmiemy się umieszczeniem serwera NGINX w obrazie
kontenera i wdrożeniem go w każdej replice. Kod źródłowy zasobu Deployment jest
następujący:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: fileserver
name: fileserver
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: fileserver
template:
metadata:
labels:
app: fileserver
spec:
containers:
- image: my-repo/static-files:v1-abcde
imagePullPolicy: Always
name: fileserver
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
resources:
request:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
dnsPolicy: ClusterFirst
restartPolicy: Always
Skoro nasz replikowany serwer pliku statycznego został przygotowany i uruchomiony,
prawdopodobnie będziesz chciał utworzyć zasób Service działający w charakterze mechanizmu
równoważenia obciążenia.
apiVersion: v1
kind: Service
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: frontend
sessionAffinity: None
type: ClusterIP
Po zdefiniowaniu zasobu Service dla serwera pliku statycznego zasób Ingress można
rozszerzyć o obsługę nowej ścieżki dostępu. Trzeba w tym miejscu zwrócić uwagę na
konieczność umieszczenia ścieżki / dopiero po ścieżce /api. W przeciwnym razie nastąpi
podciągnięcie do serwera pliku statycznego żądań /api i bezpośrednich żądań API. Oto
zmodyfikowany kod zasobu Ingress.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- http:
paths:
- path: /api
backend:
serviceName: frontend
servicePort: 8080
# UWAGA: ta ścieżka powinna być zdefiniowana w ścieżce /api, w przeciwnym
razie nastąpi przechwytywanie żądań.
- path: /
backend:
serviceName: nginx
servicePort: 80
Parametryzowanie aplikacji za pomocą
menedżera pakietów Helm
Dotychczas koncentrowaliśmy się na kwestiach związanych z wdrożeniem pojedynczego
egzemplarza usługi w pojedynczym serwerze. Jednak w rzeczywistości niemal każda usługa i
każdy zespół zajmujący się obsługą usługi będą ją wdrażały w wielu różnych środowiskach
(nawet jeśli współdzielą one klaster). Jeżeli działasz w pojedynkę i pracujesz nad jedną
aplikacją, prawdopodobnie masz co najmniej wersję programistyczną i wersję produkcyjną
aplikacji, aby móc rozwijać aplikację bez obawy, że ją uszkodzisz po wdrożeniu w środowisku
produkcyjnym. Po uwzględnieniu testów integracji oraz technik CI/CD jest spore
prawdopodobieństwo, że nawet w przypadku pojedynczej usługi i niewielu programistów
wdrożenie będziesz chciał przeprowadzać w co najmniej trzech różnych środowiskach. Może
być ich jeszcze więcej, jeśli rozważysz obsługę awarii na poziomie centrum danych.
W wielu zespołach standardową reakcją na awarię jest po prostu skopiowanie plików z jednego
klastra do innego. Zamiast pojedynczego katalogu frontend/ zwykle mają parę, np. frontendproduction/ i frontend-development/. Takie rozwiązanie jest niebezpieczne, ponieważ stajesz się
odpowiedzialny za zapewnienie synchronizacji między plikami w wymienionych katalogach.
Jeżeli mają one być całkowicie identyczne, to może być bardzo łatwe zadanie. Jednak pewne
różnice między środowiskami programistycznym i produkcyjnym są oczekiwane, co wiąże się z
opracowywaniem nowych funkcjonalności. Dlatego też te różnice powinny być wprowadzone
celowo i pozostawać łatwe do zarządzania.
Inną możliwością jest zastosowanie gałęzi i systemu kontroli wersji, w którym gałęzie
produkcyjna i programistyczna odchodzą od repozytorium centralnego, aby wszelkie różnice
między gałęziami były jasno widoczne. Takie rozwiązanie może być akceptowalne dla
niektórych zespołów. Jednak mechanika przenoszenia między gałęziami jest wymagającym
zadaniem, gdy rozwiązanie (np. stosujący techniki CI/CD system przeprowadzający wdrożenie
do wielu różnych regionów chmury) chcesz wdrażać jednocześnie w różnych środowiskach.
W efekcie większość osób decyduje się na system szablonów. W takim systemie mamy
połączenie szablonów tworzących scentralizowany szkielet konfiguracji aplikacji i parametrów
pozwalających na dostosowanie szablonu do konfiguracji określonego środowiska. W ten
sposób można mieć ogólną, współdzieloną konfigurację i zarazem zachować możliwość łatwego
wprowadzania w niej zmian. Istnieje wiele różnych systemów szablonów przeznaczonych dla
Kubernetes, a najpopularniejszym z nich jest Helm (https://helm.sh/).
Jeśli korzystasz z menedżera pakietów Helm, aplikacja zostaje zapakowana w kolekcję plików
określaną mianem formatu chart (w świecie kontenerów i Kubernetes wiążą się z tym pewne
żarty).
Przygotowanie wspomnianej kolekcji w formacie chart rozpoczyna się od utworzenia pliku
chart.yaml definiującego niezbędne metadane.
apiVersion: v1
appVersion: "1.0"
description: Kolekcja w formacie chart menedżera pakietów Helm dla naszego
serwera frontendu.
name: frontend
version: 0.1.0
Ten plik należy umieścić w katalogu głównym kolekcji w formacie chart (np. frontend/). W
wymienionym katalogu znajduje się podkatalog templates, przeznaczony dla szablonów.
Szablon to w zasadzie nic innego jak plik YAML z wcześniejszych przykładów, przy czym pewne
wartości w pliku są zastąpione odwołaniami do parametrów. Dla przykładu, wyobraź sobie
parametryzowanie liczby replik frontendu. Wcześniej kod źródłowy zasobu Deployment
zawierał następujące polecenia:
...
spec:
replicas: 2
...
W pliku szablonu (frontend-deployment.tmpl) ten fragment wygląda nieco inaczej:
...
spec:
replicas: {{ .replicaCount }}
...
To oznacza, że podczas wdrażania kolekcji w formacie chart ta wartość dla repliki będzie
zastąpiona odpowiednim parametrem. Wspomniane parametry są definiowane w pliku
values.yaml. Będzie istniał jeden taki plik dla każdego środowiska, w którym aplikacja ma
zostać wdrożona. W omawianym przykładzie zawartość pliku wartości jest bardzo prosta:
replicaCount: 2
Po zebraniu wszystkiego w całość omawianą kolekcję w formacie chart można wdrożyć za
pomocą narzędzia helm:
$ helm install ścieżka/dostępu/do/kolekcji/chart --values ścieżka/dostępu/do/
środowiska/values.yaml
W ten sposób aplikacja została sparametryzowana i wdrożona w Kubernetes. Wraz z upływem
czasu liczba tych parametrów będzie się zwiększała, odzwierciedlając w ten sposób
zróżnicowanie środowisk, w których jest wdrażana aplikacja.
Najlepsze praktyki dotyczące wdrożenia
Kubernetes to system o potężnych możliwościach, który może wydawać się skomplikowany.
Jednak podstawowa konfiguracja zapewniająca poprawne działanie aplikacji okaże się prosta, o
ile będziesz się stosować do wymienionych tutaj najlepszych praktyk.
Większość usług powinna być wdrażana w postaci zasobów Deployment. Pozwalają one na
utworzenie identycznych replik, co jest przydatne do zapewniania nadmiarowości i
podczas skalowania.
Wdrożenie można przeprowadzać za pomocą usługi (zasób Service), która właściwie jest
mechanizmem równoważenia obciążenia. Usługa może być udostępniona w klastrze
(rozwiązanie domyślne) lub zewnętrznie. Jeżeli chcesz udostępnić ruch HTTP aplikacji,
możesz skorzystać z kontrolera Ingress w celu dodania np. routingu żądań i obsługi SSL.
Ostatecznie będziesz chciał parametryzować aplikację, aby jej konfiguracja była możliwa
do użycia w różnych środowiskach. Narzędzia pakowania, takie jak menedżer pakietów
Helm (https://helm.sh/), okazują się najlepszym rozwiązaniem do takiej parametryzacji.
Podsumowanie
Wprawdzie aplikacja utworzona w tym rozdziale jest bardzo prosta, ale pozwoliła przedstawić
właściwie wszystkie koncepcje, które są stosowane podczas budowy znacznie większych i
bardziej skomplikowanych aplikacji. Poznanie sposobu, w jaki poszczególne fragmenty łączą się
w całość, oraz sposobu użycia podstawowych komponentów Kubernetes jest kluczem do
sukcesu podczas pracy z tym systemem.
Przygotowanie solidnych podstaw za pomocą systemu kontroli wersji, technik przeglądu kodu i
technik ciągłego wdrażania usługi gwarantuje, że niezależnie od tego, co będziesz tworzyć,
produkt zostanie zbudowany solidnie. Gdy w kolejnych rozdziałach książki będziesz poznawać
bardziej zaawansowane tematy, pamiętaj o przedstawionych tutaj podstawach.
1 Helm to menedżer pakietów dla Kubernetes, chart zaś to format pakietów, w którym kolekcja
plików opisuje zbiór powiązanych ze sobą zasobów Kubernetes — przyp. tłum.
Rozdział 2. Sposób pracy
programisty
Kubernetes zbudowano, aby zapewnić niezawodny sposób działania oprogramowania. Ta
technologia upraszcza więc wdrażanie aplikacji i zarządzanie nimi za pomocą zorientowanego
pod kątem aplikacji API, samodzielnie naprawiających się właściwości, a także użytecznych
narzędzi, które pozwalają np. na przeprowadzenie wdrożenia bez przestoju podczas wydawania
nowej wersji oprogramowania. Wprawdzie wszystkie wymienione możliwości są użyteczne, ale
nie oferują zbyt wiele, aby ułatwić tworzenie aplikacji dla Kubernetes. Co więcej, choć wiele
klastrów zostało zaprojektowanych do uruchamiania aplikacji produkcyjnych, co sprawia, że
programista rzadko ich używa w pracy nad projektem, ale uwzględnienie celów Kubernetes w
trakcie pracy programisty ma znaczenie krytyczne. To najczęściej oznacza posiadanie klastra —
lub przynajmniej jego części — przeznaczonego do używania podczas pracy nad aplikacją.
Przygotowanie takiego klastra mającego na celu ułatwienie procesu tworzenia aplikacji dla
Kubernetes jest ważne, jeśli chcesz osiągnąć sukces w pracy z Kubernetes. Nie powinno ulegać
wątpliwości, że jeśli nie istnieje kod przeznaczony do uruchomienia w klastrze, taki klaster sam
w sobie nie ma zbyt dużej wartości.
Cele
Zanim przejdziemy do omówienia najlepszych praktyk w zakresie tworzenia klastrów
programistycznych, dobrze jest zacząć od zdefiniowania celów dla takich klastrów. Oczywiście
ostatecznym celem jest umożliwienie programistom szybkiego i łatwego tworzenia aplikacji w
Kubernetes. Co to tak naprawdę oznacza w praktyce i jak jest odzwierciedlone w praktycznych
funkcjonalnościach klastra programistycznego?
Użyteczne będzie określenie faz współpracy programisty z klastrem.
Pierwsza faza to tzw. wejście na pokład. Mamy z nią do czynienia, gdy nowy programista
dołącza do zespołu. W trakcie tej fazy programista otrzymuje nazwę użytkownika pozwalającą
na zalogowanie się do klastra, a także zapoznaje się z pierwszym wdrożeniem. Celem tej fazy
jest umożliwienie programiście rozpoczęcia pracy w możliwie krótkim czasie. Powinieneś
zdefiniować współczynnik KPI (ang. key performance indicator) dla tego procesu. Rozsądnym
celem będzie umożliwienie programiście w czasie krótszym niż pół godziny przejścia od niczego
do aplikacji znajdującej się w repozytorium systemu kontroli wersji w stanie określonym jako
HEAD. Za każdym razem, gdy ktoś nowy dołącza do zespołu, dobrze jest sprawdzić, czy ten cel
został osiągnięty.
Druga faza to programowanie. To są codzienne zadania programisty. Celem tej fazy jest
zagwarantowanie dużej szybkości iteracji i debugowania. Programiści muszą szybko i
nieustannie przekazywać kod do klastra. Muszą mieć również możliwość łatwego testowania
kodu i jego debugowania, jeśli nie działa poprawnie. Wartość współczynnika KPI dla tej fazy jest
znacznie trudniejsza do ustalenia, choć można ją oszacować przez pomiar czasu, jaki upłynął do
wykonania żądania aktualizacji (tzw. pull request) w systemie kontroli wersji lub wprowadzenia
zmiany i jej uruchomienia w klastrze. Ewentualnie można przeprowadzać ankiety dotyczące
postrzeganej produktywności użytkownika lub też stosować wszystkie wymienione podejścia.
Powinieneś mieć możliwość mierzenia ogólnej wydajności zespołu.
Trzecia faza to testowanie. Ta faza jest stosowana naprzemiennie z programowaniem i ma na
celu weryfikację kodu przed jego przekazaniem do systemu kontroli wersji i połączeniem z już
istniejącym. Cele dla tej fazy są dwojakie. Po pierwsze, programista powinien mieć możliwość
wykonania wszystkich testów w swoim środowisku, zanim zainicjuje żądanie aktualizacji. Po
drugie, wszystkie testy powinny zostać uruchomione automatycznie, zanim kod zostanie
połączony z kodem istniejącym w repozytorium. Poza wymienionymi celami powinieneś określić
współczynnik KPI dla czasu potrzebnego na wykonanie testów. Gdy projekt stanie się bardziej
skomplikowany, będzie naturalne, że istnieje w nim coraz więcej testów, których wykonywanie
zabiera coraz więcej czasu. W takim przypadku cennym rozwiązaniem może być określenie
mniejszego zestawu testów, które programista może przeprowadzać podczas początkowej
weryfikacji kodu, przed zainicjowaniem żądania aktualizacji. Powinieneś mieć również dość
ściśle zdefiniowany współczynnik KPI związany z tzw. test flakiness, czyli testami zaliczanymi
okazjonalnie (lub nie do końca okazjonalnie). W rozsądnie aktywnym projekcie współczynnik
dla test flakiness wynoszący więcej niż jedno niezaliczenie na tysiąc wykonanych testów będzie
prowadził do tarć między programistami. Trzeba się upewnić, że środowisko klastra nie będzie
umożliwiało powstawania takich testów. Wprawdzie czasami raz zaliczane, a raz niezaliczane
testy występują ze względu na problem w kodzie, ale mogą pojawiać się również na skutek
pewnych zakłóceń w środowisku programistycznym (takich jak wyczerpanie zasobów i
hałaśliwe sąsiedztwo). Należy zagwarantować, że środowisko programistyczne jest pozbawione
wymienionych problemów, co oznacza konieczność pomiaru współczynnika dla test flakiness i
aktywne działanie w celu poprawy jego wartości.
Tworzenie klastra programistycznego
Kiedy ktoś zaczyna zastanawiać się nad rozpoczęciem programowania w Kubernetes, jedna z
pierwszych decyzji do podjęcia dotyczy klastra: czy utworzyć jeden ogromny klaster
programistyczny, czy też przygotować po jednym klastrze dla każdego programisty. Warto w
tym miejscu dodać, że taka decyzja ma sens jedynie w środowisku, w którym dynamiczne
tworzenie klastra jest łatwym zadaniem, czyli np. w publicznej chmurze. W fizycznym
środowisku istnieje prawdopodobieństwo, że jedynym możliwym wyborem będzie utworzenie
pojedynczego, ogromnego klastra.
Jeżeli masz wybór, powinieneś rozważyć wady i zalety każdej z opcji. W przypadku oddzielnego
klastra dla każdego z programistów poważną wadą takiego rozwiązania jest wyraźnie większy
koszt i mniejsza efektywność, a także większa liczba różnych klastrów programistycznych,
którymi trzeba będzie zarządzać. Dodatkowe koszty wiążą się z tym, że prawdopodobnie żaden
z takich klastrów nie będzie w pełni wykorzystany. Co więcej, gdy programiści tworzą różne
klastry, znacznie trudniejsze jest monitorowanie i usuwanie już nieużywanych zasobów.
Natomiast zaletą oddzielnego klastra dla każdego użytkownika jest prostota: każdy programista
może samodzielnie zajmować się obsługą klastra. Ponadto izolacja oznacza, że poszczególni
programiści nie będą mogli tak łatwo utrudniać sobie pracy.
Z drugiej strony pojedynczy klaster programistyczny będzie znacznie efektywniejszy. Jeden
współdzielony klaster prawdopodobnie zapewni obsługę tej samej liczby programistów, a jego
koszt wyniesie co najwyżej jedną trzecią kosztu oddzielnych klastrów dla wszystkich
programistów. Przy tym o wiele łatwiejsze będzie instalowanie usług klastra współdzielonego,
np. w zakresie monitorowania i rejestrowania danych, co z kolei znacznie ułatwia
przygotowanie klastra przyjaznego programistom. Natomiast wadą współdzielonego klastra
programistycznego jest proces zarządzania użytkownikami i to, że programiści mogą wchodzić
sobie w drogę. Ponieważ proces dodawania nowych użytkowników i przestrzeni nazw do
Kubernetes nie jest jeszcze do końca płynny, konieczne będzie aktywowanie procesu w celu
przygotowania zasobów dla nowego członka zespołu. Wprawdzie zarządzanie zasobami i
kontrola dostępu na podstawie roli użytkownika (ang. role-based access control, RBAC) mogą
zmniejszyć prawdopodobieństwo konfliktów między programistami, ale mimo to zawsze istnieje
niebezpieczeństwo, że użytkownik uszkodzi klaster programistyczny przez wykorzystanie zbyt
wielu zasobów, co uniemożliwi działanie innych aplikacji, a tym samym uniemożliwi pracę
pozostałym programistom. Ponadto wciąż trzeba zagwarantować, że programiści nie będą
doprowadzać do wycieków pamięci i zapominać o zwalnianiu zaalokowanych zasobów. Istnieje
łatwiejsze rozwiązanie niż podejście, w którym programiści mogą tworzyć własne klastry.
Wprawdzie oba omówione podejścia są wykonywalne, ale ogólnie zalecamy przygotowanie
pojedynczego, ogromnego klastra dla wszystkich programistów. Pomimo niebezpieczeństwa
związanego z utrudnianiem sobie pracy przez programistów te problemy da się rozwiązać, a
zalety wynikające z ostatecznego kosztu efektywności i możliwości łatwego dodawania do
klastra funkcjonalności są na poziomie całej organizacji większe niż wady związane z
wzajemnym przeszkadzaniem sobie w pracy. Konieczne będzie zainwestowanie w proces
przygotowania zasobów dla nowych programistów, zarządzanie zasobami i zwalnianie
nieużywanych zasobów. Naszym zaleceniem jest wypróbowanie najpierw jednego ogromnego
klastra. Wraz ze zwiększaniem się organizacji (lub jeśli już jest ogromna) można rozważyć
użycie jednego klastra dla zespołu lub grupy (10 do 20 osób) zamiast jednego gigantycznego
klastra dla setek użytkowników. Takie rozwiązanie będzie znacznie łatwiejsze do fakturowania i
zarządzania.
Konfiguracja klastra współdzielonego
przez wielu programistów
Podczas przygotowywania ogromnego klastra podstawowym celem jest umożliwienie, aby
mogło z niego korzystać jednocześnie wielu użytkowników i przy tym sobie nie przeszkadzać.
Oczywistym sposobem na oddzielenie programistów od siebie jest wykorzystanie przestrzeni
nazw w Kubernetes. Wymieniona przestrzeń nazw może działać w charakterze zasięgu dla
wdrożenia usług, aby usługa frontendu jednego programisty nie zakłócała działania usługi
frontendu innego. Przestrzenie nazw są również zasięgiem dla mechanizmu RBAC, co
gwarantuje, że jeden programista nie będzie mógł przypadkowo usunąć pracy drugiego.
Dlatego też we współdzielonym klastrze sensowne jest stosowanie przestrzeni nazw w
charakterze przestrzeni roboczych dla programistów. Proces przygotowywania zasobów dla
użytkowników, a także tworzenia i zabezpieczania przestrzeni nazw zostanie omówiony w
kolejnych punktach.
Przygotowywanie zasobów dla użytkownika
Zanim użytkownika będzie można przypisać do przestrzeni nazw, trzeba umożliwić mu
korzystanie z samego klastra Kubernetes. Dostęp do klastra można zapewnić mu na dwa
sposoby. Pierwszy polega na użyciu uwierzytelniania opartego na certyfikacie w celu
utworzenia nowego certyfikatu dla użytkownika i przekazania mu pliku kubeconfig, który
pozwoli mu zalogować się do klastra. Druga to skonfigurowanie klastra do użycia zewnętrznego
systemu identyfikacji (np. Microsoft Azure Active Directory lub AWS Identity and Access
Management).
Ogólnie rzecz biorąc, użycie zewnętrznego systemu tożsamości jest najlepszą praktyką,
ponieważ wówczas nie trzeba utrzymywać dwóch odmiennych źródeł tożsamości. Jednak w
niektórych sytuacjach takie rozwiązanie jest niemożliwe i trzeba skorzystać z certyfikatów. Na
szczęście oferowanego przez Kubernetes API certyfikatów można użyć do tworzenia
wspomnianych certyfikatów i zarządzania nimi. Zapoznaj się z przedstawionym tutaj procesem
dodawania nowego użytkownika do istniejącego klastra.
Przede wszystkim trzeba wygenerować żądanie podpisania certyfikatu, potrzebne
wygenerowania nowego certyfikatu. Oto prosty program w języku Go, który do tego służy:
do
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"os"
)
func main() {
name := os.Args[1]
user := os.Args[2]
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
keyDer := x509.MarshalPKCS1PrivateKey(key)
keyBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: keyDer,
}
keyFile, err := os.Create(name + "-key.pem")
if err != nil {
panic(err)
}
pem.Encode(keyFile, &keyBlock)
keyFile.Close()
commonName := user
// Uaktualnij adres e-mail.
emailAddress := "someone@myco.com"
org := "My Co, Inc."
orgUnit := "Widget Farmers"
city := "Seattle"
state := "WA"
country := "US"
subject := pkix.Name{
CommonName: commonName,
Country: []string{country},
Locality: []string{city},
Organization: []string{org},
OrganizationalUnit: []string{orgUnit},
Province: []string{state},
}
asn1, err := asn1.Marshal(subject.ToRDNSequence())
if err != nil {
panic(err)
}
csr := x509.CertificateRequest{
RawSubject: asn1,
EmailAddresses: []string{emailAddress},
SignatureAlgorithm: x509.SHA256WithRSA,
}
bytes, err := x509.CreateCertificateRequest(rand.Reader, &csr, key)
if err != nil {
panic(err)
}
csrFile, err := os.Create(name + ".csr")
if err != nil {
panic(err)
}
pem.Encode(csrFile, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes:bytes})
csrFile.Close()
}
Aby uruchomić ten program, należy wydać następujące polecenie:
$ go run csr-gen.go client <user-name>
Wynikiem działania programu będzie utworzenie plików client-key.pem i client.csr. Następnym
krokiem jest wykonanie przedstawionego poniżej skryptu w celu utworzenia i pobrania nowego
certyfikatu.
#!/bin/bash
csr_name="my-client-csr"
name="${1:-my-user}"
csr="${2}"
cat <<EOF | kubectl create -f apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
name: ${csr_name}
spec:
groups:
- system:authenticated
request: $(cat ${csr} | base64 | tr -d '\n')
usages:
- digital signature
- key encipherment
- client auth
EOF
echo
echo "Zatwierdzanie żądania podpisania."
kubectl certificate approve ${csr_name}
echo
echo "Pobieranie certyfikatu."
kubectl get csr ${csr_name} -o jsonpath='{.status.certificate}' \
| base64 --decode > $(basename ${csr} .csr).crt
echo
echo "Porządkowanie."
kubectl delete csr ${csr_name}
echo
echo "Dodaj następujące polecenia do listy 'users' w pliku kubeconfig:"
echo "- name: ${name}"
echo " user:"
echo " client-certificate: ${PWD}/$(basename ${csr} .csr).crt"
echo " client-key: ${PWD}/$(basename ${csr} .csr)-key.pem"
echo
echo "Następnym krokiem jest konfiguracja roli dla tego użytkownika."
Ten skrypt wyświetla ostateczne informacje, które można dodać do pliku kubeconfig w celu
włączenia danego konta użytkownika. Oczywiście użytkownik nie ma uprawnień dostępu, więc
trzeba zastosować opartą na roli kontrolę dostępu w Kubernetes, aby przypisać użytkownikowi
pewne uprawnienia do przestrzeni nazw.
Tworzenie i zabezpieczanie przestrzeni nazw
Pierwszym krokiem w procesie przygotowywania przestrzeni nazw jest jej faktyczne
utworzenie. Można to zrobić za pomocą polecenia kubectl create namespace my-namespace.
Jednak podczas tworzenia przestrzeni nazw zwykle dołącza się do niej mnóstwo metadanych,
np. informacje kontaktowe dla zespołu zajmującego się kompilacją komponentu wdrażanego w
przestrzeni nazw. Ogólnie rzecz biorąc, to jest forma adnotacji: można wygenerować plik YAML
za pomocą systemu szablonów, takiego jak Jinja (https://palletsprojects.com/p/jinja/), bądź też
utworzyć przestrzeń nazw i później dodać adnotację. Spójrz na prosty skrypt, który będzie
wykonywał to zadanie.
ns='my-namespace'
kubectl create namespace ${ns}
kubectl annotate namespace ${ns} annotation_key=annotation_value
Po utworzeniu przestrzeni nazw należy ją zabezpieczyć, co odbywa się przez zagwarantowanie,
że dostęp do niej będzie miał określony użytkownik. W tym celu należy dołączyć rolę do
użytkownika w kontekście przestrzeni nazw. To się odbywa przez utworzenie obiektu
RoleBinding w samej przestrzeni nazw. Oto przykładowy kod wymienionego obiektu:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: example
namespace: my-namespace
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: edit
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: myuser
Ostateczne dołączenie roli nastąpi po wydaniu polecenia kubectl create -f rolebinding.yaml. Zwróć uwagę na możliwość wielokrotnego użycia tego skryptu, o ile będziesz
uaktualniał przestrzeń nazw w punkcie dołączania, aby prowadził do odpowiedniej przestrzeni
nazw. Jeżeli zagwarantujesz brak jakiejkolwiek innej dołączanej roli dla użytkownika, wówczas
będziesz miał pewność, że ta przestrzeń nazw to jedyny fragment klastra, do którego dany
użytkownik ma dostęp. Rozsądną praktyką jest również udzielanie uprawnień do odczytu całego
klastra. Dzięki temu programiści mogą widzieć, co robią inni, np. gdy pewne działanie zakłóca
ich pracę. Jednak zachowaj ostrożność w trakcie nadawania takich uprawnień odczytu,
ponieważ oznaczają one także dostęp do zasobów danych poufnych w klastrze. W przypadku
klastra programistycznego nie jest to problemem, skoro wszyscy należą do tej samej
organizacji, a dane poufne są używane jedynie podczas pracy nad aplikacjami. Jeżeli jednak to
jest dla Ciebie źródłem obaw, możesz zastosować znacznie dokładniejszy poziom kontroli
dostępu, który uniemożliwi dostęp do zasobów zawierających dane poufne.
Jeżeli chcesz ograniczyć ilość zasobów używanych przez określoną przestrzeń nazw, możesz
skorzystać z ResourceQuota w celu ograniczenia całkowitej ilości zasobów, które mogą być
użyte przez daną przestrzeń nazw. Przykładowo przedstawiony tutaj fragment kodu powoduje
ograniczenie zasobów przestrzeni nazw do 10 rdzeni i 100 GB pamięci dla zasobów Request i
Limit podów w przestrzeni nazw.
apiVersion: v1
kind: ResourceQuota
metadata:
name: limit-compute
namespace: my-namespace
spec:
hard:
requests.cpu: "10"
requests.memory: 100Gi
limits.cpu: "10"
limits.memory: 100Gi
Zarządzanie przestrzeniami nazw
Skoro się dowiedziałeś, jak przygotować zasoby dla nowego użytkownika i jak utworzyć
przestrzeń nazw przeznaczoną do użycia w charakterze przestrzeni roboczej, pozostało już
tylko przypisanie tej przestrzeni nazw nowego programisty. Podobnie jak w przypadku wielu
innych kwestii, także tu nie istnieje jedno, doskonałe rozwiązanie, lecz raczej dwa podejścia.
Pierwsze polega na nadaniu każdemu użytkownikowi oddzielnej przestrzeni nazw, jako części
procesu przygotowywania dla niego zasobów. Takie podejście jest użyteczne, ponieważ
użytkownik po dodaniu do zespołu zawsze będzie miał przeznaczoną tylko dla niego przestrzeń
roboczą, w ramach której będzie mógł opracowywać aplikacje i zarządzać nimi. Jednak
zdefiniowanie przestrzeni nazw użytkownika jako zbyt trwałej zachęca go do pozostawiania w
niej różnych zasobów po zakończeniu pracy z nimi, więc zbieranie i usuwanie nieużytków oraz
ocena pozostałych zasobów stają się znacznie bardziej skomplikowane. Alternatywne podejście
polega na tymczasowym utworzeniu i przypisaniu przestrzeni nazw w ramach ograniczenia TTL
(ang. time to live). Dzięki temu programista będzie traktował zasoby klastra jako tymczasowe,
co ułatwi opracowanie automatyzacji odpowiedzialnej za usuwanie całych przestrzeni nazw po
wygaśnięciu TTL.
W takim modelu, gdy programista rozpoczyna pracę nad nowym projektem, używa narzędzia do
alokacji nowej przestrzeni nazw dla danego projektu. Podczas tworzenia przestrzeni nazw
dodawana jest do niej pewna ilość metadanych potrzebnych w procesie zarządzania
przestrzeniami nazw. Oczywiście te metadane zawierają również wartość TTL dla przestrzeni
nazw, dane programisty, który został do niej przypisany, zasoby przeznaczone dla danej
przestrzeni nazw (np. procesor i pamięć), a także informacje o zespole i przeznaczeniu
przestrzeni nazw. Te metadane gwarantują możliwość monitorowania poziomu użycia zasobów i
jednoczesnego usuwania przestrzeni nazw w odpowiednim momencie.
Opracowanie narzędzia przeznaczonego do alokacji przestrzeni nazw na żądanie wydaje się
wyzwaniem, choć proste narzędzia można względnie łatwo stworzyć. Przykładowo w celu
alokacji przestrzeni nazw można wykorzystać prosty skrypt, którego działanie będzie polegało
na utworzeniu przestrzeni nazw i umożliwieniu użytkownikowi podania odpowiednich
metadanych dołączanych do tej przestrzeni.
Jeżeli oczekujesz rozwiązania znacznie ściślej zintegrowanego z Kubernetes, możesz skorzystać
z definicji zasobów niestandardowych (ang. custom resource definition, CRD) i tym samym
umożliwić użytkownikowi dynamiczne tworzenie i alokowanie przestrzeni nazw za pomocą
narzędzia kubectl. Jeżeli masz czas i chęci, to zdecydowanie będzie dobra praktyka, ponieważ
powoduje, że zarządzanie przestrzeniami nazw staje się deklaratywne, a ponadto pozwala na
stosowanie w Kubernetes mechanizmu RBAC.
Mając w ręku narzędzia przeznaczone do alokacji przestrzeni nazw, trzeba mieć również
narzędzia pozwalające na usuwanie przestrzeni nazw po wygaśnięciu wspomnianego wcześniej
ograniczenia TTL. Także to zadanie można wykonać za pomocą prostego skryptu, który będzie
analizował przestrzenie nazw i usuwał te, których wartość TTL wygasła.
Wspomniany skrypt można umieścić w kontenerze i wykorzystać zasób ScheduledJob do jego
uruchamiania w ustalonych odstępach czasu, np. co godzina. W połączeniu z innymi te
narzędzia zagwarantują, że programiści będą mieli możliwość łatwego alokowania niezależnych
zasobów niezbędnych dla projektu. Ponadto mamy pewność, że te zasoby zostaną zwolnione w
odpowiednim czasie, więc nie będą marnowane na przechowywanie starych zasobów,
niepotrzebnych w nowym wdrożeniu.
Usługi na poziomie klastra
Poza narzędziami przeznaczonymi do alokowania przestrzeni nazw i zarządzania nimi istnieją
jeszcze użyteczne usługi na poziomie klastra. Ich włączenie w klastrze programistycznym jest
dobrym pomysłem. Taką usługą jest przede wszystkim agregacja dzienników zdarzeń do
systemu LaaS (ang. logging as a service). Jednym z najłatwiejszych sposobów pozwalających
programiście zrozumieć to, jak działa aplikacja, jest wyświetlanie pewnych danych za pomocą
standardowego wyjścia, tzw. STDOUT. Wprawdzie dostęp do dzienników zdarzeń jest możliwy
za pomocą polecenia kubectl logs, ale ten dziennik ma ograniczoną wielkość i nie jest łatwy
do przeszukiwania. Jeżeli informacje dziennika zdarzeń będą automatycznie przekazywane do
systemu LaaS, takiego jak usługa w chmurze lub klaster Elasticsearch, wówczas programiści
będą mogli bardzo łatwo przeglądać dane w dziennikach w poszukiwaniu istotnych informacji,
a także agregować te dane między wieloma kontenerami w usłudze.
Umożliwienie pracy programistom
Skoro udało się przygotować konfigurację współdzielonego klastra i zasoby dla nowych
programistów aplikacji, następnym krokiem jest umożliwienie im rozpoczęcia pracy. Pamiętaj,
że jeden ze współczynników KPI dotyczy pomiaru czasu, jaki upłynął od przygotowania zasobów
dla programisty do chwili początkowego uruchomienia aplikacji w klastrze. Nie ulega
wątpliwości, że za pomocą omówionych wcześniej skryptów można szybko uwierzytelnić
użytkownika w klastrze i zaalokować dla niego przestrzeń nazw. Pozostaje więc kwestia
rozpoczęcia pracy z aplikacją. Niestety, mimo że istnieje kilka technik pomagających w tym
procesie, ogólnie rzecz biorąc, wymagają one więcej konwencji niż automatyzacji w celu
przygotowania i uruchomienia początkowej aplikacji. W kolejnych podrozdziałach przedstawimy
jedno z możliwych podejść w tym zakresie. Oczywiście nie sugerujemy, że to jest jedyne
podejście lub rozwiązanie. Możesz zastosować przedstawione tutaj rozwiązanie lub
potraktować je jako inspirację do opracowania własnego.
Konfiguracja początkowa
Jednym z podstawowych wyzwań podczas opracowywania aplikacji jest instalacja jej wszystkich
zależności. W wielu sytuacjach, zwłaszcza w przypadku nowoczesnej architektury mikrousług,
aby w ogóle rozpocząć pracę nad mikrousługą, trzeba wdrożyć wiele zależności, np. w postaci
baz danych lub innych mikrousług. Wprawdzie samo wdrożenie aplikacji jest względnie proste,
ale identyfikacja i wdrożenie wszystkich zależności w celu przygotowania pełnej aplikacji często
są frustrującym doświadczeniem, pełnym prób i błędów w sytuacji, gdy informacje są
niekompletne lub nieaktualne.
Rozwiązaniem tego problemu często jest wprowadzenie konwencji dotyczącej opisywania i
instalowania zależności. To może być np. odpowiednik rozwiązania podobnego do polecenia npm
install, którego wydanie powoduje zainstalowanie wszystkich niezbędnych zależności
JavaScriptu. Ewentualnie będzie to narzędzie podobne do menedżera pakietów npm,
zapewniającego usługę dla aplikacji opartych na Kubernetes. Jednak zanim to nastąpi,
najlepszą praktyką jest opieranie się na konwencji stosowanej w zespole.
W przypadku takiej konwencji jedną z opcji jest utworzenie skryptu setup.sh w katalogu
głównym wszystkich repozytoriów projektu. Ten skrypt będzie odpowiedzialny za utworzenie
wszystkich zależności w określonej przestrzeni nazw, aby tym samym zapewnić poprawne
zainstalowanie wszystkich zależności wymaganych przez aplikację. Spójrz na przykładowy kod,
który można umieścić w takim skrypcie.
kubectl create my-service/database-stateful-set-yaml
kubectl create my-service/middle-tier.yaml
kubectl create my-service/configs.yaml
Wspomniany skrypt często można zintegrować z menedżerem pakietów npm przez dodanie
następującego fragmentu kodu do pliku package.json.
{
...
"scripts": {
"setup": "./setup.sh",
...
}
}
Mając dostępną konfigurację w przedstawionej tutaj postaci, nowy programista może po prostu
wydać polecenie npm run, a niezbędne zależności klastra zostaną zainstalowane. Oczywiście to
jest przykład integracji przeznaczonej dla Node.js i npm. W innych językach programowania
sensowne będzie przeprowadzenie integracji z odpowiednimi narzędziami. Przykładowo w Javie
to może być integracja z plikiem pom.xml.
Umożliwienie aktywnego programowania
Gdy programista ma przygotowane środowisko pracy z wymaganymi zależnościami, następnym
krokiem jest umożliwienie mu jak najszybszego rozpoczęcia pracy nad aplikacją. To przede
wszystkim oznacza możliwość utworzenia i przekazania obrazu kontenera. Przyjmujemy
założenie, że to zostało już zrobione. Jeżeli nie, informacje o tym znajdziesz w wielu innych
zasobach opublikowanych w internecie, a także w innych książkach.
Po utworzeniu i przekazaniu obrazu kontenera kolejne zadanie polega na umieszczeniu obrazu
w serwerze. W przeciwieństwie do tradycyjnego sposobu pracy, w przypadku programisty
iteracja i zapewnienie dostępności tak naprawdę nie są powodem do zmartwienia. Dlatego też
najłatwiejszym sposobem na wdrożenie nowego kodu jest usunięcie obiektu Deployment
powiązanego z poprzednim wdrożeniem, a następnie utworzenie nowego obiektu Deployment,
wskazującego nowo utworzony obraz. Istnieje również możliwość uaktualnienia istniejącego
obiektu Deployment, choć to spowoduje wywołanie w zasobie Deployment logiki
odpowiedzialnej za przeprowadzenie zmian. Wprawdzie można skonfigurować zasób
Deployment do szybkiego wprowadzania nowego kodu, ale to doprowadzi do powstania różnicy
między środowiskami programistycznym i produkcyjnym, co może okazać się niebezpieczne i
destabilizujące. Wyobraź sobie np. przekazanie konfiguracji programistycznej do środowiska
produkcyjnego. To oznacza nagłe i przypadkowe wdrożenie nowej wersji do produkcji bez
wcześniejszego jej przetestowania i zachowania przerw między fazami wprowadzania nowej
wersji aplikacji. Z powodu takiego ryzyka najlepszą praktyką jest usunięcie obiektu Deployment
i jego ponowne utworzenie.
Podobnie jak w przypadku instalowania zależności dobrą praktyką jest również przygotowanie
skryptu przeznaczonego dla takiego wdrożenia. Spójrz na kod, który można umieścić w
przykładowym skrypcie deploy.sh.
kubectl delete -f ./my-service/deployment.yaml
perl -pi -e 's/${old_version}/${new_version}/' ./my-service/deployment.yaml
kubectl create -f ./my-service/deployment.yaml
Także ten skrypt można zintegrować z istniejącymi narzędziami języka programowania, więc
programista może np. wydać polecenie npm run i tym samym wdrożyć nowy kod do klastra.
Umożliwienie testowania i debugowania
Po zakończonym sukcesem wdrożeniu programistycznej wersji aplikacji programista musi mieć
możliwość jej przetestowania i, jeśli występują jakiekolwiek problemy, debugowania wszelkich
błędów, które mogą w niej występować. To może być utrapieniem podczas tworzenia aplikacji w
Kubernetes, ponieważ nie zawsze wiadomo, jak pracować z klastrem. Polecenie kubectl jest
wszechstronnym narzędziem przeznaczonym do różnych celów, od kubectl logs poprzez
kubectl exec aż do kubectl port-forward. Jednak poznanie sposobu użycia tego narzędzia z
różnymi opcjami i osiągnięcie możliwości komfortowej pracy z nim będzie wymagało dużo czasu
i sporego doświadczenia. Co więcej, ponieważ to narzędzie działa w powłoce, często wymaga
łączenia wielu okien w celu jednoczesnego analizowania zarówno kodu źródłowego, jak i
uruchomionej aplikacji.
Aby ułatwić testowanie i debugowanie, narzędzia Kubernetes są coraz bardziej integrowane ze
środowiskami programistycznymi. Przykładem takiej integracji jest opracowanie dostępnego
jako oprogramowanie typu open source rozszerzenia zapewniającego obsługę Kubernetes w
Visual Studio Code. Wymienione rozszerzenie jest łatwe do zainstalowania z poziomu VS Code
Marketplace. Po zainstalowaniu automatycznie wykrywa wszelkie klastry zdefiniowane w pliku
kubeconfig i zapewnia panel nawigacji za pomocą widoku drzewa, który pozwala na
przeglądanie zawartości klastra.
Poza przeglądaniem zawartości klastra integracja umożliwia programistom korzystanie z
dostępnych za pomocą kubectl narzędzi w sposób intuicyjny i możliwy do odkrycia. W widoku
drzewa można kliknąć pod Kubernetes prawym przyciskiem myszy, aby natychmiast użyć
funkcji przekazywania portu i zapewnić bezpośrednie połączenie sieciowe z komputera
lokalnego do poda. W podobny sposób można uzyskać dostęp do dziennika zdarzeń poda lub
nawet do powłoki w uruchomionym kontenerze.
Integracja tych poleceń z interfejsem użytkownika (czyli prawy przycisk myszy powoduje
wyświetlenie menu kontekstowego), jak również integracja tego sposobu działania z kodem
aplikacji pozwalają programistom rozpocząć tworzenie aplikacji pomimo niewielkiego
doświadczenia w pracy z Kubernetes oraz szybko osiągnąć dobrą produktywność w klastrze
programistycznym.
Oczywiście omówione rozszerzenie VS Code to niejedyne rozwiązanie w zakresie integracji
między Kubernetes a środowiskiem wdrożenia. Dostępnych jest jeszcze wiele innych, które
można instalować w zależności od wybranego stylu i środowiska programowania (vi, emacs
itd.).
Najlepsze praktyki dotyczące konfiguracji
środowiska programistycznego
Zapewnienie właściwego sposobu pracy z Kubernetes ma duże znaczenie dla produktywności.
Stosowanie się do przedstawionych tutaj najlepszych praktyk pomoże zagwarantować, że
programiści będą mogli szybko rozpocząć pracę.
O pracy programistów myśl w kategoriach trzech faz: wejścia na pokład, programowania i
testowania. Upewnij się, że tworzone środowisko programistyczne obsługuje wszystkie
trzy wymienione fazy.
Podczas tworzenia klastra programistycznego masz do wyboru dwa podejścia: jeden duży
klaster dla wszystkich lub oddzielne klastry dla poszczególnych programistów. Każde z
nich ma swoje wady i zalety, ale ogólnie rzecz biorąc, jeden ogromny klaster jest lepszym
rozwiązaniem.
Podczas dodawania użytkowników do klastra zapewnij każdemu własną tożsamość i
dostęp do oddzielnej przestrzeni nazw. Wykorzystaj mechanizmy ograniczania zasobów do
określenia, ile zasobów klastra użytkownik ma do dyspozycji.
Podczas zarządzania przestrzeniami nazw zastanów się nad sposobami usuwania starych,
nieużywanych już zasobów. Programiści mają złe nawyki w zakresie usuwania
nieużywanych zasobów. Wykorzystaj automatyzację do usuwania tych zasobów w imieniu
użytkowników.
Zastanów się nad usługami na poziomie klastra, takimi jak monitorowanie i rejestrowanie
zdarzeń, które można zdefiniować dla wszystkich użytkowników. Czasami także zależności
na poziomie klastra, np. bazy danych, dobrze jest instalować w imieniu wszystkich
użytkowników, np. za pomocą szablonów, takich jak pliki Helm w formacie chart.
Podsumowanie
Dotarliśmy do miejsca, w którym tworzenie klastra Kubernetes, zwłaszcza w chmurze, jest
względnie
prostym
zadaniem.
Jednak
umożliwienie
programistom
produktywnego
wykorzystania tego klastra jest nieco trudniejsze, a to, jak należy to zrobić, nie jest równie
oczywiste. Gdy zastanawiasz się nad tym, co zrobić, aby tworzenie aplikacji Kubernetes
zakończyło się sukcesem, jest bardzo ważne, byś pamiętał o celach takich jak przygotowanie
zasobów, iteracja, testowanie i debugowanie aplikacji. Na pewno opłaci się inwestycja w pewne
podstawowe narzędzia przeznaczone do wymienionych celów, a także do przygotowywania
przestrzeni nazw i usług klastra, takich jak podstawowa agregacja dzienników zdarzeń.
Wyświetlanie zawartości klastra programistycznego i repozytoriów kodu źródłowego umożliwia
standaryzację i zastosowanie najlepszych praktyk, dzięki którym będziesz miał zadowolonych i
produktywnych programistów, a także zakończone sukcesem wdrożenie kodu w produkcyjnych
klastrach Kubernetes.
Rozdział 3. Monitorowanie i
rejestrowanie danych w
Kubernetes
W tym rozdziale przedstawimy najlepsze praktyki w zakresie monitorowania i rejestrowania danych
w Kubernetes. Zagłębimy się w szczegóły związane z różnymi wzorcami monitorowania, ważnymi
wskaźnikami do zebrania i budowaniem paneli głównych na podstawie zebranych wskaźników.
Zaprezentujemy również przykłady implementacji technik monitorowania klastra Kubernetes.
Wskaźniki kontra dzienniki zdarzeń
Przede wszystkim trzeba zrozumieć różnicę między zbieraniem informacji dzienników zdarzeń i
wskaźników. Wprawdzie te informacje wzajemnie się uzupełniają, ale służą do zupełnie odmiennych
celów.
Wskaźniki
Seria wartości liczbowych zmierzonych w pewnym okresie.
Dzienniki zdarzeń
Ciągi tekstowe używane do wyjaśnienia zdarzeń zachodzących w systemie.
Przykładem sytuacji, w której trzeba będzie wykorzystać zarówno wskaźniki, jak i dzienniki
zdarzeń, jest kiepska wydajność działania aplikacji. Pierwszym symptomem informującym o
problemie jest wysokie opóźnienie w działaniu podów zawierających aplikację. W tym przypadku
wskaźniki niekoniecznie będą dobrym mechanizmem informującym o potencjalnym problemie.
Należy więc sprawdzić dzienniki zdarzeń i poszukać w nich wygenerowanych przez aplikację
komunikatów o błędzie.
Techniki monitorowania
Technika monitorowania nazywana czarnym pudełkiem koncentruje się na monitorowaniu aplikacji
z zewnątrz i tradycyjnie jest stosowana w przypadku komponentów takich jak procesor, pamięć
operacyjna i pamięć masowa. Taki rodzaj monitorowania nadal może się okazać użyteczny na
poziomie infrastruktury, choć nie zapewnia informacji np. o kontekście działania aplikacji.
Przykładowo w celu sprawdzenia poprawności działania klastra można przygotować poda i jeśli ta
operacja zakończy się sukcesem, wówczas będzie wiadomo, że tzw. zarządca procesów (ang.
scheduler) i funkcjonalność wykrywania usług działają w klastrze poprawnie. To pozwala przyjąć
założenie o właściwym działaniu komponentów klastra.
W trakcie monitorowania z użyciem techniki określanej mianem białego pudełka nacisk kładzie się
na szczegóły kontekstu stanu aplikacji, takie jak całkowita liczba żądań HTTP, liczba błędów o
kodzie 500 i opóźnienie podczas wykonywania żądań. Podczas tego rodzaju monitorowania
zaczynamy rozumieć, „dlaczego” system znajduje się w danym stanie. To pozwala zadawać sobie
pytania w rodzaju: „Dlaczego dysk został zapełniony?”, zamiast ograniczać się do stwierdzeń typu:
„Dysk został zapełniony”.
Wzorce monitorowania
Być może wiesz, czym jest monitorowanie, i zadajesz sobie pytanie: „Co może być w tym trudnego?
Od zawsze zajmowałem się monitorowaniem systemu”. Faktycznie, typowy wzorzec monitorowania
stosowany na co dzień jest przez część czytelników używany również podczas monitorowania
Kubernetes. Jednak różnica polega na tym, że platformy takie jak Kubernetes są znacznie bardziej
tymczasowe i ulotne, więc trzeba nieco zmienić sposób myślenia o monitorowaniu w tych
środowiskach. Przykładowo podczas monitorowania maszyny wirtualnej (ang. virtual machine, VM)
być może oczekujesz, że będzie ona działała 24 godziny na dobę przez 7 dni w tygodniu, a jej stan
zostanie zachowany. W Kubernetes pody mogą być niezwykle dynamiczne i istnieć przez bardzo
krótki czas, dlatego w zakresie monitoringu trzeba zastosować rozwiązanie, które będzie w stanie
obsłużyć tę dynamiczną i ulotną naturę.
Istnieje kilka różnych wzorców monitorowania, na których się skoncentrujemy w trakcie
monitorowania systemów rozproszonych.
Spopularyzowana przez Brendana
wymienionych tutaj obszarach:
Gregga
metoda
USE
oznacza
koncentrację
na
trzech
poziomie wykorzystania (ang. Utilization) zasobu,
poziomie nasycenia (ang. Saturation) zasobu,
poziomie błędów (ang. Errors) zasobu.
Podczas stosowania tej metody nacisk kładzie się na monitorowanie infrastruktury, ponieważ
istnieją pewne ograniczenia w jej użyciu do monitorowania na poziomie aplikacji. Metoda USE
została opisana następująco: „w przypadku każdego zasobu należy sprawdzić poziom jego
wykorzystania, nasycenia i błędów”. Ta metoda pozwala na szybkie zidentyfikowanie ograniczeń
dotyczących zasobów i współczynnika błędów. Przykładowo, jeśli chcesz sprawdzić stan sieci
węzłów w klastrze, powinieneś monitorować poziom wykorzystania, nasycenia i błędów, aby dzięki
temu łatwo wychwycić wąskie gardła lub błędy w stosie sieci. Metoda USE należy do większego
zestawu narzędziowego i na pewno nie jest jedyną, którą będziesz stosować do monitorowania
systemu.
Inne podejście w zakresie monitorowania to tzw. metoda RED, spopularyzowana przez Toma
Willke’a. Nacisk jest w niej kładziony na następujące kwestie:
tempo (ang. Rate),
poziom błędów (ang. Errors),
czas trwania (ang. Duration).
Idea tej metody została zaczerpnięta ze standardu czterech złotych reguł firmy Google:
opóźnienie (ile czasu potrzeba na obsługę żądania),
ruch sieciowy (w jak dużym stopniu jest obciążony system),
błędy (współczynnik żądań zakończonych niepowodzeniem),
poziom nasycenia (stopień wykorzystania usługi).
Przykładowo tę metodę można wykorzystać do monitorowania działającej w Kubernetes usługi
frontendu odpowiedzialnej za przeprowadzanie następujących obliczeń:
Ile żądań jest przetwarzanych przez usługę frontendu?
Ile błędów o kodzie stanu 500 otrzymują użytkownicy usługi?
Czy usługa jest przeciążona liczbą kierowanych do niej żądań?
Jak możesz stwierdzić na podstawie poprzedniego przykładu, w omawianej metodzie nacisk jest
kładziony na wrażenia odbierane przez użytkownika i jego odczucia podczas pracy z usługą.
Metody USE i RED uzupełniają się nawzajem — metoda USE jest skoncentrowana na
komponentach infrastruktury, podczas gdy metoda RED jest skoncentrowana na monitorowaniu
wrażeń odbieranych przez użytkownika końcowego.
Ogólne omówienie wskaźników Kubernetes
Skoro poznałeś różne techniki monitorowania i wzorce, warto spojrzeć na komponenty, które
powinny być monitorowane w klastrze Kubernetes. Taki klaster składa się z komponentów
płaszczyzny kontrolnej i komponentów węzła roboczego. Do komponentów płaszczyzny kontrolnej
zaliczamy API serwera, usługi etcd, zarządcę procesów i menedżera kontrolera. Z kolei węzeł
roboczy składa się z kubeletu, środowiska uruchomieniowego kontenera, kube-proxy, kube-dns i
podów. Aby mieć pewność, że stan klastra jest dobry, trzeba monitorować wszystkie te komponenty.
Kubernetes udostępnia te wskaźniki na różne sposoby, więc warto spojrzeć na różne dostępne
komponenty, które można wykorzystać w celu zbierania wartości wskaźników w klastrze.
cAdvisor
cAdvisor (ang. container advisor) to projekt typu open source, którego celem jest zbieranie
zasobów i wskaźników dla kontenerów działających w węźle. cAdvisor został wbudowany w
kubelecie Kubernetes, działającym w każdym węźle klastra. Wskaźniki dotyczące pamięci i
procesora są zbierane za pomocą drzewa grup kontrolnych (cgroup) systemu Linux. Jeżeli nie znasz
grup kontrolnych, powinieneś wiedzieć, że to jest funkcja jądra systemu Linux umożliwiająca
izolację zasobów procesora oraz dyskowych i sieciowych operacji wejścia-wyjścia. cAdvisor zbiera
wskaźniki za pomocą statfs, czyli mechanizmu wbudowanego w jądro systemu Linux. To są
szczegóły implementacji, którymi tak naprawdę nie musisz się przejmować, choć powinieneś
wiedzieć, jak te wskaźniki są udostępniane, a także jakiego rodzaju informacje są zbierane.
Powinieneś potraktować cAdvisor jako źródło danych dla wszystkich wskaźników kontenera.
Wskaźniki serwera
Wskaźniki serwera Kubernetes i API Server Metrics Kubernetes są zamiennikami dla uznanego za
przestrzały mechanizmu Heapster. Miał on pewne architekturalne wady związane z implementacją,
które doprowadziły do powstania wielu rozwiązań pochodnych na podstawie kodu Heapster. Ten
problem został rozwiązany przez implementację zasobu i API niestandardowych wskaźników, jako
zagregowanego API w Kubernetes. W ten sposób istnieje możliwość zmiany implementacji bez
konieczności zmiany API.
Są dwa aspekty, które należy zrozumieć w API Server Metrics i serwera wskaźników.
Pierwszy to kanoniczna implementacja API Resource Metrics, uznawana za serwer wskaźników.
Wspomniany serwer wskaźników zbiera dane wskaźników, takie jak informacje o procesorze i
pamięci. Te dane są pobierane za pomocą API kubeletu, a następnie przechowywane w pamięci.
Kubernetes wykorzysta te wskaźniki zasobu w zarządcy procesów oraz w mechanizmach HPA (ang.
horizontal pod autoscaler) i VPA (vertical pod autoscaler).
Drugi, API niestandardowych wskaźników, pozwala systemom monitorowania na zbieranie danych
dowolnych wskaźników. To z kolei umożliwia rozwiązaniom z zakresu monitorowania tworzenie
niestandardowych adapterów, które pozwolą stosować zewnętrzne rozszerzenia dla podstawowych
wskaźników zasobu. Przykładowo twórcy oprogramowania Prometheus zbudowali jeden z
pierwszych adapterów wskaźników niestandardowych, który pozwala używać HPA na podstawie
wskaźników niestandardowych. W ten sposób są zapewnione lepsze możliwości w zakresie
skalowania na podstawie sposobu użycia, ponieważ teraz można stosować wskaźniki takie jak
wielkość kolejki i skala na podstawie wskaźników, które są zewnętrzne dla Kubernetes.
Mamy również standaryzowane API wskaźników, zapewniające wiele możliwości w zakresie
skalowania doskonale znanych wskaźników dotyczących procesora i pamięci.
kube-state-metrics
kube-state-metrics
to
dodatek
Kubernetes
pozwalający
na
monitorowanie
obiektu
przechowywanego w Kubernetes. cAdvisor i serwer wskaźników są używane w celu dostarczenia
szczegółowych informacji dotyczących poziomu zużycia zasobu, a kube-state-metrics koncentruje
się na określeniu warunków dotyczących obiektów Kubernetes wdrożonych w klastrze.
Oto kilka pytań, na które kube-state-metrics może udzielić odpowiedzi.
Pody
Ile podów zostało wdrożonych w klastrze?
Ile podów znajduje się w stanie oczekiwania?
Czy dostępnych jest wystarczająco dużo zasobów, aby można było obsłużyć żądania podów?
Wdrożenia
Ile podów znajduje się w stanie działania, a ile w stanie oczekiwanym?
Ile jest dostępnych replik?
Które wdrożenia zostały uaktualnione?
Węzły
W jakim stanie znajdują się węzły robocze?
Czy klaster ma dostępne do przydzielenia rdzenie procesora?
Czy którekolwiek węzły nie mogą być użyte?
Zadania
Kiedy zaczęło się dane zadanie?
Kiedy zadanie zostało zakończone?
Ile zadań zakończyło się niepowodzeniem?
W czasie gdy pisaliśmy tę książkę, istniały 22 typy obiektów monitorowanych za pomocą kubestate-metrics. Ta liczba się zwiększa, a więcej informacji na ten temat znajdziesz w dokumentacji
zamieszczonej w repozytorium GitHub na stronie https://github.com/kubernetes/kube-statemetrics/tree/master/docs.
Które wskaźniki powinny być
monitorowane?
Najłatwiejszą odpowiedzią na to pytanie jest „wszystkie”, ale jeśli spróbujesz monitorować zbyt
wiele, wówczas możesz doprowadzić do powstania zbyt dużego szumu przykrywającego te sygnały,
które naprawdę są ważne i na które chciałbyś zwrócić uwagę. Kiedy mówimy o monitorowaniu w
Kubernetes, mamy na myśli podejście warstwowe, w którym zostaną uwzględnione następujące
czynniki:
węzły fizyczne lub wirtualne,
komponenty klastra,
dodatki klastra,
aplikacje użytkownika końcowego.
Użycie takiego opartego na warstwach podejścia do monitorowania pozwala na znacznie łatwiejsze
identyfikowanie właściwych sygnałów w systemie monitorowania. Umożliwia także rozwiązywanie
problemów w znacznie bardziej odpowiedni sposób. Przykładowo, jeżeli pod znajduje się w stanie
oczekiwania, można zacząć od sprawdzenia poziomu wykorzystania zasobów węzłów i jeśli
wszystko jest w porządku, przejść do komponentów na poziomie celu.
Oto wybrane wskaźniki, które można uznać za cel w systemie:
Węzły
poziom wykorzystania procesora,
poziom wykorzystania pamięci,
poziom wykorzystania sieci,
poziom wykorzystania dysku.
Komponenty klastra
opóźnienie etcd.
Dodatki klastra
komponent automatycznego skalowania klastra,
kontroler ingress.
Aplikacja
poziom wykorzystania i nasycenia pamięci kontenera,
poziom wykorzystania procesora kontenera,
poziom wykorzystania sieci kontenera i współczynnik błędów,
wskaźniki typowe dla frameworka aplikacji.
Narzędzia do monitorowania
Istnieje wiele narzędzi do monitorowania, które mogą być zintegrowane z Kubernetes. Każdego
dnia pojawiają się nowe, które oferują zestaw funkcjonalności zapewniający lepszą integrację z
Kubernetes. Oto kilka popularnych narzędzi zintegrowanych z Kubernetes.
Prometheus
Prometheus to system monitorowania i ostrzegania dostępny jako oprogramowanie typu open
source, które pierwotnie zostało opracowane w firmie SoundCloud i udostępnione w 2012
roku. Od tego czasu zaadaptowało go wiele firm i organizacji, a sam projekt ma teraz bardzo
aktywnych programistów i społeczność użytkowników. Obecnie to oddzielny projekt typu open
source, rozwijany niezależnie od jakiejkolwiek firmy. Aby to podkreślić i wyjaśnić strukturę
projektu, Prometheusa dołączono w 2016 roku do fundacji CNCF (ang. Cloud Native
Computing Foundation) jako jej drugi projekt po Kubernetes.
InfluxDB
InfluxDB to baza danych serii czasu zaprojektowana do obsługi ogromnych obciążeń
związanych z wykonywaniem zapytań i zapisem danych. To ogólny komponent stosu TICK
(Telegraf, InfluxDB, Chronograf i Kapacitor). Baza danych InfluxDB jest przeznaczona do
używania jako magazyn danych backendu dla wszelkich rozwiązań wykorzystujących ogromne
ilości danych wraz ze znacznikami czasu, czyli m.in. podczas monitorowania DevOps,
wskaźników aplikacji, danych czujników IoT oraz w trakcie analizy prowadzonej w czasie
rzeczywistym.
Datadog
Datadog oferuje usługę monitorowania dla aplikacji skalowanych w chmurze, zapewniając
możliwości w zakresie monitorowania serwerów, baz danych, narzędzi i usług za pomocą
opartej na modelu SaaS platformy analizy.
Sysdig
Sysdig to narzędzie komercyjne zapewniające możliwości w zakresie monitorowania
natywnych aplikacji Dockera i Kubernetes. Sysdiag pozwala również na zbieranie, korelowanie
i sprawdzanie wskaźników Prometheusa z bezpośrednią integracją z Kubernetes.
Narzędzia dostawców chmury
GCP Stackdriver
Narzędzie Stackdriver Kubernetes Engine Monitoring zostało zaprojektowane do
monitorowania klastrów GKE (ang. Google Kubernetes Engine). Zarządza usługami
monitorowania i rejestrowania danych, a także zapewnia funkcje i interfejsy dostarczające
panel główny dostosowany do klastrów GKE. Stackdriver Monitoring zapewnia możliwość
sprawdzenia wydajności działania, czasu działania i ogólnego stanu aplikacji działających
w chmurze. Zbiera wskaźniki, zdarzenia i metadane z GCP (ang. Google Cloud Platform),
AWS (ang. Amazon Web Services), próbek i instrumentacji aplikacji.
Microsoft Azure Monitor for containers
To narzędzie zaprojektowane do monitorowania wydajności działania kontenerów
wdrożonych do Azure Container Instances lub zarządzanych klastrów Kubernetes w Azure
Kubernetes Service. Monitorowanie kontenerów ma krytyczne znaczenie, zwłaszcza w
przypadku klastra produkcyjnego zawierającego wiele aplikacji. Microsoft Azure Monitor
for containers dostarcza informacji o wydajności działania kontenera na podstawie
dostępnych w Kubernetes za pomocą API wskaźników pamięci i procesora zebranych z
kontrolerów, węzłów i kontenerów. Dzienniki zdarzeń kontenerów również są zbierane. Po
włączeniu monitorowania klastra Kubernetes wskaźniki i dzienniki zdarzeń są zbierane
automatycznie poprzez skonteneryzowaną wersję agenta Log Analytics w systemie Linux.
AWS Container Insights
Jeżeli korzystasz z ECS (ang. Amazon Elastic Container Service), Amazon Elastic
Kubernetes Service lub innych platform Kubernetes w chmurze Amazon EC2, wówczas
CloudWatch Container Insights można wykorzystać w celu zebrania, agregowania i
podsumowania wskaźników oraz dzienników zdarzeń z działających w kontenerach
aplikacji i mikrousług. Te wskaźniki obejmują m.in. poziom wykorzystania zasobów, takich
jak procesor, pamięć, dysk i sieć. Container Insights zapewnia również informacje
diagnostyczne, np. o awariach kontenera, pomagające w szybkim znalezieniu i usunięciu
problemu.
Ważnym aspektem podczas szukania narzędzia przeznaczonego do monitorowania wskaźników jest
sprawdzenie sposobu, w jaki te wskaźniki są przechowywane. Narzędzia dostarczające bazę danych
serii czasu przechowującą pary klucz-wartość zapewniają wyższy stopień atrybutów dla
wskaźników.
Zawsze należy sprawdzić dostępne narzędzia monitorowania, ponieważ zastosowanie
nowego wiąże się z koniecznością poznania go i kosztem jego implementacji. Wiele
narzędzi monitorowania oferuje teraz integrację z Kubernetes, więc przeanalizuj te, które
masz, i sprawdź, czy spełniają Twoje wymagania.
Monitorowanie Kubernetes za pomocą
narzędzia Prometheus
W tym podrozdziale skoncentrujemy się na monitorowaniu wskaźników za pomocą narzędzia
Prometheus, które zapewnia dobrą integrację z Kubernetes, odkrywanie usług i metadane.
Wysokiego poziomu koncepcje implementowane w rozdziale mają również zastosowanie do innych
systemów monitorowania.
Prometheus to projekt typu open source nadzorowany przez fundację CNCF. Pierwotnie został
opracowany w firmie SoundCloud, a wiele jego koncepcji zostało opartych na wewnętrznym
systemie monitorowania firmy Google o nazwie BorgMon. Prometheus implementuje
wielowymiarowy model danych z parami kluczy działającymi w sposób podobny do systemu etykiet
w Kubernetes. Prometheus udostępnia wskaźniki w formacie czytelnym dla użytkownika, np.:
# HELP node_cpu_seconds_total — liczba sekund, które procesor spędził w
poszczególnych trybach.
# TYPE node_cpu_seconds_total — licznik.
node_cpu_seconds_total{cpu="0",mode="idle"} 5144.64
node_cpu_seconds_total{cpu="0",mode="iowait"} 117.98
W celu zebrania wskaźników Prometheus używa modelu pull, w którym zbiera wskaźniki punktu
końcowego i przekazuje je do swojego serwera. System taki jak Kubernetes udostępnia wskaźniki w
formacie Prometheusa, co znacznie ułatwia ich pobieranie. Również wiele innych projektów
ekosystemu Kubernetes (NGINX, Traefik, Istio, LinkerD itd.) udostępnia wskaźniki w formacie
Prometheusa. Ponadto Prometheus używa komponentów pozwalających na pobranie wskaźników
wyemitowanych przez usługę i ich konwersję na własny format tego narzędzia.
Architektura Prometheusa jest bardzo prosta, jak możesz zobaczyć na rysunku 3.1.
Rysunek 3.1. Architektura Prometheusa
Prometheusa można zainstalować w klastrze lub na zewnątrz klastra. Dobrą praktyką
jest monitorowanie klastra z poziomu „narzędzia klastra”, co pozwoli uniknąć problemów
w systemie produkcyjnym i wpływu systemu monitorowania. Istnieje wiele narzędzi,
takich jak Thanos (https://github.com/thanos-io/thanos), zapewniających wysoką
dostępność Prometheusa oraz umożliwiających eksport wskaźników do zewnętrznego
systemu monitorowania.
Dokładne omówienie architektury narzędzia Prometheus wykracza poza zakres tematyczny tej
książki. Jeżeli chcesz dowiedzieć się więcej na ten temat, sięgnij po jedną z pozycji poświęconych
temu narzędziu. Godna polecenia jest na przykład książka Prometheus: Up and Running
(https://www.oreilly.com/library/view/prometheus-up/9781492034131/), wydana przez O’Reilly, w
której znajdziesz wiele dokładnych informacji o sposobie działania Prometheusa.
Przechodzimy teraz do konfiguracji Prometheusa w klastrze Kubernetes. Istnieje wiele różnych
sposobów na przeprowadzenie konfiguracji, a samo wdrożenie będzie zależało od konkretnej
implementacji. W tym rozdziale omówimy proces instalacji oprogramowania Prometheus Operator.
Prometheus Server
Pobiera i przechowuje wskaźniki zebrane z systemów.
Prometheus Operator
Powoduje, że konfiguracja Prometheusa jest natywna dla Kubernetes, i pozwala na
przeprowadzanie operacji na klastrach Prometheusa i Alertmanagera oraz zarządzanie tymi
klastrami. Masz możliwość tworzenia, usuwania i konfigurowania zasobów Prometheusa za
pomocą natywnych dla Kubernetes definicji zasobów.
Node Exporter
Eksportuje wskaźniki hosta z węzłów Kubernetes w klastrze.
kube-state-metrics
Pobiera wskaźniki związane z Kubernetes.
Alertmanager
Pozwala na konfigurowanie i przekazywanie ostrzeżeń do systemów zewnętrznych.
Grafana
Zapewnia wizualizację możliwości panelu głównego Prometheusa.
$ helm install --name prom stable/prometheus-operator
Po zainstalowaniu oprogramowania Prometheus Operator powinieneś w klastrze zobaczyć
wdrożone następujące pody:
$ kubectl get pods -n monitoring
NAME
READY
STATUS
RESTARTS
AGE
alertmanager-main-0
2/2
Running
0
5h39m
alertmanager-main-1
2/2
Running
0
5h39m
alertmanager-main-2
2/2
Running
0
5h38m
grafana-5d8f767-ct2ws
1/1
Running
0
5h39m
kube-state-metrics-7fb8b47448-k6j6g
4/4
Running
0
5h39m
node-exporter-5zk6k
2/2
Running
0
5h39m
node-exporter-874ss
2/2
Running
0
5h39m
node-exporter-9mtgd
2/2
Running
0
5h39m
node-exporter-w6xwt
2/2
Running
0
5h39m
prometheus-adapter-66fc7797fd-ddgk5
1/1
Running
0
5h39m
prometheus-k8s-0
3/3
Running
1
5h39m
prometheus-k8s-1
3/3
Running
1
5h39m
prometheus-operator-7cb68545c6-gm84j
1/1
Running
0
5h39m
Spójrz na serwer Prometheusa i zobacz, jak można wykonywać pewne zapytania związane z
pobieraniem wskaźników Kubernetes.
$ kubectl port-forward svc/prom-prometheus-operator-prometheus 9090
To polecenie powoduje utworzenie tunelu do portu numer 9090 komputera lokalnego. Teraz możesz
uruchomić przeglądarkę WWW i nawiązać połączenie z serwerem Prometheusa, dostępnym pod
adresem http://127.0.0.1:9090.
Na rysunku 3.2 możesz zobaczyć ekran wyświetlany po zakończonym sukcesem wdrożeniu
Prometheusa w klastrze.
Rysunek 3.2. Panel główny Prometheusa
Po wdrożeniu Prometheusa można przystąpić do analizy wybranych wskaźników Kubernetes za
pomocą
języka
zapytań
Prometheus
PromQL.
Na
stronie
https://prometheus.io/docs/prometheus/latest/querying/basics/ znajduje się przewodnik zawierający
omówienie podstaw pracy z PromQL.
Wcześniej dowiedziałeś się nieco o używaniu metody USE, więc przechodzimy teraz do zebrania
pewnych wskaźników węzła związanych z poziomem wykorzystania i nasycenia procesora.
W polu tekstowym Expression wpisz następujące zapytanie:
avg(rate(node_cpu_seconds_total[5m]))
Wartością zwrotną będzie średni poziom wykorzystania procesora w całym klastrze.
Jeżeli chcesz otrzymać dane dotyczące poziomu wykorzystania procesora w poszczególnych
węzłach, możesz wykonać zapytanie:
avg(rate(node_cpu_seconds_total[5m])) by (node_name)
Wartością zwrotną będzie średni poziom wykorzystania procesora w poszczególnych węzłach
klastra.
W ten sposób zyskałeś pewne doświadczenie związane z wykonywaniem zapytań Prometheusa.
Przekonaj się więc, jak Grafana może pomóc w przygotowaniu wizualizacji panelu głównego dla
wskaźników metody USE, które są sprawdzane najczęściej. Doskonałą cechą narzędzia Prometheus
Operator jest to, że instaluje się ze wstępnie przygotowanymi panelami głównymi Grafana, które
można wykorzystać.
Musisz wiedzieć, jak tworzyć tunel przekazywania portu do poda Grafana, aby można było uzyskać
do niego dostęp z poziomu komputera lokalnego:
$ kubectl port-forward svc/prom-grafana 3000:3000
Teraz w przeglądarce WWW przejdź pod adres http://localhost:3000 i zaloguj się z użyciem
następujących danych uwierzytelniających:
nazwa użytkownika: admin,
hasło: admin.
W panelu głównym Grafana znajduje się sekcja Kubernetes/USE Method/Cluster. W tym miejscu
znajdują się dobre informacje o poziomie wykorzystania i nasycenia klastra Kubernetes, który jest
sercem metody USE. Przykład takiego panelu pokazaliśmy na rysunku 3.3.
Rysunek 3.3. Panel Grafana
Śmiało, poświęć nieco czasu na poznanie różnych sekcji panelu głównego i wskaźników, za pomocą
których można wizualizować dane w Grafana.
Unikaj tworzenia zbyt wielu paneli (tzw. ściany wykresów), ponieważ to może utrudnić
inżynierom rozwiązywanie problemów, gdy takie wystąpią. Być może sądzisz, że im
większa ilość informacji, tym lepsze monitorowanie. Jednak w większości przypadków
ogromna ilość danych wywołuje więcej zamieszania u użytkownika analizującego te
wykresy. Podczas przygotowywania panelu skoncentruj się na danych wyjściowych i
czasie, który będzie potrzebny na rozwiązanie problemu.
Ogólne omówienie rejestrowania danych
Dotychczas powiedzieliśmy wiele o wskaźnikach i Kubernetes. Aby jednak uzyskać pełen obraz
środowiska, trzeba również zebrać i scentralizować dzienniki zdarzeń z klastra Kubernetes i
wdrożonych w nim aplikacji.
Podczas rejestrowania danych bardzo łatwo można stwierdzić „rejestrujemy wszystkie dane”, choć
takie podejście wiąże się z dwoma problemami:
danych jest zbyt wiele, co będzie utrudniało szybkie odszukanie tych najważniejszych,
dzienniki zdarzeń zużywają ogromną ilość zasobów, co wiąże się z dużym kosztem.
Nie ma doskonałej odpowiedzi na pytanie o to, jakie dane należy rejestrować, ponieważ dzienniki
zdarzeń procesu debugowania stały się złem koniecznym. Wraz z upływem czasu zaczniesz
znacznie lepiej rozumieć środowisko i nauczysz się, których danych można się pozbyć z systemu
rejestrowania danych. Ponadto w celu rozwiązania problemu związanego z przechowywaniem coraz
większej ilości danych dzienników zdarzeń konieczna jest implementacja polityki ich rotacji i
archiwizacji. Z perspektywy użytkownika końcowego przechowywanie dzienników zdarzeń z
ostatnich 30 – 45 dni wydaje się rozsądnym rozwiązaniem. To pozwala na analizowanie problemów
z dość długiego okresu, a zarazem zmniejsza ilość zasobów niezbędnych do przechowywania
dzienników zdarzeń. Jeżeli z jakichkolwiek względów potrzebne są dane z dłuższego okresu, trzeba
będzie archiwizować dzienniki zdarzeń, aby w ten sposób jak najefektywniej wykorzystać zasoby.
W klastrze Kubernetes istnieje wiele komponentów odpowiedzialnych za rejestrowanie danych. Oto
lista komponentów, z których powinieneś pobierać wskaźniki:
dzienniki zdarzeń węzła,
dzienniki zdarzeń płaszczyzny kontrolnej Kubernetes:
API serwera,
menedżer kontrolera,
zarządca procesów.
dzienniki zdarzeń audytu Kubernetes,
dzienniki zdarzeń kontenera aplikacji.
W przypadku dzienników zdarzeń węzła konieczne jest zbieranie informacji o zdarzeniach, które
mają duże znaczenie dla usług węzła. Przykładowo powinieneś zbierać dzienniki zdarzeń z demona
Dockera działającego w węzłach roboczych. Bezbłędne działanie demona Dockera ma krytyczne
znaczenie dla uruchamiania kontenerów w węźle roboczym. Zbieranie tych danych dzienników
zdarzeń pomoże w diagnozowaniu wszelkich problemów, które możesz mieć z demonem Dockera, i
dostarczy informacji o potencjalnych problemach z demonem. Istnieje jeszcze wiele innych usług, o
których dane powinny być umieszczane w węźle.
Płaszczyzna kontrolna Kubernetes składa się z wielu komponentów, z których są pobierane dane
umieszczane w dziennikach zdarzeń, a następnie te dane pomagają w zrozumieniu istoty
napotkanych problemów. Płaszczyzna kontrolna Kubernetes ma duże znaczenie dla poprawnego
działania klastra. Prawdopodobnie będziesz chciał agregować dzienniki zdarzeń przechowywane w
plikach
/var/log/kube-APIserver.log,
/var/log/kube-scheduler.log
i
/var/log/kube-controllermanager.log hosta. Menedżer kontrolera jest odpowiedzialny za tworzenie obiektów definiowanych
przez użytkownika końcowego. Przykładowo jako użytkownik tworzysz usługę Kubernetes o typie
LoadBalancer, która pozostaje w trybie oczekiwania. Zdarzenia Kubernetes niekoniecznie
zapewnią wszystkie informacje niezbędne do ustalenia źródła problemu. Jeżeli zbierasz dzienniki
zdarzeń w systemie scentralizowanym, otrzymasz więcej informacji szczegółowych o napotkanym
problemie i zyskasz możliwość jego szybszego rozwiązania.
Możesz rozważać audyt dzienników zdarzeń Kubernetes jako narzędzie do monitorowania
bezpieczeństwa, ponieważ zyskujesz wgląd i informacje o tym, kto i kiedy zrobił coś w systemie.
Takie dzienniki zdarzeń mogą zawierać ogromną ilość danych, więc zdecydowanie trzeba je
dostosować do potrzeb używanego środowiska. W wielu przypadkach mogą powodować duży skok
aktywności systemu rejestrowania danych po jego inicjalizacji. Dlatego też należy koniecznie
zapoznać się z dokumentacją Kubernetes dotyczącą monitorowania audytu dzienników zdarzeń.
Dzienniki zdarzeń kontenera aplikacji dostarczają użytecznych informacji o rzeczywistych
zdarzeniach emitowanych przez aplikację. Masz wiele sposobów na przekazywanie tych dzienników
do centralnego repozytorium. Pierwszym i zalecanym jest wysyłanie wszystkich dzienników
zdarzeń aplikacji do standardowego wyjścia (STDOUT), ponieważ w ten sposób zapewniasz
uniwersalny sposób rejestrowania danych aplikacji, a demon monitorowania może pobierać
informacje bezpośrednio z demona Dockera. Drugim sposobem jest wykorzystanie wzorca tzw.
przyczepy (ang. sidecar) i przekazywanie dzienników zdarzeń kontenera do kontenera aplikacji w
podzie Kubernetes. Z tego wzorca można skorzystać, gdy dzienniki zdarzeń trafiają do systemu
plików.
Istnieje wiele opcji i konfiguracji w zakresie zarządzania dziennikami zdarzeń audytu. Te
dzienniki mogą zawierać naprawdę wiele informacji, a rejestrowanie wszystkich danych
może się okazać niezwykle kosztowne. Powinieneś rozważyć zapoznanie się z
dokumentacją
dotyczącą
dzienników
zdarzeń
(https://kubernetes.io/docs/tasks/debug-application-cluster/audit/),
co
dostosować je do własnych potrzeb.
audytu
pozwoli
Ci
Narzędzia przeznaczone do rejestrowania
danych
Podobnie jak w przypadku zbierania wskaźników, także do pobierania dzienników zdarzeń z
Kubernetes i aplikacji uruchomionych w klastrze jest przeznaczonych wiele różnych narzędzi. Być
może korzystasz już z tego rodzaju oprogramowania, ale musisz zwrócić uwagę na to, jak
implementuje mechanizm rejestrowania danych. Takie narzędzie powinno mieć możliwość
uruchomienia go jako Kubernetes DaemonSet, a także zapewniać rozwiązanie w zakresie działania
jako tzw. przyczepa dla aplikacji, która nie przekazuje dzienników zdarzeń do STDOUT.
Wykorzystanie dotychczasowego narzędzia może mieć pewne zalety, ponieważ prawdopodobnie
masz już sporą wiedzę na temat sposobu jego działania.
Oto lista najpopularniejszych narzędzi tego typu zapewniających integrację z Kubernetes:
Elastic Stack,
Datadog,
Sumo Logic,
Sysdig,
Usługi dostawcy chmury (GCP Stackdriver, Microsoft Azure Monitor for containers i Amazon
CloudWatch).
Jeśli szukasz narzędzia pozwalającego na scentralizowanie dzienników zdarzeń, wiedz, że oparte na
hostingu rozwiązania mogą zaoferować sporą wartość, ponieważ znacznie zmniejszają koszt
operacyjny. Samodzielny hosting rozwiązania w zakresie rejestrowania danych wydaje się
doskonałym pomysłem w dniu N, ale wraz ze wzrostem środowiska obsługa tego rozwiązania może
się okazać niezwykle czasochłonna.
Rejestrowanie danych za pomocą stosu
EFK
Na potrzeby niniejszego omówienia skorzystamy ze stosu EFK (Elasticsearch, Fluentd i Kibana)
skonfigurowanego do monitorowania klastra. Implementacja stosu EFK może być dobrym
sposobem na początek; w pewnym momencie prawdopodobnie zaczniesz zadawać sobie pytanie,
czy naprawdę warto ponosić wysiłek związany z zarządzaniem własną platformą rejestrowania
danych. Zwykle odpowiedź jest przecząca, ponieważ oparta na samodzielnym hostingu platforma
rejestrowania danych sprawdza się doskonale pierwszego dnia, ale zanim nadejdzie dzień 365.,
staje się nadmiernie skomplikowana. Takie rozwiązania robią się coraz bardziej złożone wraz ze
skalowaniem środowiska. Tutaj nie ma uniwersalnej odpowiedzi i należy samodzielnie ocenić, czy
dana sytuacja wymaga hostingu własnego rozwiązania. Istnieje wiele rozwiązań opartych na stosie
EFK, więc zawsze można je łatwo zmienić, jeśli nie zdecydujesz się na samodzielny hosting.
Konieczne jest wdrożenie wymienionych tutaj komponentów stosu monitorowania:
Elasticsearch Operator,
Fluentd (przekazywanie dzienników zdarzeń ze środowiska Kubernetes do Elasticsearch),
Kibana (narzędzie do wizualizacji, przeznaczone do wyszukiwania i wyświetlania dzienników
zdarzeń przechowywanych w Elasticsearch oraz pracy z nimi).
Nie zapomnij o wdrożeniu manifestu dla klastra Kubernetes, co wymaga wydania poniższych
poleceń:
$ kubectl create namespace logging
$ kubectl apply -f
https://raw.githubusercontent.com/dstrebel/kbp/master/elasticsearchoperator.yaml -n logging
Konieczne jest również wdrożenie
przekazanych dzienników zdarzeń.
Elasticsearch
Operator
w
celu
agregacji
wszystkich
$ kubectl apply -f
https://raw.githubusercontent.com/dstrebel/kbp/master/efk.yaml -n logging
W ten sposób mamy wdrożone komponenty Fluentd i Kibana, które pozwalają na przekazanie
dzienników zdarzeń do Elasticsearch i wizualizację dzienników zdarzeń za pomocą Kibana.
W klastrze powinieneś mieć wdrożone następujące pody:
$ kubectl get pods -n logging
efk-kibana-854786485-knhl5
1/1
Running
0
4m
elasticsearch-operator-5647dc6cb-tc2st
1/1
Running
0
5m
elasticsearch-operator-sysctl-ktvk9
1/1
Running
0
5m
elasticsearch-operator-sysctl-lf2zs
1/1
Running
0
5m
elasticsearch-operator-sysctl-r8qhb
1/1
Running
0
5m
es-client-efk-cluster-9f4cc859-sdrsl
1/1
Running
0
4m
es-data-efk-cluster-default-0
1/1
Running
0
4m
es-master-efk-cluster-default-0
1/1
Running
0
4m
fluent-bit-4kxdl
1/1
Running
0
4m
fluent-bit-tmqjb
1/1
Running
0
4m
fluent-bit-w6fs5
1/1
Running
0
4m
Gdy wszystkie pody są w trybie działania, należy zająć się połączeniem z Kibana za pomocą
mechanizmu przekazywania portów do komputera lokalnego.
$ export POD_NAME=$(kubectl get pods --namespace logging -l
"app=kibana,release=efk" -o jsonpath="{.items[0].metadata.name}")
$ kubectl port-forward $POD_NAME 5601:5601
Teraz w przeglądarce WWW przejdź pod adres http://localhost:5601 w celu uruchomienia panelu
głównego Kibana.
Aby pracować z dziennikami zdarzeń przekazanymi z klastra Kibana, najpierw trzeba utworzyć
indeks.
W trakcie pierwszego uruchomienia Kibana należy przejść do karty Management i utworzyć
wzorzec indeksu dla dzienników zdarzeń Kubernetes. System przeprowadzi Cię przez wymagane
kroki.
Po utworzeniu indeksu można zacząć wyszukiwanie informacji w dziennikach zdarzeń z użyciem
składni zapytań Lucene, jak pokazaliśmy w kolejnym fragmencie kodu.
log:(WARN|INFO|ERROR|FATAL)
Wynikiem działania tego przykładu są wszystkie wpisy dziennika zdarzeń zawierające wymienione
pola (WARN|INFO|ERROR|FATAL). Przykład możesz zobaczyć na rysunku 3.4.
Rysunek 3.4. Panel główny Kibana
Kibana pozwala na wykonywanie zapytań tymczasowych do dzienników zdarzeń oraz tworzenie
paneli, dzięki którym otrzymasz informacje o środowisku.
Śmiało, poświęć nieco czasu na poznanie różnych dzienników zdarzeń, które można wizualizować
za pomocą Kibana.
Ostrzeganie
Ostrzeganie można postrzegać jako miecz obosieczny. Powinieneś zachowywać równowagę między
tym, o czym chcesz być ostrzegany, i tym, co powinno być monitorowane. Generowanie zbyt wielu
ostrzeżeń oznacza ich nadmiar, więc ważne zdarzenia mogą umknąć w natłoku innych. Przykładem
może być tutaj generowanie ostrzeżenia za każdym razem, gdy pod ulegnie awarii. Być może
chciałbyś zapytać: „Dlaczego miałbym nie monitorować pod kątem awarii poda?”. Piękno
Kubernetes polega m.in. na tym, że zapewnia funkcjonalność, która automatycznie monitoruje stan
kontenera i w razie potrzeby automatycznie go uruchamia. Naprawdę powinieneś skoncentrować
się na ostrzeganiu i zdarzeniach mających wpływ na tzw. SLO (ang. service-level objectives). SLO
to możliwe do zmierzenia cechy charakterystyczne, takie jak dostępność, przepustowość,
częstotliwość i czas udzielania odpowiedzi, które zostały uzgodnione z użytkownikiem końcowym
usługi. Zdefiniowanie SLO powoduje powstanie pewnych oczekiwań ze strony użytkowników
końcowych i zapewnia przejrzystość w zakresie spodziewanego sposobu działania systemu. Bez
SLO użytkownicy mogą formułować własne opinie, które mogą się okazać nierealnymi
oczekiwaniami względem usługi. Mechanizm ostrzegania w systemie takim jak Kubernetes wymaga
zupełnie innego podejścia od tego, do którego jesteś przyzwyczajony. Ponadto trzeba
skoncentrować się na oczekiwaniach użytkownika końcowego względem usługi. Przykładowo, jeśli
SLO dla usługi frontendu to np. czas udzielenia odpowiedzi wynoszący 20 ms, wówczas chcesz być
powiadomiony o tym, że wystąpiło opóźnienie większe od średniego.
Trzeba zdecydować, które ostrzeżenia wymagają interwencji. W typowym środowisku
monitorowania prawdopodobnie przywykłeś do ostrzeżeń związanych z wysokim poziomem
wykorzystania procesora, pamięci lub brakiem reakcji procesu na jakiekolwiek działania. Te
zdarzenia wydają się na tyle istotne, by trafić do ostrzeżeń, choć nie wskazują problemu
wymagającego natychmiastowej interwencji ze strony inżyniera. Ostrzeżenie i wezwanie inżyniera
powinno dotyczyć problemu wymagającego natychmiastowej interwencji człowieka i związanego z
UX aplikacji. Jeżeli kiedykolwiek spotkałeś się ze scenariuszem typu „problem sam się rozwiązał”,
wówczas jest bardzo prawdopodobne, że ostrzeżenie nie wymagało wezwania inżyniera.
Jednym ze sposobów na obsługę ostrzeżeń niewymagających natychmiastowej reakcji ze strony
człowieka jest skoncentrowanie się na automatyzacji procedury naprawczej. Przykładowo po
zapełnieniu wolnego miejsca na dysku podjętym automatycznie działaniem może być usunięcie z
dysku starszych dzienników zdarzeń i tym samym zwolnienie pewnej ilości miejsca. Ponadto
wykorzystywane przez Kubernetes tzw. liveness probess pomagają w zmniejszeniu problemów
związanych z procesami, które uległy awarii.
Podczas definiowania ostrzeżeń trzeba zwrócić uwagę na tzw. poziom graniczny ostrzeżeń. Jeżeli
będzie on ustawiony zbyt nisko, otrzymasz wiele fałszywych alarmów. Ogólnie rzecz biorąc,
zalecana wielkość poziomu granicznego powinna wynosić przynajmniej 5 minut, aby pomóc
wyeliminować fałszywe alarmy. Zastosowanie standardowej wartości granicznej może pomóc w
zdefiniowaniu standardu i uniknięciu mikrozarządzania poszczególnymi poziomami granicznymi.
Przykładowo możesz stosować określony wzorzec 5, 10, 30, 60 minut itd.
W trakcie tworzenia powiadomień dla ostrzeżeń trzeba się upewnić, że w powiadomieniu są
dostarczane odpowiednie informacje. Przykładem może być tutaj łącze do dokumentu
zawierającego pewne dane przydatne podczas rozwiązywania problemów lub inne użyteczne
informacje. Należy również dołączać informacje o centrum danych, regionie, właścicielu aplikacji i
systemie, którego dotyczy powiadomienie. Zapewnienie takich informacji umożliwi inżynierom
szybkie sprawdzenie diagnozy dotyczącej powstałego problemu.
Konieczne jest także utworzenie kanałów powiadomień przeznaczonych do przekazywania
wygenerowanych ostrzeżeń. Gdy się zastanawiasz, kto powinien zostać poinformowany o
wygenerowaniu ostrzeżenia, to powinieneś się upewnić, że powiadomienia nie są przekazywane
osobom umieszczonym na liście dystrybucyjnej lub na liście adresów e-mail członków zespołu.
Jeżeli powiadomienie jest przekazywane do większej grupy odbiorców, najczęściej będzie
filtrowane, ponieważ użytkownicy będą je postrzegali jako zbędne. Powiadomienie powinno zostać
skierowane do użytkownika odpowiedzialnego za rozwiązanie danego problemu.
Ostrzeżenia nigdy nie będą doskonałe już od pierwszego dnia, a niektórzy uważają, że nigdy nie
osiągną perfekcji. Możesz nieustannie usprawniać ostrzeżenia, aby nie doprowadzić do zmęczenia
użytkowników liczbą otrzymywanych komunikatów.
Więcej informacji na temat podejścia w zakresie ostrzegania i zarządzania systemami znajdziesz
w
opracowaniu
zatytułowanym
My
Philosophy
on
Alerting
(https://docs.google.com/document/d/199PqyG3UsyXlwieHaqbGiWVa8eMWi8zzAn0YfcApr8Q/edit),
które powstało na podstawie obserwacji Roba poczynionych z perspektywy pracującego w firmie
Google inżyniera niezawodności witryny internetowej (ang. site reliability engineer).
Najlepsze praktyki dotyczące
monitorowania, rejestrowania danych i
ostrzegania
Oto kilka najlepszych praktyk, które powinieneś zaadaptować w zakresie monitorowania,
rejestrowania danych i ostrzegania.
Monitorowanie
Węzły i wszystkie komponenty Kubernetes monitoruj pod kątem poziomu wykorzystania,
nasycenia i błędów, aplikacje zaś monitoruj pod kątem tempa, poziomu błędów i czasu
trwania.
Tak zwane monitorowanie czarnego pudełka wykorzystuj do monitorowania pod kątem
symptomów, a nie do przewidywania stanu systemu.
Tak zwane monitorowanie białego pudełka wykorzystuj do sprawdzania za pomocą
instrumentacji systemu i jego komponentów wewnętrznych.
Implementuj oparte na czasie wskaźniki, by otrzymywać dokładne dane, które pozwolą
również na zebranie informacji o zachowaniu aplikacji.
Wykorzystuj systemy monitorowania, takie jak Prometheus, w celu dostarczenia niezbędnych
etykiet zapewniających dużą wymiarowość. To zapewni lepsze sygnalizowanie symptomów
problemu.
Korzystaj ze średnich wartości wskaźników w celu wizualizacji sum częściowych i wskaźników
na podstawie rzeczywistych danych. Wykorzystaj wskaźniki sum do wizualizacji rozkładu
określonego wskaźnika.
Rejestrowanie danych
Rejestrowanie danych powinieneś stosować w połączeniu ze wskaźnikami monitorowania, aby
w ten sposób otrzymać pełny obraz sposobu działania środowiska.
Zachowaj ostrożność podczas przechowywania dzienników zdarzeń dłużej niż 30 – 45 dni. W
razie potrzeby zdecyduj się na tańsze zasoby pozwalające na długotrwałą archiwizację
dzienników zdarzeń.
Ograniczaj przekazywanie dzienników zdarzeń we wzorcu przyczepy, ponieważ takie
rozwiązanie wymaga większej ilości zasobów. Podczas przekazywania dzienników zdarzeń
m.in. do standardowego wyjścia wybieraj zasób DaemonSet.
Ostrzeganie
Zachowaj ostrożność, aby nie generować nadmiernej liczby ostrzeżeń, ponieważ to może
prowadzić do niewłaściwego zachowania użytkowników i procesów.
Zawsze szukaj możliwości stopniowego usprawniania mechanizmu ostrzeżeń i zaakceptuj to,
że nie zawsze będzie doskonały.
Generuj ostrzeżenia dla symptomów, które mają wpływ na SLO i użytkowników, a nie dla tych,
które dotyczą przejściowych problemów niewymagających interwencji ze strony człowieka.
Podsumowanie
W rozdziale zostały przedstawione wzorce, techniki i narzędzia, które można zastosować do
monitorowania systemów oraz zbierania wskaźników i dzienników zdarzeń. Najważniejsze, co
powinieneś wynieść z jego lektury, to świadomość, że trzeba przemyśleć sposoby monitorowania i
zaimplementować je od samego początku. Zbyt wiele razy spotykaliśmy się z implementowaniem
ich już po fakcie — w takich przypadkach efekt okazywał się inny od oczekiwanego. Monitorowanie
wiąże się z uzyskaniem lepszych informacji o systemie, a także pozwala zapewnić mu większą
odporność na awarie, co z kolei przekłada się na lepsze wrażenia użytkownika końcowego aplikacji.
Monitorowanie aplikacji rozproszonych i systemów rozproszonych, takich jak Kubernetes, wymaga
wiele pracy. Musisz być na to przygotowany już od samego początku.
Rozdział 4. Konfiguracja, dane
poufne i RBAC
Złożona natura kontenerów pozwala nam jako operatorom na przekazanie danych
konfiguracyjnych do kontenera w trakcie jego działania. W ten sposób można oddzielić funkcję
aplikacji od środowiska, w którym została uruchomiona. Zgodnie z konwencją do
uruchomionego kontenera dane można przekazać za pomocą zmiennych środowiskowych bądź
też przez zamontowane woluminy zewnętrzne. Te dane pozwalają na zmianę konfiguracji
aplikacji już po jej uruchomieniu. Programista musi brać pod uwagę dynamiczną naturę tego
zachowania i umożliwić użycie zmiennych środowiskowych lub odczyt danych konfiguracyjnych
z określonej ścieżki dostępnej dla użytkownika aplikacji po jej uruchomieniu.
Podczas przenoszenia danych poufnych do natywnego obiektu API Kubernetes bardzo duże
znaczenie ma zrozumienie, jak Kubernetes zabezpiecza dostęp do API. Najczęściej stosowaną
metodą jest kontrola dostępu na podstawie roli użytkownika (ang. role-based access control,
RBAC). Pozwala ona na implementację dokładnej struktury uprawnień związanych z akcjami,
które mogą być podjęte względem API przez określonych użytkowników lub grupy. W rozdziale
przedstawimy wybrane najlepsze praktyki związane z RBAC, a także krótkie wprowadzenie do
tego tematu.
Konfiguracja za pomocą zasobu
ConfigMap i danych poufnych
Kubernetes pozwala na natywne przekazywanie informacji konfiguracyjnych do aplikacji za
pomocą zasobów ConfigMap lub danych poufnych. Podstawowa różnica między nimi wiąże się
ze sposobem, w jaki pod przechowuje otrzymywane informacje i w jaki dane są przechowywane
w magazynie danych etcd.
ConfigMap
Bardzo często zdarza się, że aplikacja pobiera informacje konfiguracyjne za pomocą pewnego
mechanizmu, takiego jak argumenty powłoki, zmienne środowiskowe lub też pliki dostępne dla
systemu. Kontener pozwala programiście na oddzielenie tych informacji konfiguracyjnych od
aplikacji, co z kolei oznacza jej prawdziwą przenośność. API zasobu ConfigMap pozwala na
wstrzyknięcie informacji konfiguracyjnych. Zasób ConfigMap można dostosować do potrzeb
aplikacji, a przekazywane informacje mogą mieć postać par klucz-wartość, złożonych danych,
np. JSON i XML, a także własnościowych danych konfiguracyjnych.
Zasób ConfigMap nie tylko zapewnia informacje konfiguracyjne podom, ale także informacje
przeznaczone do wykorzystania przez znacznie bardziej zaawansowane usługi systemowe, takie
jak kontrolery, CRD i operatory. Jak już wspomnieliśmy, API zasobu ConfigMap jest
przeznaczone do przechowywania danych, które tak naprawdę nie są danymi wrażliwymi. Jeżeli
aplikacja wymaga danych wrażliwych, wówczas znacznie bardziej odpowiednim rozwiązaniem
będzie użycie API zasobu Secrets.
Aby aplikacja używała danych zasobu ConfigMap, mogą one zostać wstrzyknięte w postaci
woluminu zamontowanego w podzie lub jako zmienne środowiskowe.
Dane poufne
Wiele atrybutów i powodów, dla których chciałbyś używać zasobu ConfigMap, ma również
zastosowanie dla danych poufnych. Podstawowa różnica kryje się w fundamentalnej naturze
danych poufnych. Powinny być one przechowywane i obsługiwane w sposób zapewniający ich
łatwe ukrycie i prawdopodobnie przechowywane w postaci zaszyfrowanej, o ile konfiguracja
środowiska na to pozwala. Dane poufne są przedstawione jako dane zakodowane w postaci
base64 i trzeba zrozumieć, że to nie oznacza ich zaszyfrowania. Po wstrzyknięciu danych do
poda będzie miał on dostęp do danych poufnych w dokładnie taki sam sposób jak do zwykłych
danych tekstowych.
Dane poufne to niewielka ilość danych, domyślnie ograniczona w Kubernetes do wielkości 1
MB. W przypadku danych zakodowanych jako base64 to oznacza rzeczywistą wielkość około
750 kB, co wynika z obciążenia związanego z kodowaniem base64. W Kubernetes są trzy
rodzaje danych poufnych:
generic
Zwykle są to pary klucz-wartość utworzone na podstawie pliku, katalogu lub literału ciągu
tekstowego za pomocą parametru --from-literal=.
$ kubectl create secret generic mysecret --from-literal=key1=$3cr3t1 --fromliteral=key2=@3cr3t2`
docker-registry
Te dane są używane przez kublet po przekazaniu w szablonie poda, o ile istnieje właściwość
imagePullsecret, w celu dostarczenia danych uwierzytelniających, które są niezbędne do
uwierzytelnienia w prywatnym rejestrze Dockera.
$ kubectl create secret docker-registry registryKey --docker-server
myreg.azurecr.io --docker-username myreg --docker-password $up3r
$3cr3tP@ssw0rd --docker-email ignore@dummy.com
tls
To powoduje utworzenie danych poufnych na poziomie TLS (ang. transport layer security)
na podstawie poprawnych par klucz-wartość. Jeżeli certyfikat jest w poprawnym formacie
PEM, para klucz-wartość zostanie zakodowana jako dane poufne i będzie mogła zostać
przekazana do poda i wykorzystana tam, gdzie wymagane jest użycie SSL/TLS.
$ kubectl create secret tls www-tls --key=./path_to_key/wwwtls.key -cert=./path_to_crt/wwwtls.crt
Dane poufne są montowane w systemie plików tmpfs jedynie w węzłach zawierających pody
wymagające danych poufnych. Gdy wymagający ich pod zostaje usunięty, dane poufne są
usuwane razem z nim. Dzięki temu unika się pozostawiania na dysku węzła jakichkolwiek
danych poufnych. Wprawdzie takie rozwiązanie może wydawać się bezpieczne, ale trzeba
wiedzieć, że domyślnie dane poufne w Kubernetes są przechowywane w magazynie danych etcd
w postaci zwykłego tekstu. Dlatego też tak ważne jest, aby administrator systemu lub dostawca
usług chmury podjął wysiłek mający na celu zapewnienie bezpieczeństwa środowisku etcd. To
oznacza użycie wzajemnego uwierzytelniania TLS (mTLS) między węzłami etcd i włączenie
szyfrowania danych przechowywanych w etcd. Najnowsze wersje Kubernetes używają
magazynu danych etcd3 i mają możliwość włączenia natywnego szyfrowania etcd. Jednak ten
ręczny proces musi być skonfigurowany w serwerze APIP przez podanie dostawcy i
odpowiedniego klucza pozwalającego na właściwe zaszyfrowanie danych poufnych
przechowywanych w etcd. W wersji Kubernetes 1.10 (w wydaniu 1.12 nowe rozwiązanie jest w
wersji beta) mamy dostawcę KMS, który zapewnia możliwość znacznie bezpieczniejszego
przetwarzania klucza za pomocą systemów zewnętrznych przechowujących odpowiednie
klucze.
Najlepsze praktyki dotyczące API zasobu
ConfigMap i danych poufnych
Najwięcej problemów powstaje na skutek użycia zasobu ConfigMap lub danych poufnych wraz z
błędnymi założeniami związanymi ze sposobem obsługi zmian po uaktualnieniu danych
przechowywanych w obiekcie. Dzięki zrozumieniu reguł i zastosowaniu kilku sztuczek
ułatwiających przestrzeganie tych reguł można uniknąć problemów.
Aby zapewnić obsługę zmian w aplikacji bez konieczności ponownego wdrażania nowych
wersji podów, zasób ConfigMap lub danych poufnych należy zamontować jako wolumin.
Następnie aplikację trzeba skonfigurować z wartownikiem pliku, który będzie wykrywał
zmiany w pliku danych i odpowiednio zmieniał konfigurację. Przedstawiony tutaj fragment
kodu pokazuje zasób Deployment montujący zasób ConfigMap i plik danych poufnych jako
wolumin.
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-http-config
namespace: myapp-prod
data:
config: |
http {
server {
location / {
root /data/html;
}
location /images/ {
root /data;
}
}
}
apiVersion: v1
kind: Secret
metadata:
name: myapp-api-key
type: Opaque
data:
myapikey: YWRtd5thSaW4=
apiVersion: apps/v1
kind: Deployment
metadata:
name: mywebapp
namespace: myapp-prod
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
name: nginx-config
- mountPath: /usr/var/nginx/html/keys
name: api-key
volumes:
- name: nginx-config
configMap:
name: nginx-http-config
items:
- key: config
path: nginx.conf
- name: api-key
secret:
name: myapp-api-key
secretname: myapikey
Podczas używania sekcji volumeMounts w specyfikacji poda trzeba uwzględnić kilka
kwestii. Po pierwsze, po utworzeniu zasobu ConfigMap lub danych poufnych nowy
element należy dodać jako wolumin w specyfikacji poda. Następnym krokiem jest
zamontowanie tego woluminu w systemie plików kontenera. Każda nazwa
właściwości w zasobie ConfigMap lub danych poufnych będzie nowym plikiem w
zamontowanym katalogu, a zawartość poszczególnych plików stanie się wartością
wymienioną w zasobie ConfigMap lub danych poufnych. Po drugie, trzeba unikać
montowania zasobu ConfigMap lub danych poufnych za pomocą właściwości
volumeMounts.subPath, ponieważ to uniemożliwi dynamiczne uaktualnianie danych
w woluminie po modyfikacji zasobu ConfigMap lub danych poufnych.
Zasób ConfigMap lub danych poufnych musi istnieć w przestrzeni nazw dla używających je
podów, zanim pod zostanie wdrożony. Można użyć opcji uniemożliwiającej uruchomienie
podów, jeśli zasób ConfigMap lub danych poufnych nie jest dostępny.
W celu zagwarantowania istnienia określonych danych konfiguracyjnych lub uniknięcia
wdrożenia, w którym nie zostały zdefiniowane określone wartości konfiguracyjne, należy
użyć kontrolera dostępu. Przykładem może być tutaj sytuacja, w której wszystkie zadania
produkcyjne Javy wymagają zdefiniowania w środowisku produkcyjnym określonych
właściwości JVM. Istnieje wersja alfa API o nazwie PodPresets, pozwalającego na
stosowanie zasobów ConfigMaps i danych poufnych we wszystkich podach na podstawie
adnotacji i bez konieczności samodzielnego tworzenia kontrolera dostępu.
Jeżeli używasz menedżera pakietów Helm do wydawania aplikacji w swoim środowisku,
możesz skorzystać z zaczepu cyklu życiowego w celu zagwarantowania, że szablon zasobu
ConfigMap lub danych poufnych zostanie wdrożony jeszcze przed zastosowaniem zasobu
Deployment.
Pewne aplikacje wymagają konfiguracji zastosowanej w postaci pojedynczego pliku,
takiego jak plik w formacie JSON lub YAML. Zasób ConfigMap lub danych poufnych
pozwala na wykorzystanie całego bloku niezmodyfikowanych danych. To wymaga użycia
znaku |, jak pokazaliśmy w kolejnym fragmencie kodu.
apiVersion: v1
kind: ConfigMap
metadata:
name: config-file
data:
config: |
{
"iotDevice": {
"name": "remoteValve",
"username": "CC:22:3D:E3:CE:30",
"port": 51826,
"pin": "031-45-154"
}
}
Jeżeli aplikacja używa zmiennych środowiskowych do ustalenia konfiguracji, wówczas
wstrzyknięte dane zasobu ConfigMap można wykorzystać do utworzenia zmiennej
środowiskowej mapowanej na poda. Są dwa podstawowe sposoby zastosowania takiego
rozwiązania. Pierwszy to zamontowanie każdej pary klucz-wartość w zasobie ConfigMap
jako serii zmiennych środowiskowych w podzie za pomocą envFrom, a następnie
wykorzystanie właściwości configMapRef lub secretRef. Drugi to przypisanie
poszczególnych kluczy z ich wartościami za pomocą właściwości configMapKeyRef lub
secretKeyRef.
Jeżeli używasz właściwości configMapKeyRef lub secretKeyRef, powinieneś wiedzieć, że
w przypadku braku danego klucza pod nie zostanie uruchomiony.
Jeżeli wszystkie pary klucz-wartość są za pomocą sekcji envFrom wczytywane do poda z
zasobu ConfigMap lub danych poufnych, wówczas wszystkie klucze z wartościami
środowiskowymi, które są uznane za niepoprawne, zostaną pominięte. Mimo to pod
będzie mógł być uruchomiony. Powód wystąpienia zdarzenia dla poda zostanie określony
jako InvalidVariableNames, ponadto zdarzenie będzie miało odpowiedni komunikat
dotyczący pominiętego klucza. W kolejnym fragmencie kodu przedstawiliśmy przykładowy
zasób Deployment z odwołaniem do zasobu ConfigMap i danych poufnych jako zmiennej
środowiskowej.
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
data:
mysqldb: myappdb1
user: mysqluser1
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
data:
rootpassword: YWRtJasdhaW4=
userpassword: MWYyZDigKJGUyfgKJBmU2N2Rm
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-db-deploy
spec:
selector:
matchLabels:
app: myapp-db
template:
metadata:
labels:
app: myapp-db
spec:
containers:
- name: myapp-db-instance
image: mysql
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: rootpassword
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: userpassword
- name: MYSQL_USER
valueFrom:
configMapKeyRef:
name: mysql-config
key: user
- name: MYSQL_DB
valueFrom:
configMapKeyRef:
name: mysql-config
key: mysqldb
Jeżeli trzeba przekazać argumenty powłoki do kontenera, wówczas dane zmiennej
środowiskowej można utworzyć za pomocą składni interpolacji: $(ENV_KEY).
[...]
spec:
containers:
- name: load-gen
image: busybox
command: ["/bin/sh"]
args: ["-c", "while true; do curl $(WEB_UI_URL); sleep 10;done"]
ports:
- containerPort: 8080
env:
- name: WEB_UI_URL
valueFrom:
configMapKeyRef:
name: load-gen-config
key: url
Podczas pobierania danych zasobu ConfigMap lub danych poufnych jako zmiennych
środowiskowych trzeba pamiętać, że uaktualnienie danych znajdujących się w zasobie
ConfigMap lub danych poufnych nie spowoduje uaktualnienia poda i będzie wymagało
jego ponownego uruchomienia. W tym celu należy usunąć poda i pozwolić kontrolerowi
ReplicaSet na utworzenie nowego poda lub też wywołać uaktualnienie zasobu
Deployment, który będzie stosował poprawną strategię aktualizacji, zgodnie z deklaracją
zamieszczoną w specyfikacji Deployment.
Znacznie łatwiej jest przyjąć założenie, że wszystkie zmiany zasobu ConfigMap lub danych
poufnych wymagają uaktualnienia całego wdrożenia. To gwarantuje, że nawet jeśli
używasz zmiennych środowiskowych lub woluminów, kod wykorzysta nowe dane
konfiguracyjne. Operację można jeszcze bardziej sobie ułatwić dzięki użyciu rozwiązania
opartego na technikach ciągłej integracji i ciągłym wdrażaniu do uaktualniania
właściwości name zasobu ConfigMap lub danych poufnych oraz uaktualnienia odwołania
we wdrożeniu. W efekcie uaktualnienie zostanie przeprowadzone za pomocą
standardowych w Kubernetes strategii służących do tego celu. Takie rozwiązanie zostało
zaprezentowane w następnym fragmencie kodu. Jeżeli do wydania aplikacji w Kubernetes
używasz menedżera pakietów Helm, wówczas możesz wykorzystać zalety adnotacji w
szablonie zasobu Deployment i sprawdzić sumę kontrolną zasobu ConfigMap lub danych
poufnych. To spowoduje wywołanie uaktualnienia zasobu Deployment za pomocą
polecenia helm upgrade ze zmodyfikowanymi danymi, które znajdują się w zasobie
ConfigMap lub zasobie danych poufnych.
apiVersion: apps/v1
kind: Deployment
[...]
spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/config
map.yaml") . | sha256sum }}
[...]
Najlepsze praktyki dotyczące danych poufnych
Ze względu na wrażliwość danych API zasobu Secrets warto omówić najlepsze praktyki
związane z zapewnieniem im bezpieczeństwa.
Pierwotna specyfikacja API zasobu Secrets wskazuje architekturę pozwalającą
skonfigurować przechowywane dane poufne na podstawie wymagań. Rozwiązania takie
jak HashiCorp Vault, Aqua Security, Twistlock, AWS Secrets Manager, Google Cloud KMS
i Azure Key Vault pozwalają używać systemów zewnętrznych do przechowywania danych
poufnych z zastosowaniem znacznie wyższego poziomu szyfrowania i przeprowadzania
audytów niż ten, który Kubernetes zapewnia natywnie.
Należy przypisać właściwości imagePullSecrets wartość serviceaccount, której pod
użyje do automatycznego zamontowania danych poufnych bez konieczności wcześniejszej
deklaracji tego w pod.spec. Istnieje możliwość uzupełnienia domyślnego konta usługi
przestrzeni nazw aplikacji i bezpośredniego dodania właściwości imagePullSecrets. W
ten sposób zostaną automatycznie dodane wszystkie pody w przestrzeni nazw.
# Należy zacząć od utworzenia docker-registry dla danych poufnych.
$ kubectl create secret docker-registry registryKey --docker-server
$ myreg.azurecr.io --docker-username myreg --docker-password
$up3r$3cr3tP@ssw0rd --docker-email ignore@dummy.com
# Zmodyfikuj domyślną wartość serviceaccount dla przestrzeni nazw, która
ma zostać skonfigurowana.
Wykorzystaj możliwości w zakresie ciągłej integracji i ciągłego wdrażania w celu pobrania
danych poufnych z magazynu danych, który może być zaszyfrowany za pomocą HSM (ang.
hardware security module). Te dane są pobierane na etapie wydawania aplikacji. W ten
sposób można zastosować podział zadań. Zespoły odpowiedzialne za bezpieczeństwo
mogą tworzyć i szyfrować dane poufne, programiści zaś potrzebują jedynie odwołań do
nazw udostępnianych danych poufnych. To jest również preferowany proces DevOps,
który ma zapewnić znacznie bardziej dynamiczny proces dostarczania aplikacji.
RBAC
Podczas pracy w ogromnych, rozproszonych środowiskach bardzo często trzeba stosować
pewne mechanizmy bezpieczeństwa, aby uniemożliwić nieupoważniony dostęp do systemów o
znaczeniu krytycznym. Istnieje wiele strategii związanych z ograniczaniem dostępu do zasobów
w systemach komputerowych, przy czym w większości z nich stosowane są te same fazy.
Analogia lotu do innego kraju może pomóc w wyjaśnieniu procesów zachodzących w systemach
takich jak Kubernetes. Do omówienia procesu wykorzystamy doświadczenia osoby podróżującej
między krajami, która używa przy tym paszportu i wizy oraz ma kontakt z celnikami i
pogranicznikami.
1. Paszport (przedmiot uwierzytelnienia). Do odbycia podróży międzynarodowej zwykle jest
potrzebny paszport wydany przez agencję rządową. Ten paszport potwierdza tożsamość
osoby podróżującej. W omawianej analogii paszport można uznać za odpowiednik konta
użytkownika w Kubernetes. Podczas uwierzytelniania użytkowników Kubernetes opiera
się na zewnętrznym podmiocie, przy czym konto usługi jest typem konta zarządzanego
bezpośrednio przez Kubernetes.
2. Wiza lub polityka podróżna (autoryzacja). Kraje podpisują oficjalne umowy, na mocy
których osoby podróżujące mogą poruszać się między krajami, o ile mają paszporty i
krótkie, oficjalne zgody, nazywane wizami. Wymieniona wiza określa uprawnienia
podróżnika w danym kraju i czas, który może w nim spędzić, w zależności od rodzaju
otrzymanej wizy. W omawianej analogii wizę można uznać za odpowiednik autoryzacji w
Kubernetes. W Kubernetes są stosowane różne metody autoryzacji, przy czym najczęściej
używaną jest RBAC, czyli kontrola dostępu na podstawie roli użytkownika. Dzięki niej
można na bardzo szczegółowym poziomie zapewniać dostęp do różnych obszarów API.
3. Straż graniczna lub celna (kontrola dostępu). Gdy osoba podróżująca wjeżdża do obcego
kraju, zwykle napotyka urzędnika, który sprawdza dokumenty (paszport i wizę), a często
także bagaż i wwożone przedmioty, aby się upewnić, czy podróżujący pozostaje w zgodzie
z obowiązującymi normami prawnymi danego kraju. To odpowiednik kontroli dostępu w
Kubernetes. Taka kontrola może zapewnić możliwość wykonania żądania, odmówić takiej
możliwości lub zmienić żądanie do API na podstawie zdefiniowanych reguł i polityki.
Kubernetes ma wiele wbudowanych mechanizmów kontroli dostępu, np. PodSecurity,
ResourceQuota i ServiceAccount. Pozwala również stosować kontrolery dynamiczne przez
wykorzystanie weryfikacji lub mutacji kontrolerów dostępu.
W tym podrozdziale skoncentrujemy się na trzech najmniej zrozumiałych i najczęściej
unikanych obszarach RBAC. Zanim przejdziemy do zaprezentowania najlepszych praktyk,
najpierw powinieneś przynajmniej pokrótce poznać mechanizm kontroli dostępu na podstawie
roli użytkownika.
Krótkie wprowadzenie do mechanizmu RBAC
Proces RBAC w Kubernetes ma trzy podstawowe komponenty, które należy zdefiniować:
podmiot, regułę i przypisanie roli.
Podmiot
Pierwszym komponentem jest podmiot, czyli element faktycznie sprawdzany pod kątem
uprawnień dostępu. Tym podmiotem zwykle jest użytkownik, konto usługi lub grupa. Jak
wcześniej wspomnieliśmy, użytkownicy, a także grupy są obsługiwani na zewnątrz Kubernetes,
przez odpowiedni moduł autoryzacji. Istnieje możliwość kategoryzowania ich jako
podstawowych modułów uwierzytelniania, certyfikatów klienta x.509 bądź też tokenów.
Najczęściej spotykaną implementacją jest użycie certyfikatów klienta x.509 lub też pewnego
rodzaju tokena korzystającego z mechanizmu typu system OpenID Connect, takiego jak Azure
AD (Azure Active Directory), Salesforce lub Google.
Konta usług w Kubernetes są inne niż konta użytkowników pod tym względem, że
mają dołączoną przestrzeń nazw i wewnętrznie są przechowywane w Kubernetes.
Zadaniem konta usługi jest przedstawienie procesu, a nie użytkownika. Konta usług
są zarządzane przez natywne kontrolery Kubernetes.
Reguła
Ujmując rzecz najprościej, reguły to lista akcji możliwych do przeprowadzenia na określonym
obiekcie (zasobie) lub grupie obiektów w API. Mamy tutaj typowe operacje CRUD (ang. create,
read, update, delete), choć z pewnymi możliwościami dodatkowymi w Kubernetes, np. watch,
list i exec. Te obiekty pozostają w zgodzie z różnymi komponentami API i są grupowane
kategoriami. Przykładowo obiekty podów są częścią podstawowego API i można się do nich
odwoływać za pomocą polecenia apiGroup: "", podczas gdy wdrożenia znajdują się w grupie
API aplikacji. W tym kryją się potężne możliwości procesu RBAC i to jest prawdopodobnie
kwestia sprawiająca najwięcej problemów użytkownikom tworzącym odpowiednie kontrolki
RBAC.
Rola
Rola pozwala na określenie zasięgu zdefiniowanej reguły. W Kubernetes mamy dwa typy ról:
role i clusterRole. Różnica między nimi polega na tym, że role jest przeznaczona dla
przestrzeni nazw, a clusterRole to rola o zasięgu całego klastra i wszystkich przestrzeni nazw.
Spójrz na przykład definicji roli z określonym zasięgiem przestrzeni nazw.
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-viewer
rules:
- apiGroups: [""] # "" Wskazuje na podstawowe API grupy.
resources: ["pods"]
verbs: ["get", "watch", "list"]
Zasób RoleBinding
Zasób RoleBinding pozwala na mapowanie podmiotu, takiego jak użytkownik lub grupa, na
określoną rolę. Podczas przypisywania roli można stosować jeden z dwóch trybów. Pierwszy,
roleBinding, jest związany z przestrzenią nazw. Drugi, clusterRoleBinding, jest związany z
całym klastrem. Spójrz na przykład użycia zasobu RoleBinding o zasięgu przestrzeni nazw.
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: noc-helpdesk-view
namespace: default
subjects:
- kind: User
name: helpdeskuser@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role # To musi być wartość Role lub ClusterRole.
name: pod-viewer # Ta wartość musi odpowiadać nazwie dołączanego zasobu
Role lub ClusterRole.
apiGroup: rbac.authorization.k8s.io
Najlepsze praktyki dotyczące mechanizmu RBAC
Kontrola dostępu na podstawie roli użytkownika to podstawowy komponent pozwalający na
uruchomienie bezpiecznego, niezawodnego i stabilnego środowiska Kubernetes. Koncepcje
stojące za mechanizmem RBAC mogą być skomplikowane. Jednak zastosowanie kilku
najlepszych praktyk może znacznie ułatwić pracę.
Aplikacje przeznaczone do uruchamiania w Kubernetes rzadko wymagają roli RBAC i
przypisywania roli. Tylko w sytuacji, w której kod aplikacji faktycznie współdziała
bezpośrednio z API Kubernetes, aplikacja będzie wymagała skonfigurowania RBAC.
Jeżeli aplikacja wymaga bezpośredniego dostępu do API Kubernetes, np. w celu zmiany
konfiguracji w zależności od punktów końcowych dodanych do usługi lub jeśli konieczne
jest wyświetlenie wszystkich podów w danej przestrzeni nazw, wówczas najlepszym
rozwiązaniem będzie utworzenie nowego konta usługi, a następnie podanie go w
specyfikacji poda. Następnym krokiem będzie utworzenie roli z najmniejszą liczbą
uprawnień pozwalających na wykonanie danego zadania.
Używaj usługi OpenID Connect, pozwalającej na zarządzanie tożsamościami, i w razie
potrzeby uwierzytelniania dwuetapowego. Dzięki temu zapewnisz znacznie wyższy poziom
uwierzytelniania tożsamości. Mapuj grupy użytkowników na role zawierające najmniejszą
liczbę uprawnień pozwalających na wykonanie danego zadania.
Wraz z wymienioną wcześniej praktyką powinieneś stosować mechanizm JIT (ang. just in
time), by umożliwić dostęp do systemu inżynierom SRE, operatorom i wszystkim innym
osobom, które mogą potrzebować zwiększonych przez krótki czas uprawnień,
niezbędnych do wykonania określonego zadania. Ewentualnie ci użytkownicy powinni
mieć inne tożsamości, dokładnie sprawdzone pod kątem logowania do systemu, a ich
konta powinny mieć większe uprawnienia przypisane kontu użytkownika lub grupie
dołączonej do roli.
Określone konta usług powinny być używane z narzędziami do ciągłej integracji i ciągłego
wdrażania stosowanymi do klastrów Kubernetes. To zapewni możliwość przeprowadzenia
audytu klastra oraz dokładnego ustalenia, kto mógł wdrożyć lub usunąć obiekty w
klastrze.
Jeżeli do wdrażania aplikacji używasz menedżera pakietów Helm, wówczas domyślnym
kontem usługi jest Tiller, wdrożone do przestrzeni nazw kube-system. Znacznie lepszym
rozwiązaniem będzie wdrożenie usługi Tiller w poszczególnych przestrzeniach nazw z
kontem usługi Tiller o zasięgu danej przestrzeni nazw. W narzędziu do ciągłej integracji i
ciągłego wdrażania wywołującym polecenie menedżera pakietów Helm dotyczące
instalacji lub uaktualnienia (jako wstępny krok) należy zainicjalizować klienta Helm z
kontem usługi i określoną przestrzenią nazw dla wdrożenia. Nazwa konta usługi może być
taka sama dla wszystkich przestrzeni nazw, natomiast nazwy przestrzeni nazw muszą być
odmienne. Trzeba w tym miejscu dodać, że w czasie, gdy ta książka powstawała,
menedżer pakietów Helm v3 był dopiero w wersji alfa. Jedną z podstawowych cech nowej
wersji menedżera Helm jest to, że usługa Tiller nie jest już wymagana do działania w
klastrze. Spójrz na przykład pokazujący inicjalizację menedżera pakietów Helm z kontem
usługi i przestrzenią nazw.
$ kubectl create namespace myapp-prod
$ kubectl create serviceaccount tiller --namespace myapp-prod
cat <<EOF | kubectl apply -f kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: tiller
namespace: myapp-prod
rules:
- apiGroups: ["", "batch", "extensions", "apps"]
resources: ["*"]
verbs: ["*"]
EOF
cat <<EOF | kubectl apply -f kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: tiller-binding
namespace: myapp-prod
subjects:
- kind: ServiceAccount
name: tiller
namespace: myapp-prod
roleRef:
kind: Role
name: tiller
apiGroup: rbac.authorization.k8s.io
EOF
$ helm init --service-account=tiller --tiller-namespace=myapp-prod
$ helm install ./myChart --name myApp --namespace myapp-prod --set
global.namespace=myapp-prod
Część publicznych plików menedżera Helm w formacie chart nie ma elementów
wartości dla przestrzeni nazw do wdrożenia komponentów aplikacji. To może
wymagać bezpośredniego dostosowania do własnych potrzeb pliku Helm w formacie
chart lub użycia uprzywilejowanego konta usługi Tiller, które będzie mogło wdrożyć
dowolną przestrzeń nazw i będzie miało uprawnienia pozwalające na tworzenie
przestrzeni nazw.
Należy ograniczyć wszelkie aplikacje wymagające watch lub list w API zasobu Secrets.
Te uprawnienia w zasadzie pozwoliłyby aplikacji lub użytkownikowi wdrażającemu pod
uzyskać dostęp do wszystkich danych poufnych w tej przestrzeni nazw. Jeżeli aplikacja
wymaga dostępu do API zasobu Secrets, aby pobrać określone dane poufne, ograniczaj
używanie funkcjonalności get do tych danych poufnych, które są wymagane przez
aplikację.
Podsumowanie
Reguły związane z tworzeniem aplikacji dla natywnej chmury to temat na zupełnie oddzielną
książkę. Panuje powszechne przekonanie, że ścisłe oddzielenie konfiguracji od kodu ma
kluczowe znaczenie dla sukcesu. Dzięki natywnym obiektom dla danych innych niż wrażliwe
(API zasobu ConfigMap) i danych wrażliwych (API zasobu Secrets) Kubernetes pozwala
zarządzać procesem w sposób deklaratywny. Skoro coraz większa ilość danych krytycznych jest
przedstawiana i przechowywana natywnie w API Kubernetes, niezwykle ważne jest zapewnienie
bezpiecznego dostępu do tego API za pomocą odpowiednich mechanizmów bezpieczeństwa,
takich jak RBAC i zintegrowane systemy uwierzytelniania.
W pozostałej części książki przekonasz się, że te podstawowe reguły przeniknęły do każdego
aspektu poprawnego wdrażania usług na platformie Kubernetes, co umożliwiło tworzenie
stabilnych, niezawodnych, bezpiecznych i solidnych systemów.
Rozdział 5. Ciągła integracja,
testowanie i ciągłe wdrażanie
W tym rozdziale zostaną przedstawione kluczowe koncepcje dotyczące integracji z
mechanizmem ciągłej integracji (ang. continuous integration, CI) i ciągłego wdrażania (ang.
continuous deployment, CD) podczas dostarczania aplikacji do Kubernetes. Przygotowanie
doskonałego sposobu pracy pozwoli na sprawne dostarczanie aplikacji do środowiska
produkcyjnego. Dlatego też w rozdziale będą zaprezentowane metody, narzędzia i procesy
umożliwiające stosowanie technik CI i CD we własnym środowisku pracy. Celem technik CI i CD
jest opracowanie w pełni zautomatyzowanego procesu, od programisty umieszczającego kod w
repozytorium po przekazanie nowej aplikacji do środowiska produkcyjnego. Najlepiej będzie
unikać ręcznego wdrażania w Kubernetes uaktualnionych aplikacji, ponieważ ten proces jest
podatny na błędy. Ponadto ręczne zarządzanie uaktualnieniami aplikacji w Kubernetes prowadzi
do zmian w konfiguracji i niepewnych uaktualnień oraz ogólnie do utraty zwinności podczas
procesu dostarczania aplikacji.
W rozdziale zostaną omówione następujące zagadnienia:
system kontroli wersji,
ciągła integracja,
testowanie,
oznaczanie tagami obrazów kontenera,
ciągłe wdrażanie,
strategie wdrażania,
testowanie wdrożeń,
testowanie w chaosie.
Zaprezentujemy również przykładowe techniki CI i CD, na które składają się wymienione tutaj
zadania:
przekazywanie do repozytorium Git zmian wprowadzonych w kodzie źródłowym,
kompilowanie kodu aplikacji,
testowanie kodu,
utworzenie obrazu kontenera po zakończeniu testów powodzeniem,
przekazywanie obrazu kontenera do rejestru kontenerów,
wdrażanie aplikacji w Kubernetes,
przeprowadzanie testów wdrożonej aplikacji,
nieustanne uaktualnianie wdrożenia.
System kontroli wersji
Każde rozwiązanie oparte na technikach CI i CD rozpoczyna się od systemu kontroli wersji,
którego zadaniem jest obsługa historii zmian kodu aplikacji i jej konfiguracji. Git stał się
standardem przemysłowym w dziedzinie platform zarządzania kodem źródłowym, a każde
repozytorium Git ma tzw. gałąź master (ang. master branch), która jest gałęzią główną
repozytorium i zawiera kod przeznaczony do wdrożenia w środowisku produkcyjnym. W
repozytorium zwykle znajdują się także inne gałęzie przeznaczone do opracowywania kolejnych
funkcjonalności aplikacji, a wprowadzone w nich zmiany ostatecznie i tak trafiają do gałęzi
master. Istnieje wiele strategii związanych z tworzeniem gałęzi, a konkretna konfiguracja
będzie zależała od struktury organizacji i separacji zadań. Według nas kod aplikacji i kod
konfiguracji, czyli np. manifest Kubernetes lub plik menedżera Helm w formacie chart,
pomagają w promowaniu dobrych praktyk DevOps w zakresie komunikacji i współpracy. Gdy
programiści aplikacji i inżynierowie operacji współpracują nad kodem znajdującym się w
jednym repozytorium, to daje przekonanie, że zespół będzie w stanie dostarczyć aplikację do
środowiska produkcyjnego.
Ciągła integracja
Ciągła integracja to proces nieustannego integrowania zmian w kodzie z repozytorium systemu
kontroli wersji. Zamiast rzadko przekazywać większe zmiany, znacznie częściej przekazuje się
mniejsze. Każde przekazanie zmian do repozytorium powoduje rozpoczęcie kompilacji kodu
źródłowego. Dzięki temu można o wiele szybciej otrzymać informacje o tym, co zostało zepsute
w aplikacji, gdy wystąpi w niej problem. W tym miejscu prawdopodobnie zadajesz sobie pytanie
w rodzaju: „Dlaczego miałbym poznawać szczegóły związane z kompilacją aplikacji, skoro to
jest zadanie programisty?”. Tradycyjnie tak było, choć w ostatnim czasie można zaobserwować
w firmach przesunięcie w stronę podejścia kultury DevOps, w którym zespół operacji jest bliżej
kodu aplikacji i procesów związanych z jej tworzeniem.
Istnieje wiele rozwiązań w dziedzinie ciągłej integracji. Jednym z najpopularniejszych narzędzi
tego typu jest Jenkins.
Testowanie
Celem testów jest szybkie dostarczenie informacji o zmianach w kodzie, które doprowadziły do
uszkodzenia aplikacji. Używany język programowania będzie miał wpływ na framework testów,
który wykorzystasz do ich przygotowania. Przykładowo aplikacje w języku Go używają go test
do uruchomienia zestawu testów jednostkowych dla bazy kodu. Opracowanie rozbudowanego
zestawu testów pomaga unikać sytuacji, gdy do środowiska produkcyjnego zostaje przekazany
niepoprawnie działający kod. Chcesz mieć pewność, że jeśli test zostanie niezaliczony w
środowisku programistycznym, natychmiast po jego zakończeniu kompilacja zakończy się
niepowodzeniem. Nie chcesz utworzyć obrazu kontenera i przekazać go do repozytorium, gdy
jakikolwiek test bazy kodu kończy się niepowodzeniem.
Także w tym przypadku być może zadajesz sobie pytanie w rodzaju: „Czy tworzenie testów nie
powinno być zadaniem programisty aplikacji?”. Gdy zaczniesz stosować zautomatyzowaną
infrastrukturę dostarczania aplikacji do środowiska produkcyjnego, musisz pomyśleć o
przeprowadzaniu zautomatyzowanych testów całej bazy kodu. Przykładowo z rozdziału 2.
dowiedziałeś się nieco o użyciu menedżera pakietów Helm w celu przygotowania aplikacji do
umieszczenia w Kubernetes. Ten menedżer zawiera narzędzie o nazwie helm lint, którego
działanie polega na wykonaniu serii testów względem pliku w formacie chart i
przeanalizowaniu kodu pod kątem potencjalnych problemów. Istnieje wiele różnych testów do
wykonania podczas przygotowywania aplikacji. Część z nich powinna być wykonywana przez
programistów, inne zaś to wysiłek podejmowany wspólnie przez wszystkich. Testowanie bazy
kodu i dostarczanie na jej podstawie gotowej aplikacji do środowiska produkcyjnego jest
wysiłkiem całego zespołu i ta operacja powinna być zaimplementowana od początku do końca.
Kompilacja kontenera
Podczas tworzenia obrazów należy optymalizować ich wielkość. Mniejszy obraz oznacza
skrócenie czasu potrzebnego na pobranie i wdrożenie obrazu, a ponadto zwiększa poziom jego
bezpieczeństwa. Istnieje wiele sposobów na optymalizację wielkości obrazu, z których część
wiąże się z pewnymi kompromisami. Zapoznaj się ze strategiami, które pomagają w tworzeniu
możliwie małych obrazów zawierających budowane aplikacje.
Kompilacja wieloetapowa
To pozwala na usunięcie zależności niepotrzebnych do działania aplikacji. Przykładowo w
przypadku języka programowania Go nie potrzebujemy wszystkich narzędzi kompilacji
używanych do utworzenia statycznych plików binarnych. Dlatego też kompilacja
wieloetapowa pozwala na użycie jednego pliku Dockerfile do przeprowadzenia kompilacji, a
ostateczny obraz będzie zawierał tylko statyczne pliki binarne wymagane do uruchomienia
aplikacji.
Obraz bazowy nieoparty na żadnej dystrybucji
To pozwala na usunięcie z obrazu wszystkich niepotrzebnych plików binarnych i powłok.
Skutkiem jest znaczne zmniejszenie wielkości obrazu i zwiększony poziom bezpieczeństwa.
Natomiast wadą obrazu nieopartego na żadnej dystrybucji jest to, że jeśli nie masz powłoki,
wówczas nie będziesz mógł dołączyć debugera do obrazu. Być może uważasz, że to świetne
rozwiązanie, ale w rzeczywistości znacznie utrudni debugowanie aplikacji. Takie obrazy nie
zawierają żadnego menedżera pakietów, powłoki ani innych typowych pakietów systemu
operacyjnego, więc możesz nie uzyskać dostępu do narzędzi debugowania znanych Ci z
typowego systemu operacyjnego.
Zoptymalizowane obrazy bazowe
W przypadku tych obrazów skoncentrowano się na usunięciu wszelkich elementów warstwy
systemu operacyjnego i dostarczeniu minimalnej wersji obrazu. Przykładowo Alpine oferuje
obraz bazowy o wielkości około 10 MB, a także pozwala na dołączenie debugera lokalnego
podczas opracowywania aplikacji w lokalnym środowisku programistycznym. Inne
dystrybucje również oferują zoptymalizowane obrazy bazowe; przykładem może być tutaj
obraz Slim dystrybucji Debian. To może być doskonałe rozwiązanie, ponieważ
zoptymalizowane
obrazy
zapewniają
możliwości
oczekiwane
w
środowisku
programistycznym, a zarazem są zoptymalizowane pod względem wielkości obrazu i
podatności na ataki.
Optymalizacja obrazów ma wyjątkowo duże znaczenie, choć często jest niedoceniana przez
użytkowników. Mogą być ku temu pewne powody, np. wynikające z przyjętych w firmie
standardów dotyczących dozwolonych do użycia systemów operacyjnych, ale warto je odłożyć
na bok, aby maksymalizować wartość kontenerów.
Zauważyliśmy, że firmy, które zaczęły używać Kubernetes, zwykle są zadowolone ze
stosowanego systemu operacyjnego, a mimo to decydują się na wybór znacznie bardziej
zoptymalizowanego obrazu, takiego jak Debian Slim. Gdy zdobędziesz większe doświadczenie w
tworzeniu aplikacji dla środowiska kontenerów, poczujesz się pewniej podczas pracy z obrazami
nieopartymi na żadnych dystrybucjach.
Oznaczanie tagiem obrazu kontenera
Kolejnym krokiem w procesie ciągłej integracji jest utworzenie obrazu Dockera, aby mógł
zostać wdrożony do wybranego środowiska. Bardzo duże znaczenie ma stosowanie strategii
nadawania tagów obrazom, co pozwoli na łatwe identyfikowanie wersjonowanych obrazów
wdrożonych w środowiskach. Jedną z najważniejszych kwestii jest zaprzestanie używania słowa
latest jako tagu obrazu. Używanie tagu obrazu nieprzedstawiającego wersji oznacza brak
możliwości ustalenia zmian, po których wprowadzeniu w kodzie nastąpiło wygenerowanie
takiego obrazu. Każdy obraz tworzony w procesie CI powinien mieć unikatowy tag.
Istnieje wiele użytecznych strategii podczas oznaczania obrazów tagami w procesie ciągłej
integracji. Wymienione tutaj strategie pozwalają bardzo łatwo identyfikować zmiany w kodzie i
konkretnej kompilacji, z którą zmiany te są powiązane.
Identyfikator kompilacji
Po rozpoczęciu kompilacji zostaje z nią powiązany pewien identyfikator. Użycie tej części
tagu pozwala odwołać się do konkretnej kompilacji powiązanej z obrazem.
Identyfikator systemu kompilacji — identyfikator kompilacji
To jest taki sam identyfikator jak poprzedni, ale zawiera także identyfikator systemu
kompilacji, co okazuje się przydatne dla użytkowników, którzy mają wiele systemów
kompilacji.
Wartość hash z repozytorium Git
W przypadku nowej operacji przekazania kodu do repozytorium następuje wygenerowanie
wartości hash w repozytorium Git. Następnie ta wartość hash jest używana jako tag,
pozwalający na łatwe odwołanie się do operacji, która spowodowała zainicjowanie
generowania obrazu.
Wartość hash dla identyfikatora kompilacji
Ta wartość pozwala odwoływać się do operacji przekazania kodu do repozytorium i
identyfikatora kompilacji, w której wyniku powstał obraz. Trzeba w tym miejscu dodać, że
ten znacznik może być dość długi.
Ciągłe wdrażanie
Ciągłe wdrażanie to proces, w którym zmiany pasywnie przekazywane z sukcesem do systemu
ciągłej integracji zostają wdrożone do środowiska produkcyjnego, bez konieczności udziału
człowieka. Kontenery mają ogromną zaletę w zakresie wdrażania zmian w środowisku
produkcyjnym. Obraz kontenera staje się obiektem niemodyfikowalnym, który poprzez
środowiska programistyczne i robocze można promować do środowiska produkcyjnego.
Przykładowo jednym z poważnych problemów, z którymi zawsze się stykamy, jest zapewnienie
spójnych środowisk. Niemal każdy napotkał sytuację, w której zasób Deployment działał w
środowisku roboczym, a przestał działać po jego przekazaniu do środowiska produkcyjnego.
Tak się dzieje na skutek tzw. przesunięcia w konfiguracji, gdy biblioteki i wersjonowane
komponenty różnią się w poszczególnych środowiskach. Kubernetes zapewnia deklaracyjny
sposób opisywania obiektów Deployments, które mogą być wersjonowane i wdrażane w spójny
sposób.
Trzeba pamiętać o tym, by najpierw zadbać o zachowanie spójnej konfiguracji procesu ciągłej
integracji, a dopiero później zająć się nieustannym wdrażaniem. Jeżeli nie masz
przygotowanego niezawodnego zestawu testów wychwytującego błędy na wstępnym etapie
procesu, skutkiem może być przekazanie niepoprawnego kodu do wszystkich środowisk.
Strategie wdrażania
Skoro poznałeś podstawowe reguły nieustannego wdrażania, warto się zapoznać z różnymi
strategiami, które są możliwe do zastosowania. Kubernetes oferuje wiele strategii
przeznaczonych do wydawania nowych wersji aplikacji. Nawet jeśli masz wbudowany
mechanizm przeznaczony do dostarczania uaktualnień, zawsze możesz skorzystać z nieco
bardziej zaawansowanych strategii. W tym podrozdziale będą przeanalizowane następujące
strategie związane z dostarczaniem uaktualnień aplikacji:
dostarczanie uaktualnień,
wdrożenie typu niebieski-zielony,
wdrożenie kanarkowe.
Mechanizm dostarczania uaktualnień jest wbudowany w Kubernetes i pozwala na
przeprowadzenie aktualizacji uruchomionej aplikacji bez jej zatrzymywania i przestoju.
Przykładowo, jeśli masz uruchomioną aplikację frontendu w wersji 1 i uaktualnisz ją do wersji
2, wówczas Kubernetes przeprowadzi aktualizację tej aplikacji w replikach, jak pokazaliśmy na
rysunku 5.1.
Rysunek 5.1. Uaktualnienia nieustanne w Kubernetes
Obiekt Deployment pozwala na skonfigurowanie maksymalnej liczby uaktualnianych replik i
maksymalnej liczby podów niedostępnych podczas aktualizacji. Spójrz na przedstawiony tutaj
manifest, który pokazuje, jak można zdefiniować strategię uaktualnień nieustannych.
kind: Deployment
apiVersion: v1
metadata:
name: frontend
spec:
replicas: 3
template:
spec:
containers:
- name: frontend
image: brendanburns/frontend:v1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Maksymalna liczba jednocześnie uaktualnianych replik.
maxUnavailable: 1 # Maksymalna liczba replik niedostępnych podczas
uaktualnienia.
Należy zachować ostrożność podczas uaktualnień nieustannych, ponieważ ta strategia może
spowodować odrzucanie połączeń. Aby sobie z tym poradzić, można wykorzystać tzw.
próbkowanie odczytu (ang. readiness probe) i zaczepy cyklu życiowego preStop. Próbkowanie
odczytu ma na celu sprawdzenie, czy nowo wdrożona wersja aplikacji jest gotowa do
przyjmowania ruchu sieciowego. Zaczep preStop może zaś zagwarantować, że połączenia
zostały zamknięte w nowo wdrożonej aplikacji. Ten zaczep cyklu życiowego jest wywoływany
przed zakończeniem działania kontenera i jest asynchroniczny, więc musi się zakończyć przed
wysłaniem ostatecznego sygnału zakończenia pracy kontenera. Spójrz na przykład
przedstawiający zastosowanie próbkowania odczytu i zaczepu cyklu życiowego.
kind: Deployment
apiVersion: v1
metadata:
name: frontend
spec:
replicas: 3
template:
spec:
containers:
- name: frontend
image: brendanburns/frontend:v1
livenessProbe:
# ...
readinessProbe:
httpGet:
path: /readiness # Punkt końcowy próbkowania.
port: 8888
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
strategy:
# ...
W omawianym przykładzie zaczep preStop cyklu życiowego spowoduje eleganckie zakończenie
procesu NGINX, podczas gdy sygnał SIGTERM spowodowałby zakończenie szybkie i
nieeleganckie.
Inną kwestią związaną z uaktualnieniami nieustannymi jest posiadanie dwóch wersji aplikacji
działających jednocześnie podczas aktualizacji. Schemat bazy danych musi obsługiwać obie
wersje aplikacji. Można również użyć strategii opcji właściwości, w której to schemat wskazuje
nowe kolumny, utworzone przez nową wersję aplikacji. Po przeprowadzeniu uaktualnienia
nieustannego stare kolumny mogą zostać usunięte.
W manifeście wdrożenia zostało zdefiniowane próbkowanie odczytu i dostępności. Próbkowanie
odczytu ma zagwarantować, że aplikacja jest gotowa do obsługi ruchu sieciowego, zanim
zacznie działać w charakterze usługi dla punktu końcowego. Z kolei próbkowanie dostępności
ma zagwarantować, że aplikacja działa poprawnie i że pod zostanie ponownie uruchomiony,
jeśli próbkowanie zakończy się niepowodzeniem. Kubernetes potrafi automatycznie ponownie
uruchomić niedziałającego poda tylko wtedy, gdy zostanie on zamknięty na skutek błędu.
Przykładowo próbkowanie dostępności może sprawdzać punkt końcowy i ponownie go
uruchomić po wykryciu zakleszczenia, z którego pod nie zdołał się wydostać.
Wdrożenie typu niebieski-zielony pozwala na wydawanie aplikacji w przewidywalny sposób.
Dzięki wdrożeniu tego typu zachowujesz kontrolę nad przeniesieniem ruchu sieciowego do
nowego środowiska, co oznacza większą kontrolę nad wydawaniem nowych wersji aplikacji. W
przypadku wdrożenia typu niebieski-zielony musisz mieć do dyspozycji wystarczająco dużą
pojemność, aby mogły jednocześnie być wdrożone środowiska istniejące i nowe. Taki rodzaj
wdrożenia ma wiele zalet, takich jak łatwość cofnięcia do poprzedniej wersji aplikacji.
Stosując tę strategię wdrożenia, trzeba uwzględnić pewne kwestie:
Migracja bazy danych może być trudna, ponieważ trzeba wziąć pod uwagę realizowane
transakcje i zgodność uaktualnienia schematu.
Istnieje niebezpieczeństwo usunięcia obu środowisk.
Trzeba zapewnić pojemność wystarczającą dla obu środowisk.
Możliwe są problemy związane z koordynacją wdrożeń hybrydowych, po których starsze
aplikacje nie będą w stanie obsłużyć danego wdrożenia.
Wizualne przedstawienie wdrożenia typu niebieski-zielony pokazaliśmy na rysunku 5.2.
Rysunek 5.2. Wdrożenie typu niebieski-zielony
Wdrożenie kanarkowe jest bardzo podobne do wdrożenia typu niebieski-zielony, choć zapewnia
większą kontrolę nad przesunięciem ruchu sieciowego do nowego wydania. Większość nowych
implementacji usługi umożliwia przekierowanie pewnej, wyrażonej w procentach, ilości ruchu
sieciowego do nowego wydania. Ponadto istnieje możliwość implementacji technologii Service
Mesh — np. Istio, Linkerd lub HashiCorp Consul — która udostępnia pewną liczbę
funkcjonalności pomagających w przygotowaniu takiej strategii wdrożenia.
Wdrożenie kanarkowe pozwala przetestować nowe funkcjonalności tylko na podzbiorze
użytkowników. Przykładowo można wydać nową wersję aplikacji i przetestować ją jedynie dla
10% bazy użytkowników. Dzięki temu na niebezpieczeństwa związane z wprowadzeniem
niepoprawnego wdrożenia lub niedziałających funkcjonalności będzie narażona znacznie
mniejsza grupa użytkowników. Jeżeli we wdrożeniu lub w nowych funkcjonalnościach nie ma
błędów, można zacząć przekierowywać do nowej wersji aplikacji coraz większy odsetek
użytkowników. Istnieją również o wiele bardziej zaawansowane technologie przeznaczone do
stosowania wraz z wdrożeniami kanarkowymi. Przykładowo aplikacja może zostać wydana dla
użytkowników pochodzących z określonego regionu lub jedynie dla użytkowników o
konkretnym profilu. Takie rodzaje wydań zwykle są określane jako A/B lub ciemne, ponieważ
użytkownicy są nieświadomi tego, że testują nową funkcjonalność wdrożenia.
W przypadku wdrożenia kanarkowego trzeba wziąć pod uwagę pewne kwestie pojawiające się
we wcześniej omówionym wdrożeniu typu niebieski-zielony, a także kilka nowych:
Możliwość przesunięcia ruchu sieciowego do wyrażonej w procentach grupy
użytkowników.
Solidna wiedza pozwalająca na porównanie istniejącej wersji aplikacji z nową.
Wskaźniki pozwalające na określenie, czy stan nowego wydania jest „dobry” czy też „zły”.
Wizualne przedstawienie wdrożenia kanarkowego pokazaliśmy na rysunku 5.3.
Rysunek 5.3. Wdrożenie kanarkowe
Wdrożenie kanarkowe jest utrudnione w przypadku wielu uruchomionych
jednocześnie wersji aplikacji. Schemat bazy danych musi mieć możliwość obsługi obu
wersji aplikacji. Gdy stosujesz tę strategię, naprawdę musisz skoncentrować się na
sposobie obsługi usług zależnych i na jednoczesnym działaniu wielu wersji. Dlatego
trzeba mieć silne API i zagwarantować, że usługi danych obsługujące wiele wersji
aplikacji zostaną wdrożone w tym samym czasie.
Testowanie w produkcji
Testowanie w produkcji pomaga się upewnić, że aplikacja jest niezawodna, skalowana i
charakteryzuje się dobrym UX. Trzeba w tym miejscu dodać, że testowanie w produkcji wiąże
się z pewnymi wyzwaniami i ryzykiem, choć warto ponieść ten wysiłek, aby zagwarantować
niezawodność systemów. Istnieją pewne ważne aspekty, które trzeba wziąć pod uwagę podczas
przygotowywania takiej implementacji. Przede wszystkim należy się upewnić, że istnieje
strategia pozwalająca na dogłębną obserwację, co pozwoli sprawdzić efekty testowania w
produkcji. Bez możliwości obserwacji wskaźników wpływających na wrażenia użytkowników
końcowych aplikacji nie będziesz miał jasno określonego celu, na którym trzeba się
skoncentrować podczas próby poprawienia odporności programu na awarie. Dobrze jest
zastosować również wysoki stopień automatyzacji i umożliwić automatyczną naprawę po awarii
w systemach.
Do dyspozycji masz wiele narzędzi, które trzeba będzie zaimplementować w celu zmniejszenia
niebezpieczeństwa i efektywnego przetestowania systemów w produkcji. O części narzędzi już
wspomnieliśmy w rozdziale; są też inne, m.in. służące do monitorowania rozproszonego,
instrumentacji, inżynierii chaosu (ang. chaos engineering) i przesłaniania ruchu sieciowego
(ang. traffic shadowing). Dla przypomnienia przedstawiamy listę narzędzi, które zostały już
wspomniane w rozdziale:
wdrożenie kanarkowe,
testowanie A/B,
przesunięcie ruchu sieciowego,
opcje właściwości.
Inżynieria chaosu została opracowana przez firmę Netflix. Polega na wdrażaniu eksperymentów
w działających systemach produkcyjnych i ma na celu odkrycie ich słabych stron. Inżynieria
chaosu pozwala poznać sposób, w jaki system się zachowuje, przez jego obserwację podczas
kontrolowanego eksperymentu. Zapoznaj się z wymienionymi tutaj krokami, które trzeba
wykonać przed przystąpieniem do eksperymentów.
1.
2.
3.
4.
Opracowanie hipotezy i poznanie aktualnego stanu systemu.
Przygotowanie rzeczywistych zdarzeń, które mogą wpływać na system.
Utworzenie grupy kontrolnej i eksperymentowanie w celu porównania stanu.
Przeprowadzenie eksperymentów w celu sformułowania hipotezy.
Ogromne znaczenie ma to, aby podczas przeprowadzania eksperymentów zminimalizować „pole
rażenia” i zagwarantować, że ewentualne problemy będą naprawdę minimalne. Chcesz mieć
pewność, że gdy zaczniesz przeprowadzać eksperymenty, skoncentrujesz się na ich
automatyzacji, ponieważ ich wykonywanie może być pracochłonne.
Jednak w tym miejscu być może zaczniesz zadawać sobie pytanie: „Dlaczego nie mogę po
prostu wykonać testu w środowisku roboczym?”. Przekonaliśmy się, że testowanie w
środowisku roboczym wiąże się z pewnymi nieuchronnymi problemami. Są to:
nieidentyczne zasoby wdrożenia,
przesunięcie konfiguracji względem tej stosowanej w produkcji,
nienaturalna symulacja ruchu sieciowego i sposobu zachowania użytkownika,
liczba generowanych żądań nie odzwierciedla rzeczywistego obciążenia,
brak monitorowania zaimplementowanego w środowisku roboczym,
wdrożone usługi danych zawierają inne dane i wiążą się z innym obciążeniem niż w
środowisku produkcyjnym.
Nie sposób podkreślić tego wystarczająco mocno, ale upewnij się o zastosowaniu w środowisku
produkcyjnym solidnego rozwiązania w zakresie monitorowania, ponieważ użytkownicy, którzy
nie mają odpowiednich możliwości obserwowania systemów produkcyjnych, są skazani na
niepowodzenie. Ponadto rozpocznij od niewielkich eksperymentów, by zwiększyć zaufanie do
środowiska produkcyjnego.
Stosowanie inżynierii chaosu i
przygotowania
Pierwszym krokiem w omawianym procesie jest utworzenie rozwidlenia repozytorium GitHub.
Dzięki temu będziesz mieć własne repozytorium przeznaczone do użycia w rozdziale. Konieczne
będzie
użycie
interfejsu
GitHub
pozwalającego
na
rozwidlenie
repozytorium
(https://github.com/dstrebel/kbp).
Konfiguracja ciągłej integracji
Skoro poznałeś technikę ciągłej integracji, zajmiesz się kompilacją kodu, który sklonowaliśmy
poprzednio.
Na potrzeby omawianego przykładu wykorzystamy serwis https://drone.io/. Będziesz musiał się
zarejestrować (https://cloud.drone.io/) i utworzyć bezpłatne konto. Podczas logowania podaj
dane uwierzytelniające z serwisu GitHub; w ten sposób zarejestrujesz swoje repozytoria w
Drone i będziesz mógł je synchronizować. Po zalogowaniu się do Drone wybierz opcję Active dla
rozwidlenia repozytorium. Pierwszym zadaniem, które prawdopodobnie będziesz musiał
wykonać, jest dodanie do konfiguracji pewnych danych poufnych. To pozwoli na przekazanie
aplikacji do rejestru Docker Hub i wdrożenie jej do klastra Kubernetes.
W ramach repozytorium w Drone kliknij Settings i dodaj następujące dane poufne (zobacz
rysunek 5.4).
Rysunek 5.4. Konfiguracja danych poufnych w Drone
docker_username,
docker_password,
kubernetes_server,
kubernetes_cert,
kubernetes_token.
Nazwa użytkownika i hasło w serwisie Docker będą tymi wartościami, których użyłeś podczas
rejestrowania konta w Docker Hub. W kolejnych krokach zobaczysz, jak przebiega utworzenie
konta usługi Kubernetes, certyfikacja i pobranie tokena.
W przypadku serwera Kubernetes potrzebujesz publicznie dostępnego punktu końcowego API
Kubernetes.
Do wykonania kroków omawianych w tej sekcji konieczne jest uprawnienie clusteradmin w klastrze Kubernetes.
W celu pobrania API punktu końcowego należy wydać następujące polecenie:
$ kubectl cluster-info
Powinieneś otrzymać komunikat informujący o działaniu Kubernetes pod adresem takim jak
https://kbp.centralus.azmk8s.io:443. Ta wartość będzie przechowywana w postaci danych
poufnych kubernetes_server.
Przechodzimy teraz do utworzenia konta usługi, które będzie używane przez Drone podczas
nawiązywania połączenia z klastrem. Skorzystaj z przedstawionego tutaj polecenia, które
tworzy serviceaccount.
$ kubectl create serviceaccount drone
Następne polecenie tworzy clusterrolebinding dla serviceaccount:
$ kubectl create clusterrolebinding drone-admin \
--clusterrole=cluster-admin \
--serviceaccount=default:drone
Kolejnym krokiem jest pobranie tokena dla serviceaccount:
$ TOKENNAME=`kubectl -n default get serviceaccount/drone -o
jsonpath='{.secrets[0].name}'`
$ TOKEN=`kubectl -n default get secret $TOKENNAME -o jsonpath='{.data.token}'
| base64 -d`
$ echo $TOKEN
Wygenerowane dane wyjściowe w postaci tokena należy przechowywać jako dane poufne
kubernetes_token.
Potrzebny jest również certyfikat użytkownika w celu uwierzytelnienia w klastrze. Skorzystaj
więc z przedstawionego tutaj polecenia i wklej zawartość ca.crt do danych poufnych
kubernetes_cert.
$ kubectl get secret $TOKENNAME -o yaml | grep 'ca.crt:'
Teraz utwórz rozwiązanie Drone, a następnie przekaż aplikację do rejestru Docker Hub.
Pierwszym krokiem jest etap kompilacji, w trakcie którego powstanie frontend opracowany w
Node.js. Drone wykorzystuje obrazy kontenera do wykonywania swoich zadań, co zapewnia
ogromną elastyczność w zakresie dostępnych możliwości. Na etapie kompilacji skorzystaj z
obrazu Node.js pochodzącego z rejestru Docker Hub:
pipeline:
build:
image: node
commands:
- cd frontend
- npm i redis --save
Po zakończeniu kompilacji należy ją przetestować, co odbędzie się na etapie testowania. Polega
on na wydaniu polecenia npm względem nowo utworzonej aplikacji.
test:
image: node
commands:
- cd frontend
- npm i redis --save
- np
Gdy kompilacja i testowanie aplikacji zakończą się sukcesem, będzie można przejść do
następnego kroku, którym jest etap publikowania. W tym kroku następuje utworzenie obrazu
Dockera aplikacji i jego przekazanie do rejestru Docker Hub.
W pliku .drone.yml wprowadź poniższą zmianę w kodzie:
repo: <twój-rejestr>/frontend
publish:
image: plugins/docker
dockerfile: ./frontend/Dockerfile
context: ./frontend
repo: dstrebel/frontend
tags: [latest, v2]
secrets: [ docker_username, docker_password ]
Po zakończeniu operacji tworzenia obrazu Dockera można go przekazać do rejestru Dockera.
Konfiguracja ciągłego wdrażania
Na etapie wdrożenia gotowa aplikacja zostanie przekazana do klastra Kubernetes. W trakcie
tego procesu będzie wykorzystany manifest wdrożenia, który znajduje się w katalogu aplikacji
frontendu w repozytorium.
kubectl:
image: dstrebel/drone-kubectl-helm
secrets: [ kubernetes_server, kubernetes_cert, kubernetes_token ]
kubectl: "apply -f ./frontend/deployment.yaml"
Gdy operacja wdrożenia zostanie zakończona, będziesz mógł zobaczyć pody działające w
klastrze. Wydanie następującego polecenia pozwoli się upewnić, że pody działają:
$ kubectl get pods
Istnieje możliwość dodania etapu testowania, w trakcie którego zostaną pobrane informacje o
stanie wdrożenia. To jest możliwe po dodaniu w definicji rozwiązania Drone następującego
kodu:
test-deployment:
image: dstrebel/drone-kubectl-helm
secrets: [ kubernetes_server, kubernetes_cert, kubernetes_token ]
kubectl: "get deployment frontend"
Przeprowadzanie operacji uaktualnienia
Teraz pokażemy, jak przeprowadzić uaktualnienie przez wprowadzenie jednej zmiany w kodzie
frontendu. W pliku server.js dodaj przedstawiony poniżej wiersz kodu, a następnie przekaż
zmiany do repozytorium.
console.log('Serwer API został uruchomiony.');
Gdy to zrobisz, powinieneś zobaczyć, jak przebiega wdrażanie i uaktualnianie oprogramowania
w istniejących podach. Po zakończeniu operacji uaktualnienia nowa wersja aplikacji będzie
wdrożona.
Prosty eksperyment z inżynierią chaosu
W ekosystemie Kubernetes mamy wiele narzędzi pomagających w przeprowadzaniu w
wybranym środowisku eksperymentów związanych z inżynierią chaosu. Gama tych narzędzi jest
naprawdę ogromna, od rozwiązań typu „Chaos as a Service” po proste narzędzia do
eksperymentów, które doprowadzą do zakończenia działania podów w środowisku. W tym
miejscu zdecydowaliśmy się na przedstawienie wybranych narzędzi, o których wiemy, że są z
powodzeniem stosowane przez użytkowników.
Gremlin
To hostingowana usługa zapewniająca zaawansowane funkcje do przeprowadzania
eksperymentów związanych z inżynierią chaosu.
PowerfulSeal
To projekt typu open source oferujący zaawansowane scenariusze inżynierii chaosu.
Chaos Toolkit
To projekt typu open source, którego zadaniem jest zapewnienie bezpłatnego, otwartego i
rozwijanego przez społeczność zestawu narzędzi oraz API dla różnych postaci narzędzi z
zakresu inżynierii chaosu.
KubeMonkey
To narzędzie typu open source oferujące podstawowe możliwości testowania odporności
podów w klastrze.
Przeprowadzimy teraz szybki eksperyment pozwalający przetestować odporność aplikacji na
awarie. W trakcie eksperymentu działanie podów zostanie zakończone. Do przeprowadzenia
eksperymentu użyjemy narzędzia Chaos Toolkit.
$ pip install -U chaostoolkit
$ pip install chaostoolkit-kubernetes
$ export FRONTEND_URL="http://$(kubectl get svc frontend -o jsonpath="
{.status.loadBalancer.ingress[*].ip}"):8080/api/"
$ chaos run experiment.json
Najlepsze praktyki dotyczące technik
ciągłej integracji i ciągłego wdrażania
Przygotowane rozwiązanie w zakresie ciągłej integracji i ciągłego wdrażania nie będzie od razu
doskonałe. Dlatego też warto rozważyć zastosowanie wybranych praktyk z poniższej listy, aby
iteracyjnie usprawniać to rozwiązanie.
W przypadku technik ciągłej integracji należy skoncentrować się na automatyzacji i
szybkiej kompilacji. Optymalizacja wydajności działania kompilacji zapewni programistom
szybkie informacje o tym, czy wprowadzone przez nich zmiany nie doprowadziły do
uszkodzenia aplikacji.
Skoncentruj się na dostarczeniu w rozwiązaniu niezawodnych testów. Dzięki nim
programiści będą szybko otrzymywali informacje o potencjalnych problemach w kodzie.
Im szybciej takie informacje dotrą do programistów, tym większą osiągną oni
produktywność w pracy.
Podczas wybierania narzędzi z zakresu ciągłej integracji i ciągłego wdrażania upewnij się,
że te narzędzia pozwolą zdefiniować rozwiązanie w postaci kodu. To umożliwi
umieszczenie kodu rozwiązania w systemie kontroli wersji.
Upewnij się co do optymalizacji obrazów. To pozwoli zmniejszyć wielkość obrazu, a tym
samym płaszczyznę ataku po wdrożeniu danego obrazu do środowiska produkcyjnego.
Wieloetapowa kompilacja w Dockerze umożliwia usunięcie pakietów niepotrzebnych do
działania aplikacji. Przykładowo pakiet Maven może być potrzebny do skompilowania
aplikacji, ale nie jest niezbędny do rzeczywistego uruchomienia obrazu.
Unikaj używania słowa latest w tagu obrazu kontenera. Zamiast tego skorzystaj z tagu
odwołującego się do identyfikatora kompilacji lub identyfikatora zatwierdzenia kodu w
repozytorium Git.
Jeżeli dopiero zaczynasz korzystanie z technik ciągłego wdrażania, użyj oferowanych
przez Kubernetes możliwości w zakresie dostarczania uaktualnień. Rozpoczęcie pracy z
nimi jest bardzo łatwe, podobnie jak ich zastosowanie we wdrożeniach. Gdy nabędziesz
większej wprawy i większej pewności siebie w pracy z technikami ciągłego wdrażania,
zainteresuj się strategiami wdrażania kanarkowego i typu niebieski-zielony.
W trakcie stosowania technik ciągłego wdrażania upewnij się, że przetestowałeś, jak
uaktualnienia połączeń klienta i schematu bazy danych są obsługiwane przez aplikację.
Testowanie w produkcji pomoże w zagwarantowaniu niezawodności działania aplikacji.
Upewnij się również, że dysponujesz dobrym rozwiązaniem w zakresie monitorowania.
Testując w produkcji, rozpocznij od operacji na mniejszą skalę i postaraj się ograniczyć
pole rażenia eksperymentu.
Podsumowanie
W rozdziale zostały omówione strategie związane z utworzeniem rozwiązania z zakresu ciągłej
integracji i ciągłego wdrażania dla aplikacji. Dzięki takiemu rozwiązaniu można zapewnić
niezawodny proces dostarczania oprogramowania. Stosowanie technik ciągłej integracji i
ciągłego wdrażania pozwala ograniczyć ryzyko i zarazem zwiększyć częstotliwość dostarczania
aplikacji do Kubernetes. Przedstawiliśmy także różne strategie wdrażania, które można
stosować podczas dostarczania aplikacji.
Rozdział 6. Wersjonowanie,
wydawanie i wdrażanie aplikacji
Jedną z największych wad tradycyjnych, monolitycznych aplikacji jest to, że wraz z upływem
czasu stają się one na tyle ogromne i nieporęczne, że poprawne przeprowadzanie wszelkich
operacji związanych z ich uaktualnianiem, wersjonowaniem i modyfikowaniem z szybkością
oczekiwaną w biznesie okazuje się trudne. Wielu czytelników może uważać, że to jeden z
najważniejszych powodów, które doprowadziły do opracowania praktyk programowania
zwinnego (ang. agile) i nadejścia architektury mikrousług. Możliwość szybkiej iteracji nowego
kodu, rozwiązywania pojawiających się problemów lub usuwania ukrytych, zanim doprowadzą
do powstania poważnych problemów, a także obietnica wdrożeń bez przestoju — te wszystkie
cele stawiają przed sobą zespoły programistów działających w nieustannie zmieniającym się
świecie ekonomii internetowej. Wymienione kwestie można praktycznie rozwiązać za pomocą
poprawnych procesów i procedur, niezależnie od typu systemu. Jednak to zwykle wiąże się ze
znacznie większym kosztem, pod względem zarówno technologicznym, jak i kapitału ludzkiego,
którym trzeba zarządzać.
Adaptacja kontenerów jako środowiska uruchomieniowego dla kodu aplikacji pozwala na
zastosowanie poziomu izolacji i złożoności użytecznego podczas projektowania systemów, które
mogły się zbliżyć. Mimo to te systemy nadal wymagają wysokiego poziomu automatyzacji
wprowadzonej przez człowieka lub zarządzania systemami w celu zapewnienia ich
niezawodności na dużych obszarach. System, w miarę jak się rozwija, staje się coraz bardziej
kruchy, a inżynierowie oprogramowania rozpoczynają tworzenie złożonych procesów
automatyzacji, mających na celu zapewnienie dostarczania złożonych mechanizmów wydań,
uaktualnień i wykrywania awarii. Usługi orkiestratorów, takie jak Apache Mesos, HashiCorp
Nomad i nawet specjalizowane orkiestratory oparte na kontenerach, np. Kubernetes i Docker
Swarm, ewoluowały do postaci podstawowych komponentów w ich środowiskach
uruchomieniowych. Obecnie inżynierowie systemów mogą rozwiązywać problemy bardziej
złożonych systemów, w których trzeba uwzględnić jeszcze inne kwestie, takie jak
wersjonowanie, wydania i wdrożenia aplikacji w systemie.
Wersjonowanie aplikacji
Ten podrozdział nie jest krótkim wprowadzeniem do tematu wersjonowania oprogramowania i
nie ma na celu przedstawienia stojącej za tym historii. Można znaleźć niezliczoną liczbę
artykułów i opracowań na ten temat. Najważniejszą kwestią jest wybór metody wersjonowania i
jej konsekwentne stosowanie. Większość firm tworzących oprogramowanie i programistów
zgodziło się, że najbardziej użytecznym podejściem jest pewna forma tzw. wersjonowania
semantycznego. To szczególnie dotyczy architektury mikrousług, w której zespół tworzący
pewną mikrousługę jest zależny od zgodności API innych mikrousług, których połączenie
prowadzi do powstania całego systemu.
Jeżeli dotąd nie zetknąłeś się z wersjonowaniem semantycznym, powinieneś wiedzieć, że u jego
podstaw leży wersja składająca się z trzech liczb, które oznaczają: wersję główną, wersję
mniejszą i wersję poprawki. Z reguły są one podane w postaci zapisu z użyciem kropek, np.
1.2.3 (odpowiednio: wersja główna, wersja mniejsza, wersja poprawki). Poprawka oznacza
wydanie, w którym usunięto błąd lub wprowadzono drobną zmianę bez wpływu na API. Wersja
mniejsza wskazuje na uaktualnienie, które może się wiązać ze zmianą API, choć pozostaje
zgodne wstecz z poprzednią wersją. To atrybut o kluczowym znaczeniu dla programistów
pracujących z innymi mikrousługami, w których rozwój mogą nie być zaangażowani.
Przyjmujemy założenie o utworzeniu usługi przystosowanej do komunikacji z inną mikrousługą
w wersji 1.4.7. Jeżeli ta inna usługa zostanie uaktualniona do wersji 1.4.8, wówczas nie
będziesz musiał zmieniać kodu swojej usługi, o ile nie zechcesz wykorzystać możliwości
oferowanych przez wszelkie nowe API wprowadzone w wersji 1.4.8. Natomiast w przypadku
nowej wersji głównej można się spodziewać, że wymienione mikrousługi nie będą ze sobą dłużej
współpracowały. W większości przypadków API pozostaje niezgodne między wersjami głównymi
tego samego kodu. Mogą być wprowadzone w tym procesie drobne modyfikacje dotyczące
wersji „4” i wskazujące na stan oprogramowania w jego cyklu programistycznym, np. wersja
alfa — 1.4.7.0, ostateczne wydanie — 1.4.7.3. Najważniejsze jest zachowanie spójności w
systemie.
Wydania aplikacji
Tak naprawdę Kubernetes nie ma kontrolera wydania, więc nie istnieje w nim natywna
koncepcja wydania. Informacje o wydaniu zwykle są dodawane w postaci specyfikacji
metadata.labels i/lub w specyfikacji pod.spec.template.metadata.label. Dołączanie tych
informacji jest bardzo ważne. Sposób wykorzystania technik ciągłego wdrażania do
uaktualniania wdrożenia może mieć różne efekty. Po wprowadzeniu menedżera pakietów Helm
dla Kubernetes jedną z podstawowych koncepcji była notacja wydania w celu odróżnienia
działającego egzemplarza tego samego pliku Helm w formacie chart w klastrze. Tę koncepcję
można łatwo odtworzyć także bez użycia menedżera pakietów Helm. Jednak Helm natywnie
monitoruje wydania i ich historię, więc wiele narzędzi ciągłego wdrażania integruje tego
menedżera w rozwiązaniu jako usługę rzeczywistego wydania. Warto w tym miejscu
przypomnieć raz jeszcze, że kluczem jest zachowanie spójności pod względem sposobu użycia
wersjonowania i miejsca jego zastosowania w informacjach o stanie klastra.
Nazwy wydań mogą być dość użyteczne, o ile obowiązuje konsensus dotyczący definicji
określonych nazw. Często są stosowane etykiety, np. stable lub canary, pomagające w nadaniu
pewnego rodzaju operacyjnej kontroli, gdy narzędzia takie jak architektura Service Mesh są
stosowane w celu zapewnienia znacznie dokładniejszych decyzji routingu. Organizacje
wprowadzające wiele zmian dla różnych odbiorców adaptują architekturę kręgu (ang. ring),
która może być oznaczona jako np. ring-0, ring-1 itd.
Ten temat wymaga poruszenia kwestii związanych ze specyfiką etykiet w modelu
deklaratywnym Kubernetes. Etykieta sama w sobie przybiera dowolną postać i tak naprawdę
może być parą klucz-wartość, zgodną z syntaktycznymi regułami API. Kluczem jest to, jak
kontroler, a nie treść, obsługuje zmiany wprowadzane w etykietach i jak selektor dopasowuje
etykiety. Zasoby Job, Deployment, ReplicaSet i DaemonSet obsługują dopasowywanie podów na
podstawie selektora z użyciem etykiet i bezpośredniego mapowania lub zbioru zdefiniowanych
wyrażeń. Trzeba pamiętać, że selektory etykiet są niemodyfikowalne po ich utworzeniu.
Dlatego jeśli dodasz nowy selektor, a etykieta poda zostanie dopasowana, wówczas nastąpi
utworzenie nowego zasobu ReplicaSet, a nie uaktualnienie istniejącego. To ma bardzo duże
znaczenie podczas pracy z wdrożeniami, którymi zajmiemy się w następnym podrozdziale.
Wdrożenia aplikacji
Przed wprowadzeniem kontrolera wdrożenia do Kubernetes jedynym istniejącym mechanizmem
pozwalającym na kontrolowanie sposobu wdrożenia aplikacji przez proces Kubernetes było
wykorzystanie polecenia powłoki kubectl rolling-update dla konkretnego zasobu
replicaController, który został uaktualniony. Takie podejście było trudne w przypadku
deklaratywnych modeli ciągłego wdrażania, ponieważ nie było częścią informacji o stanie
pierwotnego manifestu. Trzeba było zachować dużą ostrożność oraz zagwarantować poprawne
uaktualnienie manifestu i właściwe wersjonowanie, aby nie przywrócić przypadkowo
wcześniejszej wersji systemu i archiwizować aplikację, gdy już nie była potrzebna. Kontroler
wdrożenia dodał możliwość automatyzacji procesu uaktualniania z użyciem określonej strategii;
następnie pozwala systemowi na odczytywanie deklaratywnych informacji o nowym stanie na
podstawie zmian wprowadzonych w spec.template wdrożenia. Początkujący użytkownicy
Kubernetes często błędnie rozumieją tę ostatnią kwestię, co prowadzi do frustracji, gdy po
zmianie etykiety w polach metadanych zasobu Deployment i ponownym zastosowaniu manifestu
nie zostaje wywołana operacja uaktualnienia. Kontroler wdrożenia ma możliwość ustalenia
zmian w specyfikacji i podjęcia akcji uaktualnienia wdrożenia na podstawie strategii
zdefiniowanej przez specyfikację. Wdrożenia Kubernetes obsługują dwie strategie,
rollingUpdate i recreate, z których pierwsza jest domyślna.
Jeżeli zostanie określona operacja uaktualnienia, wówczas wdrożenie utworzy nowy zasób
ReplicaSet w celu skalowania liczby wymaganych replik. Z kolei stary zasób ReplicaSet
zostanie przeskalowany do zera, na podstawie określonych wartości maxUnavailable i
maxSurge. W praktyce te dwie wartości uniemożliwiają Kubernetes usuwanie starszych podów
aż do czasu, gdy będzie dostępna wystarczająca liczba nowych. Ponadto nowe pody nie będą
tworzone aż do chwili usunięcia określonej liczby starszych. Doskonałą cechą kontrolera
wdrożenia jest przechowywanie historii uaktualnień i możliwość wycofania, za pomocą powłoki,
wdrożenia i tym samym powrotu do poprzedniej wersji.
Strategia recreate jest właściwa w przypadku określonych rozwiązań, które potrafią obsłużyć
całkowitą awarię podów w zasobie ReplicaSet i zarazem w ogóle nie doprowadzić do
degradacji usługi lub ograniczyć degradację do minimum. W takiej strategii kontroler
wdrożenia będzie tworzył nowy zasób ReplicaSet z nową konfiguracją i usuwał poprzedni
zasób ReplicaSet przed uruchomieniem nowych podów. Usługi kryjące się za systemami
opartymi na kolejkach są doskonałym przykładem usług, które mogą obsłużyć taki rodzaj
zakłóceń. To jest możliwe, ponieważ komunikaty będą kolejkowane w oczekiwaniu na
uruchomienie nowych podów, a przetwarzanie komunikatów zostanie wznowione, gdy tylko
nowe pody staną się dostępne.
Połączenie wszystkiego w całość
We wdrożeniu pojedynczej usługi wersjonowanie, wydania i zarządzanie wydawaniem
oprogramowania mają wpływ na kilka kluczowych aspektów wdrożenia. Przeanalizujemy teraz
przykładowe wdrożenie, a następnie przejdziemy do wybranych obszarów, które wiążą się z
najlepszymi praktykami.
# Wdrożenie aplikacji internetowej.
apiVersion: apps/v1
kind: Deployment
metadata:
name: gb-web-deploy
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
release number: 34e57f01
spec:
strategy:
type: rollingUpdate
rollingUpdate:
maxUnavailbale: 3
maxSurge: 2
selector:
matchLabels:
app: gb-web
ver: 1.5.8
matchExpressions:
- {key: environment, operator: In, values: [production]}
template:
metadata:
labels:
app: gb-web
ver: 1.5.8
environment: production
spec:
containers:
- name: gb-web-cont
image: evillgenius/gb-web:v1.5.5
env:
- name: GB_DB_HOST
value: gb-mysql
- name: GB_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
--# Wdrożenie bazy danych.
apiVersion: apps/v1
kind: Deployment
metadata:
name: gb-mysql
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
release number: 34e57f01
spec:
selector:
matchLabels:
app: gb-db
tier: backend
strategy:
type: Recreate
template:
metadata:
labels:
app: gb-db
tier: backend
ver: 1.5.9
environment: production
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
--# Wdrożenie zadania utworzenia kopii zapasowej bazy danych.
apiVersion: batch/v1
kind: Job
metadata:
name: db-backup
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
release number: 34e57f01
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook": pre-delete
"helm.sh/hook": pre-rollback
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
metadata:
labels:
app: gb-db-backup
tier: backend
ver: 1.6.1
environment: production
spec:
containers:
- name: mysqldump
image: evillgenius/mysqldump:v1
env:
- name: DB_NAME
value: gbdb1
- name: GB_DB_HOST
value: gb-mysql
- name: GB_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
volumeMounts:
- mountPath: /mysqldump
name: mysqldump
volumes:
- name: mysqldump
hostPath:
path: /home/bck/mysqldump
restartPolicy: Never
backoffLimit: 3
Na pierwszy rzut oka to rozwiązanie może nie prezentować się najlepiej. Jak to możliwe, że
wdrożenie ma inny tag wersji niż obraz kontenera w tym wdrożeniu? Co się stanie w przypadku
zmiany jednego z tych tagów? Jakie znaczenie ma w tym przykładzie wydanie i jaki wpływ na
system będzie miała zmiana wydania? Czy w razie zmiany określonej etykiety zostanie
zainicjowana operacja uaktualnienia we wdrożeniu? Odpowiedzi na te pytania można znaleźć
przez analizę wybranych najlepszych praktyk związanych z wersjonowaniem, wydaniami i
wycofywaniem wdrożeń.
Najlepsze praktyki dotyczące wersjonowania,
wydawania i wycofywania wdrożeń
Aby móc użyć rozwiązania wykorzystującego techniki ciągłej integracji i ciągłego wdrażania
oraz zapewnić krótki czas przestoju lub w ogóle go wyeliminować, należy stosować spójne
praktyki w zakresie wersjonowania wydań i zarządzania nimi. Wybrane najlepsze praktyki
przedstawione w tej sekcji mogą pomóc w zdefiniowaniu spójnych parametrów, które następnie
będą wspomagały zespoły DevOps w przeprowadzaniu wdrożeń oprogramowania.
Używaj wersjonowania semantycznego aplikacji całkowicie odmiennego od wersjonowania
kontenerów i podów wdrożenia tworzących całą aplikację. Dzięki temu zyskujesz
niezależne cykle życiowe kontenerów tworzących aplikację i aplikacji jako całości. Na
początku takie rozwiązanie może być nieco dezorientujące, ale jeśli do zmian będzie
stosowane podejście hierarchiczne, zyskasz możliwość łatwego ich monitorowania. W
przedstawionym przykładzie kontener był w wersji 1.5.5, a specyfikacja poda — w wersji
1.5.8. To mogło oznaczać wprowadzenie zmian w specyfikacji poda, np. nowych zasobów
ConfigMap, dodatkowych danych poufnych lub uaktualnionych wartości replik, ale wersja
używanego kontenera nie zmieniła się. Aplikacja, czyli książka gości i jej wszystkie usługi,
jest w wersji 1.6.9. To może oznaczać, że zostały wprowadzone zmiany operacyjne,
wykraczające poza określoną usługę, np. dodanie innych usług tworzących aplikację.
Używaj nazwy wydania lub wersji wydania w metadanych wdrożenia, aby w ten sposób
monitorować wydania pochodzące z rozwiązania opartego na technikach ciągłej integracji
i ciągłego wdrażania. Nazwa wydania i jego numer powinny być skoordynowane z
informacjami o rzeczywistych wydaniach przechowywanych w rekordach narzędzi do
ciągłej integracji i ciągłego wdrażania. To pozwoli na monitorowanie zmian za pomocą
procesu CI/CD w klastrze, a także na znacznie łatwiejsze identyfikowanie operacji
wycofania. W poprzednim przykładzie numer wydania pochodził bezpośrednio z
identyfikatora wydania w rozwiązaniu ciągłego wdrażania, które utworzyło manifest.
Jeżeli menedżer pakietów Helm jest używany do pakowania usług do wdrożenia w
Kubernetes, należy zachować ostrożność podczas pakowania ze sobą usług, które będą
musiały być wycofane lub uaktualnione razem w tym samym pliku Helm w formacie chart.
Menedżer pakietów Helm pozwala na łatwe wycofywanie wszystkich komponentów
aplikacji w celu przywrócenia stanu sprzed uaktualnienia. Skoro Helm w rzeczywistości
przetwarza szablony i wszystkie dyrektywy tego menedżera pakietów przed przekazaniem
spłaszczonej konfiguracji YAML, użycie zaczepów cyklu życiowego pozwala na ułożenie w
poprawnej kolejności określonych szablonów aplikacji. Operatory mogą używać
właściwych zaczepów cyklu życiowego w celu zagwarantowania poprawności operacji
uaktualnienia i wycofania. W poprzednim przykładzie specyfikacja Job używa zaczepów
cyklu życiowego menedżera Helm do zagwarantowania, że szablon będzie korzystał z
kopii zapasowej bazy danych przed wycofaniem, uaktualnieniem lub usunięciem wydania
Helm. To gwarantuje również, że zasób Job zostanie usunięty po zakończonym sukcesem
wykonaniu zadania. Zanim w Kubernetes pojawił się kontroler TTL, tę operację usunięcia
trzeba było przeprowadzać ręcznie.
Zdecyduj się na nomenklaturę wydania, która będzie miała sens w kontekście tempa
działania organizacji. W większości sytuacji wystarczające jest stosowanie stanów
określanych jako stable, canary i alpha.
Podsumowanie
Dzięki Kubernetes firmy, zarówno duże, jak i małe, mogą adaptować znacznie bardziej złożone,
tzw. zwinne, procesy wdrożenia. Możliwość automatyzacji większości skomplikowanych
procesów, które zwykle wymagałyby ogromnej ilości pracy człowieka i ogromnego kapitału
technicznego, stała się dostępna nawet dla startupów i pozwala na dość łatwe wykorzystanie
takiego wzorca chmury. Prawdziwie deklaracyjna natura Kubernetes pokazuje pełnię swoich
zalet pod warunkiem, że etykiety i natywne możliwości oferowane przez kontrolery są
stosowane
prawidłowo.
Dzięki
poprawnej
identyfikacji
stanów
operacyjnych
i
programistycznych we właściwościach deklaracyjnych aplikacji wdrożonej w Kubernetes
organizacja może powiązać narzędzia i automatyzację w celu jeszcze łatwiejszego zarządzania
skomplikowanymi procesami uaktualnień, wdrożeniami, a także możliwościami w zakresie
wydawania oprogramowania.
Rozdział 7. Rozpowszechnianie
aplikacji na świecie i jej wersje
robocze
Dotąd w tej książce miałeś okazję poznać wiele spośród najlepszych praktyk związanych z
opracowywaniem, kompilowaniem i wdrażaniem aplikacji. Jednak mamy jeszcze do omówienia
zupełnie inny zbiór kwestii dotyczących wdrażania aplikacji, która ma być dostępna na całym
świecie, i zarządzania nią.
Jest wiele różnych powodów, dla których może być konieczne skalowanie aplikacji do postaci
wdrożenia globalnego. Pierwszym, oczywistym, jest po prostu skala. Opracowana przez Ciebie
aplikacja mogła odnieść ogromny sukces lub mieć na tyle krytyczne znaczenie, że konieczne
będzie jej wdrożenie na całym świecie tak, aby można było zapewnić możliwości wystarczające
do obsługi użytkowników. Przykładami takich aplikacji są te zawierające bramy API dla
publicznych dostawców chmury, projekty IoT (ang. internet of things) o światowym zasięgu i
serwisy społecznościowe, które osiągnęły ogromną popularność.
Większość czytelników nie będzie musiała zajmować się tworzeniem systemów wymagających
dostępności na skalę światową, ale wiele aplikacji może tego wymagać w celu zmniejszenia
opóźnienia w działaniu. Nawet w przypadku kontenerów i Kubernetes nie sposób osiągnąć
prędkości światła. Dlatego też czasami aplikację trzeba wdrożyć na całym świecie, aby
zmniejszyć fizyczną odległość dzielącą ją od użytkowników i tym samym zminimalizować
opóźnienia.
Znacznie częstszym powodem dystrybucji globalnej jest lokalizacja aplikacji. To może mieć
związek z przepustowością (np. zdalnej platformy) lub z polityką prywatności (ograniczenia
geograficzne). Czasami, aby aplikacja mogła odnieść sukces lub w ogóle mogła funkcjonować,
może być konieczne jej wdrożenie w określonych lokalizacjach.
W tych wszystkich przypadkach aplikacja już nie znajduje się w jedynie niewielkiej liczbie
klastrów produkcyjnych. Zamiast tego zostaje rozproszona w setki, lub nawet tysiące położeń
geograficznych, a zarządzanie nimi oraz globalne udostępnienie usługi stają się poważnym
wyzwaniem. W tym rozdziale zostaną przedstawione podejścia i praktyki, które pomagają
przeprowadzić taką operację.
Rozpowszechnianie obrazu aplikacji
Zanim w ogóle rozważysz globalne udostępnienie aplikacji, zawierający ją obraz musi być
dostępny w klastrach rozmieszczonych na całym świecie. Pierwszą kwestią do rozważenia jest
to, czy rejestr obrazów ma funkcję automatycznej georeplikacji. Wiele rejestrów obrazów
utworzonych przez dostawców chmury będzie automatycznie rozpowszechniało obraz na całym
świecie, aby w ten sposób żądanie kierowane do danego obrazu było obsługiwane przez klaster
fizycznie znajdujący się najbliżej użytkownika. Wielu dostawców chmury pozwala na wybór
miejsc, w których obraz ma być replikowany. Przykładowo możesz wiedzieć, że w pewnych
położeniach geograficznych Twoja aplikacja będzie niedostępna. Rejestrem umożliwiającym
replikację globalną jest np. rejestr kontenerów Microsoft Azure (https://azure.microsoft.com/pl-
pl/services/container-registry/), przy czym inni dostawcy też udostępniają podobne usługi. Jeżeli
korzystasz z oferowanego przez dostawcę chmury rejestru zapewniającego możliwość
georeplikacji, wówczas rozpowszechnianie aplikacji na świecie jest bardzo łatwe. Musisz
umieścić obraz w rejestrze, następnie wybrać obszary geolokalizacji, a resztą zajmie się rejestr.
Natomiast jeśli nie korzystasz z rejestru oferowanego przez dostawcę chmury lub wybrany
dostawca nie oferuje możliwości automatycznej geolokalizacji obrazów, wówczas problem
będziesz musiał rozwiązać samodzielnie. Jedną z możliwości jest wykorzystanie rejestru
znajdującego się na określonym obszarze. Trzeba uwzględnić wiele różnych kwestii związanych
z takim podejściem. Opóźnienie podczas pobierania obrazów często decyduje o szybkości, z
jaką można uruchamiać kontenery w klastrze. To z kolei określa szybkość, z jaką można
reagować na awarię komputera, biorąc pod uwagę to, że ogólnie w razie awarii komputera
konieczne jest pobranie obrazu kontenera do nowej maszyny.
Następną kwestią związaną z pojedynczym rejestrem jest to, że może on być pojedynczym
miejscem awarii. Jeżeli ten rejestr znajduje się w jednym regionie lub w jednym centrum
danych, wówczas istnieje niebezpieczeństwo, że przestanie być dostępny na skutek incydentu o
dużym zasięgu. Jeżeli rejestr stanie się niedostępny, oparte na technikach ciągłej integracji i
ciągłego wdrażania rozwiązanie przestanie działać i utracisz możliwość wdrażania nowego
kodu. To oczywiście ma istotny wpływ zarówno na produktywność programisty, jak i działanie
aplikacji. Ponadto pojedynczy rejestr może być znacznie kosztowniejszy, ponieważ każda
operacja uruchamiania nowego kontenera będzie się wiązać z dużym użyciem przepustowości.
Nawet jeśli obrazy kontenera są dość małe, to użycie przepustowości będzie się sumowało.
Pomimo wymienionych wad wariant oparty na jednym rejestrze może być odpowiedni dla
niewielkich aplikacji dostępnych w kilku globalnych regionach. To rozwiązanie zdecydowanie
prostsze niż definiowanie pełnej replikacji obrazu.
Jeżeli nie można wykorzystać georeplikacji oferowanej przez dostawcę chmury, a konieczna jest
replikacja obrazu, wówczas trzeba będzie przygotować własne rozwiązanie w tym zakresie.
Podczas implementacji takiej usługi masz dwie możliwości. Pierwsza to użycie nazw
geograficznych dla każdego rejestru obrazu (np. us.my-registry.io, eu.my-registry.io itd.).
Zaletą takiego podejścia jest łatwość jego przygotowania i zarządzania nim. Poszczególne
rejestry są całkowicie niezależne i można je umieścić na końcu rozwiązania opartego na
technikach ciągłej integracji i ciągłego wdrażania. Wadą takiego rozwiązania jest to, że każdy
klaster będzie wymagał nieco innej konfiguracji w celu pobierania obrazu z najbliższego
geograficznie położenia. Jednak biorąc pod uwagę to, że prawdopodobnie będą występować
różnice geograficzne w konfiguracji aplikacji, ta wada okazuje się stosunkowo niewielka i łatwa
do usunięcia, a przy tym na pewno i tak już występuje w środowisku.
Parametryzacja wdrożenia
Gdy obrazy są replikowane wszędzie, być może trzeba będzie parametryzować wdrożenia dla
różnych położeń geograficznych. Podczas wdrażania aplikacji będą występowały różnice
zależne od regionu, w którym znajduje się użytkownik. Przykładowo, jeśli nie korzystasz z
rejestru stosującego georeplikację, wówczas prawdopodobnie będziesz musiał zmodyfikować
nazwę obrazu i przystosować ją do różnych regionów. Nawet w przypadku stosowania
georeplikacji wciąż istnieje możliwość, że obciążenie aplikacji będzie się zmieniało w zależności
od położenia geograficznego. Dlatego też wielkość (czyli liczba replik) i konfiguracja mogą być
różne w poszczególnych regionach. Zarządzanie tą złożonością tak, by nie wiązało się to z
nadmiernym wysiłkiem, jest kluczowe, jeśli globalne wdrożenie aplikacji ma się udać.
Pierwszą kwestią do rozważenia jest sposób organizacji poszczególnych konfiguracji na dysku.
Częstym rozwiązaniem stosowanym w tym zakresie jest używanie osobnych katalogów dla
poszczególnych regionów geograficznych. Gdy osobne katalogi są dostępne, kusząca może być
możliwość skopiowania jednej konfiguracji do każdego z nich. Jednak takie rozwiązanie
doprowadzi do przesunięć i zmian między konfiguracjami, w których pewne regiony zostaną
zmodyfikowane, inne natomiast zostaną pominięte. Zamiast tego najlepiej zastosować podejście
oparte na szablonach, w którym większość konfiguracji będzie zdefiniowana w pojedynczym
szablonie, współdzielonym przez wszystkie regiony. Następnie parametry dla tego szablonu
pozwolą na wygenerowanie szablonów przeznaczonych dla konkretnych regionów. Menedżer
pakietów Helm (https://helm.sh/) to najczęściej używane narzędzie w przypadku tego typu
rozwiązań opartych na szablonach (więcej informacji na ten temat znajdziesz w rozdziale 2.).
Mechanizm równoważenia obciążenia
związanego z ruchem sieciowym w
globalnie wdrożonej aplikacji
Skoro aplikacja została wdrożona globalnie, następnym krokiem jest ustalenie, jak
przekierować do niej ruch sieciowy. Ogólnie rzecz biorąc, należy skorzystać z zalet bliskości
geograficznej, aby w ten sposób zagwarantować niskie opóźnienie podczas dostępu do usługi.
Prawdopodobnie chcesz również przygotować odporne na awarie rozwiązanie stosowane w
różnych regionach geograficznych na wypadek awarii lub niedostępności któregokolwiek z
innych źródeł usługi. Poprawne skonfigurowanie mechanizmu równoważenia obciążenia
związanego z ruchem sieciowym w różnych wdrożeniach regionalnych ma kluczowe znaczenie
podczas przygotowywania systemu charakteryzującego się wysoką wydajnością i
niezawodnością działania.
Rozpoczynamy od przyjęcia założenia o istnieniu pojedynczego hosta, który będzie udostępniał
usługę, np. myapp.myco.com. Jedna z pierwszych decyzji dotyczy użycia protokołu DNS (ang.
domain name service) do implementacji mechanizmu równoważenia obciążenia między
regionalnymi punktami końcowymi. Jeżeli w celu równoważenia obciążenia korzystasz z usługi
DNS, adres IP zwracany po wykonaniu przez użytkownika zapytania DNS do myapp.myco.com
będzie zależał zarówno od położenia użytkownika uzyskującego dostęp do usługi, jak i od jej
bieżącej dostępności.
Niezawodne wydawanie oprogramowania
udostępnianego globalnie
Po utworzeniu dla aplikacji szablonów pozwalających na poprawne zdefiniowanie konfiguracji
dla poszczególnych regionów trzeba rozwiązać następny ważny problem, związany ze sposobem
wdrożenia tej konfiguracji na całym świecie. Może Cię kusić, żeby wdrożyć aplikację na całym
świecie jednocześnie, co pozwoliłoby na jej efektywną i szybką iterację. Jednak takie podejście,
choć uznawane za zwinne, może doprowadzić do globalnej niedostępności aplikacji. W
większości aplikacji produkcyjnych zamiast niego o wiele lepiej będzie zastosować inne, które
polega na znacznie ostrożniejszym wdrażaniu oprogramowania. W połączeniu z m.in. globalnym
mechanizmem równoważenia obciążenia wspomniane podejścia pozwalają na zachowanie
wysokiej dostępności aplikacji nawet w razie jej poważnej awarii.
Gdy pojawia się problem globalnego wdrożenia, celem jest jak najszybsze udostępnienie
oprogramowania przy jednoczesnym wykryciu potencjalnych problemów — najlepiej zanim
dotkną użytkowników. Przyjmujemy założenie, że przed tym, jak rozpoczniesz globalne
udostępnianie aplikacji, zaliczy ona podstawowe testy związane z funkcjonowaniem i
obciążeniem. Zanim dany obraz (lub obrazy) zostanie certyfikowany do globalnego wdrożenia,
powinien zostać dokładnie przetestowany, abyś miał pewność, że aplikacja działa poprawnie.
Trzeba zwrócić uwagę na jedno: to nie oznacza, że aplikacja działa poprawnie. Wprawdzie
testowanie pozwala wychwycić wiele problemów, ale w rzeczywistości problemy często są
zauważane dopiero po publicznym udostępnieniu aplikacji, gdy zaczyna ona obsługiwać
produkcyjny ruch sieciowy. To wynika z faktu, że natura produkcyjnego ruchu sieciowego
często utrudnia jego doskonałą symulację. Być może aplikacja została przetestowana z danymi
wejściowymi w tylko jednym języku, podczas gdy po udostępnieniu musi sobie radzić z danymi
wejściowymi w różnych językach. Być może przygotowane testy danych wejściowych okazały
się wystarczające do sprawdzenia aplikacji z rzeczywistymi danymi wejściowymi. Oczywiście za
każdym razem, gdy w środowisku produkcyjnym zostaje ujawniony błąd niewychwycony na
etapie testów, można to uznać za wskazówkę informującą o konieczności przeprowadzania
znacznie bardziej rozszerzonego zestawu testów. Należy jednak mieć świadomość, że wiele
problemów można wychwycić dopiero po wdrożeniu aplikacji do środowiska produkcyjnego.
Z tego względu każde wdrożenie w kolejnym regionie może ujawnić nowy problem. Ponieważ
region jest regionem produkcyjnym, mamy do czynienia z potencjalną niedostępnością aplikacji
i na tę sytuację trzeba będzie zareagować.
Weryfikacja przed wydaniem oprogramowania
Zanim nawet rozważysz wydanie określonej wersji oprogramowania na całym świecie, najpierw
musisz je sprawdzić za pomocą pewnego rodzaju syntetycznego środowiska testowego. Jeżeli
właściwie przygotowałeś rozwiązanie w zakresie ciągłego wdrażania, wówczas jeszcze przed
kompilacją określonej wersji są wykonywane pewne testy jednostkowe, a prawdopodobnie
także, w ograniczonym zakresie, testy integracji. Jednak nawet jeżeli stosowany jest etap
testowania, trzeba rozważyć zastosowanie dwóch rodzajów testów skompilowanej aplikacji,
zanim zostanie publicznie udostępniona. Pierwszy to pełny zakres testów integracji. To oznacza
sprawdzenie całego stosu w pełnym wdrożeniu aplikacji, choć bez udziału rzeczywistego ruchu
sieciowego. Ten pełny stos będzie obejmował kopię danych produkcyjnych lub dane
symulowane o takiej samej wielkości i skalowanie zgodnie z faktycznymi danymi
produkcyjnymi. Jeżeli w rzeczywistości aplikacja używa danych o wielkości 500 GB, wówczas
krytyczne znaczenie ma przetestowanie przedprodukcyjnej wersji aplikacji z danymi o mniej
więcej podobnej wielkości (najlepiej z dosłownie tym samym zbiorem danych).
Ogólnie rzecz biorąc, to jest najtrudniejszy etap podczas przygotowywania pełnego środowiska
testów integracji. Bardzo często się zdarza, że dane produkcyjne istnieją jedynie w środowisku
produkcyjnym, a wygenerowanie syntetycznego zbioru danych o takiej samej wielkości i skali
jest dość trudne. Z powodu tej trudności przygotowanie zbioru danych dla testów integracji,
który będzie możliwie zbliżony do rzeczywistego, to doskonały przykład zadania, które opłaca
się wykonać na wczesnym etapie pracy nad aplikacją. Jeżeli wcześniej utworzysz syntetyczną
kopię zbioru danych, gdy jest on jeszcze dość mały, wówczas dane testów integracji będą
zwiększały się stopniowo, w tym samym tempie, co dane produkcyjne. Takie rozwiązanie jest
zdecydowanie łatwiejsze do zarządzania niż próba powielenia danych produkcyjnych, których
skala już jest duża.
Niestety, wiele osób nie zdaje sobie sprawy z konieczności utworzenia kopii danych aż do
chwili, gdy skala tych danych jest już ogromna, a samo zadanie bardzo trudne. W takich
przypadkach można wdrożyć warstwę odczytu i zapisu dla produkcyjnego magazynu danych.
Oczywiście nie chcemy, aby testy integracji przeprowadzały operacje zapisu w danych
produkcyjnych. Często istnieje możliwość skonfigurowania dla produkcyjnego magazynu
danych proxy pozwalającego na odczytywanie danych produkcyjnych, ale zapisywanie ich w
tabeli, która będzie sprawdzana podczas kolejnych operacji odczytu.
Niezależnie od sposobu, w jaki zarządzasz środowiskiem testów integracji, cel pozostaje taki
sam: sprawdzenie, czy aplikacja działa zgodnie z oczekiwaniami po otrzymaniu serii testowych
danych wejściowych i akcji. Mamy do dyspozycji wiele rozwiązań w zakresie definiowania i
wykonywania takich testów — od właściwie w całości ręcznego, będącego połączeniem testów i
pracy człowieka (takie podejście jest niezalecane ze względu na dość dużą podatność na błędy),
aż po testy symulujące działanie przeglądarek WWW i użytkowników, np. przez kliknięcia.
Gdzieś pomiędzy znajdują się testy przeznaczone dla API REST, ale ich wykonywanie względem
interfejsu użytkownika opartego na tym API nie jest niezbędne. Niezależnie od sposobu
zdefiniowania testów integracji cel powinien być ten sam: zautomatyzowany zestaw testów
pozwalających na sprawdzenie poprawności działania aplikacji w reakcji na pełny zbiór danych
wejściowych odpowiadających tym rzeczywistym. W przypadku prostych aplikacji istnieje
możliwość przeprowadzenia takiej operacji sprawdzenia na wczesnym etapie testowania,
natomiast w większości ogromnych aplikacji konieczne będzie użycie pełnego środowiska
testów integracji.
Testy integracji będą sprawdzały poprawność działania aplikacji. Powinieneś sprawdzić również
zachowanie aplikacji pod obciążeniem. Upewnienie się co do poprawności działania aplikacji
jako takiego to jedno, ale jej poprawne działanie również pod obciążeniem to już zupełnie inna
kwestia. W każdym rozsądnym systemie o dużej skali znaczna regresja wydajności działania —
np. 20-procentowe zwiększenie opóźnienia podczas obsługi żądania — ma poważny wpływ na
UX i aplikację. Poza tym, że wywoła frustrację użytkowników, może doprowadzić do pełnej
awarii aplikacji. Dlatego też trzeba się upewnić, że wspomniana regresja nie wystąpi w
środowisku produkcyjnym.
Podobnie jak w przypadku testów integracji ustalenie odpowiedniego sposobu testowania
aplikacji pod obciążeniem może być dość skomplikowanym zadaniem. Konieczne będzie
wygenerowanie obciążenia podobnego do istniejącego w środowisku produkcyjnym, choć
trzeba to będzie zrobić w sposób syntetyczny i możliwy do odtworzenia. Jednym z
najłatwiejszych rozwiązań jest wykorzystanie dzienników zdarzeń ruchu sieciowego, który
został obsłużony w rzeczywistych systemach produkcyjnych. To doskonały sposób na
sprawdzenie aplikacji pod obciążeniem, którego charakterystyka będzie odpowiadała
obciążeniu obsługiwanemu przez aplikację po jej wdrożeniu w produkcji. Jednak wykorzystanie
dzienników zdarzeń nie zawsze jest możliwe. Przykładowo, jeśli te dzienniki zdarzeń są stare, a
aplikacja lub zbiór danych się zmieniły, wówczas jest prawdopodobne, że wydajność działania
po zastosowaniu tych starych dzienników zdarzeń będzie inna niż wydajność podczas obsługi
bieżącego ruchu sieciowego. Ponadto, jeśli istnieją rzeczywiste zależności, które nie są
imitowane, wówczas może się zdarzyć, że stary ruch sieciowy po przekazaniu go przez te
zależności będzie niepoprawny (np. dane mogą już nie istnieć).
Z wymienionych powodów wiele systemów, także tych o krytycznym znaczeniu, było przez długi
czas opracowywanych bez użycia testów pod obciążeniem. Podobnie jak w przypadku
modelowania danych produkcyjnych jest to przykład zadania, które będzie łatwiej wykonać,
jeśli zostanie zainicjowane na wczesnym etapie pracy nad aplikacją. Jeżeli przygotowujesz test
mający na celu sprawdzenie aplikacji pod obciążeniem, ma ona jedynie kilka zależności, a
usprawnienia i iteracje tego testu wprowadzasz wraz z jej rozwojem, wówczas będziesz miał
znacznie łatwiejsze zadanie niż w przypadku istniejącej aplikacji o dużej skali.
Jeśli założymy, że przygotowałeś test sprawdzający działanie aplikacji pod obciążeniem,
następną kwestią są wskaźniki do obserwowania podczas takiego testu. Na myśl nasuwa się
liczba żądań na sekundę i opóźnienie w trakcie obsługi żądania, ponieważ nie ulega
wątpliwości, że będą one związane z wrażeniami użytkownika.
Podczas pomiaru opóźnienia trzeba zwrócić uwagę na to, że tak naprawdę mamy do czynienia z
rozkładem prawdopodobieństwa. Trzeba sprawdzić zarówno średnie opóźnienie, jak i skrajne
percentyle (np. 90. i 99.), ponieważ przedstawiają one „najgorszą” wartość UX dla aplikacji.
Problemy związane z dużym opóźnieniem mogą być niewidoczne, gdy zwracasz uwagę jedynie
na wartość średnią. Jednak gdy 10% użytkowników doświadcza dużego opóźnienia podczas
obsługi żądań, to może mieć istotnie wpłynąć na to, czy dany produkt osiągnie sukces.
Warto również zwracać uwagę na poziom użycia zasobów (procesora, pamięci, sieci, dysku)
przez aplikację pod obciążeniem. Wprawdzie te wskaźniki nie mają bezpośredniego przełożenia
na UX, ale ogromne zmiany w poziomie użycia zasobów przez aplikację powinny zostać
zidentyfikowane i wyjaśnione na etapie testów przed jej umieszczeniem w środowisku
produkcyjnym. Jeżeli aplikacja zaczyna nagle zużywać dwa razy więcej pamięci, to warto się
tym zająć, nawet jeśli test aplikacji pod obciążeniem
wzrost zużycia poziomu zasobów będzie miał negatywny
W zależności od okoliczności można kontynuować
środowisku produkcyjnym i jednocześnie postarać się
poziomie zużycia zasobów.
zakończy się sukcesem. Tak znaczny
wpływ na jakość i dostępność aplikacji.
operacje umieszczenia aplikacji w
zrozumieć, skąd wzięła się zmiana w
Region kanarkowy
Gdy wydaje się, że aplikacja działa poprawnie, pierwszym etapem powinno być jej wdrożenie w
tzw. regionie kanarkowym. To wdrożenie otrzymujące rzeczywisty ruch sieciowy od
użytkowników i zespołów, które chcą potwierdzić poprawność wdrożenia. To mogą być zespoły
wewnętrzne używające danej usługi lub korzystający z niej klienci zewnętrzni. Region
kanarkowy istnieje, aby dostarczyć programistom wczesnych ostrzeżeń o tym, że wprowadzane
zmiany mogą coś zepsuć. Niezależnie od jakości testów integracji i aplikacji pod obciążeniem
zawsze istnieje niebezpieczeństwo przeoczenia jakiegoś błędu niewychwytywanego przez testy,
a zarazem mającego znaczenie krytyczne dla pewnych użytkowników lub klientów. W takich
przypadkach znacznie lepszym rozwiązaniem będzie wychwycenie tych problemów w
środowisku, w którym każdy, kto używa usługi lub ją wdraża, ma świadomość, że jest większe
niebezpieczeństwo jej awarii. Do tego celu służy właśnie region kanarkowy.
Region kanarkowy musi być traktowany jako produkcyjny w kategoriach monitorowania, skali,
funkcjonalności itd. Jednak skoro to pierwszy przystanek w procesie wydawania aplikacji, jest
to zarazem miejsce, w którym można wykryć błędne wydanie. To nie stanowi problemu, a
szczerze mówiąc, nawet jest celem istnienia regionu kanarkowego. Twoi klienci świadomie
używają regionu kanarkowego do zadań o mniejszym stopniu ryzyka (np. do programowania lub
dla użytkowników wewnętrznych), więc będą mogli wcześniej zasygnalizować niewłaściwe
zmiany, które mogły zostać uwzględnione w danym wydaniu.
Skoro celem regionu kanarkowego jest jak najwcześniejsze zapewnienie informacji dotyczących
danego wydania, dobrze jest pozostawić wydanie w tym regionie przez kilka dni. Dzięki temu
większa grupa klientów będzie mogła uzyskać do niego dostęp, zanim zostanie przekazane do
następnych regionów. Tych kilka dni jest potrzebnych, ponieważ prawdopodobieństwo
wystąpienia błędu może być małe (np. dla 1% żądań) lub błąd ujawnia się w przypadkach
skrajnych. Błąd nawet nie musi być na tyle poważny, aby wywoływał zautomatyzowane
ostrzeżenia, ale może być związany z logiką biznesową i ujawniać się tylko podczas interakcji
aplikacji z klientem.
Identyfikacja typów regionów
Gdy zaczynasz zastanawiać się nad wydaniem oprogramowania na całym świecie, pod uwagę
musisz wziąć różne cechy charakterystyczne poszczególnych regionów. Po rozpoczęciu operacji
przekazywania oprogramowania do regionów produkcyjnych trzeba będzie przeprowadzić testy
integracji oraz początkowe testowanie kanarkowe. To oznacza, że wszelkie znalezione problemy
będą problemami, które nie ujawniały się w żadnych z wymienionych wcześniej kategorii.
Zastanów się nad poszczególnymi regionami. Czy którykolwiek z nich będzie otrzymywał
większy ruch sieciowy niż pozostałe? Czy dostęp do niego odbywa się w odmienny sposób?
Przykładem takiej różnicy może być to, że w krajach rozwijających się jest bardziej
prawdopodobne, że ruch sieciowy będzie pochodził z mobilnych przeglądarek WWW. Dlatego
też region znajdujący się geograficznie bliżej krajów rozwijających się będzie się
charakteryzował większą ilością mobilnego ruchu sieciowego niż obserwowany w regionie
testowym lub kanarkowym.
Następnym przykładem może być język danych wejściowych. W regionach, w których
mieszkańcy posługują się językami innymi niż angielski, częściej mogą być używane znaki
Unicode, co z kolei może ujawnić błędy związane z obsługą ciągów tekstowych lub znaków.
Jeżeli tworzysz usługę opartą na API, wybrane API mogą osiągnąć większą popularność w
pewnych regionach niż pozostałe. Wszystkie te sytuacje są przykładami różnic, które mogą
występować w aplikacji, a niekoniecznie zostaną ujawnione podczas obsługi przez aplikację
kanarkowego ruchu sieciowego. Każda z tych różnic może stać się źródłem incydentu w
środowisku produkcyjnym. Powinieneś opracować tabelę różnych cech charakterystycznych,
które według Ciebie mają duże znaczenie. Identyfikacja tych cech pomoże podczas globalnego
wdrażania aplikacji.
Przygotowywanie wdrożenia globalnego
Po zidentyfikowaniu cech charakterystycznych Twoich regionów powinieneś przygotować plan
wdrożenia aplikacji we wszystkich regionach. Oczywiście chcesz zminimalizować wpływ, jaki
będzie miała przerwa w funkcjonowaniu aplikacji. Dlatego też doskonałym regionem do
rozpoczęcia wdrożenia jest region kanarkowy i ten, w którym jest notowany najmniejszy poziom
ruchu sieciowego użytkowników aplikacji. Istnieje znikome niebezpieczeństwo wystąpienia
problemów w takim regionie, a jeśli nawet tak się stanie, ich wpływ będzie zdecydowanie
mniejszy ze względu na mniejszą ilość ruchu sieciowego w tym regionie.
Po zakończonej sukcesem operacji wdrożenia aplikacji w pierwszym regionie produkcyjnym
trzeba podjąć decyzję o czasie oczekiwania przed przejściem do następnego regionu. Powodem
oczekiwania nie jest sztuczne opóźnianie wydania. Należy poczekać na tyle długo, aby
ewentualne problemy miały szansę się ujawnić. Ten okres to ogólny wskaźnik, ile czasu upływa
od zakończenia operacji wdrożenia do chwili, gdy monitorowanie ujawni pierwsze symptomy
problemów. Jeżeli we wdrożeniu znajduje się błąd, wówczas z chwilą zakończenia operacji
wdrożenia ten błąd trafia do infrastruktury. Jednak mimo to ujawnienie się błędu może
wymagać nieco czasu. Przykładowo po wycieku pamięci może upłynąć co najmniej godzina,
zanim monitorowanie pozwoli zauważyć problem lub wyciek zacznie mieć wpływ na
użytkowników. Czas upływający od zakończenia wdrożenia do ujawnienia problemu jest
rozkładem prawdopodobieństwa wskazującym, ile czasu należy odczekać, aby mieć solidne
podstawy do przyjęcia założenia o poprawnym działaniu wdrożonego oprogramowania. Ogólnie
rzecz biorąc, dobrą regułą jest w tym przypadku podwojenie średniej ilości czasu potrzebnego
na ujawnienie się problemu.
Jeżeli w ciągu ostatnich 6 miesięcy ujawnienie błędu następowało średnio po godzinie od
zakończenia wdrożenia, wówczas, jeśli odczekasz 2 godziny i nic złego się w tym czasie nie
wydarzy, możesz przyjąć założenie, że wdrożenie najpewniej zakończyło się sukcesem. Jeżeli
pobierasz jeszcze większą (i znacznie bardziej rozszerzoną) ilość danych statystycznych na
podstawie historii aplikacji, możesz jeszcze dokładniej oszacować ten czas.
Po zakończeniu sukcesem wdrożenia w regionie kanarkowym o niewielkiej ilości ruchu
sieciowego następnym krokiem jest wdrożenie w regionie kanarkowym o większym poziomie
ruchu sieciowego. To region, w którym dane wejściowe są podobne do tych w regionie
kanarkowym, ale jest ich znacznie więcej. Skoro udało się z sukcesem wdrożyć aplikację w
podobnym regionie, ale o mniejszym poziomie ruchu sieciowego, to w tym drugim przypadku
tak naprawdę jedynie testujemy możliwości aplikacji w zakresie skalowania. Jeżeli i to
wdrożenie zakończy się sukcesem, będziesz mógł być pewny jakości danego wydania.
Po wdrożeniu w regionie kanarkowym o dużym poziomie ruchu sieciowego ten sam wzorzec
można zastosować dla innych potencjalnych różnic w ruchu sieciowym. Przykładowo kolejnym
krokiem może być wdrożenie aplikacji w regionie o małym natężeniu ruchu sieciowego w Azji
lub Europie. Na tym etapie może Cię kusić, by przyspieszyć operację wdrażania. Jednak
krytyczne znaczenie ma wdrażanie w tylko jednym regionie, przedstawiającym pewną znaczącą
zmianę w danych wejściowych lub obciążeniu aplikacji. Gdy masz pewność, że wdrożenie
zostało dokładnie przetestowane pod kątem wszelkich możliwych odchyleń w produkcyjnych
danych wejściowych aplikacji, możesz zacząć stosować równoległość podczas jej wydawania. W
ten sposób przyspieszysz operację, a zarazem zachowasz pewność, że przebiega poprawnie, i
będziesz mógł zakończyć wdrożenie sukcesem.
Gdy coś pójdzie nie tak
Dotychczas poznałeś fragmenty układanki przedstawiającej globalne wdrożenie systemu
oprogramowania i miałeś okazję zobaczyć, jaka struktura wdrożenia pozwala na
zminimalizowanie niebezpieczeństwa, że coś pójdzie źle. Jednak co można zrobić w sytuacji,
gdy faktycznie coś pójdzie nie tak? Wszyscy ratownicy wiedzą, że w gorączce i panice mózg
człowieka znajduje się w dużym stresie i znacznie trudniej jest zapamiętać nawet najprostsze
procesy. Jeżeli dodać do tego ciśnienie związane z awarią, wówczas każdy pracownik w firmie,
łącznie z szefem, czeka na sygnał, że wszystko jest w porządku. W sytuacji takiego napięcia
bardzo łatwo jest popełnić błąd. Ponadto w takich sytuacjach prosta pomyłka, taka jak
pominięcie określonego kroku w procesie naprawy systemu po awarii, może doprowadzić do
znacznego pogorszenia sytuacji.
Z wymienionych tutaj powodów krytyczne znaczenie ma możliwość szybkiej i właściwej reakcji
na problem, który wystąpił podczas wdrożenia oprogramowania. Aby zagwarantować, że
zostało zrobione wszystko, co można było zrobić, a na dodatek odbyło się to we właściwej
kolejności, dobrze jest przygotować listę rzeczy do zrobienia, ułożoną w odpowiedniej
kolejności i z zapisanymi oczekiwanymi danymi wyjściowymi poszczególnych kroków. Zapisz
każdy krok, nawet ten najbardziej oczywisty. W sytuacji kryzysowej nawet te najłatwiejsze i
oczywiste kroki mogą być tymi, które się przypadkowo pominie.
Jednym ze sposobów, w które ratownicy upewniają się co do właściwości swoich działań w
wysoce stresujących sytuacjach, są ćwiczenia przeprowadzane bez stresu związanego z
wystąpieniem sytuacji kryzysowej. Te same reguły mają zastosowanie do wszystkich działań,
które możesz podejmować w odpowiedzi na problem pojawiający się podczas wdrożenia.
Rozpocznij od identyfikacji wszystkich kroków niezbędnych do reakcji na problem i
przeprowadź operację wycofania wdrożenia. W idealnej sytuacji pierwszą reakcją jest
„zatrzymanie krwawienia” i przeniesienie ruchu sieciowego użytkowników z regionu
dotkniętego problemem do regionu, w którym system działa bezbłędnie. To pierwsza rzecz,
którą należy przećwiczyć. Czy jesteś w stanie z powodzeniem przekierować region do
poprawnie działającego? Ile czasu na to potrzebujesz?
Podczas pierwszej próby przekierowania ruchu sieciowego za pomocą opartego na protokole
DNS mechanizmu równoważenia obciążenia przekonasz się, jak długo i na ile sposobów
komputery buforują informacje dotyczące DNS-a. Pełne przekierowanie ruchu sieciowego z
regionu dotkniętego awarią wymaga niemalże całego dnia, gdy używane jest narzędzie oparte
na DNS-ie. Niezależnie od wyniku podjętej próby przekierowania ruchu sieciowego zanotuj go.
Co w trakcie tej operacji sprawdziło się doskonale? Co poszło kiepsko? Mając te dane, przyjmij
cel określający, ile czasu powinno zabrać przekierowanie ruchu sieciowego. Cel zdefiniuj np. w
następujący sposób: „możliwość przekierowania 99% ruchu sieciowego w czasie poniżej 10
minut”. Ćwicz tak długo, aż będziesz w stanie osiągnąć założony cel. Być może konieczne
będzie wprowadzenie zmian architektonicznych, aby cel stał się osiągalny. Być może trzeba
będzie zdefiniować pewien rodzaj automatyzacji, aby człowiek nie musiał ręcznie kopiować i
wklejać poleceń. Niezależnie od niezbędnych zmian nieustanne ćwiczenia pozwolą
zagwarantować, że będziesz lepiej przygotowany do reakcji na potencjalne incydenty. Ponadto
zorientujesz się, które aspekty systemu wymagają usprawnień.
Ten sam rodzaj ćwiczeń ma zastosowanie względem każdej akcji, którą możesz podejmować w
systemie. Powinieneś przećwiczyć odzyskiwanie danych na pełną skalę. Należy przećwiczyć
globalne przywrócenie systemu do poprzedniej wersji. Przyjmij cele określające ilość czasu, jaki
jest potrzebny na przeprowadzenie tych operacji. Zapisz informacje o wszystkich miejscach, w
których zostały popełnione błędy. Dodaj procedury weryfikacyjne do automatyzacji, aby
wyeliminować możliwość popełnienia błędów. Praktyczne sprawdzenie reakcji na incydenty da
Ci pewność, że w momencie faktycznego wystąpienia problemu będziesz umiał odpowiednio na
niego zareagować. Podobnie jak każdy ratownik nieustannie ćwiczy i się rozwija, także Ty
musisz regularnie ćwiczyć i się upewnić, że każdy członek zespołu potrafił właściwie
zareagować i (co jeszcze ważniejsze) że będzie umiał to zrobić w miarę wprowadzania zmian w
systemie.
Najlepsze praktyki dotyczące globalnego
wdrożenia aplikacji
Wszystkie obrazy powinny być rozpowszechnione na całym świecie. Sukces wdrożenia
zależy od tego, czy wszystkie elementy wydania (pliki binarne, obrazy itd.) znajdują się jak
najbliżej miejsca, w którym zostaną użyte. To gwarantuje również niezawodność
wdrożenia w przypadku wolniejszego lub zakłóconego działania sieci. Rozkład
geograficzny powinien zostać uwzględniony w zautomatyzowanym rozwiązaniu, ponieważ
to gwarantuje zachowanie spójności.
Jak największą liczbę testów powinieneś przesunąć w lewą stronę przez przygotowanie
rozbudowanych testów integracji oraz powtarzanie testów aplikacji, o ile to możliwe.
Wdrożenie należy rozpocząć tylko wtedy, gdy jest się absolutnie przekonanym o
poprawnym działaniu aplikacji.
Wdrożenie rozpocznij w regionie kanarkowym, czyli środowisku przedprodukcyjnym, w
którym inne zespoły lub ogromni klienci mogą zweryfikować swój sposób użycia Twojej
usługi, zanim rozpoczniesz wdrożenie na ogromną skalę.
Zidentyfikuj odmienne cechy charakterystyczne regionów, w których ma się odbyć
wdrożenie. Każda różnica może być tą, która doprowadzi do awarii oraz pełnej lub
częściowej niedostępności aplikacji. Spróbuj wdrożyć aplikację najpierw w regionach o
niewielkim ryzyku.
Dokumentuj i ćwicz reakcje na wszelkie problemy oraz procesy (np. wycofanie
wdrożenia), które możesz napotkać. Próba zapamiętania w sytuacji kryzysowej tego, co
powinno być zrobione, to prosta droga do pominięcia czegoś, a tym samym znacznego
pogorszenia sytuacji.
Podsumowanie
Wprawdzie dzisiaj to może wydawać się nieprawdopodobne, ale pewnego dnia większość z nas
stanie przed koniecznością wprowadzenia wdrożenia aplikacji na całym świecie. Z tego
rozdziału dowiedziałeś się, jak można stopniowo przygotować system, aby stał się prawdziwie
globalnym projektem. Zobaczyłeś również, jak skonfigurować wdrożenie, by zapewnić
minimalizację czasu przestoju systemu podczas jego uaktualniania. Zaprezentowaliśmy także
sposób opracowania i przećwiczenia procesów oraz procedur niezbędnych do wdrożenia, gdy
coś pójdzie nie tak (zwróć uwagę na brak w tym zdaniu słowa „jeżeli”).
Rozdział 8. Zarządzanie
zasobami
W tym rozdziale skoncentrujemy się na najlepszych praktykach związanych z zarządzaniem
zasobami Kubernetes i ich optymalizacją. Omówione zostaną kwestie dotyczące mechanizmu
zarządcy procesów, zarządzania klastrem, zarządzania zasobami poda, zarządzania przestrzenią
nazw oraz skalowania aplikacji. Zagłębimy się również w wybrane z zaawansowanych technik
mechanizmu zarządcy procesów, które Kubernetes oferuje poprzez podobieństwo, brak
podobieństwa, wartości taint, tolerancję i właściwość nodeSelector.
Dowiesz się także, jak można implementować mechanizmy ograniczania zasobów, żądań
zasobów, jakości usługi poda, PodDisruptionBudgets, LimitRangers i polityki braku
podobieństwa.
Zarządca procesów w Kubernetes
Zarządca procesów (ang. scheduler) w Kubernetes to jeden z podstawowych komponentów
istniejących na płaszczyźnie kontrolnej. Pozwala on Kubernetes na podejmowanie decyzji
związanych z umieszczaniem podów wdrażanych w klastrze. Zarządca procesów ma do
czynienia z optymalizacją zasobów na podstawie ograniczeń klastra, a także ograniczeń
zdefiniowanych przez użytkownika. Wykorzystywany jest algorytm oceny, którego działanie
opiera się na predykatach i priorytetach.
Predykaty
Pierwszą funkcją używaną przez Kubernetes w celu podejmowania decyzji związanych z
zarządcą procesów jest funkcja predykatu, która pozwala ustalić, w których węzłach pod może
zostać umieszczony. Nakładane jest sztywne ograniczenie, więc wartością zwrotną funkcji
predykatu jest true lub false. Przykładem może być tutaj sytuacja, gdy pod wymaga 4 GB
pamięci RAM, a węzeł nie jest w stanie spełnić tego wymagania. Węzeł zwróci zatem wartość
false i zostanie usunięty ze zbioru tych, w których pod może być uruchomiony. Innym
przykładem jest sytuacja, w której węzeł nie pozwala na tworzenie w nim nowych podów.
Wówczas ten węzeł zostanie usunięty z listy tych, w których pod może być utworzony.
Mechanizm zarządcy procesów sprawdza predykaty na podstawie kolejności ograniczeń i
poziomu ich złożoności. W czasie gdy ta książka powstawała, zarządca procesów przeprowadzał
operacje sprawdzenia pod kątem następujących predykatów:
CheckNodeConditionPred,
CheckNodeUnschedulablePred,
GeneralPred,
HostNamePred,
PodFitsHostPortsPred,
MatchNodeSelectorPred,
PodFitsResourcesPred,
NoDiskConflictPred,
PodToleratesNodeTaintsPred,
PodToleratesNodeNoExecuteTaintsPred,
CheckNodeLabelPresencePred,
CheckServiceAffinityPred,
MaxEBSVolumeCountPred,
MaxGCEPDVolumeCountPred,
MaxCSIVolumeCountPred,
MaxAzureDiskVolumeCountPred,
MaxCinderVolumeCountPred,
CheckVolumeBindingPred,
NoVolumeZoneConflictPred,
CheckNodeMemoryPressurePred,
CheckNodePIDPressurePred,
CheckNodeDiskPressurePred,
MatchInterPodAffinityPred
Priorytety
Podczas gdy predykat zwraca wartość true lub false i uniemożliwia użycie danego węzła przez
mechanizm zarządcy procesów, wartość priorytetu klasyfikuje wszystkie poprawne węzły na
podstawie ich względnej wartości. Oto lista priorytetów ocenianych podczas pracy z węzłami:
EqualPriority
MostRequestedPriority
RequestedToCapacityRatioPriority
SelectorSpreadPriority
ServiceSpreadingPriority
InterPodAffinityPriority
LeastRequestedPriority
BalancedResourceAllocation
NodePreferAvoidPodsPriority
NodeAffinityPriority
TaintTolerationPriority
ImageLocalityPriority
ResourceLimitsPriority
Oceny zostaną dodane, a węzeł otrzyma ostateczną ocenę wskazującą na jego priorytet.
Przykładowo, jeśli pod wymaga wartości 600 millicores1, a dostępne są dwa węzły, z których
jeden zapewnia 900 millicores, a drugi 1800 millicores, wówczas drugi z wymienionych węzłów
będzie miał wyższy priorytet.
W przypadku gdy zostaną zwrócone węzły o takim samym priorytecie, zarządca procesów
wykorzysta funkcję selectHost(), odpowiedzialną za wybór węzła.
Zaawansowane techniki stosowane przez
zarządcę procesów
W większości przypadków Kubernetes doskonale radzi sobie z optymalnym stosowaniem
mechanizmu zarządcy procesów podczas zarządzania podami. Bierze pod uwagę pody i
umieszcza je w węzłach tylko wtedy, gdy mają wystarczającą ilość zasobów. Próbuje również
rozproszyć między węzłami pody tego samego zasobu ReplicaSet i tym samym zwiększyć
dostępność zasobów oraz zrównoważyć poziom ich wykorzystania. Jeśli to okazuje się
niewystarczające, Kubernetes zapewnia elastyczność w zakresie możliwości wpływania na
sposób używania zasobów. Przykładowo pody można umieszczać w strefach dostępności, aby w
ten sposób złagodzić skutki awarii strefowych prowadzących do przestoju aplikacji. Być może
będziesz chciał przenieść pody do określonego hosta, aby działały wydajniej.
Podobieństwo i brak podobieństwa podów
Podobieństwo i brak podobieństwa podów pozwalają na zdefiniowanie reguł prowadzących do
umieszczania podów względem innych podów. Te reguły umożliwiają modyfikację sposobu
działania mechanizmu zarządcy procesów i zmianę podjętych przez niego decyzji związanych z
miejscem umieszczenia poda.
Przykładowo reguła braku podobieństwa pozwoli na rozproszenie na wiele stref centrum
danych podów pochodzących z zasobu ReplicaSet. To się odbywa z użyciem etykiet kluczy w
podach. Przypisanie par klucz-wartość w mechanizmie zarządcy procesów umieszczać pody w
tym samym węźle (podobieństwo) lub uniknąć umieszczania podów w tym samym węźle (brak
podobieństwa).
Spójrz na przykład reguły braku podobieństwa:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: frontend
replicas: 4
template:
metadata:
labels:
app: frontend
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- frontend
topologyKey: "kubernetes.io/hostname"
containers:
- name: nginx
image: nginx:alpinie
Ten manifest wdrożenia NGINX zawiera cztery repliki i selektor etykiet app=frontend.
Omawiane wdrożenie zawiera sekcję podAntiAffinity skonfigurowaną w taki sposób, że
zarządca procesów nie będzie umieszczać tych replik w pojedynczym węźle. Dzięki temu
zyskujemy pewność, że w razie awarii jednego z węzłów nadal będzie istniała wystarczająca
liczba replik NGINX, aby można było udostępniać dane z bufora.
nodeSelector
Sekcja nodeSelector to najłatwiejszy sposób na przypisywanie podów do określonych węzłów.
Mechanizm zarządcy procesów podczas podejmowania decyzji używa selektorów etykiet wraz z
parami klucz-wartość. Przykładowo być może będziesz chciał uruchamiać pody w określonych
węzłach wyposażonych w specjalizowane wyposażenie, np. kartę graficzną. Być może w tym
miejscu zadajesz sobie pytanie: „Czy do tego celu nie można użyć wartości taint węzła?”.
Oczywiście, że można. Różnica polega na tym, że z sekcji nodeSelector korzystasz, gdy chcesz
żądać trybu działania z kartą graficzną, natomiast wartość taint rezerwuje węzeł jedynie dla
zadań związanych z kartą graficzną. Można użyć wartości taint i sekcji nodeSelector razem w
celu zarezerwowania węzłów do zadań związanych z kartą graficzną i wykorzystać wymienioną
sekcję w celu automatycznego wybrania węzła z kartą graficzną.
Oto przykład polecenia pozwalającego
nodeSelector w specyfikacji poda:
na
oznaczenie
węzła
etykietą
i
użycie
sekcji
kubectl label node <nazwa_węzła> disktype=ssd
Przechodzimy teraz do utworzenia specyfikacji z parą klucz-wartość disktype: ssd w sekcji
nodeSelector:
apiVersion: v1
kind: Pod
metadata:
name: redis
labels:
env: prod
spec:
containers:
- name: frontend
image: nginx:alpine
imagePullPolicy: IfNotPresent
nodeSelector:
disktype: ssd
Dzięki wykorzystaniu sekcji nodeSelector pod zostanie umieszczony jedynie w węźle
zawierającym etykietę disktype: ssd.
Wartość taint i tolerancje
Wartość taint jest używana w węzłach, aby uniknąć w nich umieszczania podów. Czy do tego
celu nie można wykorzystać braku podobieństwa? Tak, choć wartość taint pozwala na
zastosowanie innego podejścia niż brak podobieństwa i jest używana w innych przypadkach.
Być może masz pody wymagające określonego profilu wydajności i nie chcesz, aby inne pody
zostały umieszczone w wybranym węźle. Wartość taint działa w połączeniu z tolerancją, która
pozwala na nadpisanie węzłów z zastosowaną wartością taint. Połączenie tych dwóch kryteriów
pozwala zapewnić dokładną kontrolę w porównaniu z regułami braku podobieństwa.
Ogólnie rzecz biorąc, wartość taint i tolerancja są używane w wymienionych tutaj sytuacjach:
specjalizowany sprzęt węzła,
oddzielne zasoby węzła,
unikanie węzłów zdegradowanych.
Istnieje wiele różnych typów wartości taint wpływających na zarządcę procesów i uruchomione
kontenery:
NoSchedule
Ta wartość uniemożliwia uruchomienie poda w danym węźle.
PreferNoSchedule
Ta wartość pozwala na umieszczenie poda w danym węźle tylko wtedy, gdy nie można go
umieścić w innych węzłach.
NoExecute
Ta wartość usuwa z węzła już działające pody.
NodeCondition
Ta wartość oznacza wartością taint węzeł, gdy spełnia on określony warunek.
Na rysunku 8.1 pokazaliśmy przykład węzła oznaczonego za pomocą wartości taint
gpu=true:NoSchedule. Specyfikacja poda 1 zawiera klucz tolerancji z gpu, co pozwala na
umieszczenie tego poda w węźle oznaczonym wymienioną wartością taint. Z kolei specyfikacja
poda 2 zawiera klucz tolerancji no-gpu, co oznacza brak możliwości umieszczenia poda w węźle
oznaczonym wymienioną wartością taint.
Rysunek 8.1. Wartość taint i tolerancja w Kubernetes
Gdy pod nie może być umieszczony w węźle oznaczonym pewną wartością taint, wówczas
otrzymasz komunikat o błędzie podobny do tutaj przedstawionego:
Warning:
are
FailedScheduling
10s (x10 over 2m)
default-scheduler
0/2 nodes
available: 2 node(s) had taints that the pod did not tolerate.
Skoro już wiesz, jak można ręcznie dodawać wartości taint, by wpłynąć na sposób działania
zarządcy procesów, warto, żebyś się dowiedział o istnieniu pewnej koncepcji o potężnych
możliwościach, usunięciu na podstawie wartości taint, która pozwala pozbywać się działających
podów. Przykładowo, jeżeli w węźle nastąpi awaria dysku twardego, wówczas usunięcie na
podstawie wartości taint może przeprowadzić operację ponownego przydzielania podów do
hostów w innym, sprawnym węźle klastra.
Zarządzanie zasobami poda
Jednym z najważniejszych aspektów zarządzania aplikacjami w Kubernetes jest odpowiednie
zarządzanie zasobami poda — procesorem i pamięcią, aby zoptymalizować ogólny poziom
wykorzystania zasobów w klastrze Kubernetes. Istnieje możliwość zarządzania tymi zasobami
na poziomie kontenera i przestrzeni nazw. Dostępne są jeszcze inne zasoby, np. sieć i pamięć
masowa, choć Kubernetes jeszcze nie pozwala definiować żądań i ograniczeń dla tych zasobów.
Aby mechanizm zarządcy procesów mógł zoptymalizować zasoby i podejmować trafne decyzje
dotyczące umieszczania podów, musi mieć informacje o wymaganiach aplikacji. Przykładowo,
jeśli kontener (aplikacja) wymaga do działania minimum 2 GB pamięci RAM, wówczas należy to
określić w specyfikacji poda. Dzięki temu mechanizm zarządcy procesów będzie wiedział, że
kontener wymaga 2 GB pamięci RAM w hoście, w którym dany kontener ma zostać
uruchomiony.
Żądanie zasobu
Żądanie zasobu Kubernetes określa, że kontener wymaga X zasobów procesora lub pamięci
RAM do zarezerwowania. Jeżeli w specyfikacji poda zostanie wskazane, że kontener wymaga 8
GB, a żaden z węzłów nie ma więcej niż 7,5 GB wolnej pamięci, wówczas taki pod nie zostanie
umieszczony w jakimkolwiek węźle. Jeżeli nie ma możliwości umieszczenia poda w węźle,
przejdzie do stanu oczekiwania aż do chwili, gdy wymagane zasoby staną się dostępne.
Zobacz, jak to wygląda w naszym klastrze.
Aby ustalić ilość wolnych zasobów w klastrze, należy skorzystać z polecenia kubectl top:
$ kubectl top nodes
Wygenerowane dane wyjściowe powinny być podobne do przedstawionych poniżej (w Twoim
klastrze wartości dotyczące pamięci mogą być inne):
NAME
CPU(cores)
CPU%
MEMORY(bytes)
MEMORY%
aks-nodepool1-14849087-0
524m
27%
7500Mi
33%
aks-nodepool1-14849087-1
468m
24%
3505Mi
27%
aks-nodepool1-14849087-2
406m
21%
3051Mi
24%
aks-nodepool1-14849087-3
441m
22%
2812Mi
22%
Jak można zobaczyć na przykładzie wyświetlonych danych wyjściowych, największa ilość wolnej
pamięci RAM w hoście wynosi 7,5 GB. Spróbujemy teraz umieścić pod wymagający 8 GB wolnej
pamięci.
apiVersion: v1
kind: Pod
metadata:
name: memory-request
spec:
containers:
- name: memory-request
image: polinux/stress
resources:
requests:
memory: "8000Mi"
Zwróć uwagę na to, że pod pozostaje w stanie oczekiwania. Jeżeli spojrzysz na zdarzenia w
podzie, wówczas zauważysz, że żaden węzeł nie jest dostępny dla danego poda.
$ kubectl describe pods memory-request
Wygenerowane dane wyjściowe tego polecenia powinny być podobne do poniższych:
Events:
Type
Reason
Age
From
Message
Warning
FailedScheduling
27s (x2 over 27s)
default-scheduler
0/3 nodes
are available: 3 Insufficient memory.
Ograniczenia zasobów i jakość usługi poda
Ograniczenia zasobów Kubernetes definiują maksymalną ilość mocy procesora i pamięci RAM,
która może zostać przydzielona podowi. W przypadku gdy zostały określone ograniczenia
dotyczące procesora i pamięci, po osiągnięciu określonej wartości granicznej jest podejmowana
odpowiednia akcja, różna w zależności od zasobu. Jeśli chodzi o ograniczenia procesora,
kontener zostanie zdławiony, aby nie mógł używać większej ilości zasobu, niż została mu
przydzielona. Jeśli zaś chodzi o pamięć, gdy pod wykorzysta całą przydzieloną mu ilość,
wówczas zostanie ponownie uruchomiony. Pod może być ponownie uruchomiony w tym samym
lub w zupełnie innym hoście w klastrze.
Definiowanie ograniczeń dla kontenerów to dobra praktyka, która ma
zagwarantowanie, że aplikacje będą sprawiedliwie współdzieliły zasoby klastra.
apiVersion: v1
kind: Pod
metadata:
name: cpu-demo
namespace: cpu-example
spec:
containers:
- name: frontend
image: nginx:alpine
resources:
limits:
cpu: "1"
requests:
cpu: "0.5"
apiVersion: v1
kind: Pod
metadata:
na
celu
name: qos-demo
namespace: qos-example
spec:
containers:
- name: qos-demo-ctr
image: nginx:alpine
resources:
limits:
memory: "200Mi"
cpu: "700m"
requests:
memory: "200Mi"
cpu: "700m"
Po utworzeniu poda zostanie mu przypisana jedna z wymienionych niżej klas jakości usługi
(ang. quality of service, QoS):
Guaranteed,
Burstable,
Best Effort.
Pod otrzymuje wartość QoS wynoszącą Guaranteed, gdy zasoby procesora i pamięci mają
żądania i ograniczenia, które są dopasowane. Wartość QoS Bustable jest przypisywana, gdy
ograniczenia mają większą wartość niż żądania, co oznacza, że kontener ma gwarancję
spełnienia jego żądań, a ponadto może nieco zwiększyć żądania w ramach zdefiniowanych
ograniczeń dla kontenera. Natomiast wartość QoS Best Effort pod otrzymuje, gdy nie zostały
zdefiniowane żądania lub ograniczenia.
Przypisywanie wartości QoS podom pokazaliśmy w sposób graficzny na rysunku 8.2.
Rysunek 8.2. Wartości QoS w Kubernetes
W przypadku przypisania wartości QoS Guaranteed, jeśli w podzie znajduje się wiele
kontenerów, wówczas konieczne jest zdefiniowanie żądań oraz ograniczeń pamięci i
procesora dla każdego z nich. Jeżeli żądania i ograniczenia nie zostaną określone dla
wszystkich kontenerów w podzie, wówczas nie będzie można przypisać wartości QoS
Guaranteed.
PodDisruptionBudget
W pewnym momencie Kubernetes może mieć potrzebę usunięcia podów z hosta. Są dwa
rodzaje operacji usunięcia: dobrowolna i przymusowa. Usunięcie przymusowe może być
spowodowane przez awarię sprzętu, partycji sieciowej, awarię jądra systemu operacyjnego
(ang. kernel panic) lub wyczerpanie zasobów węzła. Z kolei usunięcie dobrowolne może
wynikać z konieczności wykonania w klastrze operacji konserwacyjnych bądź wiązać się z
usuwaniem węzłów przez dodatek Cluster Autoscaler lub uaktualnianiem szablonów podów.
Aby zminimalizować negatywny wpływ operacji usunięcia podów na aplikację, można
zdefiniować zasób PodDisruptionBudget, gwarantujący zachowanie działania aplikacji w
trakcie operacji usunięcia poda. Ten zasób pozwala ustalić politykę określającą minimalną i
maksymalną dostępność podów podczas ich usuwania. Przykładem dobrowolnej operacji
usunięcia poda jest sytuacja, gdy jest on wyłączany w celu przeprowadzenia w nim operacji
konserwacyjnych.
Przykładowo możesz ustalić, że maksymalnie 20% podów należących do aplikacji może
jednocześnie nie działać. Tę politykę można zdefiniować jako liczbę replik, które zawsze muszą
być dostępne.
Dostępność minimalna
Poniżej pokazaliśmy, jak należy zdefiniować zasób PodDisruptionBudget, by zapewnić obsługę
minimum pięciu replik dla aplikacji frontendu.
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: frontend-pdb
spec:
minAvailable: 5
selector:
matchLabels:
app: frontend
W tym przykładzie zasób PodDisruptionBudget określa, że aplikacja frontendu zawsze musi
mieć dostępnych przynajmniej pięć replik podów. W takim przypadku podczas operacji
usunięcia może być usunięta dowolna liczba podów, o ile pięć pozostanie dostępnych.
Dostępne maksimum
W następnym przykładzie zdefiniowaliśmy zasób PodDisruptionBudget w celu zapewnienia
obsługi maksimum 10 replik dla aplikacji frontendu.
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: frontend-pdb
spec:
maxUnavailable: 20%
selector:
matchLabels:
app: frontend
Zgodnie z ustawieniami PodDisruptionBudget użytymi w tym przykładzie w danej chwili może
być niedostępnych maksymalnie 20% podów. W takim przypadku w trakcie operacji
dobrowolnego usunięcia podów może być usunięte maksymalnie 20%.
Podczas projektowania klastra Kubernetes trzeba wziąć pod uwagę wielkość zasobów klastra,
aby można było obsłużyć określoną liczbę węzłów, które uległy awarii. Przykładowo, jeśli
klaster składa się z czterech węzłów i jeden z nich się uszkodzi, wówczas nastąpi utrata jednej
czwartej pojemności klastra.
Gdy wartość PodDisruptionBudget jest podana jako procentowa, niekoniecznie
będzie ona skorelowana z określoną liczbą podów. Przykładowo, jeśli aplikacja ma
siedem podów i określisz wartość maxAvailable na 50%, wówczas nie będzie jasne,
czy to oznacza trzy pody, czy może cztery. W takim przypadku Kubernetes zaokrągla
wartość do najbliższej liczby całkowitej, więc w omawianej sytuacji maxAvailable
oznacza cztery pody.
Zarządzanie zasobami za pomocą przestrzeni nazw
Przestrzenie nazw w Kubernetes zapewniają elegancką, logiczną separację zasobów
wdrożonych w klastrze. To pozwala na zdefiniowanie ograniczeń zasobów, kontroli dostępu na
podstawie roli użytkownika (ang. role-based access control, RBAC), a także polityk sieciowych
dla poszczególnych przestrzeni nazw. W ten sposób zyskujesz pewną namiastkę
wielodostępności, więc możesz podzielić zadania w klastrze bez konieczności przypisywania na
wyłączność określonej infrastruktury zespołowi lub aplikacji. Zyskujesz możliwość
maksymalnego wykorzystania zasobów klastra przy jednoczesnym zachowaniu pewnej logicznej
formy separacji.
Przykładowo można utworzyć po jednej przestrzeni nazw dla poszczególnych zespołów i
przydzielić im pewną liczbę zasobów do wykorzystania, takich jak moc obliczeniowa procesora i
pamięć operacyjna.
Podczas projektowania sposobu konfiguracji przestrzeni nazw powinieneś zastanowić się nad
tym, jak chcesz zapewnić kontrolę dostępu do określonych zbiorów aplikacji. Jeżeli masz wiele
zespołów używających pojedynczego klastra, wówczas najlepszym rozwiązaniem zwykle będzie
alokowanie przestrzeni nazw dla każdego zespołu. Natomiast jeśli klaster jest przeznaczony
tylko dla jednego zespołu, sensowne może być alokowanie po jednej przestrzeni nazw dla
każdej usługi wdrożonej w klastrze. Tutaj nie istnieje jedno dobre rozwiązanie — sposób
organizacji zespołu i podział obowiązków będą miały wpływ na projekt.
Po wdrożeniu klastra Kubernetes będziesz w nim miał dostępne wymienione poniżej
przestrzenie nazw:
kube-system
W tej przestrzeni nazw są wdrożone wewnętrzne komponenty Kubernetes, takie jak
coredns, kube-proxy i metrics-server.
default
To domyślna przestrzeń nazw, która jest używana w sytuacji, gdy w obiekcie zasobu nie
została podana żadna przestrzeń nazw.
kube-public
Ta przestrzeń nazw jest używana dla treści anonimowej i nieuwierzytelnionej oraz jest
zarezerwowana na potrzeby systemu.
Raczej powinieneś unikać używania domyślnej przestrzeni nazw, ponieważ bardzo ułatwia
popełnianie błędów podczas zarządzania zasobami klastra.
W trakcie pracy z przestrzenią nazw, gdy wydawane jest polecenie kubectl, należy używać
opcji -namespace lub jej krótszej wersji, -n.
$ kubectl create ns team-1
$ kubectl get pods --namespace team-1
Istnieje również możliwość zdefiniowania kontekstu dla kubectl w postaci określonej
przestrzeni nazw, co okazuje się bardzo użyteczne, ponieważ uwalnia od konieczności
dodawania opcji -namespace do każdego polecenia. Oto polecenie pozwalające na
zdefiniowanie kontekstu przestrzeni nazw:
$ kubectl config set-context my-context --namespace=team-1
Podczas pracy z wieloma przestrzeniami nazw i klastrami zdefiniowanie odmiennych
przestrzeni nazw i kontekstu klastrów może być naprawdę żmudnym zadaniem.
Przekonaliśmy
się,
że
wykorzystanie
narzędzi
kubens
(https://github.com/lokabin/kubens) i kubectx (https://github.com/ahmetb/kubectx)
może znacznie ułatwić przełączanie między poszczególnymi przestrzeniami nazw i
kontekstami.
ResourceQuota
Gdy wiele zespołów lub aplikacji współdzieli pojedynczy klaster, bardzo duże znaczenie ma
zdefiniowanie obiektu ResourceQuota w przestrzeniach nazw. Ten obiekt pozwala podzielić
klaster na logiczne jednostki, aby żadna z przestrzeni nazw nie mogła wykorzystać więcej
zasobów, niż zostało jej przydzielonych w klastrze. Wymienione tutaj zasoby mają zdefiniowane
ograniczenia.
Zasoby obliczeniowe:
cpu — suma żądań dostępu do procesora nie może przekroczyć tej wartości,
limits.cpu — suma ograniczeń dostępu do procesora nie może przekroczyć tej
wartości,
memory — suma żądań dostępu do pamięci nie może przekroczyć tej wartości.
Zasoby pamięci masowej:
requests.storage — suma żądań dostępu do pamięci masowej nie może
przekroczyć tej wartości,
persistentvolumeclaims — całkowita liczba oświadczeń PersistentVolume, które
mogą istnieć w danej przestrzeni nazw,
storageclass.request — wielkość oświadczeń powiązanych z określoną klasą
pamięci masowej nie może przekroczyć tej wartości,
storageclass.pvc — całkowita liczba oświadczeń PersistentVolume, które mogą
istnieć w danej przestrzeni nazw.
Ograniczenia związane z liczbą obiektów (to jedynie wybrane przykłady):
count/pvc,
count/services,
count/deployments,
count/replicasets.
Jak możesz zobaczyć na podstawie tej listy, Kubernetes zapewnia dość dokładną kontrolę nad
sposobem nakładania ograniczeń dla zasobów w przestrzeniach nazw. Dzięki temu można
znacznie efektywniej posługiwać się zasobami w klastrze wielodostępnym.
Zobaczysz teraz te ograniczenia w akcji, na podstawie ich definicji dla przestrzeni nazw.
Przedstawiony tutaj fragment kodu umieść w pliku YAML dotyczącym przestrzeni nazw team-1.
apiVersion: v1
kind: ResourceQuota
metadata:
name: mem-cpu-demo
namespace: team-1
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
persistentvolumeclaims: "5"
requests.storage: "10Gi
kubectl apply quota.yaml -n team-1
W tym przykładzie zostały nałożone ograniczenia dla zasobów procesora, pamięci operacyjnej i
pamięci masowej w przestrzeni nazw team-1.
Spróbujemy teraz wdrożyć aplikację i zobaczymy, jak zdefiniowane wcześniej ograniczenia
wpływają na proces wdrożenia.
$ kubectl run nginx-quotatest --image=nginx --restart=Never --replicas=1 -port=80 --requests='cpu=500m,memory=4Gi' --limits='cpu=500m,memory=4Gi' -n
team-1
To wdrożenie kończy się niepowodzeniem i generowany jest poniższy komunikat o błędzie,
ponieważ nałożone ograniczenie w wysokości 2 GB dostępnej pamięci jest mniejsze niż ilość
pamięci operacyjnej wymagana przez aplikację (4 GB):
Error from server (Forbidden): pods "nginx-quotatest" is forbidden: exceeded
quota: mem-cpu-demo
Jak możesz zobaczyć w omawianym przykładzie, nakładanie ograniczeń na zasoby pozwala
uniemożliwić wdrożenie na podstawie zdefiniowanej dla przestrzeni nazw polityki zarządzania
zasobami.
LimitRange
Dotychczas omówiliśmy definiowanie zasobów request i limits na poziomie kontenera. Co się
stanie, gdy użytkownik zapomni o ustawieniu tych wartości w specyfikacji poda? Kubernetes
oferuje tzw. kontroler dopuszczenia (ang. admission controller), pozwalający na automatyczne
definiowanie wymienionych wartości, gdy nie zostały podane w specyfikacji poda.
Zacznij od utworzenia przestrzeni nazw, która będzie używana do pracy z ograniczeniami i
obiektem LimitRanges.
$ kubectl create ns team-1
Teraz zdefiniuj obiekt LimitRange dla przestrzeni nazw i utwórz sekcję defaultRequests w
zasobie limits.
apiVersion: v1
kind: LimitRange
metadata:
name: team-1-limit-range
spec:
limits:
- default:
memory: 512Mi
defaultRequest:
memory: 256Mi
type: Container
Zapisz ten kod w pliku limitranger.yaml, a następnie wydaj polecenie kubectl apply.
$ kubectl apply -f limitranger.yaml -n team-1
Upewnij się, że obiekt LimitRange powoduje zastosowanie wartości domyślnych dla ograniczeń
i żądań.
$ kubectl run team-1-pod --image=nginx -n team-1
Następnym krokiem jest wyświetlenie informacji o podzie i sprawdzenie, jakie ma przypisane
wartości dotyczące ograniczeń i żądań.
$ kubectl describe pod team-1-pod -n team-1
Powinieneś otrzymać następujące dane zdefiniowanych ograniczeń i żądań w specyfikacji poda:
Limits:
memory:
512Mi
Requests:
memory:
256Mi
Jest bardzo ważne, by stosować obiekt LimitRange, gdy używany jest obiekt ResourceQuota,
ponieważ w razie braku zdefiniowanych w specyfikacji wartości ograniczeń lub żądań
wdrożenie zakończy się niepowodzeniem.
Skalowanie klastra
Jedna z pierwszych decyzji, które trzeba podjąć podczas wdrażania klastra, dotyczy wielkości
egzemplarza, który będzie używany w klastrze. To przypomina bardziej sztukę niż naukę,
zwłaszcza w trakcie łączenia różnych rodzajów zadań w pojedynczym klastrze. Przede
wszystkim należy ustalić dobry punkt wyjścia dla klastra — jedno z rozwiązań to zapewnienie
dobrej równowagi między procesorem i pamięcią. Po określeniu sensownej wielkości klastra
można wykorzystać kilka podstawowych funkcjonalności Kubernetes do zarządzania
skalowaniem klastra.
Skalowanie ręczne
Kubernetes niezwykle ułatwia skalowanie klastra, zwłaszcza jeśli są używane narzędzia takie
jak kops lub te oferowane przez Kubernetes. Ręczne skalowanie klastra zwykle sprowadza się
do podania nowej liczby węzłów — następnie usługa spowoduje dodanie nowych węzłów do
klastra.
Wymienione narzędzia pozwalają również na tworzenie puli węzłów, co z kolei umożliwia
dodawanie nowych typów egzemplarzy do już działającego klastra. Taka możliwość okazuje się
bardzo użyteczna, gdy w pojedynczym klastrze zostały uruchomione różne zadania.
Przykładowo jedno może wykorzystywać procesor, podczas gdy inne będzie stanowiło
obciążenie dla pamięci. Pula węzłów pozwala na łączenie różnych typów egzemplarzy w
pojedynczym klastrze.
Prawdopodobnie nie będziesz chciał ręcznie przeprowadzać takich operacji i zechcesz
skorzystać z automatycznego skalowania. Istnieją pewne kwestie, które trzeba wziąć pod
uwagę podczas automatycznego skalowania klastra. Przekonaliśmy się, że w przypadku
większości użytkowników lepszym rozwiązaniem jest ręczne skalowanie węzłów, proaktywnie
wedle potrzeb. Jeżeli obciążenie często się zmienia, wówczas automatyczne skalowanie klastra
może być niezwykle użyteczne.
Skalowanie automatyczne
Kubernetes oferuje dodatek Cluster Autoscaler, pozwalający na określenie minimalnej liczby
dostępnych węzłów klastra, a także maksymalnej liczby węzłów, które mogą istnieć w klastrze.
Dotyczące skalowania decyzje podejmowane przez ten dodatek opierają się na liczbie podów
oczekujących. Przykładowo, jeśli zarządca procesów próbuje utworzyć poda żądającego 4 GB
pamięci operacyjnej, a klaster ma jedynie 2 GB wolnej pamięci, wówczas taki pod będzie się
znajdował w stanie oczekiwania. Gdy pod oczekuje na utworzenie, Cluster Autoscaler doda
nowy węzeł do klastra. Tuż po dodaniu nowego węzła do klastra oczekujący pod zostanie w nim
umieszczony. Wadą omawianego dodatku jest dodawanie nowego węzła w przypadku, gdy
istnieje pod w stanie oczekiwania, więc zlecone zadanie będzie musiało zaczekać na
udostępnienie nowego węzła. Począwszy od wydania Kubernetes 1.15 dodatek Cluster
Autoscaler nie obsługuje skalowania na podstawie wskaźników niestandardowych.
Omawiany dodatek potrafi również zmniejszyć wielkość klastra, gdy jego zasoby nie są już
potrzebne. Gdy zasób nie jest potrzebny, nastąpi opróżnienie węzła i przeniesienie pozostałych
podów tego węzła do innych węzłów w klastrze. Powinieneś stosować obiekt
PodDisruptionBudget w celu zagwarantowania, że operacja usunięcia węzła z klastra nie
będzie miała negatywnego wpływu na aplikację.
Skalowanie aplikacji
Kubernetes oferuje wiele sposobów na skalowanie aplikacji w klastrze. Skalowanie można
przeprowadzić ręcznie przez zmianę liczby replik używanych we wdrożeniu. Masz również
możliwość zmiany obiektu kontrolera replikacji, ReplicaSet, choć szczerze mówiąc, nie
zalecamy zarządzania aplikacjami za pomocą takich implementacji. Skalowanie ręczne
sprawdza się doskonale w przypadku zadań statycznych lub gdy wiadomo, kiedy nastąpi wzrost
liczby zadań. Natomiast gdy liczba zadań nieustannie się zmienia lub nie są one statyczne,
wówczas skalowanie ręczne nie będzie najlepszym rozwiązaniem dla aplikacji. Na szczęście
Kubernetes oferuje mechanizm HPA (ang. horizontal pod autoscaler), odpowiedzialny za
przeprowadzanie skalowania automatycznego.
Przede wszystkim zobacz, w jaki sposób można zastosować skalowanie ręczne wdrożenia przez
wykorzystanie przedstawionego tutaj manifestu.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 3
template:
metadata:
name: frontend
labels:
app: frontend
spec:
containers:
- image: nginx:alpine
name: frontend
resources:
requests:
cpu: 100m
Ten kod powoduje wdrożenie trzech replik dla usługi frontendu. Następnie zajmiemy się
skalowaniem ręcznym tego wdrożenia za pomocą polecenia kubectl scale:
$ kubectl scale deployment frontend --replicas 5
W wyniku wykonania tego polecenia otrzymujemy pięć replik usługi frontendu. Wprawdzie to
doskonałe rozwiązanie, ale zobacz, jak można zastosować nieco sprytniej działające, które
będzie automatycznie skalowało aplikację na podstawie pewnych wskaźników.
Skalowanie za pomocą HPA
Mechanizm HPA w Kubernetes pozwala na skalowanie wdrożenia na podstawie wskaźników
dotyczących procesora, pamięci oraz innych wskaźników niestandardowych. Przeprowadzana
jest operacja monitorowania wdrożenia, a wskaźniki są pobierane z komponentu metricsserver. Możliwe jest również zdefiniowanie minimalnej i maksymalnej liczby dostępnych
podów. Przykładowo można zdefiniować politykę HPA, zgodnie z którą minimalna liczba podów
wynosi 3, maksymalna zaś 10, a skalowanie będzie przeprowadzone, gdy poziom użycia
procesora przez wdrożenie wyniesie 80%. Określenie minimalnej i maksymalnej liczby podów
ma znaczenie krytyczne, ponieważ nie chcesz, aby mechanizm HPA skalował repliki w
nieskończoność, np. ze względu na błąd w aplikacji.
Mechanizm HPA ma wymienione tutaj ustawienia domyślne związane z synchronizacją
wskaźników oraz ze skalowaniem replik w górę i w dół.
horizontal-pod-autoscaler-sync-period
Wartość domyślna wynosi 30 sekund dla synchronizacji wskaźników.
horizontal-pod-autoscaler-upscale-delay
Wartość domyślna wynosi 3 minuty między dwiema operacjami skalowania w górę.
horizontal-pod-autoscaler-downscale-delay
Wartość domyślna wynosi 5 minut między dwiema operacjami skalowania w dół.
Istnieje możliwość zmiany tych wartości domyślnych za pomocą odpowiednich opcji, choć jeśli
się na to zdecydujesz, musisz zachować ostrożność. Gdy obciążenie często się zmienia, wtedy
warto poeksperymentować z tymi ustawieniami i zoptymalizować je pod kątem określonego
sposobu użycia.
Przechodzimy teraz do zdefiniowania polityki HPA dla aplikacji frontendu wdrożonej w
poprzednim ćwiczeniu.
Zaczynamy od udostępnienia wdrożenia na porcie 80.
$ kubectl expose deployment frontend --port 80
Następnym krokiem jest zdefiniowanie polityki automatycznego skalowania.
$ kubectl autoscale deployment frontend --cpu-percent=50 --min=1 --max=10
Zgodnie z tą polityką aplikacja będzie skalowana od minimum 1 repliki do maksimum 10, samo
zaś skalowanie zostanie zainicjowane, gdy obciążenie procesora osiągnie 50%.
Kolejnym krokiem jest wygenerowanie pewnego obciążenia, aby można było zobaczyć
skalowanie automatyczne w akcji.
$ kubectl run -i --tty load-generator --image=busybox /bin/sh
Hit enter for command prompt
while true; do wget -q -O- http://frontend.default.svc.cluster.local; done
kubectl get hpa
Być może będziesz musiał zaczekać kilka minut, aby zaobserwować automatyczne skalowanie
replik.
Jeżeli chcesz dowiedzieć się więcej na temat wewnętrznego sposobu działania
algorytmu automatycznego skalowania, zapoznaj się z informacjami, które zostały
opublikowane
pod
adresem
https://github.com/kubernetes/community/blob/master/contributors/designproposals/autoscaling/horizontal-pod-autoscaler.md?source=post_page--------------------------#autoscaling-algorithm.
HPA ze wskaźnikami niestandardowymi
W rozdziale 4. poznałeś rolę, jaką wskaźniki serwera odgrywają podczas monitorowania
systemów w Kubernetes. Dzięki użyciu API wskaźników serwera można zapewnić obsługę
skalowania aplikacji na podstawie wskaźników niestandardowych. API wskaźników
niestandardowych i API Metrics Aggregator pozwalają podmiotom zewnętrznym na
dostarczenie wtyczki i rozszerzenie wskaźników, by mechanizm HPA mógł następnie
przeprowadzać skalowanie na podstawie tych zewnętrznych wskaźników. Przykładowo
skalowanie, zamiast z wykorzystaniem jedynie podstawowych wskaźników dotyczących
procesora i pamięci operacyjnej, może się odbywać na podstawie wskaźników zebranych w
kolejce zewnętrznej pamięci masowej. Wykorzystanie wskaźników niestandardowych podczas
automatycznego skalowania daje możliwość skalowania aplikacji według ważnych dla niej
wskaźników lub wskaźników usług zewnętrznych.
Vertical Pod Autoscaler
Mechanizm VPA (ang. vertical pod autoscaler) w Kubernetes różni się od HPA pod tym
względem, że nie skaluje replik, a zamiast tego zajmuje się automatycznym skalowaniem żądań.
Wcześniej w rozdziale wspomnieliśmy o definiowaniu żądań dla podów i o tym, że to gwarantuje
przypisanie X zasobów danemu kontenerowi. Mechanizm VPA uwalnia Cię od konieczności
ręcznego dostosowywania tych żądań i automatycznie zajmuje się skalowaniem żądań poda w
górę oraz w dół. Jeżeli obciążenie nie pozwala na skalowanie, np. ze względu na użytą
architekturę, takie rozwiązanie sprawdza się doskonale w przypadku automatycznego
skalowania zasobów. Przykładowo baza danych MySQL nie skaluje się w taki sam sposób jak
bezstanowa aplikacja internetowa frontendu. W przypadku MySQL należy zdefiniować węzły
główne, które będą automatycznie skalowane w górę na podstawie obciążenia.
Mechanizm VPA jest znacznie bardziej skomplikowany niż HPA i składa się z trzech
komponentów:
Recommender
Monitorowanie bieżącego i poprzedniego poziomu użycia zasobów, a także dostarczanie
zalecanych wartości dla żądań dotyczących wykorzystania procesora i pamięci operacyjnej
przez kontener.
Updater
Sprawdzenie, które pody mają poprawnie zdefiniowane zasoby. Jeżeli nie mają, zostaną
zamknięte, aby mogły zostać ponownie utworzone przez kontrolery z uaktualnionymi
wartościami dotyczącymi żądań.
Admission Plugin
Zdefiniowanie odpowiednich wartości żądań w nowych podach.
W wydaniu Kubernetes 1.15 nie zaleca się stosowania VPA we wdrożeniach przeprowadzanych
w środowiskach produkcyjnych.
Najlepsze praktyki dotyczące zarządzania
zasobami
Wykorzystaj brak podobieństwa do rozproszenia obciążenia na wiele stref dostępności,
aby w ten sposób zapewnić wysoką dostępność aplikacji.
Jeżeli używasz specjalizowanego sprzętu, np. węzłów zawierających karty graficzne,
wówczas upewnij się, że tylko zadania wymagające karty graficznej będą uruchamiane w
tych węzłach. Do tego celu można posłużyć się wartościami taint.
Wartość taint wynoszącą NodeCondition wykorzystaj w celu proaktywnego unikania
awarii lub degradacji węzłów.
Stosuj sekcję nodeSelectors w specyfikacjach podów, aby przekazywać pody do
specjalizowanego sprzętu, który został wdrożony w klastrze.
Zanim aplikację przekażesz do środowiska produkcyjnego, poeksperymentuj z różnymi
wielkościami węzłów, aby znaleźć jak najlepszą równowagę między kosztem a wydajnością
typów węzłów.
Jeżeli będą wdrażane różnego typu zadania o odmiennej charakterystyce obciążenia,
wówczas korzystaj z puli węzłów. W ten sposób będziesz mógł mieć w pojedynczym
klastrze węzły różnych typów.
Upewnij się, że zostały zdefiniowane dotyczące procesora i pamięci operacyjnej
ograniczenia dla wszystkich podów wdrożonych w klastrze.
Używaj obiektów ResourceQuota do zagwarantowania, że wiele zespołów lub aplikacji
będzie sprawiedliwie współdzieliło zasoby w klastrze.
Implementuj obiekt LimitRance w celu ustawienia wartości domyślnych dla ograniczeń i
żądań specyfikacji podów, w których te wartości nie zostały określone.
Zacznij od ręcznego skalowania klastra i stosuj je do czasu, aż w pełni zrozumiesz profil
obciążenia w Kubernetes. Wprawdzie możesz skorzystać ze skalowania automatycznego,
ale to wymaga uwzględnienia kolejnych kwestii związanych z dostępnością węzła i
skalowaniem klastra w dół.
Korzystaj z mechanizmu HPA dla obciążeń, które się zmieniają i charakteryzują
nieoczekiwanymi wzrostami poziomu użycia zasobów.
Podsumowanie
Z tego rozdziału dowiedziałeś się, jak można optymalnie zarządzać Kubernetes i zasobami
aplikacji. Kubernetes oferuje wiele przeznaczonych do zarządzania zasobami funkcji
wbudowanych, których można używać do zapewnienia niezawodnego, w pełni wykorzystanego i
efektywnie działającego klastra. Określenie wielkości klastra i poda może być na początku
trudne, ale dzięki monitorowaniu aplikacji w środowisku produkcyjnym będzie można odkryć
sposoby na optymalizację zasobów.
1 Millicore to wskaźnik Kubernetes stosowany do pomiaru użycia procesora. Wartość 1
millicore oznacza jedną z tysiąca jednostek, na które można podzielić rdzeń procesora — przyp.
tłum.
Rozdział 9. Sieć,
bezpieczeństwo sieci i
architektura Service Mesh
Kubernetes jest w praktyce menedżerem systemów rozproszonych w klastrze połączonych ze
sobą maszyn. Ten fakt natychmiast powinien zwrócić uwagę na to, jak ważny jest sposób
prowadzenia komunikacji przez te maszyny — w tej kwestii kluczowe znaczenie ma sieć.
Poznanie sposobów, w jakie Kubernetes prowadzi komunikację między zarządzanymi przez
siebie rozproszonymi usługami, ma duże znaczenie dla skuteczności tej komunikacji.
W tym rozdziale skoncentrujemy się na regułach działania sieci w Kubernetes i najlepszych
praktykach dotyczących stosowania w różnych sytuacjach koncepcji związanych z siecią. We
wszelkich dyskusjach o sieci zwykle wyłaniają się również kwestie bezpieczeństwa. Tradycyjne
modele zapewniania bezpieczeństwa sieci, kontrolowane na poziomie warstwy sieciowej, także
funkcjonują w nowym świecie systemów rozproszonych w Kubernetes. Jednak są
implementowane nieco inaczej, jak również oferują nieco inne możliwości. Kubernetes
zapewnia natywne API przeznaczone do obsługi polityk sieciowych, co powinno przywodzić Ci
na myśl doskonale znane reguły definiowane w zaporach sieciowych.
W ostatniej części rozdziału przejdziemy do nowego i przerażającego świata architektury
Service Mesh. Słowo „przerażający” w poprzednim zdaniu zostało użyte żartem, choć
architekturę Service Mesh można uznać za „Dziki Zachód” w świecie technologii Kubernetes.
Reguły działania sieci w Kubernetes
Aby móc efektywnie planować architekturę aplikacji, trzeba zrozumieć, jak Kubernetes używa
sieci do prowadzenia komunikacji między usługami. W większości przypadków to właśnie
zagadnienia związane z siecią sprawiają najwięcej problemów. Zagadnienia sieciowe postaramy
się omówić możliwie prosto, ponieważ w tej dziedzinie częściej mamy do czynienia ze
wskazówkami dotyczącymi najlepszych praktyk niż ze ścisłą wiedzą o działaniu sieci w
kontenerze. Na szczęście w Kubernetes zaimplementowano pewne reguły związane z
działaniem sieci, co powinno ułatwić rozpoczęcie pracy. Dotyczą one oczekiwanych sposobów
komunikacji między poszczególnymi komponentami. W tym podrozdziale zapoznasz się z tymi
regułami.
Komunikacja między kontenerami w tym samym podzie
Wszystkie kontenery w danym podzie współdzielą tę samą przestrzeń sieci. Dzięki temu
między kontenerami hosta może być prowadzona komunikacja. To również oznacza, że
kontenery w tym samym podzie muszą udostępniać odmienne porty. To jest możliwe dzięki
wykorzystaniu potężnych możliwości oferowanych przez przestrzenie nazw systemu Linux i
sieć Dockera — kontenery mogą się znajdować w tej samej sieci lokalnej dzięki użyciu w
każdym podzie tzw. wstrzymanego kontenera, który zajmuje się tylko obsługą sieci dla
danego poda. Na rysunku 9.1 pokazaliśmy, że kontener A może bezpośrednio komunikować
się z kontenerem B z użyciem komputera lokalnego i numeru portu, na którym nasłuchuje.
Rysunek 9.1. Komunikacja między kontenerami w podzie
Komunikacja między podami
Wszystkie pody muszą mieć możliwość komunikowania się ze sobą bez konieczności użycia
jakiegokolwiek mechanizmu NAT (ang. network address translation). Dlatego też adres IP,
pod którym dany pod jest widoczny, jest rzeczywistym adresem IP nadawcy. Takie
rozwiązanie jest obsługiwane na różne sposoby, w zależności od użytej wtyczki sieciowej —
do tego tematu jeszcze powrócimy w dalszej części rozdziału. Ta reguła ma zastosowanie
między podami znajdującymi się w tym samym węźle i podami znajdującymi się w różnych
węzłach tego samego klastra. Pozwala również na bezpośrednią komunikację węzła z
podem, bez konieczności użycia jakiegokolwiek mechanizmu NAT. Dlatego też oparte na
hostach agenty lub demony systemowe mogą się w razie potrzeby komunikować z podami.
Na rysunku 9.2 pokazaliśmy proces komunikacji zachodzącej między podami w tym samym
węźle i podami w różnych węzłach klastra.
Rysunek 9.2. Komunikacja między podami w węźle
Komunikacja między usługą a podem
Usługi w Kubernetes przedstawiają trwałe adresy IP i numery portów, które w
poszczególnych węzłach będą przekazywały cały ruch sieciowy do punktów końcowych
mapowanych na usługę. Na przestrzeni różnych wersji Kubernetes zmieniły się metody
włączania możliwości przekazywania ruchu sieciowego. Dwie najważniejsze metody to
użycie narzędzia powłoki iptables i nowsze rozwiązanie, oparte na IPVS (ang. ip virtual
server). Większość obecnie stosowanych implementacji używa narzędzia powłoki iptables
do włączenia w każdym węźle mechanizmu równoważenia obciążenia pseudowarstwy
czwartej. Na rysunku 9.3 pokazaliśmy w sposób graficzny powiązanie usługi z podami za
pomocą etykiet selektorów.
Rysunek 9.3. Komunikacja między usługą a podem
Wtyczki sieci
Grupa SIG (ang. special interest group) zachęcała do stosowania architektury sieciowej opartej
na wtyczkach. Takie podejście otworzyło drogę do opracowania przez podmioty zewnętrzne
wielu różnych projektów sieciowych, z których wiele pozwoliło na dodanie do Kubernetes
nowych możliwości. Wtyczki sieciowe, będące tematem tego podrozdziału, są dostarczane w
dwóch odmianach. Pierwsza, najbardziej podstawowa, nosi nazwę Kubenet i jest wtyczką
domyślną, którą Kubernetes zapewnia natywnie. Druga odmiana jest zgodna ze specyfikacją
CNI (ang. container network interface) i stanowi ogólne rozwiązanie w zakresie wtyczki
sieciowej dla kontenera.
Kubenet
Kubenet to najprostsza wtyczka sieciowa spośród dostarczanych standardowo z Kubernetes.
Oferuje pomost dla systemu Linux, cbr0, czyli parę wirtualnych interfejsów Ethernet, z którymi
jest połączony pod. Następnie pod pobiera adres IP z zakresu CIDR (ang. classless inter-domain
routing), który zostaje rozproszony między węzły klastra. Istnieje również opcja maskarady IP,
która powinna zezwalać na ruch sieciowy kierowany do adresów IP spoza poddanego jej
zakresu CIDR. To powoduje obejście reguł związanych z komunikacją między podami, ponieważ
tylko ruch sieciowy przeznaczony do komponentów znajdujących się na zewnątrz zakresu CIDR
poda przechodzi przez mechanizm NAT. Gdy pakiet opuszcza węzeł i przechodzi do innego
węzła, przeprowadzane są pewne operacje związane z routingiem, aby można było przekazać
ruch sieciowy do właściwego węzła.
Najlepsze praktyki dotyczące pracy z Kubenet
Kubernetes pozwala stosować prosty stos sieciowy i nie zużywa cennych adresów IP w już
zatłoczonych sieciach. To w szczególności dotyczy sieci chmury, które są rozszerzane z
wykorzystaniem centrum danych.
Upewnij się, że zakres CIDR poda jest na tyle duży, aby mógł zapewnić obsługę klastrów o
możliwej wielkości i podów znajdujących się w każdym z nich. Domyślnie zdefiniowana
liczba podów w węźle wynosi 110, choć tę wartość można zmienić.
Dokładnie poznaj sposoby działania reguł tras i przygotuj je, aby tym samym umożliwić
poprawne przekazywanie ruchu sieciowego do odpowiednich podów i węzłów. W
przypadku dostawców chmury ta operacja zwykle jest automatyzowana. Natomiast w
innych przypadkach, np. skrajnych, konieczne będą automatyzacja i solidne zarządzanie
siecią.
Wtyczka zgodna ze specyfikacją CNI
Wtyczka zgodna ze specyfikacją CNI ma jeszcze dodatkowo pewne proste wymagania.
Wspomniana specyfikacja decyduje o wyborze interfejsu i użyciu minimalnych akcji API, które
CNI ma do zaoferowania, a także określa sposób, w jaki interfejs będzie działał ze
środowiskiem uruchomieniowym kontenera w klastrze. Wprawdzie komponenty zarządzania
siecią są zdefiniowane przez CNI, ale wszystkie muszą zawierać ten sam rodzaj rozwiązania w
zakresie zarządzania adresami IP oraz choćby w minimalnym stopniu umożliwiać dodawanie i
usuwanie kontenerów w sieci. Pełna specyfikacja została opracowana na podstawie pierwotnej
propozycji rkt; znajdziesz ją na stronie https://github.com/containernetworking/cni.
Projekt Core CNI udostępnia biblioteki, które można wykorzystać do tworzenia wtyczek
zapewniających podstawową funkcjonalność i wywołujących inne wtyczki, wykonujące różne
zadania. To doprowadziło do powstania wielu wtyczek CNI gotowych do wykorzystania w
ustawieniach sieciowych kontenerów pochodzących od dostawców chmury — przykładami są
tutaj natywne wtyczki Microsoft Azure CNI i Amazon Web Services VPC CNI — a także od
tradycyjnych dostawców sieciowych, np. Nuage CNI, Juniper Networks Contrail/Tungsten
Fabric i VMware NSX.
Najlepsze praktyki dotyczące pracy z wtyczkami
zgodnymi ze specyfikacją CNI
Obsługa sieci ma znaczenie krytyczne dla funkcjonowania środowiska Kubernetes. Interakcje
zachodzące między komponentami wirtualnymi w Kubernetes a środowiskiem fizycznym sieci
powinny być starannie zaprojektowane, aby zapewnić aplikacji możliwość prowadzenia
komunikacji.
1. Konieczne jest określenie zbioru funkcjonalności niezbędnych do osiągnięcia ogólnych
celów sieciowych infrastruktury. Część wtyczek CNI zapewnia natywnie wysoką
dostępność, możliwość łączenia z wieloma chmurami, obsługę polityki sieciowej
Kubernetes oraz wiele innych funkcji.
2. Jeżeli do uruchamiania klastrów używasz publicznych dostawców chmury, upewnij się, że
faktycznie są obsługiwane wszystkie wtyczki, które nie są natywne dla SDN (ang.
software-defined network) dostawcy chmury.
3. Upewnij się, że wszelkie narzędzia przeznaczone do zapewnienia bezpieczeństwa sieci, do
jej monitorowania, a także do zarządzania nią są zgodne z wybraną wtyczką CNI. Jeżeli
tak nie jest, spróbuj znaleźć zgodne narzędzia, którymi będzie można zastąpić obecnie
używane. Bardzo ważne jest zachowanie możliwości w zakresie bezpieczeństwa sieci i jej
monitorowania, ponieważ potrzeby będą się zwiększały wraz z przejściem do ogromnego
systemu rozproszonego, takiego jak Kubernetes. Istnieje możliwość dodawania narzędzi
typu Weaveworks Weave Scope, Dynatrace i Sysdig do dowolnego środowiska Kubernetes,
a użycie każdego z wymienionych narzędzi ma pewne zalety. Jeżeli rozwiązanie zostało
uruchomione w ramach zarządzanej usługi dostawcy chmury — przykładami są Azure
AKS, Google GCE i AWS EKS — wówczas należy poszukać narzędzi natywnych, np. Azure
Container Insights and Network Watcher, Google Stackdriver i AWS CloudWatch.
Niezależnie od używanego narzędzia powinno ono przynajmniej zapewnić możliwość
monitorowania stosu sieciowego i czterech złotych wskaźników, spopularyzowanych przez
wspaniały zespół Google SRE i Roba Ewashucka: opóźnienia, wielkości ruchu sieciowego,
błędów i poziomu wykorzystania sieci.
4. Jeżeli używasz wtyczki CNI, która nie oddziela sieci od przestrzeni sieciowej SDN, upewnij
się, że masz poprawną przestrzeń adresową przeznaczoną do obsługi adresów IP węzłów i
podów, wewnętrznych mechanizmów równoważenia obciążenia oraz obciążenia
związanego z procesami uaktualnienia i skalowania.
Usługi w Kubernetes
Gdy pody są wdrażane w klastrze Kubernetes, ze względu na podstawowe reguły sieci
Kubernetes (których stosowanie ułatwiają wtyczki sieciowe) nie mają one możliwości
prowadzenia bezpośredniej komunikacji ze sobą w obrębie danego klastra. Część wtyczek CNI
przydziela podom adresy IP w tej samej przestrzeni sieciowej, w której znajdują się węzły, więc
z technicznego punktu widzenia, gdy adres IP poda jest znany, to do tego poda można uzyskać
bezpośredni dostęp z zewnątrz klastra. Jednak nie jest to efektywny sposób na uzyskanie
dostępu do usług oferowanych przez poda, co wynika z natury podów w Kubernetes. Wyobraź
sobie następującą sytuację: masz funkcję lub system wymagające dostępu do API działającego
w podzie Kubernetes. Przez pewien czas takie rozwiązanie będzie działało bezproblemowo, ale
w pewnym momencie może wystąpić zakłócenie, które spowoduje usunięcie poda. Kubernetes
może utworzyć poda zastępczego z nową nazwą i adresem IP, więc powinien istnieć mechanizm
pozwalający na odszukanie tego nowego poda. W tym miejscu do gry wchodzi API usług.
API usług pozwala na przypisywanie trwałego adresu IP i portu w klastrze Kubernetes, a także
na automatyczne mapowanie do odpowiednich podów jako punktów końcowych usług. Efektem
jest utworzenie mapowania przypisanego adresu IP i numeru portu usługi na rzeczywisty adres
IP punktu końcowego lub poda. Zarządzający tym procesem kontroler ma postać usługi kubeproxy, która faktycznie jest uruchomiona we wszystkich węzłach klastra. Wymieniona usługa
przeprowadza operacje na regułach narzędzia powłoki iptables w poszczególnych węzłach.
Po zdefiniowaniu obiektu usługi następnym krokiem jest określenie typu usługi, który będzie
wskazywał, czy punkt końcowy zostanie udostępniony jedynie wewnątrz klastra czy również na
zewnątrz niego. Wyróżniamy cztery podstawowe typy usług, pokrótce omówione w kolejnych
sekcjach.
Typ usługi ClusterIP
ClusterIP to typ usługi wybierany domyślnie, jeśli żaden nie został zadeklarowany w
specyfikacji. Oznacza, że usłudze zostanie przypisany adres IP pochodzący ze wskazanego
zakresu CIDR usługi. Ten adres IP pozostanie w użyciu, dopóki istnieje obiekt usługi, więc
zapewnia adres IP, numer portu i mapowanie protokołu na pody za pomocą pola selektora.
Jednak jak się wkrótce przekonasz, zdarzają się sytuacje, w których nie ma selektora.
Deklaracja usługi zapewnia również nazwę DNS dla danej usługi. To ułatwia odkrycie usługi w
klastrze i pozwala zadaniom łatwo się komunikować z innymi usługami w klastrze za pomocą
operacji wyszukiwania DNS na podstawie nazwy usługi. Przykładowo, jeżeli masz definicję
usługi przedstawioną w kolejnym fragmencie kodu i za pomocą wywołania HTTP chcesz
uzyskać do niej dostęp z poziomu innego poda w klastrze, wówczas to wywołanie może użyć po
prostu http://web1-svc, o ile klient znajduje się w tej samej przestrzeni nazw, w której jest
dostępna usługa.
apiVersion: v1
kind: Service
metadata:
name: web1-svc
spec:
selector:
app: web1
ports:
- port: 80
targetPort: 8081
Jeżeli konieczne jest znalezienie usług w innych przestrzeniach nazw, wówczas wzorcem DNS
będzie <nazwa_usługi>.<przestrzeń_nazw>.svc.cluster.local.
Jeżeli w danej definicji usługi nie został podany żaden selektor, wówczas punkty końcowe mogą
być wyraźnie zdefiniowane dla tej usługi za pomocą definicji API punktu końcowego. W ten
sposób dodasz adres IP i numer portu jako określony punkt końcowy usługi, zamiast opierać się
na atrybucie selektora w celu automatycznego uaktualnienia punktów końcowych z podów,
które znajdują się w zakresie dopasowanym przez selektor. Takie podejście może być użyteczne
w kilku sytuacjach, gdy masz określoną bazę danych, która nie znajduje się w klastrze
używanym do testowania, a później zmieniasz usługę na bazę danych wdrożoną w Kubernetes.
Taka usługa jest często określana mianem usługi typu headless, ponieważ nie jest zarządzana
przez kube-proxy, jak to ma miejsce w przypadku innych usług, choć ma możliwość
bezpośredniego zarządzania punktami końcowymi, jak pokazuje rysunek 9.4.
Rysunek 9.4. Typ usługi ClusterIP i wirtualizacja usługi
Typ usługi NodePort
Typ usługi NodePort powoduje przypisanie wysokiego poziomu numeru portu w każdym węźle
klastra do adresu IP i numeru portu usługi w poszczególnych węzłach. NodePort wysokiego
poziomu mieści się w zakresie od 30 000 do 32 767 i może być przypisany statycznie lub
wyraźnie zdefiniowany w specyfikacji usługi. NodePort zwykle jest stosowany w klastrach
lokalnych lub w rozwiązaniach niestandardowych, które nie oferują automatycznej konfiguracji
mechanizmu równoważenia obciążenia. W celu bezpośredniego uzyskania dostępu do usługi z
zewnątrz klastra należy skorzystać z zapisu NodeIP:NodePort, jak pokazaliśmy na rysunku 9.5.
Rysunek 9.5. NodePort-Pod, usługa i wirtualizacja sieci hosta
Typ usługi ExternalName
Typ usługi ExternalName w praktyce jest rzadko używany, ale może być przydatny podczas
przekazywania trwałych nazw DNS klastra do zewnętrznych usług nazw DNS. Dość często
spotykanym przykładem jest oferowana przez dostawcę chmury zewnętrzna usługa bazy
danych o unikatowej nazwie DNS, również zapewnionej przez dostawcę chmury, np.
mymongodb.documents.azure.com.
Z technicznego punktu widzenia można to bardzo łatwo dodać do specyfikacji poda za pomocą
zmiennej środowiskowej Environment, tak jak zostało dokładnie omówione w rozdziale 6.
Jednak o wiele lepszym rozwiązaniem może być użycie znacznie ogólniejszej nazwy w klastrze,
np. prod-mongodb, pozwalającej na zmianę rzeczywistej bazy danych wskazywanej przez tę
nazwę przez zmianę specyfikacji usługi. Dzięki temu unikamy konieczności recyklingu podów ze
względu na zmianę zmiennej Environment.
kind: Service
apiVersion: v1
metadata:
name: prod-mongodb
namespace: prod
spec:
type: ExternalName
externalName: mymongodb.documents.azure.com
Typ usługi LoadBalancer
LoadBalancer to bardzo specjalny typ usługi, ponieważ pozwala na automatyzację za pomocą
dostawcy chmury oraz innych programowalnych usług infrastruktury chmury. LoadBalancer to
pojedyncza metoda zapewniająca mechanizm równoważenia obciążenia pochodzący od
dostawcy klastra Kubernetes. To oznacza, że w większości przypadków typ usługi LoadBalancer
będzie z grubsza działać w dokładnie ten sam sposób w AWS, Azure, GCE, OpenStack itd. W
większości sytuacji nastąpi utworzenie dostępnej publicznie usługi mechanizmu równoważenia
obciążenia. Jednak każdy dostawca chmury stosuje pewne adnotacje pozwalające na włączenie
innych funkcjonalności, takich jak dostępny jedynie wewnętrznie mechanizm równoważenia
obciążenia i parametry konfiguracyjne AWS ELB. Istnieje również możliwość zdefiniowania
rzeczywistego adresu IP mechanizmu równoważenia obciążenia do użycia i dozwolonego
zakresu źródłowego w specyfikacji usługi, jak pokazaliśmy w kolejnym fragmencie kodu oraz na
rysunku 9.6.
kind: Service
apiVersion: v1
metadata:
name: web-svc
spec:
type: LoadBalancer
selector:
app: web
ports:
- protocol: TCP
port: 80
targetPort: 8081
loadBalancerIP: 13.12.21.31
loadBalancerSourceRanges:
- "142.43.0.0/16"
Rysunek 9.6. Mechanizm równoważenia obciążenia — pod, usługa, węzeł i dostawca chmury
Ingress i kontrolery Ingress
Z technicznego punktu widzenia specyfikacja Ingress nie jest typem usługi w Kubernetes. Mimo
to jest to bardzo ważna koncepcja, jeśli chodzi o przychodzący ruch sieciowy w zadaniach
Kubernetes. Usługi definiowane za pomocą API usług pozwalają stosować podstawowy
mechanizm równoważenia obciążenia na poziomie warstwy trzeciej i czwartej modelu OSI.
Jednak w rzeczywistości wiele usług bezstanowych, które zostały wdrożone w Kubernetes,
wymaga wysokiego poziomu zarządzania ruchem sieciowym i zwykle kontroli na poziomie
aplikacji, a dokładniej: zarządzania protokołem HTTP.
API Ingress to w zasadzie działający na poziomie HTTP router pozwalający na bezpośrednie
stosowanie dla określonych usług backendu reguł opartych na hoście i ścieżce. Wyobraź sobie
witrynę internetową hostingowaną w www.evilgenius.com i zawierającą dwie odmienne ścieżki,
/registration i /labaccess, udostępniane przez dwie odmienne usługi działające w Kubernetes,
reg-svc i labaccess-svc. Możesz zdefiniować regułę specyfikacji Ingress gwarantującą, że
żądania kierowane do www.evilgenius.com/registration zostaną przekierowane do usługi regsvc
i
odpowiedniego
punktu
końcowego,
a
żądania
kierowane
do
www.evillgenius.com/labaccess będą przekierowywane do właściwych punktów końcowych
usługi labaccess-svc. API Ingress pozwala również, aby routing oparty na hostach umożliwiał
przekierowywanie do różnych hostów w pojedynczym egzemplarzu specyfikacji Ingress.
Funkcją dodatkową jest możliwość zadeklarowania danych poufnych Kubernetes
przechowujących informacje o certyfikacje dla TLS (ang. transport layer security) na porcie
443. Gdy ścieżka nie zostaje zdefiniowana, zwykle można wykorzystać domyślny backend,
pozwalający zapewnić użytkownikom lepsze wrażenie niż standardowy błąd o kodzie HTTP 404.
Szczegóły związane z konfiguracją TLS i domyślnego backendu są w rzeczywistości
obsługiwane przez tzw. kontroler Ingress. Ten kontroler jest oddzielony od API Ingress i
pozwala operatorom wdrażać wybrany kontroler Ingress, np. NGINX, Traefik, HAProxy.
Zgodnie z nazwą kontroler Ingress jest komponentem kontrolera, podobnie jak każdy inny
kontroler w Kubernetes, ale nie jest częścią systemu. Zamiast tego mamy do czynienia z
kontrolerem zewnętrznym, który obsługuje API Ingress w Kubernetes w celu zapewnienia
konfiguracji dynamicznej. Najczęściej stosowaną implementacją kontrolera Ingress jest NGINX,
ponieważ po części jest rozwijany w ramach projektu Kubernetes. Istnieje wiele innych
przykładów kontrolerów Ingress, zarówno typu open source, jak i komercyjnych.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: labs-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
tls:
- hosts:
- www.evillgenius.com
secretName: secret-tls
rules:
- host: www.evillgenius.com
http:
paths:
- path: /registration
backend:
serviceName: reg-svc
servicePort: 8088
- path: /labaccess
backend:
serviceName: labaccess-svc
servicePort: 8089
Najlepsze praktyki dotyczące usług i kontrolerów
Ingress
Tworzenie skomplikowanych środowisk sieci wirtualnych z połączonymi ze sobą aplikacjami
wymaga starannego planowania. Efektywne zarządzanie tym, jak poszczególne usługi aplikacji
komunikują się ze sobą oraz ze światem zewnętrznym, wymaga, by nieustannie zwracać uwagę
na zmiany wprowadzane w aplikacji. Przedstawione tutaj najlepsze praktyki ułatwiają
zarządzanie rozwiązaniem.
Ograniczaj liczbę usług, które muszą być dostępne z zewnątrz klastra. W idealnej sytuacji
większość usług będzie typu ClusterIP, jedynie usługi przeznaczone do użycia na zewnątrz
będą dostępne na zewnątrz klastra.
Jeżeli usługi wymagające udostępnienia to przede wszystkim usługi oparte na HTTP i
HTTPS, wówczas najlepszym rozwiązaniem jest użycie API Ingress i kontrolera Ingress do
przekierowania ruchu do usług zapewniających obsługę TLS. W zależności od typu
użytego kontrolera Ingress funkcje takie jak ograniczanie tempa, ponowny zapis
nagłówków, uwierzytelnianie OAuth, monitorowanie i inne usługi mogą być dostępne bez
konieczności wbudowywania ich w aplikacje.
Wybieraj kontroler Ingress zawierający niezbędną funkcjonalność dla zadań związanych z
siecią. Zdecyduj się na jeden i stosuj go we wszystkich rozwiązaniach w firmie, ponieważ
wiele konkretnych adnotacji konfiguracyjnych zmienia się między implementacjami, a te
różnice mogą uniemożliwić przenoszenie kodu między implementacjami Kubernetes w
firmie.
Przeanalizuj oferowane przez dostawców chmury opcje w zakresie dostępnych
kontrolerów Ingress, aby przenosić zadania związane z zarządzaniem infrastrukturą i
obciążeniem poza klaster i zarazem zachować możliwość stosowania API konfiguracji w
Kubernetes.
Podczas udostępniania na zewnątrz większości API przeanalizuj dostępne kontrolery
Ingress, takie jak Kong i Ambassador, które są znacznie lepiej przystosowane do pracy z
zadaniami opartymi na API. Wprawdzie kontrolery NGINX, Traefik itd. mogą oferować
pewne możliwości w zakresie dostrajania API, nie będą one tak dokładne jak w przypadku
określonych systemów API proxy.
Gdy kontroler Ingress jest wdrażany w Kubernetes jako zadanie oparte na podzie, należy
się upewnić, że wdrożenie zostało zaprojektowane z myślą o zapewnieniu wysokiej
dostępności i agregacji wydajności działania. Skorzystaj z możliwości analizowania
wskaźników i zapewnij poprawne skalowanie egzemplarza specyfikacji Ingress, choć
jednocześnie postaraj się unikać zakłóceń pracy klientów podczas skalowania zadań.
Polityka zapewnienia bezpieczeństwa
sieci
Wbudowane w Kubernetes API NetworkPolicy umożliwia zdefiniowanie w zadaniach kontroli
dostępu egzemplarza specyfikacji Ingress i Egress na poziomie sieci. Polityka sieci pozwala na
kontrolowanie tego, jak grupy podów komunikują się ze sobą oraz z innymi punktami
końcowymi. Jeżeli chcesz bardziej zagłębić się w specyfikację NetworkPolicy, wcześniejsze
stwierdzenie może się okazać dezorientujące, zwłaszcza ze względu na zdefiniowanie jej jako
API Kubernetes, choć wymaga wtyczki sieciowej zapewniającej obsługę API NetworkPolicy.
Polityka sieci ma prostą strukturę YAML, która może wydawać się skomplikowana. Będzie Ci
nieco łatwiej ją zrozumieć, jeżeli potraktujesz ją jako prostą zaporę sieciową. Każda
specyfikacja polityki ma właściwości podSelector, ingress, egress i policyType. Jedyną
wymaganą właściwością jest podSelector, która stosuje tę samą konwencję, co każdy inny
selektor matchLabels. Istnieje możliwość utworzenia wielu definicji NetworkPolicy
przeznaczonych dla tych samych podów, a efekt ich działania zostanie połączony. Skoro obiekty
specyfikacji NetworkPolicy są obiektami w przestrzeni nazw, to jeżeli żaden selektor nie będzie
zdefiniowany dla podSelector, wszystkie pody w danej przestrzeni nazw będą stosowały tę
samą politykę. Jeżeli istnieje zdefiniowana jakakolwiek reguła ingress lub egress, spowoduje
powstanie białej listy tego, co może dostać się do poda lub wydostać z niego. Trzeba w tym
miejscu wspomnieć o jednej ważnej kwestii: jeżeli pod stosuje pewną politykę ze względu na
dopasowanie selektora, cały ruch sieciowy będzie blokowany, o ile nie zostanie wyraźnie
zdefiniowany w regule ingress lub egress. Ten drobny szczegół oznacza, że jeśli pod nie
stosuje żadnej polityki ze względu na dopasowanie selektora, w podzie jest dozwolony cały ruch
sieciowy. Takie rozwiązanie zastosowano celowo, aby ułatwić wdrażanie nowych zadań w
Kubernetes bez żadnego blokowania.
Właściwości ingress i egress to w zasadzie lista reguł opartych na adresach źródłowych i
docelowych; mogą być specyficzne dla zakresów CIDR, podSelector lub nameSelector. Jeżeli
pozostawisz pustą właściwość ingress, wówczas wynikiem będzie zablokowanie całego
przychodzącego ruchu sieciowego. Podobnie pozostawienie pustej właściwości egress oznacza
zablokowanie całego wychodzącego ruchu sieciowego. Listy protokołów i numerów portów są
obsługiwane, co pozwala na jeszcze dokładniejsze zdefiniowanie dozwolonego typu
komunikacji.
Właściwość policyTypes wskazuje, do których typów reguł polityki sieci został przypisany dany
obiekt polityki. Jeżeli ta właściwość nie istnieje, zostaną sprawdzone właściwości ingress i
egress. Różnica ponownie polega na konieczności wyraźnego wskazania wartości egress w
policyTypes i zdefiniowaniu reguły egress, aby ta polityka działała. Domyślnie przyjęte jest
założenie o zdefiniowaniu właściwości ingress, co oznacza, że nie trzeba wyraźnie definiować
takiej reguły.
Przechodzimy do przykładu trójwarstwowej aplikacji wdrożonej w pojedynczej przestrzeni
nazw. Poszczególne warstwy mają następujące etykiety: tier: "web", tier: "db" i tier:
"api". Jeżeli chcesz zagwarantować poprawne ograniczenie ruchu sieciowego do odpowiednich
warstw, wówczas utwórz manifest specyfikacji NetworkPolicy w sposób podobny do tutaj
przedstawionego.
Domyślna reguła blokująca ruch sieciowy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
Warstwa sieciowa polityki sieci:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: webaccess
spec:
podSelector:
matchLabels:
tier: "web"
policyTypes:
- Ingress
ingress:
- {}
Warstwa API polityki sieci:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-access
spec:
podSelector:
matchLabels:
tier: "api"
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: "web"
Warstwa bazy danych polityki sieci:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-db-access
spec:
podSelector:
matchLabels:
tier: "db"
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: "api"
Najlepsze praktyki dotyczące polityki sieci
Zabezpieczenie ruchu sieciowego w systemie firmy kiedyś wymagało pracy z urządzeniami
fizycznymi oraz skomplikowanymi zbiorami reguł sieciowych. Teraz dzięki istnieniu polityki
sieci w Kubernetes można zastosować bardziej związane z aplikacją podejście w zakresie
segmentowania i kontrolowania ruchu sieciowego aplikacji działającej w Kubernetes. Część z
najlepszych praktyk ma zastosowanie niezależnie od używanej wtyczki polityki sieci.
Rozpocznij powoli i skoncentruj się na ruchu sieciowym przychodzącym do podów.
Niepotrzebna komplikacja kwestii związanych z właściwościami ingress i egress może
spowodować, że monitorowanie sieci stanie się koszmarem. Gdy ruch będzie przepływał
zgodnie z oczekiwaniami, można zacząć przyglądać się regułom egress w celu dalszego
dostosowania przepływu ruchu do zadań wrażliwych. Specyfikacja preferuje przychodzący
ruch sieciowy, ponieważ domyślnie stosowanych jest wiele opcji, nawet jeśli nie została
zdefiniowana żadna reguła na liście reguł ingress.
Upewnij się, że używana wtyczka sieci ma własny interfejs dla API NetworkPolicy lub
obsługuje inne, doskonale znane wtyczki. Przykładami wtyczek są Calico, Cilium, Kuberouter, Romana i Weave Net.
Jeżeli zespół odpowiedzialny za sieć stosuje politykę „domyślnego blokowania ruchu
sieciowego”, wówczas powinieneś zdefiniować politykę sieciową, taką jak przedstawiona
w poniższym fragmencie kodu, w każdej przestrzeni nazw klastra, która będzie zawierała
zadania wymagające ochrony. To gwarantuje, że nawet w przypadku usunięcia innej
polityki sieci żaden pod nie zostanie przypadkowo „udostępniony”.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
Jeżeli masz pody, które muszą być dostępne z internetu, wówczas skorzystaj z etykiety w
celu wyraźnego zastosowania polityki sieciowej zezwalającej na przychodzący ruch
sieciowy. Pod uwagę należy wziąć cały przepływ, na wypadek gdyby rzeczywisty źródłowy
adres IP pakietu nie pochodził z internetu, ale z wewnętrznego mechanizmu
równoważenia obciążenia, zapory sieciowej lub innego urządzenia sieciowego.
Przykładowo, aby zezwolić na przychodzący ruch sieciowy ze wszystkich źródeł (w tym
zewnętrznych) do podów o etykiecie allow-internet=true, trzeba skorzystać z
następującej reguły:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: internet-access
spec:
podSelector:
matchLabels:
allow-internet: "true"
policyTypes:
- Ingress
ingress:
- {}
Spróbuj umieścić zadania aplikacji w jednej przestrzeni nazw, aby w ten sposób ułatwić
sobie tworzenie reguł, ponieważ reguły są związane z przestrzenią nazw. Jeżeli trzeba
zapewnić możliwość prowadzenia komunikacji między przestrzeniami nazw, postaraj się
maksymalnie dokładnie to zdefiniować, prawdopodobnie z wykorzystaniem konkretnych
etykiet określających wzorzec przepływu ruchu sieciowego.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: namespace-foo-2-namespace-bar
namespace: bar
spec:
podSelector:
matchLabels:
app: bar-app
policyTypes:
- Ingress
ingress:
- from:
-
namespaceSelector:
matchLabels:
networking/namespace: foo
podSelector:
matchLabels:
app: foo-app
Przygotuj testową przestrzeń nazw z mniej restrykcyjnymi politykami sieciowymi lub
nawet bez zdefiniowanych polityk, aby mieć możliwość sprawdzenia, jakie są wymagane
poprawne wzorce przepływu ruchu sieciowego.
Architektura Service Mesh
Bardzo łatwo wyobrazić sobie pojedynczy klaster zawierający setki usług, które są przez
mechanizm równoważenia obciążenia rozpraszane między tysiące punktów końcowych
komunikujących się między sobą, uzyskujących dostęp do zasobów zewnętrznych, a także
potencjalnie dostępnych z zewnątrz. W takim przypadku zabezpieczanie, monitorowanie i
śledzenie wszystkich połączeń między usługami oraz zarządzanie tymi połączeniami może być
bardzo żmudnym zadaniem, zwłaszcza w przypadku dynamicznej natury punktów końcowych
przychodzących i wychodzących z ogólnego systemu. Koncepcja architektury Service Mesh nie
jest unikatowa dla Kubernetes, a pozwala na kontrolowanie sposobu, w jaki te usługi są ze sobą
połączone i zabezpieczone. Ta kontrola odbywa się za pomocą oddzielnych płaszczyzn danych i
sterowania. Wprawdzie omawiana architektura ma różne możliwości, ale zwykle zapewnia
przynajmniej niektóre z poniższych:
Mechanizm równoważenia obciążenia dla ruchu sieciowego i potencjalnie dokładna
kontrola nad polityką kierowania ruchem sieciowym w architekturze Service Mesh.
Usługa pozwalająca na odkrywanie usług należących do architektury Service Mesh. To
może obejmować usługi w bieżącym klastrze lub innym, a także na zewnątrz systemu
będącego częścią składową architektury Service Mesh.
Możliwość monitorowania ruchu sieciowego i usług. To obejmuje m.in. monitorowanie
usług rozproszonych z użyciem takich systemów jak Jaeger lub Zipkin, zgodnych ze
standardami OpenTracking.
Zabezpieczanie ruchu sieciowego w architekturze Service Mesh za pomocą wzajemnego
uwierzytelniania. W niektórych przypadkach nie tylko będzie zabezpieczony ruch sieciowy
między podami, ale również zostanie dostarczony kontroler Ingress zapewniający
bezpieczeństwo typu północ-południe.
Odporność na awarie, sprawne działanie, a także zabezpieczenie przez awariami, aby
można było w ten sposób stosować wzorce takie jak wzorzec bezpiecznika, ponawiania,
terminu.
Kluczowe znaczenie ma tutaj fakt, że wszystkie wymienione funkcje zostały zintegrowane z
aplikacją uwzględnioną w architekturze Service Mesh oraz wymagają niewiele zmian w
aplikacji lub nawet żadnych nie wymagają. Jak to możliwe w przypadku tak zaskakująco
świetnej funkcjonalności? Zwykle wykorzystywane jest proxy. W większości przypadków
dostępna obecnie architektura Service Mesh wstrzykuje proxy, które są częścią płaszczyzny
danych, do poszczególnych podów tworzących architekturę Service Mesh. To pozwala na
synchronizację polityk i ustawień dotyczących bezpieczeństwa, co odbywa się za pomocą
komponentów płaszczyzny kontrolnej. W ten sposób następuje ukrycie związanych z siecią
szczegółów przed kontenerem zawierającym zadanie i pozostawiane jest proxy operacji
związanych z obsługą wszelkich złożoności dotyczących sieci rozproszonej. Aplikacja prowadzi
za pomocą proxy komunikację z hostem lokalnym. W wielu przypadkach płaszczyzna kontrolna i
płaszczyzna danych mogą stosować odmienne technologie, choć jednocześnie będą wzajemnie
się uzupełniały.
W wielu sytuacjach pierwszą architekturą Service Mesh, jaka przychodzi na myśl, jest Istio,
czyli projekt firm Google, Lyft i IBM, używający Envoy jako proxy płaszczyzny danych i
wykorzystujący własnościowe komponenty płaszczyzny kontrolnej — Mixer, Pilot, Galley i
Citadel. Dostępne są jeszcze inne architektury Service Mesh, o zróżnicowanych możliwościach,
np. Linkerd2, używająca własnego proxy płaszczyzny danych, wbudowanego za pomocą Rust.
Firma HashiCorp w ostatnim czasie zaimplementowała w projekcie Consul przeznaczone dla
Kubernetes możliwości w zakresie architektury Service Mesh. Te zmiany pozwalają na wybór
między własnym proxy projektu Consul a proxy Envoy. Ponadto została zapewniona komercyjna
pomoc techniczna związana z architekturą Service Mesh.
Temat architektury Service Mesh w Kubernetes jest płynny — wręcz budzi zbyt duże emocje w
kręgach technicznych wielu serwisów społecznościowych — więc dokładne wyjaśnienie
poszczególnych aspektów tej architektury nie ma tutaj żadnego sensu. Nie możemy jednak
pominąć wzmianki o obiecujących wysiłkach poczynionych przez firmy Microsoft, Linkerd,
HashiCorp, Solo.io, Kinvolk i Weaveworks, które są związane z interfejsem SMI (ang. service
mesh interface). Mamy nadzieję, że SMI zdefiniuje standardowy interfejs przeznaczony dla
podstawowego zbioru funkcjonalności oczekiwanych od każdej architektury Service Mesh. W
czasie gdy ta książka powstawała, specyfikacja obejmowała politykę ruchu sieciowego, np.
identyfikację i szyfrowanie na poziomie warstwy transportowej, telemetrię ruchu sieciowego
przechwytującą ważne wskaźniki między usługami architektury Service Mesh, zarządzanie
ruchem sieciowym pozwalające na jego przekierowywanie i rozkładanie między poszczególne
usługi. Uważa się, że ten interfejs umożliwi przynajmniej częściowe ujednolicenie architektury
Service Mesh, a zarazem pozwoli jej dostawcom rozwijać ją i dodawać kolejne możliwości
odróżniające oferowane przez nich rozwiązania od rozwiązań innych firm.
Najlepsze praktyki dotyczące architektury Service
Mesh
Społeczność architektury Service Mesh rozwija się nieustannie każdego dnia, coraz więcej firm
pomaga w określaniu ich potrzeb, ekosystem zaś mocno się zmienia. Wymienione tutaj
najlepsze praktyki są, przynajmniej w czasie, gdy pisaliśmy tę książkę, oparte na powszechnych
potrzebach, które obecnie próbuje rozwiązać architektura Service Mesh.
Oszacuj wagę kluczowych funkcjonalności oferowanych przez architekturę Service Mesh i
ustal, które z obecnie dostępnych rozwiązań zapewnia większość najważniejszych funkcji
przy jak najmniejszym obciążeniu. W tym miejscu mamy na myśli obciążenie dotyczące
zarówno braku wystarczających umiejętności technicznych pracowników, jak i
niedostatków związanych z zasobami infrastruktury. Jeżeli tak naprawdę potrzebne jest
tylko wzajemne uwierzytelnianie TLS między określonymi podami, wówczas łatwiej będzie
znaleźć odpowiednią wtyczkę CNI, która zapewni niezbędne możliwości zintegrowane
bezpośrednio z wtyczką.
Czy kluczowe znaczenie ma dostarczenie wielosystemowej architektury Service Mesh, np.
typu multicloud lub hybrydowej? Nie wszystkie architektury oferują taką możliwość, a
nawet jeśli to robią, jest to skomplikowany proces, który często osłabia środowisko.
Wiele rozwiązań w zakresie architektury Service Mesh jest dostępnych w postaci
projektów typu open source. Jeśli jednak dla zespołu zajmującego się zarządzaniem tym
środowiskiem architektura Service Mesh jest nowością, wówczas lepszym wyborem
będzie rozwiązanie zawierające komercyjną pomoc techniczną. Istnieją firmy
zapewniające oparte na Istio zarządzane architektury Service Mesh wraz z komercyjną
pomocą techniczną. Takie rozwiązanie jest dobre, ponieważ powszechnie uważa się Istio
za system skomplikowany w zarządzaniu.
Podsumowanie
Obok zarządzania aplikacją jedną z najważniejszych cech Kubernetes jest możliwość
wzajemnego połączenia różnych fragmentów aplikacji. W tym rozdziale przedstawiliśmy
szczegóły związane ze sposobem działania Kubernetes, np. pobieranie przez pody adresów IP
za pomocą wtyczki zgodnej ze specyfikacją CNI, grupowanie tych adresów w celu uformowania
usług, a także sposoby, w jakie większa liczba aplikacji lub routing na warstwie 7. mogą być
zaimplementowane za pomocą zasobów specyfikacji Ingress (które z kolei używają usług).
Dowiedziałeś się również, jak można ograniczyć ruch sieciowy w celu zabezpieczenia sieci za
pomocą polityk oraz jak technologie architektury Service Mesh zmieniają sposoby, w jakie
następuje tworzenie połączeń między usługami i ich monitorowanie. Skonfigurowanie aplikacji
do niezawodnego wdrożenia i działania to nie wszystko, ważne jest też poprawne
skonfigurowanie sieci — to istotny aspekt, bo właściwa konfiguracja pozwala na
bezproblemowe działanie Kubernetes. Dokładne zrozumienie stosowanego przez Kubernetes
podejścia w zakresie obsługi sieci i współpracy z wdrażanymi aplikacjami ma kluczowe
znaczenie na drodze do ostatecznego sukcesu.
Rozdział 10. Bezpieczeństwo
poda i kontenera
W kwestii zapewnienia bezpieczeństwa poda za pomocą API Kubernetes masz do dyspozycji
dwie podstawowe możliwości: API PodSecurityPolicy i API RuntimeClass. W tym rozdziale
przedstawimy przeznaczenie i sposób użycia wymienionych API, a także związane z tym
najlepsze praktyki.
API PodSecurityPolicy
API PodSecurityPolicy jest aktywnie rozwijane. W wydaniu Kubernetes 1.15 to API
było dostępne w wersji beta. Więcej informacji na temat najnowszych uaktualnień
związanych z funkcjonalnością, jaką zapewnia, znajdziesz w dokumentacji
opublikowanej na stronie https://kubernetes.io/docs/concepts/policy/pod-securitypolicy/.
Zasoby o zasięgu klastra tworzą pojedyncze miejsce, w którym można definiować wszystkie
informacje wrażliwe zamieszczone w specyfikacji poda i nimi zarządzać. Przed utworzeniem
zasobu PodSecurityPolicy administratorzy klastra i/lub jego użytkownicy będą musieli
niezależnie zdefiniować poszczególne ustawienia sekcji securityContext dla zadań lub
włączyć tzw. kontrolery dopuszczenia w klastrze, aby wymusić stosowanie pewnych ustawień
dotyczących bezpieczeństwa poda.
Czy to wszystko nie wydaje się zbyt proste? Rozwiązanie oparte na API PodSecurityPolicy jest
zaskakująco trudne do efektywnego zaimplementowania i znacznie częściej jest wyłączone, niż
włączone, bądź też ograniczone na inne sposoby. Mimo to gorąco zachęcamy Cię, byś poświęcił
czas na dokładne poznanie zasobu PodSecurityPolicy, ponieważ to jeden z
najefektywniejszych sposobów pozwalających zmniejszyć płaszczyznę ataku przez ograniczenie
tego, co może zostać uruchomione w klastrze, a także przez ograniczenie poziomu uprawnień.
Włączenie zasobu PodSecurityPolicy
Wraz z API zasobu trzeba włączyć odpowiedni kontroler dopuszczenia, aby w ten sposób
wymusić stosowanie warunków zdefiniowanych w zasobie PodSecurityPolicy. To oznacza, że
wymuszenie stosowania polityki następuje w trakcie fazy przepływu żądania. Jeżeli chcesz
dowiedzieć się więcej na temat sposobu działania kontrolera zatwierdzenia, zajrzyj do rozdziału
17.
Warto w tym miejscu dodać, że włączenie zasobu PodSecurityPolicy często nie jest możliwe w
przypadku dostawców chmury publicznej oraz w narzędziach klastrów. Jeżeli zasób
PodSecurityPolicy jest dostępny, najczęściej będzie to funkcjonalność opcjonalna.
Zachowaj ostrożność po włączeniu zasobu PodSecurityPolicy, ponieważ może on
doprowadzić do zablokowania zadania, jeśli zasób nie zostanie odpowiednio
przygotowany.
Mamy dwa podstawowe komponenty wymagane do skompletowania, zanim będzie można
skorzystać z zasobu PodSecurityPolicy:
1. Trzeba się upewnić, że włączone jest API PodSecurityPolicy (ten krok powinien być już
wykonany, jeżeli korzystasz z obecnie obsługiwanej wersji Kubernetes).
Włączenie wymienionego API można potwierdzić za pomocą polecenia kubectl get
psp. O ile udzielona odpowiedź nie wskazuje na brak zasobu typu
PodSecurityPolicy, to wszystko jest w porządku i można kontynuować procedurę.
2. Następnym krokiem jest włączenie kontrolera dopuszczenia zasobu PodSecurityPolicy.
Służy do tego opcja api-server o nazwie --enable-admission-plugins.
Jeżeli zasób PodSecurityPolicy włączasz w istniejącym klastrze z działającymi
zadaniami, musisz utworzyć wszystkie niezbędne polityki, konta usługi, role, a także
dołączyć role jeszcze przed włączeniem kontrolera dopuszczenia.
Zaleca się również dodanie opcji --use-service-account-credentials=true do polecenia
kube-controller-manager, co spowoduje włączenie kont usługi przeznaczonych do użycia dla
poszczególnych kontrolerów w kube-controller-manager. W ten sposób zapewniasz sobie
znacznie dokładniejszą kontrolę nawet w przestrzeni nazw kube-system. Wydanie
przedstawionego tutaj polecenia pozwoli ustalić, które opcje zostały ustawione. Wygenerowane
dane wyjściowe polecenia wskazują, że faktycznie mamy konto usługi dla każdego kontrolera.
$ kubectl get serviceaccount -n kube-system | grep '.*-controller'
attachdetach-controller
1
6d13h
certificate-controller
1
6d13h
clusterrole-aggregation-controller
1
6d13h
cronjob-controller
1
6d13h
daemon-set-controller
1
6d13h
deployment-controller
1
6d13h
disruption-controller
1
6d13h
endpoint-controller
1
6d13h
expand-controller
1
6d13h
job-controller
1
6d13h
namespace-controller
1
6d13h
node-controller
1
6d13h
pv-protection-controller
1
6d13h
pvc-protection-controller
1
6d13h
replicaset-controller
1
6d13h
replication-controller
1
6d13h
resourcequota-controller
1
6d13h
service-account-controller
1
6d13h
service-controller
1
6d13h
statefulset-controller
1
6d13h
ttl-controller
1
6d13h
Trzeba pamiętać, że brak zdefiniowanego zasobu PodSecurityPolicy będzie
skutkował niejawnym odrzuceniem wszelkich żądań. Dlatego też w przypadku
niedopasowania polityki do zadania pod nie zostanie utworzony.
Anatomia zasobu PodSecurityPolicy
Aby jak najlepiej zrozumieć to, jak zasób PodSecurityPolicy pozwala na zabezpieczanie
podów, najlepiej będzie zapoznać się z kompletnym przykładem. Dzięki temu poznasz kolejność
operacji, począwszy od utworzenia polityki aż po jej użycie.
Zanim wznowisz pracę, pamiętaj, że przykład przedstawiony w następnej sekcji wymaga do
poprawnego działania włączenia zasobu PodSecurityPolicy. Jeżeli chcesz się dowiedzieć, jak
włączyć ten zasób, powróć do poprzedniej sekcji.
W działającym klastrze nie powinieneś włączać zasobu PodSecurityPolicy bez
wcześniejszego rozważenia ostrzeżeń przedstawionych w poprzedniej sekcji.
Zachowaj ostrożność, jeśli będziesz kontynuował pracę.
Zaczynamy od przetestowania rozwiązania bez wprowadzania jakichkolwiek zmian ani
tworzenia jakichkolwiek polityk. Przedstawione tutaj zadanie testowe powoduje uruchomienie
kontenera w zasobie Deployment (w tym punkcie omawiany kod został umieszczony w pliku
pause-deployment.yaml w lokalnym systemie plików).
apiVersion: apps/v1
kind: Deployment
metadata:
name: pause-deployment
namespace: default
labels:
app: pause
spec:
replicas: 1
selector:
matchLabels:
app: pause
template:
metadata:
labels:
app: pause
spec:
containers:
- name: pause
image: k8s.gcr.io/pause
Dzięki wydaniu przedstawionego tutaj polecenia można się upewnić co do istnienia zasobu
Deployment i odpowiadającego mu zasobu ReplicaSet, przy czym pod NIE istnieje.
$ kubectl get deploy,rs,pods -l app=pause
NAME
READY
UP-TO-DATE
AVAILABLE
AGE
deployment.extensions/pause-delpoyment
0/1
0
0
41s
NAME
DESIRED
CURRENT
READY
1
0
0
AGE
replicaset.extensions/pause-delpoyment-67b77c4f69
41s
Możesz to potwierdzić
ReplicaSet.
przez
wydanie
polecenia
dokładnie
przedstawiającego
$ kubectl describe replicaset -l app=pause
Name:
pause-delpoyment-67b77c4f69
Namespace:
default
Selector:
app=pause,pod-template-hash=67b77c4f69
Labels:
app=pause
pod-template-hash=67b77c4f69
Annotations:
deployment.kubernetes.io/desired-replicas: 1
deployment.kubernetes.io/max-replicas: 2
deployment.kubernetes.io/revision: 1
Controlled By:
Deployment/pause-delpoyment
Replicas:
0 current / 1 desired
Pods Status:
0 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels:
app=pause
pod-template-hash=67b77c4f69
zasób
Containers:
pause:
Image:
k8s.gcr.io/pause
Port:
<none>
Host Port:
<none>
Environment:
<none>
Mounts:
<none>
Volumes:
<none>
Conditions:
Type
Status
Reason
----
------
------
ReplicaFailure
True
FailedCreate
Events:
Type
Reason
Age
From
Message
----
------
----
----
-------
FailedCreate
45s (x15 over 2m7s)
replicaset-controller
Error
Warning
cre-
ating: pods "pause-delpoyment-67b77c4f69-" is forbidden: unable to validate
against any pod security policy: []
Otrzymaliśmy taki wynik, ponieważ nie została zdefiniowana polityka zapewnienia
bezpieczeństwa poda lub konto usługi nie ma zgody na uzyskanie dostępu do zasobu
PodSecurityPolicy. Prawdopodobnie zauważyłeś również, że wszystkie pody systemu w
przestrzeni nazw kube-system znajdują się w stanie o nazwie RUNNING. Tak się dzieje, ponieważ
te żądania zostały przekazane fazie dopuszczenia w trakcie żądania. Jeżeli wystąpi zdarzenie
prowadzące do ponownego uruchomienia podów, wówczas powróci ten sam problem, z którym
spotkaliśmy się w przypadku zadania testowego, związany z brakiem zdefiniowanego zasobu
PodSecurityPolicy.
replicaset-controller
Error creating: pods "pause-delpoyment-67b77c4f69-" is
forbidden: unable to validate against any pod security policy: []
Przystępujemy do usunięcia zadania testowego przez wydanie następującego polecenia:
$ kubectl delete deploy -l app=pause
deployment.extensions "pause-delpoyment" deleted
Teraz zajmiemy się rozwiązaniem problemu przez zdefiniowanie polityki zapewnienia
bezpieczeństwa poda. Pełną listę ustawień polityki znajdziesz w dokumentacji Kubernetes
opublikowanej
na
stronie
https://kubernetes.io/docs/concepts/policy/pod-security-policy/.
Zaprezentowane tutaj polityki są nieco zmodyfikowanymi wersjami przykładów zamieszczonych
w dokumentacji Kubernetes.
Pierwsza polityka nosi nazwę privileged i użyjemy jej do pokazania sposobu, w jaki działa
zadanie uprzywilejowane. Do zastosowania wymienionych tutaj zasobów można użyć polecenia
kubectl create -f <nazwa_pliku>:
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: privileged
spec:
privileged: true
allowPrivilegeEscalation: true
allowedCapabilities:
- '*'
volumes:
- '*'
hostNetwork: true
hostPorts:
- min: 0
max: 65535
hostIPC: true
hostPID: true
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
Następna polityka definiuje ściśle ograniczony dostęp i będzie wystarczająca do wielu zadań z
wyjątkiem tych, które są odpowiedzialne za uruchamianie usług Kubernetes o zasięgu klastra,
np. kube-proxy w przestrzeni nazw kube-system.
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
privileged: false
allowPrivilegeEscalation: false
requiredDropCapabilities:
- ALL
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'MustRunAs'
ranges:
- min: 1
max: 65535
fsGroup:
rule: 'MustRunAs'
ranges:
- min: 1
max: 65535
readOnlyRootFilesystem: false
Jeżeli chcesz potwierdzić utworzenie polityki, możesz wydać poniższe polecenie.
$ kubectl get psp
NAME
PRIV
SUPGROUP
READONLYROOTFS
privileged
sAny
CAPS
true
false
restricted
*
SELINUX
RUNASUSER
FSGROUP
RunAsAny
RunAsAny
RunAsAny
RunA-
RunAsAny
MustRunAsNonRoot
MustRunAs
MustRu-
VOLUMES
*
false
nAs
false
configMap,emptyDir,projected,secret,downwardAPI,persistentVolumeClaim
Po zdefiniowaniu tych polityk trzeba zapewnić każdemu kontu usługi dostęp, który pozwoli je
stosować. Do tego celu wykorzystamy mechanizm RBAC (ang. role-based access control), czyli
możliwość uzyskania dostępu na podstawie roli użytkownika.
Zacznij od utworzenia roli ClusterRole pozwalającej na uzyskanie dostępu i użycie
ograniczonego zasobu PodSecurityPolicy zdefiniowanego w poprzednim kroku.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: psp-restricted
rules:
- apiGroups:
- extensions
resources:
- podsecuritypolicies
resourceNames:
- restricted
verbs:
- use
Następnym krokiem jest utworzenie roli ClusterRole pozwalającej na uzyskanie dostępu i
użycie uprzywilejowanego zasobu PodSecurityPolicy zdefiniowanego w poprzednim kroku.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: psp-privileged
rules:
- apiGroups:
- extensions
resources:
- podsecuritypolicies
resourceNames:
- privileged
verbs:
- use
Teraz konieczne jest utworzenie odpowiedniego zasobu ClusterRoleBinding pozwalającego
grupie system:serviceaccounts uzyskać dostęp do psp-restricted ClusterRole. Ta grupa
zawiera wszystkie konta usługi kontrolera kube-controller-manager.
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: psp-restricted
subjects:
- kind: Group
name: system:serviceaccounts
namespace: kube-system
roleRef:
kind: ClusterRole
name: psp-restricted
apiGroup: rbac.authorization.k8s.io
Teraz można ponownie utworzyć zadanie testowe. Powinieneś zobaczyć, że pod został
utworzony i uruchomiony.
$ kubectl create -f pause-deployment.yaml
deployment.apps/pause-deployment created
$ kubectl get deploy,rs,pod
NAME
READY
UP-TO-DATE
AVAILABLE
AGE
deployment.extensions/pause-deployment
1/1
1
1
10s
NAME
DESIRED
CURRENT
READY
1
1
1
AGE
replicaset.extensions/pause-deployment-67b77c4f69
10s
NAME
READY
STATUS
RESTARTS
AGE
pod/pause-deployment-67b77c4f69-4gmdn
1/1
Running
0
9s
Zmodyfikuj zadanie w taki sposób, aby spowodowało złamanie reguł polityki ograniczonej. Tutaj
powinno pomóc dodanie opcji privileged=true. Zapisz manifest w pliku o nazwie pauseprivileged-deployment.yaml, umieszczonym w lokalnym systemie plików, a następnie zastosuj
ten manifest za pomocą polecenia kubectl apply -f <nazwa_pliku>.
apiVersion: apps/v1
kind: Deployment
metadata:
name: pause-privileged-deployment
namespace: default
labels:
app: pause
spec:
replicas: 1
selector:
matchLabels:
app: pause
template:
metadata:
labels:
app: pause
spec:
containers:
- name: pause
image: k8s.gcr.io/pause
securityContext:
privileged: true
Także w tym przypadku nastąpiło utworzenie zasobów Deployment i ReplicaSet, natomiast pod
nie został utworzony. Szczegółowe informacje na ten temat są umieszczone w dzienniku
zdarzeń dotyczącym zasobu ReplicaSet.
$ kubectl create -f pause-privileged-deployment.yaml
deployment.apps/pause-privileged-deployment created
$ kubectl get deploy,rs,pods -l app=pause
NAME
AVAILABLE
READY
UP-TO-DATE
0/1
0
AGE
deployment.extensions/pause-privileged-deployment
0
37s
NAME
CURRENT
DESIRED
READY
AGE
replicaset.extensions/pause-privileged-deployment-6b7bcfb9b7
0
0
37s
$ kubectl describe replicaset -l app=pause
Name:
pause-privileged-deployment-6b7bcfb9b7
1
Namespace:
default
Selector:
app=pause,pod-template-hash=6b7bcfb9b7
Labels:
app=pause
pod-template-hash=6b7bcfb9b7
Annotations:
deployment.kubernetes.io/desired-replicas: 1
deployment.kubernetes.io/max-replicas: 2
deployment.kubernetes.io/revision: 1
Controlled By:
Deployment/pause-privileged-deployment
Replicas:
0 current / 1 desired
Pods Status:
0 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels:
app=pause
pod-template-hash=6b7bcfb9b7
Containers:
pause:
Image:
k8s.gcr.io/pause
Port:
<none>
Host Port:
<none>
Environment:
<none>
Mounts:
<none>
Volumes:
<none>
Conditions:
Type
Status
Reason
----
------
------
ReplicaFailure
True
FailedCreate
Events:
Type
Reason
Age
From
Message
----
------
----
----
-------
FailedCreate
78s (x15 over 2m39s)
replicaset-controller
Error
Warning
cre-
ating: pods "pause-privileged-deployment-6b7bcfb9b7-" is forbidden: unable to
validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
Komunikat wygenerowany po wykonaniu tego przykładu dokładnie wyjaśnia powód:
niedozwolone jest tworzenie kontenerów uprzywilejowanych. Usuwamy więc wdrożone zadanie
testowe.
$ kubectl delete deploy pause-privileged-deployment
deployment.extensions "pause-privileged-deployment" deleted
Dotychczas zajmowaliśmy się jedynie wiązaniami na poziomie klastra. Teraz zobaczysz, jak
można pozwolić zadaniu testowemu na uzyskanie za pomocą konta usługi dostępu do polityki
uprzywilejowanej.
Przede wszystkim trzeba utworzyć konto serviceaccount w domyślnej przestrzeni nazw.
$ kubectl create serviceaccount pause-privileged
serviceaccount/pause-privileged created
Następnym krokiem jest dołączenie konta serviceaccount do roli ClusterRole. Zapisz
manifest w pliku o nazwie pause-privileged-psp-permissive.yaml, umieszczonym w lokalnym
systemie plików, a następnie zastosuj go za pomocą polecenia kubectl apply -f
<nazwa_pliku>.
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: pause-privileged-psp-permissive
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: psp-privileged
subjects:
- kind: ServiceAccount
name: pause-privileged
namespace: default
Pozostało już tylko uaktualnienie zadania, aby było użyte konto usługi pause-privileged. Do
zastosowania go w klastrze wykorzystaj polecenie kubectl apply.
apiVersion: apps/v1
kind: Deployment
metadata:
name: pause-privileged-deployment
namespace: default
labels:
app: pause
spec:
replicas: 1
selector:
matchLabels:
app: pause
template:
metadata:
labels:
app: pause
spec:
containers:
- name: pause
image: k8s.gcr.io/pause
securityContext:
privileged: true
serviceAccountName: pause-privileged
Teraz możesz zobaczyć, że pod istnieje i używa polityki uprzywilejowanej.
$ kubectl create -f pause-privileged-deployment.yaml
deployment.apps/pause-privileged-deployment created
$ kubectl get deploy,rs,pod
NAME
AVAILABLE
READY
UP-TO-DATE
1/1
1
AGE
deployment.extensions/pause-privileged-deployment
1
14s
NAME
DESIRED
CURRENT
READY
AGE
replicaset.extensions/pause-privileged-deployment-658dc5569f
1
1
1
14s
NAME
READY
STATUS
RESTARTS
1/1
Running
0
AGE
pod/pause-privileged-deployment-658dc5569f-nslnw
14s
Jeżeli chcesz sprawdzić, który zasób PodSecurityPolicy został dopasowany,
skorzystaj z przedstawionego tutaj polecenia.
$ kubectl get pod -l app=pause -o yaml | grep psp
kubernetes.io/psp: privileged
Wyzwania związane z zasobem PodSecurityPolicy
Skoro dowiedziałeś się, jak skonfigurować zasób PodSecurityPolicy i go używać, warto
wspomnieć o kilku wyzwaniach pojawiających się podczas pracy w rzeczywistych środowiskach.
W tej sekcji przedstawimy kilka kwestii, które uznaliśmy za wymagające nieco większej uwagi.
Rozsądne polityki domyślne
Prawdziwie potężne możliwości zasobu PodSecurityPolicy pozwalają zapewnić administratora
i/lub użytkownika, że ich zadania mają określony poziom zabezpieczeń. W praktyce często
można nie dostrzegać, jak wiele zadań jest uruchamianych z uprawnieniami użytkownika root,
korzysta z woluminów hostPath lub ma inne niebezpieczne ustawienia wymuszające
definiowanie polityk z lukami w zabezpieczeniach, aby zadanie mogło zostać uruchomione i
wykonane.
Wiele mozolnej pracy
Właściwe zdefiniowanie polityki to ogromna inwestycja, zwłaszcza w przypadku ogromnej
liczby zadań już działających w Kubernetes bez zasobu PodSecurityPolicy.
Czy programiści są zainteresowani poznawaniem zasobu
PodSecurityPolicy?
Czy
programiści,
z
którymi
współpracujesz,
są
zainteresowani
poznawaniem
PodSecurityPolicy? Czy mają do tego jakąkolwiek motywację? Bez odpowiedniej koordynacji i
automatyzacji pozwalającej na bezproblemowe włączenie zasobu PodSecurityPolicy jest
bardzo prawdopodobne, że wymieniony zasób w ogóle nie zostanie użyty.
Debugowanie jest uciążliwe
Rozwiązywanie problemów dotyczących sposobu zastosowania polityki jest trudnym zadaniem.
Przykładowo próbujesz zrozumieć, dlaczego zadanie zostało lub nie zostało dopasowane do
określonej polityki. Na tym etapie nie istnieją jeszcze narzędzia lub dzienniki zdarzeń, które
mogłyby to ułatwić.
Czy opierasz się na komponentach, które są poza Twoją
kontrolą?
Czy pobierasz obrazy z rejestru Docker Hub lub innego publicznego repozytorium? Jest bardzo
prawdopodobne, że to w pewien sposób spowoduje złamanie zdefiniowanej polityki, a
rozwiązanie problemu będzie poza Twoją kontrolą. Inną problematyczną kwestią są pliki Helm
w formacie chart. Czy są dostarczane wraz z odpowiednimi politykami?
Najlepsze praktyki dotyczące zasobu
PodSecurityPolicy
Zasób PodSecurityPolicy to złożony temat, do tego z jego użyciem wiąże się duża podatność
na błędy. Zanim przystąpisz do implementacji tego zasobu w swoich klastrach, zapoznaj się z
przedstawionymi tutaj najlepszymi praktykami.
Wszystko sprowadza się do mechanizm kontroli RBAC. Niezależnie od tego, czy to Ci się
podoba czy nie, na działanie zasobu PodSecurityPolicy ma wpływ kontrola dostępu na
podstawie roli użytkownika. To relacja, która faktycznie ujawnia wszystkie problemy
występujące w bieżącym projekcie polityki RBAC. Nie sposób wystarczająco mocno
podkreślić wagi, jaką ma automatyzacja zadań związanych z tworzeniem i
konserwowaniem mechanizmu RBAC i zasobu PodSecurityPolicy.
Postaraj się poznać zasięg polityki. Duże znaczenie ma ustalenie tego, jak polityka będzie
stosowana w klastrze. Definiowane przez Ciebie polityki mogą mieć zasięg klastra,
przestrzeni nazw lub określonego zadania. W klastrze zawsze będą znajdowały się
zadania, które są częścią operacji klastra Kubernetes wymagających znacznie większych
uprawnień. Dlatego też upewnij się, że obowiązują odpowiednie reguły RBAC, aby polityki
zapewniające większe uprawnienia nie były stosowane w niewymagających tego
zadaniach.
Czy chcesz włączyć zasób PodSecurityPolicy w istniejącym klastrze? Skorzystaj z
przydatnego narzędzia typu open source (https://github.com/sysdiglabs/kube-psp-advisor)
do wygenerowania polityk na podstawie bieżących zasobów. To jest doskonały punkt
wyjścia. Od tego momentu możesz zacząć udoskonalać polityki.
Następne kroki związane z zasobem
PodSecurityPolicy
Jak już pokazaliśmy, PodSecurityPolicy to API o potężnych możliwościach, pomagające w
zapewnieniu bezpieczeństwa klastra, choć praca z nim zdecydowanie należy do trudnych.
Jednak dzięki starannemu zaplanowaniu i zastosowaniu pragmatycznego podejścia zasób
PodSecurityPolicy można z powodzeniem zaimplementować w dowolnym klastrze, co na
pewno ucieszy zespół odpowiedzialny za bezpieczeństwo klastra.
Izolacja zadania i API RuntimeClass
Uznaje się, że środowiska uruchomieniowe kontenerów nie zapewniają bezpiecznej izolacji
poszczególnych zadań. Jak dotąd nic nie wskazuje na to, że większość obecnie stosowanych
środowisk
uruchomieniowych
kiedykolwiek
będzie
można
uznać
za
bezpieczne.
Zainteresowanie Kubernetes i rozwój tej technologii doprowadziły do powstania różnych
środowisk uruchomieniowych kontenerów, które oferują zróżnicowane poziomy izolacji. Część z
nich została oparta na doskonale znanych i cieszących się zaufaniem stosach technologicznych,
podczas gdy inne stanowią zupełnie nowe podejście do problemu. Projekty typu open source,
takie jak kontenery Kata, gVisor i Firecracker, dają nadzieję na ściślejszą izolację między
zadaniami. Wymienione projekty zostały oparte na zagnieżdżonej wirtualizacji (polegającej na
uruchomieniu niezwykle lekkiej maszyny wirtualnej w innej maszynie wirtualnej) lub też na
filtrowaniu i obsłudze wywołań systemowych.
Wprowadzenie wymienionych środowisk uruchomieniowych kontenerów oferujących odmienne
poziomy izolacji pozwala użytkownikom wybierać sposób wielu różnych środowisk
uruchomieniowych na podstawie gwarancji izolacji w tym samym klastrze. Przykładowo w tym
samym klastrze, ale w różnych środowiskach uruchomieniowych użytkownik może mieć
działające zaufane i niezaufane zadania.
RuntimeClass to wprowadzone w Kubernetes API, które pozwala na wybór środowiska
uruchomieniowego kontenera. Jest używane do przedstawienia jednego z obsługiwanych w
klastrze środowisk uruchomieniowych, które zostały skonfigurowane przez administratora
klastra. Użytkownik Kubernetes zyskuje możliwość zdefiniowania dla zadania klas określonego
środowiska uruchomieniowego za pomocą właściwości RuntimeClassName w specyfikacji poda.
Rozwiązanie działa na następującej zasadzie: w tle zasób RuntimeClass wskazuje egzemplarz
RuntimeHandler przekazywany do zaimplementowania przez CRI (ang. container runtime
interface). Etykiety węzła mogą być w połączeniu z sekcją nodeSelector lub tolerancjami
wykorzystane do zagwarantowania, że zadanie będzie wykonywane w węźle przez wskazany
egzemplarz RuntimeClass. Na rysunku 10.1 pokazaliśmy, jak kubelet używa API RuntimeClass
do uruchamiania podów.
Rysunek 10.1. Sposób działania API RuntimeClass
API RuntimeClass jest aktywnie rozwijane. Więcej informacji na temat jego
najnowszych uaktualnień i oferowanej funkcjonalności znajdziesz w dokumentacji
opublikowanej na stronie https://kubernetes.io/docs/concepts/containers/runtimeclass/.
Używanie API RuntimeClass
Jeżeli administrator klastra zdefiniował różne egzemplarze RuntimeClass, możesz z nich
korzystać za pomocą odpowiedniej wersji właściwości runtimeClassName w specyfikacji poda,
jak pokazaliśmy w następnym fragmencie kodu.
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
runtimeClassName: firecracker
Implementacje środowiska uruchomieniowego
W tej sekcji przedstawiliśmy kilka wybranych implementacji środowisk uruchomieniowych
oferujących zróżnicowane poziomy bezpieczeństwa i izolacji. Te implementacje warto wziąć pod
uwagę. Trzeba w tym miejscu wspomnieć, że ta lista nie jest kompletna i nie ma służyć jako
przewodnik.
CRI (https://github.com/containerd/cri)
API fasady dla środowisk uruchomieniowych. W tej implementacji nacisk położono na
prostotę, niezawodność i przenośność.
cri-o (https://cri-o.io/)
Oparta na specyfikacji OCI (ang. open container initiative) implementacja środowiska
uruchomieniowego kontenerów dla Kubernetes.
Firecracker (https://firecracker-microvm.github.io/)
To rozwiązanie zostało oparte na KVM, a zastosowana technologia wirtualizacji pozwala na
bardzo szybkie uruchamianie mikromaszyn wirtualnych w niewirtualizowanych
środowiskach z wykorzystaniem poziomu bezpieczeństwa i izolacji znanego z tradycyjnych
maszyn wirtualnych.
gVisor (https://gvisor.dev/)
Zgodne ze specyfikacją OCI środowisko uruchomieniowe, które uruchamia kontenery za
pomocą nowego jądra przestrzeni użytkownika. W ten sposób zostaje zapewnione
środowisko uruchomieniowe kontenerów charakteryzujące się małym obciążeniem oraz
wysokim bezpieczeństwem i dużą izolacją.
Kontenery Kata (https://katacontainers.io/)
Społeczność zajmująca się tworzeniem bezpiecznego środowiska uruchomieniowego
kontenerów, które oferuje poziom bezpieczeństwa znany z maszyn wirtualnych oraz
izolację. Jest to możliwe dzięki uruchamianiu lekkich maszyn wirtualnych, które działają jak
kontenery i przypominają kontenery.
Najlepsze praktyki dotyczące izolacji zadań i API
RuntimeClass
W tej sekcji zamieściliśmy najlepsze praktyki, które powinny pomóc w uniknięciu najczęściej
występujących problemów związanych z izolacją zadań i problemów dotyczących API
RuntimeClass.
Implementacja różnych poziomów izolacji środowisk za pomocą API RuntimeClass
doprowadzi do skomplikowania środowiska operacyjnego. To oznacza, że zadania
niekoniecznie będą przenośne między poszczególnymi środowiskami uruchomieniowymi
kontenerów, biorąc pod uwagę naturę oferowanej izolacji. Zrozumienie wszystkich kwestii
związanych z funkcjonalnością obsługiwaną przez różne środowiska uruchomieniowe
może być trudne, a brak możliwości poprawnego wykonania doprowadzi do kiepskich
wrażeń użytkownika korzystającego z produktu końcowego. Jeżeli to możliwe, zalecamy
zdefiniowanie oddzielnych klastrów, z których każdy powinien mieć tylko jedno
środowisko uruchomieniowe.
Izolowanie zadań nie oznacza bezpiecznej wielodostępności. Nawet jeśli uda Ci się
zaimplementować bezpieczne środowisko uruchomieniowe kontenerów, to wcale nie
będzie oznaczać, że klaster Kubernetes i API zostały zabezpieczone w taki sam sposób.
Konieczne jest uwzględnienie całej powierzchni rozwiązania Kubernetes, od początku do
końca. Odizolowanie zadania nie gwarantuje, że osoba przeprowadzająca atak nie będzie
mogła go zmodyfikować za pomocą API Kubernetes.
Narzędzia dostępne w poszczególnych środowiskach uruchomieniowych są niespójne. Być
może masz użytkowników, którzy narzędzi środowiska uruchomieniowego kontenerów
używają do debugowania i introspekcji. Posiadanie różnych środowisk uruchomieniowych
oznacza, że już nie będzie można wydać polecenia docker ps w celu wyświetlenia listy
uruchomionych kontenerów. To prowadzi do zamieszania i komplikuje usuwanie
problemów.
Pozostałe rozważania dotyczące
zapewnienia bezpieczeństwa poda i
kontenera
Poza zasobem PodSecurityPolicy i izolacją zadań masz jeszcze do dyspozycji inne narzędzia,
których użycie warto rozważyć podczas ustalania sposobu, w jaki należy zapewnić
bezpieczeństwo podowi i kontenerowi.
Kontrolery dopuszczenia
Jeżeli nie chcesz zbytnio zagłębiać się w kwestie związane z zasobem PodSecurityPolicy,
wiedz, że dostępnych jest kilka opcji oferujących ułamek jego funkcjonalności, która jednak
może się okazać wartą uwagi alternatywną opcją. Do dyspozycji masz kontrolery dopuszczenia,
takie jak DenyExecOnPrivileged i DenyEscalatingExec, w połączeniu z zaczepem
dopuszczenia, co pozwala dodać ustawienia sekcji securityContext i osiągnąć podobny efekt.
Więcej informacji na temat sterowania dopuszczeniem znajdziesz w rozdziale 17.
Narzędzia do wykrywania włamań i anomalii
W tym rozdziale przedstawiliśmy zagadnienia związane z politykami zapewnienia
bezpieczeństwa i środowiskami uruchomieniowymi kontenerów. Być może zastanawiasz się, co
się stanie w przypadku introspekcji i wymuszenia polityki w środowisku uruchomieniowym
kontenera. Dostępne są narzędzia typu open source, które potrafią to i wiele więcej. Ich
działanie polega na nasłuchiwaniu i filtrowaniu wywołań systemowych Linuksa lub z
wykorzystaniem BPF (ang. Berkeley packet filters). Jednym z takich narzędzi jest Falco
(https://falco.org/). To instalowany w postaci demona projekt fundacji CNCF (Cloud Native
Computing Foundation), który pozwala skonfigurować politykę i wymusza jej stosowanie w
trakcie działania. Falco to tylko jeden z przykładów dostępnych rozwiązań. Zachęcamy Cię do
poszukania narzędzi, które będą odpowiednie do Twoich potrzeb.
Podsumowanie
W tym rozdziale przedstawiliśmy w miarę obszernie API PodSecurityPolicy i API RuntimeClass,
które pozwalają dość dokładnie skonfigurować poziom zabezpieczeń zadań. Miałeś okazję
poznać również wybrane narzędzia typu open source umożliwiające monitorowanie polityki i
wymuszenie jej stosowania w środowisku uruchomieniowym kontenera. Zaprezentowaliśmy
także ogólne informacje, dzięki którym powinieneś być w stanie podejmować świadome decyzje
związane z zapewnieniem poziomu bezpieczeństwa odpowiedniego do wykonywanych zadań.
Rozdział 11. Polityka i
zarządzanie klastrem
Czy kiedykolwiek się zastanawiałeś, jak można zagwarantować, że wszystkie kontenery
uruchomione w klastrze będą pochodziły jedynie z zaakceptowanego rejestru kontenerów? A
może zostałeś poproszony o zagwarantowanie, że usługi nigdy nie będą udostępnione w
internecie? To są dokładnie te problemy, do których rozwiązania używa się polityki i
zarządzania klastrem. W miarę jak technologia Kubernetes staje się coraz bardziej
dopracowana i jest stosowana przez coraz większą liczbę podmiotów, pytania związane z
polityką i zarządzaniem pojawiają się znacznie częściej. Wprawdzie ta dziedzina jest
stosunkowo nowa i dopiero nabiera rozpędu, ale w tym rozdziale zamierzamy się podzielić
informacjami o tym, co można zrobić w celu zagwarantowania zgodności klastra z politykami
zdefiniowanymi przez firmę.
Dlaczego polityka i zarządzanie są
ważne?
Gdy działasz w wysoce regulowanym środowisku — np. związanym ze służbą zdrowia bądź
usługami finansowymi — lub chcesz mieć pewność, że zachowasz pewien poziom kontroli nad
tym, co jest uruchamiane w klastrach, wówczas będziesz musiał zaimplementować polityki. Po
zdefiniowaniu polityki następnym krokiem jest określenie sposobu jej implementacji oraz
zapewnienie, że klastry pozostaną zgodne z tą polityką. Celem zdefiniowania danej polityki
może być zapewnienie zgodności lub po prostu wymuszenie zastosowania najlepszych praktyk.
Niezależnie od powodu trzeba się upewnić, że podczas implementowania tej polityki nie
zostaną ograniczone możliwości programisty.
Co odróżnia tę politykę od innych?
W Kubernetes polityka jest wszędzie. Mamy do czynienia z polityką sieciową i polityką
zapewnienia bezpieczeństwa poda — powinieneś więc doskonale wiedzieć, czym jest polityka i
jak należy jej używać. Ufamy, że cokolwiek zostanie zadeklarowane w specyfikacji zasobu
Kubernetes, będzie również zaimplementowane jako definicja polityki. Obie wymienione
polityki, sieciowa i zapewnienia bezpieczeństwa poda, są implementowane w trakcie działania
aplikacji. Jednak mógłbyś w tym miejscu zapytać, kto zarządza treścią, która faktycznie jest
zdefiniowana we wspomnianych specyfikacjach zasobów Kubernetes. To jest zadanie dla
polityki i zarządzania. Gdy mówimy o polityce w kontekście zarządzania, mamy na myśli nie jej
implementowanie w trakcie działania aplikacji, ale zdefiniowanie polityki nadzorującej
właściwości i wartości w samych specyfikacjach zasobów Kubernetes. Tylko specyfikacja
zasobu Kubernetes zgodna z tymi politykami będzie dozwolona do użycia i będzie mogła być
wykorzystana do zdefiniowania informacji o stanie klastra.
Silnik polityki natywnej chmury
Możliwość podejmowania decyzji dotyczących zgodności zasobów wymaga silnika polityki,
który będzie na tyle elastyczny, aby spełnić różne potrzeby. Agent OPA, czyli Open Policy Agent
(https://www.openpolicyagent.org/), to dostępny jako oprogramowanie typu open source
elastyczny i lekki silnik protokołu, który zyskał ogromną popularność w ekosystemie natywnej
chmury. Obecność agenta OPA w ekosystemie umożliwiła powstanie wielu różnych narzędzi
przeznaczonych do zarządzania Kubernetes. Jednym z projektów w zakresie polityki i
zarządzania
jest
opracowany
przez
społeczność
agent
o
nazwie
Gatekeeper
(https://github.com/open-policy-agent/gatekeeper). W pozostałej części rozdziału będziemy
używać wymienionego agenta jako kanonicznego przykładu ilustrującego, jak można
zdefiniować politykę i zarządzać klastrem. Wprawdzie w ekosystemie są jeszcze inne
implementacje narzędzi polityki i zarządzania, ale zapewniają one ten sam poziom wrażeń
użytkownika, ponieważ pozwalają na przekazywanie do klastra jedynie tych zasobów
Kubernetes, które są zgodne z polityką specyfikacji.
Wprowadzenie do narzędzia Gatekeeper
Gatekeeper to zaczep sieciowy Kubernetes, przeznaczony do obsługi polityki i zarządzania
klastrem. Jest dostępny jako oprogramowanie typu open source i ma duże możliwości w
zakresie dostosowania do własnych potrzeb. Gatekeeper wykorzystuje zalety frameworka OPA
w celu wymuszenia stosowania polityki opartej na definicji zasobu niestandardowego (ang.
custom resource definition, CRD). Użycie CRD pozwala zapewnić spójną pracę z Kubernetes, w
trakcie której definiowanie polityki jest oddzielone od implementacji. Szablony polityk są
określane mianem szablonów ograniczeń i mogą być współdzielone oraz wielokrotnie używane
w klastrach. Narzędzie Gatekeeper umożliwia weryfikację zasobu i audyt funkcjonalności.
Jedną z doskonałych cech narzędzia Gatekeeper jest jego przenośność, która oznacza
możliwość implementacji w dowolnym klastrze Kubernetes, a także (o ile już używasz OPA)
możliwość przeniesienia tej polityki do Gatekeeper.
Narzędzie Gatekeeper jest aktywnie rozwijane i nieustannie się zmienia. Jeżeli chcesz
dowiedzieć się więcej na temat ostatnio wprowadzonych w nim zmian, zajrzyj do jego
repozytorium,
które
znajdziesz
na
stronie
https://github.com/open-policyagent/gatekeeper.
Przykładowe polityki
Ważne jest to, aby zbytnio nie utknąć i właściwie przeanalizować problem, który trzeba
rozwiązać. Zapoznaj się z wybranymi politykami, których przeznaczeniem jest rozwiązywanie
najczęściej spotykanych problemów.
Usługi nie mogą być udostępnione publicznie w internecie.
Kontenery mogą pochodzić jedynie z zaufanych rejestrów kontenerów.
Wszystkie kontenery muszą mieć ograniczenia dotyczące używanych zasobów.
Nazwy hostów specyfikacji Ingress nie mogą się nakładać.
Ruch sieciowy musi używać wyłącznie protokołu HTTPS.
Terminologia stosowana podczas pracy z Gatekeeper
Podczas pracy z narzędziem Gatekeeper w większości stosowana jest taka sama terminologia,
jaką znamy z frameworka OPA. Należy ją omówić, aby ułatwić Ci zrozumienie sposobu działania
narzędzia. Gatekeeper używa frameworka o nazwie OPA Constraint Framework. Musisz więc
poznać trzy nowe pojęcia:
ograniczenie,
Rego,
szablon ograniczenia.
Ograniczenie
Ograniczenie można najlepiej określić jako restrykcje nakładane względem pewnych
właściwości i wartości w specyfikacji zasobu Kubernetes. To jest naprawdę długi sposób na
wyrażenie polityki. Oznacza to, że podczas definiowania ograniczenia w praktyce wskazujesz,
na co się NIE ZGADZASZ. Konsekwencją takiego podejścia jest to, że zastosowanie danego
zasobu jest niejawnie dozwolone, o ile ograniczenie nie wyklucza danego zasobu. To bardzo
ważne, ponieważ zamiast zezwalać na używanie szerokiej gamy właściwości i wartości w
specyfikacji zasobu Kubernetes, jedynie wykluczasz te niepożądane. Taka decyzja
architektoniczna doskonale sprawdza się w specyfikacjach zasobów Kubernetes, ponieważ
często się one zmieniają.
Rego
Rego to natywny dla OPA język zapytań. Zapytania Rego są asercjami dla danych
przechowywanych w OPA. Gatekeeper przechowuje Rego w szablonie ograniczenia.
Szablon ograniczenia
Szablon ograniczenia można potraktować jak szablon polityki. Jest przenośny i wielokrotnego
użycia. Szablon ograniczenia składa się z parametrów i celu — są one parametryzowane, by
mogły być wielokrotnie używane.
Definiowanie szablonu ograniczenia
Szablon
ograniczenia
jest
niestandardową
definicją
zasobu
(https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)
dostarczającą szablon dla polityki, aby można było ją współdzielić i wielokrotnie jej używać.
Ponadto możliwe jest sprawdzanie poprawności parametrów polityki. Spójrz teraz na szablon
ograniczenia w kontekście wcześniejszych przykładów. W przedstawionym tutaj przykładzie
współdzielimy szablon ograniczenia dostarczającego politykę „zezwalaj tylko na kontenery
pochodzące z zaufanych rejestrów kontenerów”.
apiVersion: templates.gatekeeper.sh/v1alpha1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
listKind: K8sAllowedReposList
plural: k8sallowedrepos
singular: k8sallowedrepos
validation:
# Schemat dla parametrów wejściowych.
openAPIV3Schema:
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
deny[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo =
input.constraint.spec.parameters.repos[_] ; good =
startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("kontener <%v> ma nieprawidłowy obraz <%v>,
dozwolone repozytoria to %v", [container.name, container.image,
input.constraint.spec.parameters.repos])
}
Ten szablon ograniczenia składa się z trzech głównych komponentów.
Wymagane przez Kubernetes metadane CRD
Nazwa jest najważniejszą częścią i będziemy się później do niej odwoływać.
Schemat dla parametrów danych wejściowych
Ta sekcja, wskazywana przez właściwość weryfikacji, definiuje parametry danych
wejściowych i powiązane z nimi typy. W omawianym przykładzie mamy pojedynczy
parametr o nazwie repo, będący tablicą ciągów tekstowych.
Definicja polityki
Ta sekcja, wskazywana przez właściwość target, zawiera szablonowy kod w języku Rego
(w OPA jest to język używany do zdefiniowania polityki). Zastosowanie szablonu
ograniczenia pozwala na wielokrotne wykorzystanie szablonowego kodu w języku Rego, co
z koli wskazuje na możliwość współdzielenia ogólnej polityki. Dopasowanie reguły oznacza
złamanie ograniczenia.
Definiowanie ograniczenia
Aby użyć szablonu ograniczenia zdefiniowanego w poprzednim przykładzie, trzeba utworzyć
zasób ograniczenia. Celem tego zasobu jest dostarczenie niezbędnych parametrów
utworzonemu wcześniej szablonowi ograniczenia. Możesz zobaczyć, że w omawianym
przykładzie rodzaj (kind) zdefiniowanego zasobu to K8sAllowedRepos, który mapuje na szablon
ograniczenia utworzony w poprzedniej sekcji.
apiVersion: constraints.gatekeeper.sh/v1alpha1
kind: K8sAllowedRepos
metadata:
name: prod-repo-is-openpolicyagent
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "production"
parameters:
repos:
- "openpolicyagent"
Ten szablon ograniczenia składa się z dwóch głównych komponentów.
Wymagane przez Kubernetes metadane
Zwróć uwagę na to, że przedstawione ograniczenie jest typu K8sAllowedRepos i
dopasowuje nazwę szablonu ograniczenia.
Specyfikacja
Właściwość match definiuje zasięg dla danej polityki. W omawianym przykładzie
dopasowane pody znajdują się tylko w produkcyjnej przestrzeni nazw.
Parametry definiują oczekiwane przeznaczenie polityki. Zwróć uwagę na to, że dopasowują
typ ze schematu szablonu ograniczenia utworzonego w poprzedniej sekcji. Zdefiniowana
tutaj polityka zezwala na używanie w kontenerze jedynie obrazów pochodzących z
repozytoriów o nazwach rozpoczynających się od openpolicyagent.
Omawiane ograniczenie ma jeszcze następujące operacyjne cechy charakterystyczne.
Zastosowanie operatora logicznego I.
Gdy wiele polityk przeprowadza weryfikację tego samego pola, znalezienie choćby
jednego przypadku złamania polityki powoduje odrzucenie żądania.
Weryfikacja schematu pozwala na wczesne wykrywanie błędów.
Stosowanie kryteriów selekcji.
Możliwość używania etykiet selektorów.
Ograniczenia dotyczące jedynie określonych typów.
Ograniczenia dotyczące jedynie określonych przestrzeni nazw.
Replikacja danych
W niektórych sytuacjach być może będziesz chciał porównywać bieżący zasób z innymi
zasobami w klastrze, np. w przypadku polityki „nazwy hostów przychodzącego ruchu
sieciowego nie mogą się nakładać”. OPA musi mieć w swoim buforze także wszystkie pozostałe
zasoby przychodzącego ruchu sieciowego, aby można było zapewnić stosowanie się do tej
reguły. Narzędzie Gatekeeper używa zasobu config do zarządzania danymi buforowanymi w
OPA, a tym samym przeprowadza operacje takie jak wcześniej wspomniana. Poza tym zasób
config jest używany również przez funkcjonalność audytu, którą dokładniej omówimy w dalszej
części rozdziału.
Spójrz na przykład zasobu config buforującego usługę v1, pody i przestrzenie nazw.
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: gatekeeper-system
spec:
sync:
syncOnly:
- kind: Service
version: v1
- kind: Pod
version: v1
- kind: Namespace
version: v1
UX
Narzędzie Gatekeeper pozwala na dostarczanie w czasie rzeczywistym informacji kierowanych
do użytkowników klastra i dotyczących zasobów, które łamią zdefiniowaną politykę. Jeżeli
przeanalizujemy przykład pochodzący z poprzednich sekcji, wówczas będzie wiadomo, że
dozwolone jest stosowanie kontenerów pochodzących jedynie z repozytoriów o nazwach
rozpoczynających się od openpolicyagent.
Spróbuj utworzyć przedstawiony tutaj zasób, który jest niezgodny z obecną polityką.
apiVersion: v1
kind: Pod
metadata:
name: opa
namespace: production
spec:
containers:
- name: opa
image: quay.io/opa:0.9.2
To spowoduje wyświetlenie komunikatu o błędzie informującego o złamaniu polityki. Ten
komunikat został zdefiniowany w szablonie ograniczenia.
$ kubectl create -f bad_resources/opa_wrong_repo.yaml
Error from server (container <opa> ma nieprawidłowy obraz <quay.io/opa:
0.9.2>, allowed repos are ["openpolicyagent"]): error when creating
"bad_resources/opa_wrong_repo.yaml": admission webhook
"validation.gatekeeper.sh" denied
the request: kontener <opa> ma nieprawidłowy obraz <quay.io/opa:0.9.2>,
dozwolone repozytoria to ["openpolicyagent"]
Audyt
Dotychczas dowiedziałeś się tylko, jak można zdefiniować politykę i jak wymuszać jej
stosowanie podczas procesu dopuszczenia żądania. Być może zastanawiasz się, jak obsługiwać
klaster zawierający wdrożone zasoby, i chcesz się dowiedzieć, które z nich są zgodne ze
zdefiniowaną polityką. Dokładnie do tego służy audyt. W trakcie audytu Gatekeeper okresowo
sprawdza zasoby przez ich porównanie względem zdefiniowanych ograniczeń. To pomaga w
wykrywaniu błędnie skonfigurowanych zasobów i we wprowadzeniu niezbędnych zmian. Wynik
audytu jest przechowywany w polu stanu ograniczenia, więc jest bardzo łatwy do wyszukania
za pomocą kubectl. Aby można było przeprowadzić audyt, poddawane mu zasoby muszą być
replikowane. Więcej informacji na ten temat znajdziesz w poprzedniej sekcji.
Spójrz teraz na ograniczenie o nazwie prod-repo-is-openpolicyagent, które zostało
zdefiniowane w poprzedniej sekcji.
$ kubectl get k8sallowedrepos prod-repo-is-openpolicyagent -o yaml
apiVersion: constraints.gatekeeper.sh/v1alpha1
kind: K8sAllowedRepos
metadata:
creationTimestamp: "2019-06-04T06:05:05Z"
finalizers:
- finalizers.gatekeeper.sh/constraint
generation: 2820
name: prod-repo-is-openpolicyagent
resourceVersion: "4075433"
selfLink: /apis/constraints.gatekeeper.sh/v1alpha1/k8sallowedrepos/prodrepo-is-openpolicyagent
uid: b291e054-868e-11e9-868d-000d3afdb27e
spec:
match:
kinds:
- apiGroups:
- ""
kinds:
- Pod
namespaces:
- production
parameters:
repos:
- openpolicyagent
status:
auditTimestamp: "2019-06-05T05:51:16Z"
enforced: true
violations:
- kind: Pod
message: kontener <nginx> ma nieprawidłowy obraz <nginx>, dozwolone
repozytoria to
["openpolicyagent"]
name: nginx
namespace: production
Po analizie datę i godzinę ostatnio przeprowadzonego audytu można odczytać z właściwości
auditTimestamp. Wszystkie zasoby, które powodują złamanie danego ograniczenia, są zaś
wymienione we właściwości violations.
Poznanie narzędzia Gatekeeper
Repozytorium narzędzia Gatekeeper zawiera wręcz fantastyczną treść prezentującą bardzo
dokładny przykład utworzenia polityki, która pozwoliłaby spełnić wymagania banku. Gorąco
zachęcamy do zapoznania się z tym materiałem, ponieważ dzięki niemu można praktycznie
wypróbować sposób działania omawianego narzędzia. Odpowiednią dokumentację znajdziesz
we wspomnianym repozytorium GitHub pod adresem https://github.com/open-policyagent/gatekeeper/tree/master/demo/agilebank.
Następne kroki podczas pracy z narzędziem
Gatekeeper
Projekt Gatekeeper jest nieustannie rozwijany, a jego twórcy szukają nowych możliwości w
zakresie rozwiązywania innych problemów związanych z polityką i zarządzaniem. Oto niektóre
z oferowanych funkcjonalności:
Mutacje (modyfikowanie zasobów na podstawie polityki, np. dodawanie etykiet).
Zewnętrzne źródła danych — integracja z LDAP (ang. lightweight directory access
protocol) lub Active Directory w celu pobrania odpowiedniej polityki.
Autoryzacja (użycie Gatekeeper jako modułu autoryzacji Kubernetes).
Uruchomienie na próbę (umożliwienie użytkownikom przetestowania polityki przed jej
aktywowaniem w klastrze).
Jeżeli te problemy uznajesz za interesujące i chciałbyś pomóc w ich rozwiązywaniu, społeczność
Gatekeeper nieustannie szuka nowych użytkowników i współpracowników, którzy pomogą w
dalszym rozwijaniu projektu. Więcej informacji na ten temat znajdziesz w repozytorium GitHub
na stronie https://github.com/open-policy-agent/gatekeeper.
Najlepsze praktyki dotyczące polityki i
zarządzania
Powinieneś rozważyć stosowanie wymienionych tutaj najlepszych praktyk
implementowania w klastrze rozwiązań związanych z polityką i zarządzaniem.
podczas
Jeżeli chcesz wymusić zastosowanie określonej właściwości w podzie, musisz ustalić,
którą specyfikację zasobu Kubernetes należy przeanalizować, i wymusić jej zastosowanie.
Weźmy np. zasób Deployment zarządzający zasobami ReplicaSet, które z kolei zarządzają
podami. Wprawdzie można wymusić politykę na wszystkich trzech wymienionych
poziomach, ale najlepszym rozwiązaniem będzie wymuszenie polityki na najniższym
poziomie przed środowiskiem uruchomieniowym, czyli w omawianym przykładzie na
poziomie poda. Jednak ta decyzja ma pewne implikacje. Przyjazny użytkownikowi
komunikat o błędzie generowany podczas próby wdrożenia poda niezgodnego z polityką,
np. przedstawiony we wcześniejszej części rozdziału, nie zostanie wyświetlony. Tak się
dzieje, ponieważ użytkownik nie tworzy zasobu niezgodnego z polityką; to zadanie
wykonuje zasób ReplicaSet. Dlatego też użytkownik będzie musiał samodzielnie ustalić,
czy zasób jest niezgodny z polityką, co można zrobić np. za pomocą polecenia kube
describe wydanego w bieżącym zasobie ReplicaSet powiązanym z zasobem Deployment.
Wprawdzie takie rozwiązanie wydaje się uciążliwe, ale pozostaje spójne z pozostałą
funkcjonalnością Kubernetes, taką jak polityka zapewnienia bezpieczeństwa poda.
Ograniczenia mogą być stosowane w zasobach Kubernetes na podstawie wymienionych
tutaj kryteriów: rodzaju, przestrzeni nazw i etykiety selektora. Gorąco zachęcamy do
możliwie ścisłego nakładania ograniczeń na zasoby. To gwarantuje spójny sposób
stosowania polityki, gdy zasoby w klastrze będą się rozrastały. Ponadto zasoby
niewymagające oszacowania nie będą przekazywane do OPA, co z kolei pozwala na
wyeliminowanie innych nieefektywnych zadań.
Synchronizacja i wymuszanie polityki względem potencjalnych danych wrażliwych, np.
danych poufnych w Kubernetes, nie są zalecane. Ponieważ OPA będzie przechowywać te
dane w buforze (o ile OPA skonfigurowano do replikacji tych danych), a zasoby zostaną
przekazane do Gatekeeper, w ten sposób powstaje płaszczyzna do przeprowadzenia
potencjalnego ataku.
Jeżeli masz zdefiniowanych zbyt wiele ograniczeń, odrzucenie ograniczenia oznacza
odrzucenie całego żądania. Nie ma możliwości, aby ograniczenie mogło funkcjonować w
ramach konstrukcji logicznego LUB.
Podsumowanie
Z tego rozdziału dowiedziałeś się, dlaczego polityka i zarządzanie to bardzo ważne kwestie.
Zaprezentowaliśmy również projekt zbudowany na podstawie OPA, silnika polityki ekosystemu
natywnej chmury, w celu zapewnienia Kubernetes natywnego podejścia w zakresie definiowania
polityki i zarządzania klastrem. Po lekturze rozdziału powinieneś być przygotowany do
udzielenia odpowiedzi, gdy następnym razem usłyszysz od zespołu zajmującego się
zapewnieniem bezpieczeństwa pytania w rodzaju: „Czy nasze klastry są zgodne ze zdefiniowaną
polityką?”.
Rozdział 12. Zarządzanie
wieloma klastrami
W tym rozdziale przedstawimy
Kubernetes. Zagłębimy się w
przeznaczonymi do zarządzania
zarządzaniu wieloma klastrami i
najlepsze praktyki dotyczące zarządzania wieloma klastrami
różnice między narzędziami oraz wzorcami operacyjnymi
wieloma klastrami, a także w szczegóły pokazujące różnice w
federacją.
Być może zastanawiasz się, dlaczego miałbyś potrzebować wielu klastrów Kubernetes. Czy
technologia Kubernetes nie została opracowana w celu konsolidacji wielu zadań w pojedynczym
klastrze? To prawda, ale mimo to zdarzają się sytuacje, w których pewne zadania są
wykonywane w różnych regionach, rodzą się obawy związane z polem rażenia, konieczne jest
zapewnienie zgodności z pewnymi normami prawnymi lub wykonywanie zadań
specjalizowanych.
Te scenariusze zostaną omówione w niniejszym rozdziale. Ponadto przedstawimy narzędzia i
techniki przeznaczone do zarządzania wieloma klastrami w Kubernetes.
Do czego potrzebujesz wielu klastrów?
Podczas adaptowania Kubernetes prawdopodobnie będziesz miał więcej niż tylko jeden klaster.
Być może nawet rozpoczniesz pracę, mając więcej niż jeden klaster, aby w ten sposób oddzielić
środowisko produkcyjne od roboczego, testowego i programistycznego. Kubernetes oferuje
funkcje związane z wielodostępnością i przestrzenie nazw, dzięki którym klaster można
podzielić na wiele mniejszych, logicznych konstrukcji. Przestrzenie nazw pozwalają na
stosowanie kontroli dostępu na podstawie roli użytkownika, RBAC (ang. role-based access
control), definiowanie limitów dla dostępnych zasobów, określanie polityki zapewnienia
bezpieczeństwa poda, a także definiowanie polityk sieciowych mających na celu umożliwienie
separacji zadań. Jest to więc doskonały sposób na rozdzielenie poszczególnych zespołów i
projektów, choć zarazem mogą się pojawić inne kwestie, które trzeba będzie wziąć pod uwagę
podczas tworzenia architektury składającej się z wielu klastrów. Oto kilka spośród tych, o
których należy pamiętać w trakcie dokonywania wyboru pomiędzy architekturą składającą się z
wielu klastrów a architekturą opartą tylko na jednym klastrze:
pole rażenia,
zapewnienie zgodności,
zapewnienie bezpieczeństwa,
trudna wielodostępność,
zadania związane z konkretnymi regionami,
zadania specjalizowane.
Podczas wyboru architektury na myśl powinno przyjść przede wszystkim tzw. pole rażenia. To
jedna z najważniejszych kwestii, z którymi borykają się użytkownicy zajmujący się
projektowaniem architektury wielodostępnej. W przypadku architektury opartej na
mikrousługach stosowane są różne wzorce (bezpiecznika, ponawiania, grodzi itd.) mające na
celu ograniczenie szkód, jakie mogą powstać w systemie. Takie samo rozwiązanie powinieneś
zaprojektować na warstwie infrastruktury, a wiele klastrów może pomóc w uniknięciu
kaskadowych awarii na skutek pewnych problemów związanych z oprogramowaniem.
Przykładowo, jeśli masz jeden klaster obsługujący 500 aplikacji i wystąpi problem związany z
platformą, wówczas dotknie on wszystkie ze wspomnianych 500 aplikacji. Natomiast jeśli
związany z platformą problem pojawi się w rozwiązaniu składającym się z pięciu klastrów
obsługujących 500 aplikacji, wówczas będzie miał wpływ na jedynie 20% ogółu aplikacji. Wadą
takiego rozwiązania jest konieczność obsługi pięciu klastrów i to, że współczynnik konsolidacji
nie będzie aż tak dobry jak w przypadku pojedynczego klastra. Dan Woods napisał i
opublikował na stronie https://medium.com/@daniel.p.woods/on-infrastructure-at-scale-acascading-failure-of-distributed-systems-7cff2a3cd2df świetny artykuł dotyczący kaskadowych
awarii w produkcyjnym środowisku Kubernetes. To jest doskonały przykład pokazujący,
dlaczego w większych środowiskach należy rozważyć zastosowanie architektury składającej się
z wielu klastrów.
Zgodność to następna kwestia, na którą trzeba zwrócić uwagę w trakcie projektowania
rozwiązania składającego się z wielu klastrów. To wynika z istnienia specjalnych warunków,
które trzeba uwzględnić podczas wykonywania zadań dotyczących np. PCI (ang. payment card
industry) i HIPAA (ang. health insurance portability and accountability). Oczywiście, w
Kubernetes nie brakuje odpowiednich funkcji przeznaczonych do obsługi wielodostępności.
Jednak wymienione zadania stają się łatwiejsze do zarządzania po ich oddzieleniu od zadań
ogólnych. To może oznaczać istnienie pewnych określonych wymagań związanych z
zapewnieniem większego bezpieczeństwa, unikaniem współdzielenia komponentów, a także
respektowaniem pewnych wymagań dotyczących zadań. Znacznie łatwiej będzie rozdzielić te
zadania, niż traktować klaster w tak specjalistyczny sposób.
Zapewnienie bezpieczeństwa w ogromnych klastrach Kubernetes staje się niezwykle trudnym
zadaniem. Gdy do klastra Kubernetes zaczniesz dodawać kolejne zespoły, które mogą się różnić
jedynie wymaganiami w zakresie zapewnienia bezpieczeństwa, ich spełnienie może się okazać
bardzo trudne w ogromnym klastrze zapewniającym wielodostępność. Nawet zarządzanie
dostępem na podstawie roli użytkownika, politykami sieciowymi lub politykami zapewnienia
bezpieczeństwa podom może po skalowaniu stać się w klastrze niezwykle trudne. Mała zmiana
w polityce sieciowej może przypadkowo narazić na niebezpieczeństwo innych użytkowników
danego klastra. Jeżeli rozwiązanie składa się z wielu klastrów, negatywne efekty działania
błędnej konfiguracji można ograniczyć. Jeżeli zdecydujesz, że większy klaster Kubernetes lepiej
spełnia Twoje wymagania, wówczas upewnij się, że przygotowany został bardzo dobry proces
operacyjny pozwalający wprowadzić zmiany w ustawieniach dotyczących zapewnienia
bezpieczeństwa i że przed wprowadzeniem zmian w RBAC, polityce sieciowej lub polityce
zapewnienia bezpieczeństwa poda dokładnie poznałeś pole rażenia.
Kubernetes nie oferuje silnej wielodostępności, ponieważ współdzieli API ze wszystkimi
zadaniami uruchomionymi w klastrze. Przestrzeń nazw zapewnia nam dobrą wielodostępność,
choć niewystarczającą do ochrony przed uruchomionymi w klastrze zadaniami, których
działanie można określić jako wrogie. Niewielu użytkowników wymaga silnej wielodostępności.
Ufają oni, że zadania będą uruchomione w klastrze. Natomiast silna wielodostępność jest
zwykle wymagana, gdy jesteś dostawcą usług chmury lub uruchamiasz oprogramowanie typu
SaaS (ang. software as a service) bądź też niezaufane zadania z niebudzącą zaufania kontrolą
ze strony użytkownika.
Gdy uruchomione zadania muszą obsługiwać ruch sieciowy pochodzący z punktów końcowych
w regionie, przygotowywany projekt powinien obejmować wiele klastrów na podstawie regionu.
Jeżeli masz globalnie rozproszoną aplikację, a zarazem wiele klastrów, to staje się ona
wymaganiem. Gdy zadania muszą być rozproszone regionalnie, mamy doskonały powód do
użycia federacji wielu klastrów. Do tego tematu jeszcze powrócimy w dalszej części rozdziału.
Zadania specjalizowane, np. typu HPC (ang. high-performance computing), uczenia
maszynowego lub przetwarzania sieciowego (ang. grid computing), również mogą być
wykonywane w architekturze składającej się z wielu klastrów. Takie rodzaje zadań mogą
wymagać określonego typu sprzętu, mieć unikatowe profile wydajności działania, a także
specjalizowanych użytkowników klastrów. Jednak ta kwestia ma coraz mniejsze znaczenie w
trakcie podejmowania decyzji projektowych, ponieważ posiadanie wielu puli węzła Kubernetes
może pomóc podczas pracy ze specjalizowanym sprzętem i profilami związanymi z wydajnością
działania. Gdy będziesz potrzebował ogromnego klastra do zadania związanego z HPC lub
uczeniem maszynowym, powinieneś brać pod uwagę użycie wyłącznie oddzielnych klastrów.
W architekturze składającej się z wielu klastrów izolację otrzymujesz niejako „bez kosztów”,
choć wiąże się to z pewnymi kwestiami projektowymi, które trzeba wziąć pod uwagę.
Kwestie do rozważenia podczas
projektowania architektury składającej
się z wielu klastrów
Podczas wyboru projektu składającego się z wielu klastrów trzeba będzie pokonać kilka
przeszkód. Część z nich może zniechęcać Cię do podejmowania prób projektowania rozwiązań
opartych na wielu klastrach ze względu na nadmierne skomplikowanie architektury. Oto
wybrane kwestie spośród tych, które należy wziąć pod uwagę:
replikacja danych,
wykrywanie usług,
routing sieci,
zarządzanie operacyjne,
ciągłe wdrażanie.
Replikacja danych i zapewnienie ich spójności od zawsze były sednem we wdrażaniu zadań w
różnych regionach geograficznych i wielu klastrach. Użycie takich usług wymaga podjęcia
decyzji o tym, co gdzie zostanie uruchomione, a także opracowania odpowiedniej strategii
replikacji. Większość baz danych ma wbudowane narzędzia przeznaczone do replikacji danych.
Jednak aplikację trzeba będzie zaprojektować w taki sposób, aby obsługiwała strategię
replikacji. W przypadku baz danych typu NoSQL to zadanie będzie łatwiejsze, ponieważ bazy
danych wymienionego typu potrafią obsłużyć skalowanie między wieloma egzemplarzami. Mimo
to trzeba będzie zagwarantować, że aplikacja zapewni spójność między regionami
geograficznymi, a przynajmniej utrzymać ten sam poziom opóźnienia między nimi. Część usług
chmury, np. Google Cloud Spanner i Microsoft Azure CosmosDB, ma wbudowane usługi bazy
danych, które pomagają w skomplikowanych kwestiach związanych z obsługą danych między
regionami geograficznymi.
Każdy klaster Kubernetes wdraża własny rejestr wykrywania usług, a poszczególne rejestry nie
są synchronizowane między klastrami. To utrudnia identyfikację aplikacji i ich wzajemne
wykrywanie. Narzędzia takie jak HashiCorp Consul potrafią w sposób niezauważalny dla
użytkowników synchronizować usługi wielu klastrów, a nawet usługi znajdujące się poza
Kubernetes. Dostępne są także inne narzędzia — np. Istio, Linkerd i Cillium — zbudowane na
podstawie architektury wielu klastrów i rozszerzające możliwości w zakresie wykrywania usług.
Kubernetes znacznie ułatwia obsługę sieci w klastrze, ponieważ mamy do czynienia z prostą
siecią, nieużywającą żadnego rozwiązania w postaci NAT (ang. network address translation).
Jeżeli trzeba przekazywać ruch sieciowy do klastra i z klastra, to zadanie staje się znacznie
bardziej skomplikowane. Przychodzący do klastra ruch sieciowy jest implementowany jako
mapowanie 1:1 ruchu sieciowego do klastra, ponieważ w przypadku zasobu Ingress nie są
obsługiwane topologie wieloklastrowe. Konieczne jest również przeanalizowanie wychodzącego
ruchu sieciowego między klastrami i sposób jego przekierowywania. Gdy aplikacja znajduje się
w pojedynczym klastrze, w tym przypadku istnieje łatwe rozwiązanie. Natomiast po
przygotowaniu architektury składającej się z wielu klastrów trzeba uwzględnić opóźnienie
wynikające z dodatkowych przeskoków dla usług, które mają zależności aplikacji w innym
klastrze. W przypadku aplikacji ze ściśle powiązanymi zależnościami warto rozważyć
uruchamianie tych usług w tym samym klastrze, aby w ten sposób wyeliminować opóźnienie i
uprościć rozwiązanie.
Jedno z największych obciążeń związanych z zarządzaniem wieloma kastrami to zarządzanie
operacyjne. W środowisku możesz mieć więcej klastrów niż jeden lub kilka, którymi trzeba
zarządzać i między którymi trzeba zachować spójność. Jednym z najważniejszych aspektów
zarządzania wieloma klastrami jest przygotowanie dobrych rozwiązań w dziedzinie
automatyzacji, ponieważ to pozwoli zmniejszyć obciążenie związane z operacjami. Podczas
automatyzacji klastrów pod uwagę należy wziąć infrastrukturę wdrożenia i kwestie związane z
zarządzaniem dodatkową funkcjonalnością klastrów. Użycie narzędzia typu HashiCorp
Terraform może pomóc we wdrażaniu spójnych informacji o stanie floty klastrów i zarządzaniu
nią.
Narzędzia infrastruktury jako kodu (ang. infrastructure as code, IaC), takie jak Terraform,
umożliwiają wdrażanie klastrów w przewidywalny sposób. Z drugiej strony musisz mieć
możliwość spójnego zarządzania dodatkami do klastra, np. narzędziami do monitorowania,
rejestrowania danych, obsługi ruchu sieciowego, zapewniania bezpieczeństwa itd. Kwestie
związane z zapewnieniem bezpieczeństwa to bardzo ważny aspekt zarządzania operacyjnego i
musisz mieć możliwości w zakresie obsługi polityki zapewnienia bezpieczeństwa, kontroli
dostępu na podstawie roli użytkownika, a także polityki sieciowej między klastrami. W dalszej
części rozdziału nieco dokładniej przedstawimy zagadnienia związane ze stosowaniem
automatyzacji do zachowania spójności klastrów.
W przypadku wielu klastrów i technik nieustannego wdrażania konieczna będzie praca z
wieloma punktami końcowymi API Kubernetes, a nie tylko z jednym. To może rodzić pewne
wyzwania pod względem dystrybucji aplikacji. Zyskasz możliwość łatwego zarządzania wieloma
potokami, ale jeśli będziesz musiał zarządzać setkami potoków, wówczas dystrybucja aplikacji
stanie się niezwykle trudna. Mając to na uwadze, trzeba szukać innych sposobów
pozwalających na zarządzanie rozwiązaniem w takiej sytuacji. W dalszej części rozdziału
przedstawimy pewne podejścia ułatwiające zarządzanie.
Zarządzanie wieloma wdrożeniami
klastrów
Jeden z pierwszych kroków do wykonania podczas zarządzania wdrożeniami składającymi się z
wielu klastrów jest wykorzystanie narzędzia IaC, takiego jak Terraform, do konfiguracji
wdrożenia. Można użyć także innych narzędzi wdrożenia, np. kubespray, kops lub oferowanych
przez dostawców usług chmury. Jednak najważniejsze jest skorzystanie z narzędzia
pozwalającego umieścić w systemie kontroli wersję kodu wdrożenia klastra, ponieważ dzięki
temu masz pewność powtarzalności wdrożenia.
Automatyzacja jest kluczem do udanego zarządzania wieloma klastrami w środowisku. Być
może nie wszystko zostanie zautomatyzowane pierwszego dnia, jednak automatyzację
wszystkich aspektów wdrożenia klastra powinieneś potraktować priorytetowo.
Projektem Kubernetes, którego rozwój warto śledzić, jest API Cluster (https://clusterapi.sigs.k8s.io/). API Cluster oferuje deklaracyjne i działające w stylu Kubernetes API
przeznaczone do tworzenia klastra, jego konfigurowania i zarządzania nim. Udostępnia
opcjonalną dodatkową funkcjonalność zbudowaną na podstawie Kubernetes. API Cluster
zapewnia konfigurację na poziome klastra, deklarowaną za pomocą API. Dzięki temu zyskujesz
możliwość łatwej automatyzacji z wykorzystaniem narzędzi kompilacji. W czasie gdy pisaliśmy
te słowa, projekt jeszcze nie był ukończony.
Wzorce wdrażania i zarządzania
Operatory Kubernetes zostały wprowadzone jako implementacja koncepcji infrastruktury jako
oprogramowania. Ich stosowanie pozwala na abstrakcję wdrożenia aplikacji i usług w klastrze
Kubernetes. Przykładowo przyjmujemy założenie, że chcesz ustandaryzować za pomocą
narzędzia Prometheus monitorowanie klastrów Kubernetes. Trzeba tworzyć wiele różnych
obiektów (wdrożenie, usługi, ruch sieciowy itd.) dla poszczególnych klastrów i zespołów oraz
nimi zarządzać. Trzeba również obsługiwać podstawową konfigurację Prometheusa, np. wersje,
trwały magazyn danych i replikację danych. Jak można sobie wyobrazić, obsługa takiego
rozwiązania może być trudna w przypadku ogromnej liczby klastrów i zespołów.
Zamiast zmagać się z tak wieloma obiektami i konfiguracjami, można zainstalować
prometheus-operator. To oprogramowanie rozszerza API Kubernetes: udostępnia wiele
nowych rodzajów obiektów — Prometheus, ServiceMonitor, PrometheusRule i AlertManager
— pozwalających na określenie za pomocą jedynie kilku obiektów wszystkich szczegółów
wdrożenia Prometheusa. Narzędzie kubectl można wykorzystać do zarządzania takimi
obiektami, podobnie jak podczas zarządzania innymi obiektami API Kubernetes.
Architektura oprogramowania prometheus-operator została pokazana na rysunku 12.1.
Rysunek 12.1. Architektura oprogramowania prometheus-operator
Zastosowanie wzorca operatora do automatyzacji kluczowych zadań operacyjnych może pomóc
w zakresie ogólnych możliwości związanych z zarządzaniem klastrem. Wzorzec operatora został
wprowadzony przez zespół CoreOS w 2016 roku, z operatorem etcd i oprogramowaniem
prometheus-operator. Wzorzec operatora został oparty na dwóch koncepcjach:
definicjach zasobów niestandardowych,
kontrolerach niestandardowych.
Definicja zasobu niestandardowego (ang. custom resource definition, CDR) to obiekt
pozwalający na rozszerzenie API Kubernetes na podstawie samodzielnie zdefiniowanego API.
Kontroler niestandardowy jest zbudowany na podstawie koncepcji Kubernetes zasobu i
kontrolera. Kontroler niestandardowy pozwala opracować własną logikę przez monitorowanie
zdarzeń z obiektów API Kubernetes, takich jak przestrzenie nazw, zasoby Deployment, pody, a
także samodzielnie przygotowane definicje zasobów niestandardowych. W przypadku
kontrolera niestandardowego definicję zasobu niestandardowego można utworzyć w sposób
deklaracyjny. Jeżeli przeanalizujesz sposób działania kontrolera Deployment w Kubernetes w
pętli, aby zawsze zachować stan obiektu wdrożenia i stan deklaratywny, te same zalety
kontrolerów będziesz mógł wykorzystać w samodzielnie tworzonych definicjach zasobów
niestandardowych.
Podczas stosowania wzorca operatora można tworzyć narzędzia do automatyzacji dla zadań
operacyjnych, które muszą być wykonane przez narzędzia operacyjne w architekturze
składającej się z wielu klastrów. Przykładowo przeanalizuj operator Elasticsearch
(https://github.com/upmc-enterprises/elasticsearch-operator). W rozdziale 3. ten właśnie
operator Elasticsearch w połączeniu z Logstash i Kibana (czyli tzw. stos ELK) został
wykorzystany do przeprowadzenia agregacji dzienników zdarzeń klastra. Operator
Elasticsearch ma możliwość wykonywania następujących operacji:
replikacji węzłów głównego, klienta i danych,
definiowania stref dla wdrożeń charakteryzujących się wysoką dostępnością,
definiowania wielkości dla węzłów głównego i danych,
zmiany wielkości klastra,
tworzenia migawek w celu przygotowania kopii zapasowej klastra Elasticsearch.
Jak możesz zobaczyć, operator zapewnia automatyzację wielu zadań, które trzeba wykonywać
podczas zarządzania Elasticsearch, np. automatyzację tworzenia migawek dla kopii zapasowej i
automatyzację zmiany wielkości klastra. Piękno tego rozwiązania polega na tym, że wszystkie
operacje zarządzania są przeprowadzane za pomocą znanych obiektów Kubernetes.
Zastanów się nad tym, jak w swoim środowisku możesz wykorzystać zalety różnych operatorów,
np. prometheus-operator, a także jak możesz samodzielnie utworzyć operatory przeznaczone
do realizacji najczęściej wykonywanych zadań operacyjnych.
Podejście GitOps w zakresie zarządzania
klastrami
Podejście GitOps zostało spopularyzowane przez firmę Weaveworks, a jego idea i podstawy
powstały na fundamencie doświadczeń, które pracownicy wymienionej firmy zdobyli podczas
używania Kubernetes w środowisku produkcyjnym. GitOps wykorzystuje koncepcję cyklu
życiowego tworzenia oprogramowania i stosuje ją względem operacji. Dzięki GitOps
repozytorium staje się źródłem prawdy, klaster zaś jest zsynchronizowany i skonfigurowany z
repozytorium Git. Przykładowo, jeśli uaktualnisz manifest zasobu Deployment w Kubernetes, te
zmiany konfiguracyjne zostaną automatycznie odzwierciedlone w informacjach zawierających
dane o stanie klastra.
Dzięki użyciu tej metody można znacznie łatwiej obsługiwać architekturę składającą się z wielu
klastrów, zapewnić spójność i uniknąć nawet drobnych różnic w konfiguracji poszczególnych
węzłów floty. Podejście GitOps pozwala na deklaracyjne opisanie klastrów dla wielu środowisk i
przechowywanie informacji o stanie klastra. Wprawdzie GitOps ma zastosowanie w zakresie
dostarczania aplikacji i operacji, ale w niniejszym rozdziale skoncentrujemy się na użyciu tego
podejścia do zarządzania klastrami i narzędziami operacji.
Weaveworks Flux to jedno z pierwszych narzędzi pozwalających na zastosowanie podejścia
GitOps. To zarazem narzędzie, z którego będziemy korzystać w pozostałej części rozdziału. W
ekosystemie natywnej chmury może być dostępnych wiele innych, nowych narzędzi, z którymi
warto się zapoznać. Przykładem jest Argo CD firmy Intuit, zaadaptowane do stosowania
podejścia GitOps.
Na rysunku 12.2 pokazaliśmy sposób pracy z wykorzystaniem podejścia GitOps.
Rysunek 12.2. Podejście GitOps
Zaczynamy od skonfigurowania operatora Flux w klastrze i zsynchronizowania repozytorium z
klastrem.
$ git clone https://github.com/weaveworks/flux
$ cd flux
W następnym kroku wprowadzimy zmiany w pliku manifestu Dockera, aby skonfigurować go z
repozytorium utworzonym w rozdziale 6. Zmodyfikuj przedstawiony tutaj wiersz kodu w pliku
Deployment, aby odpowiadał wspomnianemu repozytorium.
$ vim deploy/flux-deployment.yaml
Teraz w repozytorium Git zmodyfikuj poniższy wiersz kodu:
--git-url=git@github.com:weaveworks/flux-get-started
url=git@github.com:nazwa_repozytorium/kbp)
(ex. --git-
Po tym można przystąpić do wdrożenia operatora Flux w klastrze.
$ kubectl apply -f deploy
Podczas instalacji operatora Flux następuje utworzenie klucza SSH, który będzie używany do
uwierzytelniania w repozytorium Git. Działające w powłoce narzędzie Flux należy wykorzystać
do pobrania klucza SSH, aby można było skonfigurować dostęp do utworzonego wcześniej
repozytorium. Zaczynamy od zainstalowania fluxctl.
W systemie macOS instalację można przeprowadzić za pomocą menedżera pakietów Brew —
wystarczy w tym celu wydać następujące polecenie:
$ brew install fluxctl
Instalacja za pomocą pakietów Snap systemu Linux wymaga wydania poniższego polecenia:
$ snap install fluxctl
W przypadku wszystkich pozostałych pakietów najnowsze wersje plików binarnych znajdziesz
na stronie https://github.com/fluxcd/flux/releases:
$ fluxctl identity
Przejdź do serwisu GitHub, następnie do utworzonego repozytorium, a potem na stronę
Setting/Deploy keys. Kliknij przycisk Add deploy key, nadaj mu tytuł, zaznacz pole wyboru
Allow write access, wklej klucz publiczny Flux i kliknij przycisk Add key. Więcej informacji na
temat zarządzania wdrożonymi kluczami znajdziesz w dokumentacji serwisu GitHub.
Jeżeli zajrzysz do dzienników zdarzeń Flux, powinieneś znaleźć informacje o synchronizacji z
repozytorium w serwisie GitHub.
$ kubectl -n default logs deployment/flux –f
Po otrzymaniu komunikatu o synchronizacji z repozytorium w serwisie GitHub powinieneś
zobaczyć, że zostały utworzone pody Elasticsearch, Prometheus, Redis i frontendu.
$ kubectl get pods –w
Dzięki wykonaniu tego przykładu zobaczyłeś, jak łatwo można synchronizować przechowywane
w repozytorium serwisu GitHub informacje o stanie z klastrem Kubernetes. Dzięki temu masz
ułatwione zadanie zarządzania wieloma narzędziami operacyjnymi w klastrze, ponieważ wiele
klastrów można synchronizować z jednym repozytorium i uniknąć sytuacji, w której między
klastrami istnieją nawet niewielkie różnice.
Narzędzia przeznaczone do zarządzania
wieloma klastrami
Podczas pracy z wieloma klastrami korzystanie z polecenia kubectl dość szybko okazuje się
męczące, ponieważ trzeba definiować odmienne konteksty i zarządzać poszczególnymi
klastrami. Dwa narzędzia powłoki, które będziesz musiał zainstalować już na samym początku,
gdy pojawi się konieczność zarządzania wieloma klastrami, to kubectx i kubens. Pozwalają one
na łatwą zmianę między wieloma kontekstami i przestrzeniami nazw.
Jeśli potrzebne jest w pełni wyposażone narzędzie przeznaczone do zarządzania wieloma
klastrami, w ekosystemie znajdziesz kilka rozwiązań. Poniżej pokrótce przedstawiliśmy trzy
spośród najpopularniejszych:
Rancher pozwala na centralne zarządzanie wieloma klastrami Kubernetes za pomocą
scentralizowanego interfejsu użytkownika. Monitoruje klastry, zarządza nimi, tworzy ich
kopie zapasowe i je przywraca. Obsługuje także klastry, które działają w środowisku
chmury oraz w środowiskach hostingu Kubernetes. Oferuje również narzędzia
przeznaczone do kontrolowania aplikacji wdrożonych w wielu klastrach i narzędzia
operacyjne.
KQueen zapewnia samoobsługowy portal do obsługi wielodostępności w klastrze
Kubernetes. To rozwiązanie jest skoncentrowane na audycie, widoczności i zapewnieniu
bezpieczeństwa wielu klastrów Kubernetes. KQueen to projekt typu open source, który
został opracowany przez firmę Mirantis.
Gardener stosuje zupełnie inne podejście w zakresie zarządzania wieloma klastrami i
wykorzystuje podstawowe komponenty Kubernetes w celu dostarczenia użytkownikom
końcowym Kubernetes w postaci usługi. Zapewniona jest obsługa wszystkich
najważniejszych dostawców chmury. Rozwiązanie zostało opracowane przez firmę SAP i
jest przeznaczone dla osób tworzących produkt dostarczany później w postaci Kubernetes
jako usługi.
Federacja Kubernetes
Pierwsza wersja federacji została wprowadzona w Kubernetes 1.3 i ostatnio została uznana za
przestarzałą, a jej miejsce zajęła federacja w wersji drugiej. Celem pierwszej wersji była pomoc
w rozproszeniu aplikacji między wieloma klastrami. Została ona zbudowana na podstawie API
Kubernetes i ściśle opierała się na adnotacjach Kubernetes, co doprowadziło do pewnych
problemów w jej projekcie. Ten projekt został zbyt ściśle powiązany z API Kubernetes, a
skutkiem była dość monolityczna natura pierwszej wersji federacji. W owym czasie podjęte
decyzje projektowe prawdopodobnie nie były złe i opierały się na dostępnych komponentach.
Wprowadzenie definicji zasobów niestandardowych w Kubernetes pozwoliło na zaprojektowanie
federacji w zupełnie inny sposób.
Druga wersja federacji (obecnie określana mianem KubeFed) wymaga Kubernetes 1.11+. W
czasie gdy pisaliśmy te słowa, prace nad nową wersją federacji były w fazie alfa. Ta wersja
została zbudowana na podstawie koncepcji definicji zasobów niestandardowych i kontrolerów
niestandardowych, co umożliwiło rozszerzenie Kubernetes za pomocą nowego API. Utworzenie
rozwiązania na fundamencie CDR pozwoliło, aby federacja miała nowy typ API i nie była
ograniczona do obiektów wdrożenia stosowanych w pierwszej wersji federacji.
Użycie KubeFed niekoniecznie wiąże się z zarządzaniem wieloma klastrami, choć zapewnia
wdrożenia charakteryzujące się wysoką dostępnością w wielu klastrach. Pozwala na połączenie
wielu klastrów w pojedynczy punkt końcowy zarządzania w celu dostarczania aplikacji w
Kubernetes. Przykładowo, jeśli masz klaster znajdujący się w wielu środowiskach publicznej
chmury, możesz te klastry połączyć w jedną płaszczyznę kontrolną w celu zarządzania
wdrożeniami we wszystkich klastrach i tym samym zwiększyć odporność aplikacji na awarie.
W czasie gdy ta książka powstawała, federacja była obsługiwana z wymienionymi tutaj
zasobami:
Namespace,
ConfigMap,
Secret,
Ingress,
Service,
Deployment,
ReplicaSet,
HPA,
DaemonSet,
Job.
Aby dowiedzieć się więcej na temat sposobu działania federacji, najpierw spójrz na jej
architekturę pokazaną na rysunku 12.3.
Rysunek 12.3. Architektura federacji Kubernetes
Trzeba pamiętać, że w przypadku federacji nie wszystko jest kopiowane do każdego klastra.
Przykładowo w zasobach Deployment i ReplicaSet definiuje się liczbę replik, które następnie
będą istniały w klastrach. To jest rozwiązanie domyślne dla zasobu Deployment, choć jego
konfigurację można zmienić. Z kolei jeśli utworzysz przestrzeń nazw, będzie miała ona zasięg
klastra i zostanie utworzona w każdym klastrze. Zasoby Secret, ConfigMap i DaemonSet
działają w taki sam sposób i są kopiowane do poszczególnych klastrów. Zasób Ingress jest
nieco inny od wymienionych obiektów, ponieważ powoduje utworzenie globalnego zasobu dla
wielu klastrów, z jednym punktem wyjścia do usługi. Na podstawie sposobu działania KubeFed
możesz zobaczyć, że oparte na nim rozwiązanie obsługuje wiele regionów, wiele chmur i
globalne wdrażanie aplikacji w Kubernetes.
Spójrz na przykład stosującego federację zasobu Deployment:
apiVersion: types.kubefed.io/v1beta1
kind: FederatedDeployment
metadata:
name: test-deployment
namespace: test-namespace
spec:
template:
metadata:
labels:
app: nginx
spec:
replicas: 5
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
placement:
clusters:
- name: azure
- name: gogle
Ten przykład powoduje utworzenie stosującego federację zasobu Deployment poda NGINX z
pięcioma replikami, które następnie zostają rozproszone między klastry w chmurze Azure i
klaster w chmurze Google.
Konfiguracja stosujących federację klastrów Kubernetes wykracza poza zakres tematyczny
naszej książki. Więcej informacji na ten temat znajdziesz z dokumentacji KubeFed
opublikowanej
na
stronie
https://github.com/kubernetessigs/kubefed/blob/master/docs/userguide.md.
Technologia KubeFed nadal jest w fazie alfa. Wprawdzie warto obserwować jej rozwój, ale
jednocześnie należy korzystać z już istniejących narzędzi pozwalających na udaną
implementację Kubernetes, charakteryzującą się wysoką dostępnością, i wdrożenia w wielu
klastrach.
Najlepsze praktyki dotyczące zarządzania
wieloma klastrami
Rozważ zastosowanie wymienionych tutaj najlepszych praktyk dotyczących zarządzania
wieloma klastrami Kubernetes.
Ograniczaj pole rażenia klastrów. W tym celu upewnij się, że kaskadowe awarie nie będą
miały większego wpływu na aplikacje.
Jeżeli masz obawy związane z PCI, HIPPA lub HiTrust, pomyśl o wykorzystaniu wielu
klastrów w celu zmniejszenia poziomu skomplikowania podczas łączenia wymienionych
zadań z zadaniami ogólnymi.
Jeżeli silna wielodostępność jest wymaganiem biznesowym, wówczas zadanie powinno
zostać wdrożone w oddzielnym klastrze.
Jeżeli aplikacja jest wymagana w wielu regionach, do zarządzania ruchem sieciowym
między klastrami wykorzystaj GLB (ang. global load balancer).
Zadania specjalizowane, np. HPC, można wydzielić do oddzielnych klastrów, aby mieć
pewność, że wymagania stawiane przez te zadania zostały spełnione.
Jeżeli wdrażane są zadania rozproszone między wiele regionalnych centrów danych,
przede wszystkim należy się upewnić, że istnieje strategia replikacji danych dla tego
zadania. Utworzenie wielu klastrów między regionami może być łatwym zadaniem, ale ich
replikowanie może stać się skomplikowane. Dlatego też upewnij się, że istnieje strategia
przeznaczona do obsługi zadań synchronicznych i asynchronicznych.
Do obsługi zautomatyzowanych zadań operacyjnych wykorzystaj operatory Kubernetes,
takie jak prometheus-operator lub Elasticsearch.
Podczas projektowania strategii dotyczącej wielu klastrów rozważ kwestie związane z
odkrywaniem usług i obsługą sieci między klastrami. Narzędzia architektury Service
Mesh, np. HashiCorp Consul lub Istio, mogą pomóc w obsłudze sieci między klastrami.
Upewnij się, że Twoja strategia w zakresie ciągłego wdrażania potrafi zapewnić obsługę
wdrożeń między regionami lub wieloma klastrami.
Przeanalizuj możliwość użycia podejścia GitOps w zakresie zarządzania komponentami
operacyjnymi wielu klastrów, aby w ten sposób zapewnić spójność między wszystkimi
klastrami floty. Podejście GitOps nie będzie odpowiednie zawsze i w każdym środowisku,
choć warto przynajmniej sprawdzić możliwość jego zastosowania, ponieważ może ułatwić
zarządzanie operacyjne w środowisku składającym się z wielu klastrów.
Podsumowanie
W tym rozdziale zostały omówione różne strategie związane z zarządzaniem wieloma klastrami
Kubernetes. Bardzo ważne jest określenie własnych potrzeb i ustalenie, czy będą one
dopasowane do topologii złożonej z wielu klastrów. Przede wszystkim należy zastanowić się nad
tym, czy naprawdę potrzebna jest silna wielodostępność, ponieważ automatycznie oznacza to
konieczność stosowania struktury składającej się z wielu klastrów. Jeżeli nie potrzebujesz silnej
wielodostępności, zastanów się nad swoimi potrzebami w zakresie zgodności i ustal, czy
pojemność operacyjna, którą masz do dyspozycji, pozwala wykorzystać architekturę złożoną z
wielu klastrów. Jeżeli zdecydujesz się na zastosowanie większej liczby mniejszych klastrów,
upewnij się, że stosujesz automatyzację wdrażania klastrów i zarządzania nimi, aby w ten
sposób zmniejszyć obciążenie operacyjne.
Rozdział 13. Integracja usług
zewnętrznych z Kubernetes
Z licznych rozdziałów tej książki dowiedziałeś się, jak można tworzyć w Kubernetes usługi,
wdrażać je i zarządzać nimi. Jednak trzeba sobie wyraźnie powiedzieć, że systemy nie istnieją w
próżni, a większość budowanych usług będzie wymagała integracji z systemami i usługami
znajdującymi się poza danym klastrem Kubernetes. To może być skutkiem tworzenia nowych
usług, które następnie są używane przez starszą infrastrukturę, działającą w urządzeniach
wirtualnych lub fizycznych. Być może tworzone usługi muszą mieć zapewniony dostęp do
istniejących baz danych bądź innych usług, które zostały uruchomione w fizycznej
infrastrukturze w centrum danych. Ewentualnie możesz mieć wiele różnych klastrów
Kubernetes z usługami wymagającymi powiązania. Z tych wszystkich powodów możliwość
ujawniania, udostępniania i tworzenia usług wykraczających poza granice klastra Kubernetes to
ważny aspekt tworzenia rzeczywistych aplikacji.
Importowanie usług do Kubernetes
Najczęściej stosowany wzorzec w zakresie połączenia Kubernetes z usługami zewnętrznymi
polega na użyciu usługi istniejącej poza klastrem Kubernetes. Bardzo często tak się dzieje,
ponieważ Kubernetes używa się do opracowania nowych aplikacji lub interfejsów dla starszych
zasobów, takich jak bazy danych. Taki wzorzec jest najsensowniejszy podczas przyrostowego
opracowywania usług natywnej chmury. Skoro warstwa bazy danych zawiera istotne dane o
znaczeniu krytycznym, jej przeniesienie do chmury, już nie wspominając o kontenerach, jest
dużym obciążeniem. Zarazem dużą wartość ma dostarczenie nowoczesnej warstwy na
podstawie wspomnianej bazy danych (np. zaoferowanie interfejsu GraphQL) będącej podstawą
podczas tworzenia nowej generacji aplikacji. Przeniesienie tej warstwy do Kubernetes często
ma duży sens, ponieważ intensywne prace programistyczne i niezawodne ciągłe wdrażanie tego
oprogramowania pośredniczącego pozwalają osiągnąć dużą zwinność przy jedynie minimalnym
ryzyku. Oczywiście, aby można było zapewnić sobie taką możliwość, baza danych musi być
dostępna w Kubernetes.
Gdy analizujesz możliwość utworzenia usługi zewnętrznej dostępnej dla Kubernetes, pierwszym
wyzwaniem będzie przygotowanie odpowiedniej konfiguracji sieci. Szczegóły związane z
przygotowaniem sieci będą zależały zarówno od lokalizacji bazy danych, jak i położenia klastra
Kubernetes. Dlatego też omówienie tego zagadnienia wykracza poza zakres tematyczny książki.
Ogólnie rzecz biorąc, dostawcy Kubernetes wykorzystujący chmurę pozwalają na wdrożenia
klastra w dostarczonej przez użytkownika wirtualnej sieci prywatnej (VNET). Te sieci wirtualne
mogą być następnie łączone z innymi sieciami.
Po przygotowaniu konfiguracji sieci łączącej pody w klastrze Kubernetes i zasoby następnym
wyzwaniem jest zapewnienie usłudze zewnętrznej wyglądu i sposobu działania takich, jakie ma
usługa Kubernetes. W Kubernetes operacja wykrywania usług odbywa się za pomocą operacji
wyszukiwania DNS. Dlatego też, aby zewnętrzna baza danych prezentowała się jak natywna
część Kubernetes, musi być możliwa do odkrycia w DNS.
Pozbawiona selektora usługa dla stabilnego adresu
IP
Pierwszym sposobem na osiągnięcie zamierzonego celu jest wykorzystanie pozbawionej
selektora usługi Kubernetes. Gdy tworzysz nową usługę Kubernetes bez selektora, wówczas nie
ma podów dopasowywanych do tej usługi, więc nie działa mechanizm równoważenia
obciążenia. Zamiast tego pozbawioną selektora usługę można zaprogramować w taki sposób,
aby miała określony adres IP zasobu zewnętrznego, który chcesz dodać do klastra Kubernetes.
W ten sposób, gdy pod Kubernetes przeprowadza wyszukiwanie (w omawianym przykładzie to
my-external-database), wbudowany serwer DNS Kubernetes konwertuje je na adres IP usługi
zewnętrznej. Spójrz na przykład pozbawionej selektora usługi dla zewnętrznej bazy danych.
apiVersion: v1
kind: Service
metadata:
name: my-external-database
spec:
ports:
- protocol: TCP
port: 3306
targetPort: 3306
Jeśli usługa istnieje, trzeba uaktualnić jej punkty końcowe, aby zawierały adres IP bazy danych
(24.1.2.3).
apiVersion: v1
kind: Endpoints
metadata:
# Ważne! Te dane muszą być dopasowane do usługi.
name: my-external-database
subsets:
- addresses:
- ip: 24.1.2.3
ports:
- port: 3306
Na rysunku 13.1 pokazaliśmy, jak takie rozwiązanie można zintegrować z Kubernetes.
Rysunek 13.1. Integracja usługi
Oparte na rekordzie CNAME usługi dla stabilnych
nazw DNS
W poprzednim przykładzie przyjęliśmy założenie, że zewnętrzna usługa, którą próbowaliśmy
zintegrować z klastrem Kubernetes, ma stabilny adres IP. Wprawdzie tak się często dzieje w
przypadku zasobów fizycznych, ale tak wcale nie musi być (zależy to od topologii sieci).
Prawdopodobnie nie będzie tak w środowiskach chmury, w których adresy IP maszyn
wirtualnych są znacznie bardziej dynamiczne. Ewentualnie usługa może mieć więcej replik
działających za pojedynczym mechanizmem równoważenia obciążenia opartym na DNS. W
takich sytuacjach usługa zewnętrzna, którą próbujesz zintegrować z klastrem, nie będzie miała
stabilnego adresu IP, choć zarazem będzie miała stabilną nazwę DNS.
W takim przypadku można zdefiniować usługę Kubernetes opartą na rekordzie CNAME. Jeżeli
nie masz doświadczenia w pracy z rekordami DNS, to musisz wiedzieć, że CNAME (ang.
canonical name) to rekord wskazujący na konieczność konwersji określonego adresu DNS na
inną kanoniczną nazwę DNS. Przykładowo rekord CNAME dla foo.com zawierający wartość
bar.com wskazuje, że operacja wyszukiwania foo.com powinna przeprowadzić rekurencyjne
wyszukiwanie bar.com w celu pobrania właściwego adresu IP. Istnieje możliwość użycia usługi
Kubernetes do zdefiniowania rekordów CNAME w serwerze DNS Kubernetes. Przykładowo,
jeśli masz zewnętrzną bazę danych o nazwie DNS database.myco.com, wówczas możesz
utworzyć usługę CNAME o nazwie mycodatabase. Taka usługa będzie zdefiniowana w
następujący sposób:
kind: Service
apiVersion: v1
metadata:
name: my-external-database
spec:
type: ExternalName
externalName: database.myco.com
W przypadku usługi zdefiniowanej w taki właśnie sposób każdy pod wywołujący myco-database
zostanie rekurencyjnie przekierowany do database.myco.com. Oczywiście, aby takie
rozwiązanie działało, nazwa DNS usługi zewnętrznej również musi być obsługiwana za pomocą
serwerów DNS Kubernetes. Jeżeli nazwa DNS jest dostępna globalnie (np. mamy do czynienia z
doskonale znanym dostawcą usługi DNS), wówczas przedstawione tutaj rozwiązanie będzie
działało automatycznie. Jeżeli jednak serwer DNS usługi zewnętrznej znajduje się w lokalnym
dla firmy serwerze DNS (np. serwerze DNS obsługującym jedynie wewnętrzny ruch sieciowy),
wówczas klaster Kubernetes może w domyślnej konfiguracji nie wiedzieć, jak obsłużyć
zapytania kierowane do korporacyjnego serwera DNS.
Aby skonfigurować serwer DNS klastra do komunikacji z alternatywnym resolverem DNS,
trzeba wprowadzić zmiany w konfiguracji. Można to zrobić przez uaktualnienie zasobu
ConfigMap w Kubernetes z użyciem pliku konfiguracyjnego dla serwera DNS. W czasie gdy ta
książka powstawała, większość klastrów stosowała serwer CoreDNS. Konfiguracja tego serwera
odbywa się przez zapisanie pliku Corefile w zasobie ConfigMap o nazwie coredns w przestrzeni
nazw kube-system. Jeżeli nadal korzystasz z serwera kube-dns, jest on skonfigurowany
podobnie, ale z użyciem innego zasobu ConfigMap.
Rekordy CNAME są użytecznym sposobem mapowania usług zewnętrznych o stabilnych
nazwach DNS na nazwy możliwe do odkrycia w klastrze. W pierwszej chwili mapowanie
doskonale znanego adresu DNS na lokalny dla klastra adres DNS może się wydawać
nieintuicyjne. Jednak spójność wyglądu i sposobu działania wszystkich usług zwykle jest warta
nieco większego poziomu skomplikowania. Ponadto, skoro usługa CNAME, podobnie jak
wszystkie usługi Kubernetes, jest definiowana dla przestrzeni nazw, poszczególne przestrzenie
nazw mogą mapować tę samą nazwę usługi (np. database) na odmienne usługi zewnętrzne (np.
canary lub production) w zależności od przestrzeni nazw.
Podejście oparte na aktywnym kontrolerze
W rzadkich sytuacjach żadna z wymienionych wcześniej metod udostępnienia usługi
zewnętrznej nie sprawdzi się w Kubernetes. Ogólnie rzecz biorąc, to wynika z niedostępności
zarówno stabilnego adresu DNS, jak i pojedynczego stabilnego adresu IP usługi w klastrze
Kubernetes. W takich przypadkach udostępnienie usługi zewnętrznej w klastrze Kubernetes
jest znacznie bardziej skomplikowane, choć nadal możliwe.
Aby osiągnąć zamierzony efekt, trzeba zrozumieć wewnętrzny sposób działania usług
Kubernetes. W rzeczywistości usługa Kubernetes składa się z dwóch oddzielnych zasobów:
Service, który powinien być Ci już znajomy, i Endpoints, przedstawiającego adresy IP
tworzące usługę. W trakcie normalnego działania menedżer kontrolera Kubernetes wypełnia
punkty końcowe usługi na podstawie selektora usługi. Jeżeli jednak tworzysz usługę
pozbawioną selektora, jak to miało miejsce w pierwszym podejściu, opartym na stabilnym
adresie IP, wówczas zasób Endpoints usługi nie został wypełniony, ponieważ nie istnieją żadne
wybrane pody. W takiej sytuacji konieczne jest dostarczenie pętli kontrolnej odpowiedzialnej za
utworzenie i wypełnienie właściwego zasobu Endpoints. Niezbędne jest dynamiczne
wykonywanie zapytań do infrastruktury w celu pobrania adresów IP zewnętrznej usługi w
Kubernetes, którą chcesz zintegrować, a następnie wypełnienie punktów końcowych usługi
tymi adresami IP. Gdy zastosujesz przedstawione rozwiązanie, zadziała mechanizm Kubernetes i
nastąpi zaprogramowanie zarówno serwera DNS, jak i kube-proxy, co zapewni właściwy
mechanizm równoważenia obciążenia ruchu sieciowego kierowanego do usługi zewnętrznej.
Takie rozwiązanie zostało pokazane w formie graficznej na rysunku 13.2.
Rysunek 13.2. Usługa zewnętrzna w akcji
Eksportowanie usług z Kubernetes
Z poprzedniego podrozdziału dowiedziałeś się, jak zaimportować istniejące usługi do
Kubernetes. Jednak może być także konieczne eksportowanie usług z Kubernetes do
istniejącego środowiska. Może się tak zdarzyć z powodu istnienia starej aplikacji wewnętrznej
przeznaczonej do zarządzania klientami i wymagającej dostępu do pewnych nowych API
opracowanych w infrastrukturze natywnej chmury. Ewentualnie tworzysz nową mikrousługę
opartą na API, ale jednocześnie musisz zapewnić możliwość pracy z istniejącą aplikacją
internetową zapory sieciowej, ze względu na politykę wewnętrzną lub inne regulacje.
Niezależnie od powodów możliwość udostępnienia usług z klastra Kubernetes innym aplikacjom
wewnętrznym ma krytyczne znaczenie podczas ustalania wymagań projektowych dla wielu
aplikacji.
Najważniejszym powodem, dla którego to może być wyzwaniem, jest fakt, że w wielu
instalacjach Kubernetes adresy IP poda nie mogą być przekierowane na zewnątrz klastra. Za
pomocą narzędzi, np. flannel, lub innych dostawców sieci routing odbywa się w klastrze
Kubernetes i umożliwia komunikację między podami, a także między węzłami a podami. Jednak
ten sam routing nie jest rozszerzany na dowolne urządzenia znajdujące się w tej samej sieci. Co
więcej, w przypadku połączeń chmury adresy IP podów nie zawsze są przekazywane z
powrotem przez VPN lub sieć. Dlatego też konfiguracja sieci między tradycyjną aplikacją a
podami Kubernetes ma kluczowe znaczenie dla umożliwienia operacji eksportu usług opartych
na Kubernetes.
Eksportowanie usług za pomocą wewnętrznych
mechanizmów równoważenia obciążenia
Najłatwiejszym sposobem na wyeksportowanie usług z Kubernetes jest użycie wbudowanego
obiektu Service. Jeżeli masz jakiekolwiek wcześniejsze doświadczenie w pracy z Kubernetes,
bez wątpienia spotkałeś się z użyciem opartego na chmurze mechanizmu równoważenia
obciążenia w celu przekierowania zewnętrznego ruchu sieciowego do kolekcji podów w
klastrze. Jednak mogłeś nie zdawać sobie sprawy, że większość dostawców chmury oferuje
również wewnętrzny mechanizm równoważenia obciążenia. Ten wewnętrzny mechanizm
oferuje dokładnie te same możliwości w zakresie mapowania wirtualnych adresów IP na
kolekcje podów, przy czym wirtualne adresy IP są pobierane w wewnętrznej przestrzeni
adresowej IP (np. 10.0.0.0/24) i tym samym podlegają jedynie routingowi wewnątrz danej sieci
wirtualnej. Aktywacja wewnętrznego mechanizmu równoważenia obciążenia następuje przez
dodanie charakterystycznej dla dostawcy chmury adnotacji do mechanizmu równoważenia
obciążenia Service. Przykładowo w przypadku dostawcy chmury Microsoft Azure można dodać
adnotację
service.beta.kubernetes.io/azure-load-balancer-internal:
"true".
W
przypadku AWS adnotacja ma postać service.beta.kubernetes.io/aws-load-balancerinternal: 0.0.0.0/0. Wymienione adnotacje umieszcza się we właściwości metadata zasobu
Service.
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
# Zastąp tę wartość odpowiednią dla danego środowiska.
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
...
Podczas eksportowania usługi za pomocą wewnętrznego mechanizmu równoważenia obciążenia
otrzymujesz stabilny, możliwy do routingu adres IP, który jest dostępny w sieci wirtualnej na
zewnątrz klastra. Następnie możesz użyć tego adresu bezpośrednio lub podczas konfiguracji
wewnętrznego DNS-a, by zapewnić możliwość odkrywania wyeksportowanej usługi.
Eksportowanie usług za pomocą usługi opartej na
NodePort
Niestety, w typowych instalacjach oparty na chmurze wewnętrzny mechanizm równoważenia
obciążenia jest niedostępny. W tym kontekście często dobrym rozwiązaniem jest użycie usługi
opartej na NodePort. Zasób Service typu NodePort eksportuje komponent nasłuchujący w
każdym węźle klastra, a jego zadaniem jest przekierowanie ruchu sieciowego z podanego
adresu IP węzła i numeru portu do zdefiniowanej usługi, jak to w sposób graficzny pokazaliśmy
na rysunku 13.3.
Rysunek 13.3. Usługa oparta na NodePort
Spójrz na przykładowy fragment pliku YAML dla usługi opartej na NodePort.
apiVersion: v1
kind: Service
metadata:
name: my-node-port-service
spec:
type: NodePort
...
Po utworzeniu usługi typu NodePort Kubernetes automatycznie wybiera port dla usługi. Można
go pobrać z zasobu Service przez sprawdzenie właściwości spec.ports[*].nodePort. Jeżeli
chcesz pobrać port samodzielnie, możesz go wskazać podczas tworzenia usługi, przy czym
wartość NodePort musi być skonfigurowana w zakresie klastra. Domyślny zakres dla tych
portów to od 30000 do 30999.
Działanie Kubernetes kończy się po udostępnieniu usługi na podanym porcie. Aby
wyeksportować usługę do istniejącej aplikacji na zewnątrz klastra, musisz (lub musi to zrobić
administrator sieci) zapewnić możliwość wykrycia usługi. W zależności od sposobu konfiguracji
aplikacji to może wymagać użycia listy par ${węzeł}:${port}, a aplikacja przeprowadzi
równoważenie obciążenia po stronie klienta. Ewentualnie być może trzeba będzie
skonfigurować fizyczny lub wirtualny mechanizm równoważenia obciążenia w sieci, aby ruch
sieciowy był kierowany bezpośrednio z wirtualnego adresu IP do zdefiniowanej wcześniej listy.
Konkretne szczegóły takiej konfiguracji będą zależały od używanego środowiska.
Integracja komputerów zewnętrznych z Kubernetes
Jeżeli żadne z przedstawionych wcześniej rozwiązań nie sprawdza się w danej sytuacji —
prawdopodobnie chcesz zapewnić ściślejszą integrację dla dynamicznego wykrywania usług —
ostatecznym rozwiązaniem w zakresie udostępniania usług Kubernetes aplikacjom
zewnętrznym jest bezpośrednia integracja maszyn zawierających tę uruchomioną aplikację z
mechanizmem sieciowym i mechanizmem wykrywania usług klastra Kubernetes. Takie
podejście jest zdecydowanie bardziej inwazyjne i skomplikowane niż wcześniej omówione i
powinno być stosowane jedynie w ostateczności (czyli naprawdę rzadko). W niektórych
zarządzanych środowiskach Kubernetes takie rozwiązanie jest niedozwolone.
Podczas integracji komputera zewnętrznego z klastrem w celu obsługi sieci trzeba się upewnić,
że routing sieci poda i oparty na DNS mechanizm wykrywania usług działają poprawnie.
Najłatwiejszym sposobem jest rzeczywiste uruchomienie kubeleta w komputerze, który ma
zostać dołączony do klastra, i jednocześnie wyłączenie zarządcy procesów w klastrze.
Omówienie zagadnienia dołączania węzła kubeleta do klastra wykracza poza zakres tematyczny
naszej książki. Na ten temat napisano wiele innych książek i artykułów opublikowanych w
internecie. Podczas dołączania węzła natychmiast trzeba oznaczyć go jako niedostępny dla
zarządcy procesów — za pomocą polecenia kubectl cordon ... — aby uniknąć
przeprowadzania z nim jakichkolwiek dalszych operacji związanych z mechanizmem zarządcy
procesów. To polecenie nie chroni zasobu DaemonSet przed umieszczeniem podów w węźle. Tym
samym pody dla routingu sieci i KubeProxy zostaną umieszczone w komputerze, a oparta na
Kubernetes usługa stanie są możliwa do odkrycia z poziomu każdej aplikacji uruchomionej w
komputerze.
Poprzednie podejście jest całkiem inwazyjne dla węzła, ponieważ wymaga instalacji Dockera
lub innego środowiska uruchomieniowego kontenerów. Dlatego też nie nadaje się do
stosowania w wielu środowiskach. Nieco lżejsze, choć znacznie bardziej skomplikowane
podejście polega na wykonaniu polecenia kube-proxy jako procesu w komputerze i
dostosowanie ustawień serwera DNS komputera. Przy założeniu, że jest możliwe poprawne
skonfigurowanie routingu, wykonanie polecenia kube-proxy spowoduje zdefiniowanie sieci na
poziomie komputera, więc wirtualne adresy IP usługi będą mogły być mapowane na pody
tworzące daną usługę. Jeżeli zmienisz również serwer DNS komputera w taki sposób, aby
wskazywał serwer DNS klastra Kubernetes, wówczas w praktyce włączysz wykrywanie
Kubernetes w komputerze, który nie jest częścią klastra Kubernetes.
Oba przedstawione podejścia są skomplikowane i zaawansowane, więc nie należy ich
lekceważyć. Jeżeli będziesz rozważał zastosowanie takiego mechanizmu wykrywania usług,
najpierw zadaj sobie pytanie, czy łatwiejszym rozwiązaniem nie będzie zintegrowanie tej usługi
z klastrem, któremu próbujesz ją dostarczyć.
Współdzielenie usług między Kubernetes
Z poprzednich podrozdziałów dowiedziałeś się, jak można połączyć aplikacje z usługami
zewnętrznymi, a także jak połączyć usługi zewnętrzne z aplikacjami Kubernetes. Inną ważną
kwestią jest łączenie usług między klastrami Kubernetes. Takie rozwiązanie może pomóc w
zapewnieniu odporności na awarie między różnymi regionalnymi klastrami Kubernetes, a także
w połączeniu usług uruchamianych przez różne zespoły. Proces, który prowadzi do takiej
interakcji, jest w rzeczywistości połączeniem rozwiązań omówionych w poprzednich
podrozdziałach.
Przede wszystkim trzeba udostępnić usługę w pierwszym klastrze Kubernetes, aby zapewnić
możliwość przepływu ruchu sieciowego. Przyjmujemy założenie, że działasz w środowisku
chmury obsługującym wewnętrzny mechanizm równoważenia obciążenia i otrzymujesz
wirtualny adres IP, 10.1.10.1, tego mechanizmu. Następnym krokiem jest integracja tego
wirtualnego adresu IP z drugim klastrem Kubernetes, aby w ten sposób umożliwić odkrywanie
usługi. To się odbywa w dokładnie taki sam sposób jak podczas importowania aplikacji
zewnętrznej do Kubernetes (zobacz początek rozdziału). Tworzysz pozbawioną selektora usługę
i przypisujesz jej adres IP 10.1.10.1. Po wykonaniu tych dwóch kroków masz zintegrowane w
klastrze Kubernetes mechanizmy wykrywania usługi i nawiązywania połączenia między
usługami.
Te kroki wymagają samodzielnego przeprowadzenia pewnych operacji. Wprawdzie to może być
do przyjęcia w przypadku małego, statycznego zbioru usług, ale jeśli chcesz zapewnić ściślejszą
lub automatyczną integrację usług z klastrami, wówczas sensownym rozwiązaniem będzie
opracowanie demona klastra uruchomionego w obu klastrach i przeprowadzającego niezbędne
operacje. Ten demon będzie obserwował pierwszy klaster pod kątem usług o określonej
adnotacji, np. myco.com/exported-service. Wszystkie usługi zawierające taką adnotację
zostaną zaimportowane do drugiego klastra za pomocą usług pozbawionych selektorów.
Ponadto ten sam demon będzie przeprowadzał operacje usuwania nieużytków i wszelkich usług
zaimportowanych do drugiego klastra, ale już nieistniejących w pierwszym. Jeżeli
skonfigurujesz demony w każdym z klastrów regionalnych, będziesz mógł się cieszyć
dynamiczną możliwością nawiązywania połączeń między wszystkimi klastrami w środowisku.
Narzędzia opracowane przez podmioty
zewnętrzne
Dotychczas w rozdziale zostały przedstawione różne sposoby pozwalające na importowanie i
eksportowanie usług, a także nawiązywanie połączeń z usługami obejmującymi klastry
Kubernetes i pewne zasoby zewnętrzne. Jeżeli masz już doświadczenie w pracy z technologiami
typu architektura Service Mesh, wówczas wymienione koncepcje mogą być Ci już znane.
Faktycznie istnieje wiele opracowanych przez podmioty zewnętrzne narzędzi i projektów, które
można wykorzystać do łączenia usług między Kubernetes a dowolnymi aplikacjami i
komputerami. Ogólnie rzecz biorąc, te narzędzia mogą dostarczyć sporo funkcjonalności, choć
pod względem operacyjnym są znacznie bardziej skomplikowane niż wcześniej omówione
podejścia. Jeżeli coraz częściej będziesz zajmował się nawiązywaniem połączeń sieciowych
między komponentami, zdecydowanie powinieneś skierować swoją uwagę na architekturę
Service Mesh, która jest dość intensywnie rozwijana. Niemal wszystkie narzędzia podmiotów
zewnętrznych mają komponent typu open source, a ponadto oferują komercyjną pomoc
techniczną, która może zmniejszyć operacyjne obciążenie związane z uruchamianiem
dodatkowej infrastruktury.
Najlepsze praktyki dotyczące
nawiązywania połączeń między klastrami
a usługami zewnętrznymi
Nawiązuj połączenia sieciowe między klastrami. Wprawdzie sieć może być zróżnicowana
w zależności od lokalizacji, chmury i konfiguracji klastrów, ale mimo to w pierwszej
kolejności upewnij się, że pody mogą komunikować się ze sobą.
Aby uzyskać dostęp do usługi oferowanej na zewnątrz klastra, możesz skorzystać z usługi
pozbawionej selektora i bezpośrednio zdefiniować adres IP komputera (np. zawierającego
bazę danych), z którym chcesz prowadzić komunikację. Jeżeli nie masz stałych adresów IP,
zamiast tego możesz użyć usług CNAME w celu przekierowania do nazwy DNS. W razie
zaś braku nazwy DNS i stałych usług możesz przygotować dynamiczny operator, który
okresowo będzie synchronizował adresy zewnętrznej usługi IP z punktami końcowymi
usługi Kubernetes.
W celu wyeksportowania usług z Kubernetes skorzystaj z wewnętrznego mechanizmu
równoważenia obciążenia lub usługi typu NodePort. Wewnętrzny mechanizm
równoważenia obciążenia zwykle działa szybciej w środowisku publicznej chmury, w
której może być połączony z samą usługą Kubernetes. Gdy taki mechanizm równoważenia
obciążenia jest niedostępny, usługa typu NodePort może udostępnić usługę wszystkim
komputerom w węźle.
Połączenia między klastrami Kubernetes mogą być nawiązywane za pomocą dowolnego z
wymienionych wcześniej podejść. Dzięki nim usługa zostaje udostępniona zewnętrznie, a
następnie jest używana przez pozbawioną selektora usługę w innym klastrze Kubernetes.
Podsumowanie
W rzeczywistych sytuacjach nie każda aplikacja jest natywna dla chmury. Tworzenie
rzeczywistych aplikacji bardzo często wymaga nawiązywania połączenia z istniejącymi
systemami zawierającymi nowsze aplikacje. Z lektury tego rozdziału dowiedziałeś się, jak
można zintegrować Kubernetes ze starszymi aplikacjami, a także jak integrować różne usługi
działające w poszczególnych, oddzielnych klastrach Kubernetes. O ile nie masz tego luksusu, że
możesz zbudować zupełnie nowe rozwiązanie, wdrożenie natywnej chmury zawsze będzie
wymagało pewnej integracji ze starszym kodem. Techniki omówione w tym rozdziale pokazały,
jak można to zrobić.
Rozdział 14. Uczenie
maszynowe w Kubernetes
Era mikrousług, systemów rozproszonych i chmury zapewniła doskonałe warunki środowiskowe
dla zdecentralizowanych modeli uczenia maszynowego i związanych z nimi narzędzi.
Infrastruktura na dużą skalę jest dostępna, a narzędzia związane z ekosystemem uczenia
maszynowego zostały dopracowane. Kubernetes to platforma zyskująca coraz większą
popularność wśród osób zajmujących się analizą danych oraz w większej społeczności typu
open source, ponieważ oferuje doskonałe środowisko pozwalające na stosowanie cyklu
życiowego w uczeniu maszynowym i związanych z nim rozwiązań. Z tego rozdziału dowiesz się,
dlaczego Kubernetes to doskonałe rozwiązanie do uczenia maszynowego. Poznasz również
najlepsze praktyki przeznaczone dla administratorów klastrów i osób zajmujących się analizą
danych — dzięki tym praktykom będą oni mogli wykorzystać pełnię możliwości Kubernetes
podczas wykonywania zadań związanych z uczeniem maszynowym. W szczególności
skoncentrujemy się na tzw. uczeniu głębokim (ang. deep learning), nie na uczeniu maszynowym
w tradycyjnym ujęciu, ponieważ uczenie maszynowe bardzo szybko stało się na platformach
takich jak Kubernetes obszarem innowacji.
Dlaczego Kubernetes doskonale
sprawdza się w połączeniu z uczeniem
maszynowym?
Technologia Kubernetes bardzo szybko stała się polem coraz szybszej innowacji w obszarze
uczenia głębokiego. Zbieżność narzędzi i bibliotek, takich jak TensorFlow, powoduje, że ta
technologia jest obecnie dostępna dla większej niż kiedyś grupy osób zajmujących się analizą
danych. Być może zastanawiasz się, co powoduje, że Kubernetes to doskonałe rozwiązanie do
wykonywania zadań związanych z uczeniem głębokim. Przekonaj się, co ma do zaoferowania
pod tym względem:
Wszechstronność
Kubernetes jest wszędzie. Wszyscy najważniejsi dostawcy chmury zapewniają obsługę
Kubernetes. Istnieją również dystrybucje przeznaczone dla prywatnej chmury i
infrastruktury. Podstawowy ekosystem narzędzi na platformie takiej jak Kubernetes
pozwala użytkownikom na uruchamianie w dowolnym miejscu zadań związanych z
uczeniem głębokim.
Skalowalność
Zadania związane z uczeniem głębokim zwykle wymagają dostępu do ogromnej mocy
obliczeniowej w celu efektywnego trenowania modeli uczenia maszynowego. Kubernetes
oferuje wbudowane możliwości w zakresie automatycznego skalowania, dzięki którym
użytkownicy zajmujący się analizą danych mogą wybrać dowolną skalę i dostosować ją do
potrzeb trenowanych modeli.
Rozszerzalność
Efektywne trenowanie modelu uczenia maszynowego zwykle wymaga dostępu do
specjalizowanego sprzętu. Kubernetes pozwala administratorom klastrów szybko i łatwo
udostępniać nowe typy sprzętu mechanizmowi zarządcy procesów, bez konieczności zmiany
kodu źródłowego Kubernetes. To pozwala z kolei bezproblemowo integrować
niestandardowe zasoby i kontrolery z API Kubernetes, aby w ten sposób obsługiwać
specjalizowane zadania, takie jak dostosowanie hiperparametru do własnych potrzeb.
Samoobsługa
Użytkownicy zajmujący się analizą danych mogą używać Kubernetes do samodzielnego
wykonywania na żądanie zadań związanych z uczeniem maszynowym. Nie trzeba mieć do
tego specjalizowanej wiedzy z zakresu Kubernetes.
Przenośność
Biorąc pod uwagę to, że na podstawie API Kubernetes opracowano odpowiednie narzędzia,
modele uczenia maszynowego mogą być uruchamiane wszędzie. Dzięki temu zadania
związane z uczeniem maszynowym stały się przenośne między poszczególnymi dostawcami
Kubernetes.
Sposób pracy z zadaniami uczenia
głębokiego
Aby efektywnie poznać potrzeby związane z uczeniem głębokim, trzeba dogłębnie zrozumieć
cały proces pracy z takimi zadaniami. Na rysunku 14.1 pokazaliśmy uproszczony sposób pracy z
zadaniami uczenia głębokiego.
Rysunek 14.1. Sposób pracy z zadaniami uczenia głębokiego
Na rysunku 14.1 pokazaliśmy sposób pracy z zadaniami uczenia głębokiego, który składa się z
następujących etapów:
Przygotowanie zbioru danych
Ten etap obejmuje pamięć masową, indeksowanie, katalogowanie i metadane powiązane ze
zbiorem danych używanym do wytrenowania modelu. Na potrzeby tej książki uwzględnimy
jedynie pamięć masową. Wielkość zbioru danych jest zróżnicowana, od kilkuset
megabajtów do setek terabajtów. Zbiór danych musi być przekazany do modelu, aby można
go było wytrenować. Powinieneś wybrać pamięć masową o właściwościach odpowiednich
do zbioru danych. Zwykle wymagane są ogromnej skali bloki i obiekty, a całość powinna
być dostępna za pomocą natywnych w Kubernetes abstrakcji pamięci masowej lub za
pomocą dostępnego bezpośrednio API.
Opracowanie modelu
Na tym etapie osoba zajmująca się analizą danych tworzy algorytmy uczenia maszynowego,
współdzieli je lub współpracuje nad nimi. Narzędzia typu open source, takie jak
JupyterHub, są łatwe do instalacji w Kubernetes, ponieważ zwykle funkcjonują podobnie
jak każde inne zadanie.
Trenowanie
W tym procesie model będzie używał zbioru danych do nauczenia się sposobu, w jaki
zadania mają być wykonywane. Wynikiem procesu trenowania zwykle jest punkt kontrolny
informacji o stanie wytrenowanego modelu. W trakcie procesu trenowania są
wykorzystywane wszystkie możliwości oferowane przez Kubernetes. Komponenty
mechanizmu zarządcy procesów, dostępu do specjalizowanego sprzętu, zarządzania
wielkością zbioru danych, skalowania i obsługi sieci będą wykonywane po kolei w celu
realizacji zleconego zadania. Z następnego podrozdziału dowiesz się więcej na temat
specyfiki etapu trenowania modelu.
Udostępnianie
To proces, w trakcie którego wytrenowany model zostaje udostępniony dla żądań
wykonywanych przez klientów. Dzięki temu klienci mogą dokonywać przewidywań na
podstawie posiadanych danych. Przykładowo, jeśli masz model rozpoznawania obrazu
wytrenowany do wykrywania w obrazie psów i kotów, wówczas klient może dostarczyć
obraz psa, a model powinien z określoną dokładnością wskazać, czy mamy do czynienia z
obrazem przedstawiającym psa.
Uczenie maszynowe dla administratorów
klastra Kubernetes
Z tego podrozdziału dowiesz się, co należy rozważyć przed rozpoczęciem wykonywania w
klastrze Kubernetes zadań związanych z uczeniem maszynowym. Jest on skierowany przede
wszystkim do administratorów klastrów. Poznanie właściwej terminologii to największe
wyzwanie, przed jakim stoi administrator klastra odpowiedzialny za zespół użytkowników
zajmujących się analizą danych. Jest mnóstwo nowych pojęć, które z czasem trzeba poznać —
możesz być spokojny, to da się zrobić. Przechodzimy więc do najważniejszych kwestii, którymi
trzeba się zająć podczas przygotowywania klastra do zadań związanych z uczeniem
maszynowym.
Trenowanie modelu w Kubernetes
Trenowanie modeli uczenia maszynowego w Kubernetes wymaga konwencjonalnego procesora
(CPU) i standardowej karty graficznej (GPU). Zwykle im więcej zasobów będzie można
przydzielić do zadania, tym szybciej nastąpi wytrenowanie modelu. W większości przypadków
trenowanie modelu można prowadzić w pojedynczym komputerze, który ma wymagane zasoby.
Wielu dostawców chmury oferuje różne typy maszyn wirtualnych wyposażonych w wiele kart
graficznych, dlatego przed przystąpieniem do trenowania rozproszonego zalecamy pionowe
skalowanie maszyny wirtualnej do czterech lub ośmiu takich kart. Osoby zajmujące się takimi
zadaniami zwykle podczas trenowania modeli korzystają z techniki określanej mianem
dostosowania hiperparametru do własnych potrzeb. To proces znalezienia optymalnego zbioru
hiperparametrów stosowanych w trakcie trenowania modeli. Hiperparametr to po prostu
parametr, którego wartość zostaje ustawiona przed rozpoczęciem procesu trenowania. Ta
technika obejmuje wykonywanie wielu tych samych zadań trenowania, ale z odmiennymi
zestawami hiperparametrów.
Wytrenowanie pierwszego modelu w Kubernetes
W omawianym przykładzie wykorzystamy zbiór danych MNIST do wytrenowania modelu
klasyfikacji obrazów. Ten zbiór jest dostępny publicznie i często stosowany podczas klasyfikacji
obrazów.
Do wytrenowania modelu będzie potrzebna karta graficzna. Należy więc sprawdzić, czy klaster
Kubernetes faktycznie ma do niej dostęp. Dane wyjściowe wygenerowane przez poniższe
polecenie potwierdzają, że klaster Kubernetes ma dostęp do czterech kart graficznych.
$ kubectl get nodes -o yaml | grep -i nvidia.com/gpu
nvidia.com/gpu: "1"
nvidia.com/gpu: "1"
nvidia.com/gpu: "1"
nvidia.com/gpu: "1"
Aby rozpocząć trenowanie, trzeba będzie w Kubernetes wykorzystać zasób typu Job, ponieważ
trenowanie modelu to operacja składająca się z wielu zadań. Trening będzie wykonywał 500
kroków i używał jednej karty graficznej. Utwórz plik o nazwie mnist-demo.yaml i z użyciem
przedstawionego niżej manifestu zapisz plik w systemie plików.
apiVersion: batch/v1
kind: Job
metadata:
labels:
app: mnist-demo
name: mnist-demo
spec:
template:
metadata:
labels:
app: mnist-demo
spec:
containers:
- name: mnist-demo
image: lachlanevenson/tf-mnist:gpu
args: ["--max_steps", "500"]
imagePullPolicy: IfNotPresent
resources:
limits:
nvidia.com/gpu: 1
restartPolicy: OnFailure
Następnym etapem jest utworzenie zasobu w klastrze Kubernetes.
$ kubectl create -f mnist-demo.yaml
job.batch/mnist-demo created
Teraz sprawdź stan utworzonego zadania.
$ kubectl get jobs
NAME
COMPLETIONS
DURATION
AGE
mnist-demo
0/1
4s
4s
Jeżeli przeanalizujesz pody, powinieneś zobaczyć uruchomione zadania trenowania modelu.
$ kubectl get pods
NAME
READY
STATUS
RESTARTS
AGE
mnist-demo-hv9b2
1/1
Running
0
3s
Po przejrzeniu zawartości dzienników zdarzeń poda zobaczysz, że operacja trenowania modelu
faktycznie jest przeprowadzana.
$ kubectl logs mnist-demo-hv9b2
2019-08-06 07:52:21.349999: I tensorflow/core/platform/cpu_feature_guard.cc:
137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA
2019-08-06 07:52:21.475416: I
tensorflow/core/common_runtime/gpu/gpu_device.cc:
1030] Found device 0 with properties:
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: d0c5:00:00.0
totalMemory: 11.92GiB freeMemory: 11.85GiB
2019-08-06 07:52:21.475459: I
tensorflow/core/common_runtime/gpu/gpu_device.cc:
1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: Tesla
K80, pci bus id: d0c5:00:00.0, compute capability: 3.7)
2019-08-06 07:52:26.134573: I tensorflow/stream_executor/dso_loader.cc:139]
successfully opened CUDA library libcupti.so.8.0 locally
Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Extracting /tmp/tensorflow/input_data/train-images-idx3-ubyte.gz
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Extracting /tmp/tensorflow/input_data/train-labels-idx1-ubyte.gz
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Extracting /tmp/tensorflow/input_data/t10k-images-idx3-ubyte.gz
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting /tmp/tensorflow/input_data/t10k-labels-idx1-ubyte.gz
Accuracy at step 0: 0.1255
Accuracy at step 10: 0.6986
Accuracy at step 20: 0.8205
Accuracy at step 30: 0.8619
Accuracy at step 40: 0.8812
Accuracy at step 50: 0.892
Accuracy at step 60: 0.8913
Accuracy at step 70: 0.8988
Accuracy at step 80: 0.9002
Accuracy at step 90: 0.9097
Adding run metadata for 99
...
Teraz możesz sprawdzić, czy trenowanie modelu już się zakończyło. W tym celu wystarczy
spojrzeć na stan zadań.
$ kubectl get jobs
NAME
COMPLETIONS
DURATION
AGE
mnist-demo
1/1
27s
112s
Aby przeprowadzić operacje porządkowe po zakończeniu zadania wytrenowania modelu, należy
wydać następujące polecenie:
$ kubectl delete -f mnist-demo.yaml
job.batch "mnist-demo" deleted
Gratulacje! W ten sposób wykonałeś w Kubernetes pierwsze zadanie polegające na
wytrenowaniu modelu.
Trenowanie rozproszone w Kubernetes
Trenowanie rozproszone nadal jest w powijakach, a ponadto jest trudne do optymalizacji.
Zadanie wytrenowania modelu wymagające ośmiu kart graficznych zawsze będzie wykonywane
szybciej w komputerze wyposażonym w osiem kart graficznych niż w dwóch komputerach, z
których każdy ma po cztery karty. Trenowanie rozproszone powinieneś stosować wyłącznie
wtedy, gdy model nie mieści się w największej z dostępnych maszyn. Jeżeli masz pewność, że
musisz skorzystać z trenowania rozproszonego, wówczas bardzo ważne jest, byś poznał
niezbędną architekturę. Na rysunku 14.2 pokazano rozproszoną architekturę TensorFlow;
możesz wyraźnie zobaczyć, że model i parametry są rozproszone.
Rysunek 14.2. Rozproszona architektura TensorFlow
Ograniczenia dotyczące zasobów
Zadania związane z uczeniem maszynowym wymagają konkretnych konfiguracji wszystkich
aspektów klastra. Faza trenowania modelu jest bez wątpienia tą, która wymaga największej
ilości zasobów. Trzeba również zwrócić uwagę na to, że — jak już wspomnieliśmy nieco
wcześniej — algorytmy uczenia maszynowego niemal zawsze wiążą się z wykonywaniem wielu
zadań. Przede wszystkim interesuje nas czas rozpoczęcia i czas zakończenia operacji
trenowania modelu. Drugi z wymienionych czasów zależy od tego, jak szybko będzie można
spełnić wymagania dotyczące zasobów niezbędnych podczas trenowania modelu. Dlatego też
niemal na pewno skalowanie pozwoli na szybsze zakończenie zadania, choć skalowanie wiąże
się z własnymi ograniczeniami.
Sprzęt specjalizowany
Trenowanie i udostępnianie modelu niemal zawsze jest znacznie efektywniejsze, gdy zostaje
użyty specjalizowany sprzęt. Typowym przykładem takiego sprzętu jest karta graficzna.
Kubernetes pozwala uzyskać dostęp do karty graficznej za pomocą wtyczki urządzenia
udostępniającej zasób znany harmonogramowi zadań Kubernetes i tym samym możliwy do
użycia. Dostępny jest framework wtyczki oferujący wymienione możliwości, co oznacza, że
dostawcy rozwiązań nie muszą modyfikować podstawowego kodu Kubernetes w celu
zapewnienia implementacji określonego urządzenia. Wspomniane wtyczki urządzeń zwykle
działają jako DaemonSet w poszczególnych węzłach, czyli są procesami odpowiedzialnymi za
przekazywanie określonych zasobów do API Kubernetes. Zapoznasz się teraz z wtyczką Nvidia
dla Kubernetes (https://github.com/NVIDIA/k8s-device-plugin), która pozwala uzyskać dostęp
do karty graficznej Nvidia. Po uruchomieniu wtyczki można utworzyć poda, a Kubernetes
zagwarantuje, że zostanie on przekazany do węzła, w którym dostępny jest odpowiedni zasób.
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: digits-container
image: nvidia/digits:6.0
resources:
limits:
nvidia.com/gpu: 2 # Wymagane są dwie karty graficzne.
Wtyczki urządzeń nie muszą być ograniczone do kart graficznych. Można je wykorzystać także
wtedy, gdy niezbędny jest inny sprzęt specjalizowany, np. FPGA (ang. field programmable gate
arrays) lub InfiniBand.
Planowanie zasobów
Trzeba podkreślić, że Kubernetes nie może podejmować decyzji dotyczących zasobów, o których
nie ma informacji. Możesz zauważyć, że podczas trenowania modeli karta graficzna nie
wykorzystuje pełni możliwości. W rezultacie nie osiągasz oczekiwanego poziomu wykorzystania.
Powróćmy jeszcze na chwilę do poprzedniego przykładu: została udostępniona tylko pewna
liczba rdzeni układu graficznego i pominięta liczba wątków, które mogą być wykonywane przez
poszczególne rdzenie. Nie zostały udostępnione również informacje o tym, na której magistrali
działa rdzeń układu graficznego karty. Dlatego też zadania wymagające dostępu do siebie
nawzajem lub do tej samej pamięci mogą być umieszczane w tych samych węzłach Kubernetes.
To wszystko są przykłady kwestii, które mogą być rozwiązane w przyszłości przez wtyczki, a
zarazem prowadzą do pytań w rodzaju: „Dlaczego nie mogę wykorzystać w stu procentach
mojej nowej karty graficznej?”. Warto w tym miejscu także wspomnieć, że nie można zażądać
dostępu jedynie do ułamka mocy układu graficznego, np. 0,1. To oznacza, że nawet jeśli dany
układ graficzny obsługuje możliwość jednoczesnego wykonywania wielu wątków, nie będzie
można skorzystać z tej zalety.
Biblioteki, sterowniki i moduły jądra
Do uzyskania dostępu do sprzętu specjalizowanego zwykłe potrzebne są specjalnie
przygotowane biblioteki, sterowniki i moduły jądra. Trzeba się upewnić, że zostały
zamontowane w środowisku uruchomieniowym kontenera, aby były dostępne dla narzędzi
uruchamianych w tym kontenerze. Mógłbyś w tym miejscu zapytać: „Dlaczego nie mogę po
prostu dodać tego oprogramowania do obrazu kontenera?”. Odpowiedź jest prosta: wersje tych
narzędzi muszą być dopasowane do wersji hosta, a także trzeba je skonfigurować w sposób
odpowiedni do określonego systemu. Istnieją środowiska uruchomieniowe kontenerów, np.
Nvidia Docker (https://github.com/NVIDIA/nvidia-docker), pozwalające wyeliminować trudności
związane z mapowaniem woluminów hosta na poszczególne kontenery. Jeśli będziesz chciał
przygotować kontener ogólnego przeznaczenia, być może będziesz musiał utworzyć zaczep
dopuszczenia, który zapewni tę samą funkcjonalność. Bardzo ważne jest rozważenie pewnej
kwestii: dostęp do sprzętu specjalizowanego może wymagać użycia uprzywilejowanych
kontenerów, a to ma wpływ na profil zapewnienia bezpieczeństwa kontenera. Instalację
niezbędnych bibliotek, sterowników i modułów jądra może ułatwić wtyczka urządzenia
Kubernetes. Wiele wtyczek urządzeń przeprowadza operacje sprawdzające w każdym
urządzeniu i potwierdza ukończenie instalacji, zanim poinformuje mechanizm zarządcy
procesów o dostępności w Kubernetes zasobów związanych z danym urządzeniem, np. kartą
graficzną.
Pamięć masowa
Pamięć masowa to jeden z najważniejszych aspektów o krytycznym znaczeniu podczas
wykonywania zadań związanych z uczeniem maszynowym. Ten aspekt musisz wziąć pod uwagę,
ponieważ ma bezpośredni związek z wymienionymi tutaj etapami wykonywania zadania
związanego z uczeniem maszynowym:
przechowywaniem zbioru danych i jego rozproszeniem między węzły robocze podczas
trenowania modelu,
punktami kontrolnymi i zapisywaniem modeli.
Przechowywanie zbioru danych i jego rozproszenie między węzły
robocze podczas trenowania modelu
Podczas trenowania modelu zbiór danych musi być możliwy do pobrania przez każdy węzeł
roboczy. Pamięć masowa musi być w trybie tylko do odczytu i zwykle im szybszy jest to dysk,
tym lepiej. Wybór pamięci masowej jest w zasadzie całkowicie zależny od wielkości zbioru
danych. Zbiory danych o wielkości setek megabajtów lub gigabajtów mogą być doskonale
obsłużone przez blokową pamięć masową, a te o wielkości setek terabajtów lepiej obsłuży
obiektowa. Korzystanie z sieci podczas przekazywania danych może prowadzić do znacznego
zmniejszenia wydajności działania, w zależności od wielkości i położenia dysków zawierających
zbiory danych.
Punkty kontrolne i zapisywanie modeli
Punkty kontrolne są tworzone podczas trenowania modeli, a zapisywanie modelu pozwala na
ich późniejsze udostępnianie. W obu przypadkach konieczne jest dołączenie pamięci masowej
do każdego węzła roboczego, który musi zapisywać dane. Wspomniane dane zwykle są
przechowywane w pojedynczym katalogu, a poszczególne węzły robocze zapisują dane do
określonego punktu kontrolnego lub pliku. Większość narzędzi oczekuje, że punkt kontrolny
będzie się znajdował w tej samej lokalizacji, w której odbywa się zapis danych, oraz wymaga
zasobu ReadWriteMany. Użycie tego zasobu oznacza, że wolumin może zostać przez wiele
węzłów zamontowany w trybie tylko do odczytu. Gdy korzystasz z API PersistentVolumes w
Kubernetes, będziesz musiał ustalić, jaka platforma pamięci masowej jest najlepsza do Twoich
potrzeb. W dokumentacji Kubernetes (https://kubernetes.io/docs/concepts/storage/persistentvolumes/#access-modes) znajdziesz listę obsługujących ReadWriteMany wtyczek woluminów.
Sieć
Etap trenowania modelu w zadaniu uczenia maszynowego ma ogromny wpływ na działanie sieci
(to dotyczy przede wszystkim sytuacji, w której prowadzone jest trenowanie rozproszone).
Jeżeli rozważymy rozproszoną architekturę TensorFlow, będziemy mogli wskazać dwie
oddzielne fazy, w trakcie których generowana jest ogromna ilość ruchu sieciowego: zmienną
dystrybucję z każdego serwera parametrów do poszczególnych węzłów roboczych oraz
przekazywanie gradientów z poszczególnych węzłów roboczych do serwera parametrów
(zobacz rysunek 14.2 we wcześniejszej części rozdziału). Ilość czasu potrzebnego na
przeprowadzenie tej operacji ma wpływ na czas niezbędny do wytrenowania modelu. Dlatego
też mamy tutaj do czynienia z sytuacją typu „im szybciej, tym lepiej”. Większość publicznie
dostępnych chmur i serwerów obsługuje połączenia sieciowe o szybkości 1-, 10-, czasem nawet
40 Gb/s, zatem przepustowość, ogólnie rzecz biorąc, jest problemem jedynie w przypadku
wolniejszych sieci. Jeżeli potrzebujesz sieci charakteryzującej się wysoką przepustowością,
możesz rozważyć też użycie rozwiązania opartego na InfiniBand.
Wprawdzie przepustowość sieci często jest czynnikiem ograniczającym, ale zdarzają się
również sytuacje, w których problemem jest pobieranie danych z jądra. Istnieją projekty typu
open source wykorzystujące tryb RDMA (ang. remote direct memory access) w celu
zwiększenia szybkości przekazywania ruchu sieciowego bez konieczności modyfikowania
węzłów roboczych lub kodu aplikacji. Tryb RDMA pozwala komputerom w sieci na wymianę
danych w pamięci głównej bez użycia procesora, bufora lub systemu operacyjnego
któregokolwiek z komputerów. Rozważ wykorzystanie projektu open source o nazwie Freeflow
(https://github.com/microsoft/Freeflow), który szczyci się wysoką wydajnością działania sieci w
przypadku nakładek zapewniających obsługę sieci kontenerów.
Protokoły specjalizowane
Istnieją jeszcze inne protokoły specjalizowane, których użycie warto rozważyć podczas
wykonywania w Kubernetes zadań związanych z uczeniem maszynowym. Te protokoły często są
charakterystyczne dla danego dostawcy i są wykorzystywane w trakcie rozwiązywania
problemów związanych ze skalowaniem rozproszonego trenowania modeli. To się odbywa przez
usuwanie obszarów architektury, które szybko stają się wąskimi gardłami, np. serwerów
parametrów. Wspomniane protokoły często pozwalają na bezpośrednią wymianę informacji
między kartami graficznymi w wielu węzłach, bez angażowania procesora lub systemu
operacyjnego węzła. Oto dwie kwestie, na które powinieneś zwrócić uwagę, aby znacznie
efektywniej skalować rozproszone trenowanie modeli:
MPI (ang. message passing interface) to standaryzowane, przenośne API przeznaczone do
przekazywania danych między procesami rozproszonymi.
NCCL (ang. Nvidia Collective Communications Library) to biblioteka uwzględniająca
topologię obiektów komunikacji z wieloma kartami graficznymi.
Obawy użytkowników zajmujących się
analizą danych
We wcześniejszej części rozdziału zostały przedstawione informacje dotyczące tego, co trzeba
zrobić, aby mieć możliwość wykonywania w klastrze Kubernetes zadań związanych z uczeniem
maszynowym. Mógłbyś w tym miejscu zapytać: „A co z użytkownikami zajmującymi się analizą
danych?”. Oto kilka popularnych narzędzi, które ułatwiają im wykorzystanie Kubernetes do
zadań związanych z uczeniem maszynowym i nie wymagają przy tym bycia ekspertem w
zakresie pracy z Kubernetes.
Kubeflow (https://www.kubeflow.org/) to zestaw narzędzi dla Kubernetes przeznaczony do
wykonywania zadań związanych z uczeniem maszynowym. Ten zestaw jest natywny dla
Kubernetes i dostarczany wraz z wieloma narzędziami niezbędnymi do wykonywania
wymienionych zadań. Narzędzia takie jak Jupyther Notebooks, pipelines i natywne
kontrolery Kubernetes pozwalają użytkownikom zajmującym się analizą danych
wykorzystać pełnię możliwości Kubernetes jako platformy do uczenia maszynowego.
Polyaxon (https://polyaxon.com/) to przeznaczone do zarządzania zadaniami związanymi z
uczeniem maszynowym narzędzie, które obsługuje wiele popularnych bibliotek i działa w
dowolnym klastrze Kubernetes. Polyaxon oferuje rozwiązania zarówno komercyjne, jak i
typu open source.
Pachyderm (https://www.pachyderm.com/) to przeznaczona do używania w korporacjach
platforma analizy danych, która zapewnia bogaty zestaw narzędzi do przygotowywania
zbiorów danych, obsługi cyklu życiowego, wersjonowania oraz możliwości tworzenia
rozwiązań z zakresu uczenia maszynowego. Pachyderm oferuje rozwiązanie komercyjne,
które można wdrożyć w dowolnym klastrze Kubernetes.
Najlepsze praktyki dotyczące
wykonywania w Kubernetes zadań
związanych z uczeniem maszynowym
Aby zapewnić optymalną wydajność działania zadań związanych z uczeniem maszynowym,
rozważ zastosowanie przedstawionych tutaj najlepszych praktyk.
Sprytne stosowanie mechanizmu zarządcy procesów i automatycznego skalowania. Biorąc
pod uwagę to, że większość zadań związanych z uczeniem maszynowym z natury składa
się z wielu operacji, zalecamy wykorzystanie automatycznego skalowania klastra (dodatek
Cluster Autoscaler w Kubernetes). Sprzęt oferujący dostęp do karty graficznej jest
kosztowny i zdecydowanie nie chcesz za niego płacić, gdy pozostaje nieużywany.
Zalecamy wykonywanie zadań o określonych porach, za pomocą wartości taint i tolerancji
lub za pomocą opartego na czasie dodatku Cluster Autoscaler. Dzięki temu klaster będzie
mógł być skalowany zgodnie z potrzebami zadań związanych z uczeniem maszynowym, w
chwili gdy zachodzi taka potrzeba, a nie wcześniej. Wartością taint przyjęło się oznaczać
węzeł, który zawiera rozszerzony zasób będący kluczem. Przykładowo węzeł zawierający
kartę graficzną Nvidia powinien mieć przypisaną wartość taint w postaci Key:
nvidia.com/gpu, Effect: NoSchedule. Zastosowanie tej metody oznacza również
możliwość użycia kontrolera dopuszczenia ExtendedResourceToleration, który będzie
automatycznie dodawał odpowiednie tolerancje dla wartości taint podom wymagającym
dostępu do rozszerzonych zasobów. Dlatego też użytkownik nie będzie musiał zajmować
się tym samodzielnie.
Wytrenowanie modelu wiąże się z kruchą równowagą. Umożliwienie szybszego
przekazywania obiektów na jednym obszarze bardzo często prowadzi do powstania
wąskich gardeł na innych. Tutaj trzeba nieustannie monitorować środowisko i
modyfikować jego konfigurację. Ogólnie rzecz biorąc, zalecamy próbę doprowadzenia do
sytuacji, w której to karta graficzna stanie się wąskim gardłem, ponieważ to najdroższy
zasób. Postaraj się w pełni wykorzystać jego możliwości. Bądź przygotowany na to, aby
zawsze szukać wąskich gardeł i skonfigurować rozwiązanie do monitorowania poziomu
wykorzystania karty graficznej, procesora, sieci i pamięci masowej.
Klastry z różnymi zadaniami. Klastry używane do wykonywania codziennych zadań mogą
być również użyte w celach związanych z uczeniem maszynowym. Biorąc pod uwagę to, że
uczenie maszynowe wymaga wysokiej wydajności, zalecamy skorzystanie z oddzielnej puli
węzłów, oznaczonej wartością taint wskazującą na przyjmowanie jedynie zadań
związanych z uczeniem maszynowym. Dzięki temu pozostałe klastry będą chronione przed
negatywnym wpływem wszelkich zadań związanych z uczeniem maszynowym
uruchomionych w puli węzłów. Co więcej, powinieneś rozważyć użycie wielu pul węzła z
kartami graficznymi, z których każda będzie miała inną charakterystykę wydajności,
dopasowaną do danego typu zadań. Zalecamy również włączenie automatycznego
skalowania węzła w pulach węzłów dla zadań związanych z uczeniem maszynowym.
Klastry wykonujące zadania różnych typów stosuj tylko wtedy, gdy dokładnie poznasz
wpływ zadań związanych z uczeniem maszynowym na wydajność działania klastra.
Osiągnięcie skalowalności liniowej za pomocą rozproszonego trenowania modelu. To
można uznać za Świętego Graala rozproszonego trenowania modelu. Niestety, większość
bibliotek nie zapewnia skalowania liniowego w przypadku rozproszonego trenowania
modelu. Wprawdzie włożono wiele wysiłku w zapewnienie lepszego rozwiązania w
zakresie skalowania, ale bardzo ważne jest dokładne zrozumienie związanego z tym
kosztu, ponieważ dodanie nowego sprzętu nie jest wystarczającym rozwiązaniem
problemu. Z naszego doświadczenia wynika, że właściwie zawsze źródłem problemu jest
sam model, a nie obsługująca go infrastruktura. Jednak przed przystąpieniem do
dokładniejszej analizy modelu jest bardzo ważne, by przeanalizować poziom zużycia karty
graficznej, procesora, sieci i pamięci masowej. Narzędzia typu open source takie jak
Horovod (https://github.com/horovod/horovod) pomagają w usprawnieniu frameworków
rozproszonego trenowania modeli i zapewniają lepsze skalowanie modelu.
Podsumowanie
W tym rozdziale przedstawiliśmy wiele materiału i mamy nadzieję, że dostarczyliśmy cennych
informacji pokazujących, dlaczego Kubernetes to doskonała platforma do wykonywania zadań
związanych z uczeniem maszynowym, a zwłaszcza z uczeniem głębokim. Poznałeś również
kwestie, które należy wziąć pod uwagę przed wdrożeniem pierwszego zadania związanego z
uczeniem maszynowym. Jeżeli zastosujesz się do zaleceń przedstawionych w tym rozdziale,
będziesz doskonale przygotowany do tworzenia i obsługiwania klastra Kubernetes
przeznaczonego do tych specjalizowanych zadań.
Rozdział 15. Tworzenie wzorców
aplikacji wysokiego poziomu na
podstawie Kubernetes
Kubernetes to skomplikowany system. Wprawdzie upraszcza wdrażanie i przeprowadzanie
operacji związanych z aplikacjami rozproszonymi, ale nie ułatwia zbyt mocno wdrożenia takiego
systemu. Dodanie nowych koncepcji i rozwiązań dla programisty oznacza dodanie kolejnej
warstwy skomplikowania w usłudze uproszczonych operacji. Dlatego też w wielu środowiskach
ma sens opracowywanie abstrakcji wysokiego poziomu w celu dostarczenia bardziej
przyjaznych programiście rozwiązań opartych na Kubernetes. Ponadto w wielu ogromnych
organizacjach sensowne jest standaryzowanie sposobu, w jaki aplikacje są konfigurowane i
wdrażane, aby każdy mógł stosować te same najlepsze praktyki operacyjne. To można osiągnąć
przez opracowanie abstrakcji wysokiego poziomu pozwalających programistom na
automatyczne stosowanie się do wspomnianych reguł. Jednak takie abstrakcje mogą ukrywać
ważne szczegóły przed programistą i jednocześnie wprowadzać pewne ograniczenia
komplikujące tworzenie niektórych rodzajów aplikacji lub integrację z już istniejącymi
rozwiązaniami. Podczas pracy nad rozwiązaniem chmury nieustannie będą się pojawiały
napięcia wynikające z konieczności stosowania pewnych kompromisów między elastycznością
infrastruktury a możliwościami oferowanymi przez platformę. Opracowanie właściwych
abstrakcji wysokiego poziomu pozwala osiągnąć idealny kompromis pod tym względem.
Podejścia w zakresie tworzenia abstrakcji
wysokiego poziomu
Gdy zastanawiasz się nad tym, jak opracować rozwiązanie na fundamencie Kubernetes, do
dyspozycji masz dwa podstawowe podejścia. Pierwsze polega na opakowaniu rozwiązania w
Kubernetes jako szczegółu implementacji. W przypadku takiego podejścia programista
korzystający z Twojej platformy powinien być w zasadzie nieświadomy, że rozwiązanie działa na
podstawie Kubernetes. Zamiast tego powinien uważać się za użytkownika otrzymanej
platformy, więc pod tym względem Kubernetes jest jedynie szczegółem implementacji.
Drugie podejście polega na wykorzystaniu oferowanych przez Kubernetes możliwości w
zakresie rozbudowy. API Server w Kubernetes jest dość elastyczne i pozwala na dynamiczne
dodawanie dowolnych nowych zasobów do samego API Kubernetes. W przypadku tego
podejścia nowe zasoby wysokiego poziomu będą istniały równolegle z wbudowanymi obiektami
Kubernetes. Użytkownicy będą zaś korzystać z wbudowanych w Kubernetes narzędzi do pracy
ze wszystkimi zasobami Kubernetes, zarówno tymi standardowymi, jak i nowo dodanymi. Model
rozszerzenia prowadzi do powstania środowiska, w którym Kubernetes z perspektywy
programisty nadal zajmuje miejsce centralne, dodatki zaś pozwalają zmniejszyć poziom
skomplikowania i ułatwiają korzystanie z rozwiązania.
Mając do dyspozycji te dwa podejścia, być może zastanawiasz się, jak wybrać odpowiednie. To
naprawdę zależy od celów stawianych przed budowaną warstwą abstrakcji. Jeżeli pracujesz nad
w pełni odizolowanym i zintegrowanym środowiskiem, w którym możesz z dużym
prawdopodobieństwem przyjąć, że użytkownik nie musi opuszczać jego granic, a łatwość użycia
ma duże znaczenie, wówczas doskonałym wyborem będzie pierwsze podejście. Dobrym
przykładem jest tutaj sytuacja, gdy budowane jest rozwiązanie dotyczące uczenia
maszynowego. Wymieniona domena jest dobrze znana. Użytkownicy zajmujący się analizą
danych prawdopodobnie nie będą mieli doświadczenia w pracy z Kubernetes. W takim
przypadku celem powinno być umożliwienie im szybkiego wykonywania zadań i
skoncentrowania się na własnych domenach zamiast nad systemami rozproszonymi. Dlatego też
zbudowanie pełnej abstrakcji na podstawie Kubernetes jest najbardziej sensownym
rozwiązaniem.
Z drugiej strony podczas budowania abstrakcji wysokiego poziomu — np. jako łatwego
rozwiązania w zakresie wdrażania aplikacji Javy — rozszerzenie Kubernetes będzie znacznie
lepszym rozwiązaniem niż użycie tej technologii w charakterze opakowania. Są dwa powody.
Pierwszym jest to, że dziedzina tworzenia aplikacji jest niezwykle szeroka. Trudno będzie
odgadnąć wszystkie wymagania i przypadki użycia, zwłaszcza że aplikacje i działania biznesowe
z czasem się zmieniają. Drugi to chęć zagwarantowania, że będzie można wykorzystać zalety
ekosystemu narzędzi Kubernetes. Istnieje niezliczona ilość narzędzi natywnej chmury
przeznaczonych do monitorowania, ciągłego wdrażania itd. Rozszerzenie API Kubernetes,
zamiast je zastąpić, pozwala dalej ich używać, podobnie jak nowo powstających.
Rozszerzanie Kubernetes
Skoro każda warstwa, którą będziesz budować na podstawie Kubernetes, jest unikatowa, w tej
książce nie znajdziesz omówienia sposobów na tworzenie takiej warstwy. Jednak narzędzia i
techniki przeznaczone do rozszerzania Kubernetes są ogólne dla dowolnej konstrukcji, którą
chciałbyś oprzeć na Kubernetes, dlatego poświęcimy tutaj nieco czasu na ich przedstawienie.
Rozszerzanie klastrów Kubernetes
Rozszerzanie klastra Kubernetes to ogromny temat, który został dokładnie omówiony w innych
książkach,
np.
Managing
Kubernetes
(https://www.oreilly.com/library/view/managingkubernetes/9781492033905/) i Kubernetes. Tworzenie niezawodnych systemów rozproszonych.
Wydanie II (https://www.oreilly.com/library/view/kubernetes-up-and/9781492046523/). Zamiast
prezentować w tym miejscu ten sam materiał, skoncentrujemy się na omówieniu sposobów
pozwalających na rozszerzenie Kubernetes. Rozszerzenie klastra Kubernetes obejmuje
poznanie wielu stosowanych w nim zasobów. Mamy tutaj trzy powiązane ze sobą rozwiązania
techniczne. Pierwsze to wzorzec przyczepy (ang. sidecar). Kontenery stosujące wzorzec
przyczepy (zobacz rysunek 15.1) zostały spopularyzowane w kontekście architektury Service
Mesh. Są to kontenery działające obok kontenera aplikacji głównej i zapewniające dodatkowe
możliwości, które nie zostały zintegrowane z aplikacją główną i często są opracowywane przez
oddzielne zespoły. Przykładowo w architekturze Service Mesh kontener przyczepy może
zapewniać obsługę wzajemnego uwierzytelniania TLS (mTLS) aplikacji działającej w
kontenerze.
Rysunek 15.1. Wzorzec przyczepy
Kontenery przyczepy pozwalają na dodawanie kolejnych funkcjonalności aplikacji do już
zdefiniowanych przez użytkownika.
Oczywiście ostatecznym celem jest ułatwienie pracy programiście, ale jeśli zmusimy go do
poznania tematu kontenerów przyczepy i sposobów pracy z nimi, wówczas tak naprawdę
spotęgujemy problem. Na szczęście istnieją inne narzędzia przeznaczone do rozszerzania
Kubernetes, które ułatwiają pracę z tą technologią. W szczególności mamy tutaj na myśli tzw.
kontrolery dopuszczenia (ang. admission controllers). Taki kontroler odczytuje kierowane do
API Kubernetes żądanie, zanim zostanie ono przekazane do klastra. Kontrolery dopuszczenia
mogą być używane do weryfikowania lub modyfikowania obiektów API. Kontrolera
dopuszczenia można używać w celu automatycznego dodawania kontenerów przyczepy do
podów utworzonych w klastrze. Dzięki temu programiści nawet nie muszą nic wiedzieć o
kontenerach przyczepy, aby móc wykorzystać ich zalety. Na rysunku 15.2 pokazaliśmy, jak
kontrolery dopuszczenia mogą współdziałać z API Kubernetes.
Rysunek 15.2. Kontroler dopuszczenia
Użyteczność kontrolerów dopuszczenia nie ogranicza się jedynie do dodawania kontenerów
przyczepy. Można je wykorzystać także do weryfikowania obiektów przekazywanych przez
programistów do Kubernetes. Przykładowo istnieje możliwość zaimplementowania w
Kubernetes tzw. lintera gwarantującego, że programiści przekazujący pody i inne zasoby
stosują najlepsze praktyki opracowane dla Kubernetes. Często popełnianym przez
programistów błędem jest pominięcie operacji rezerwowania zasobów dla aplikacji. W takich
przypadkach oparty na kontrolerze dopuszczenia linter może przechwytywać żądania i je
odrzucać. Oczywiście należy pozostawić pewne wyjście awaryjne (np. adnotację specjalną), aby
zaawansowany użytkownik w razie potrzeby mógł pominąć daną regułę. Więcej informacji na
ten temat znajdziesz w dalszej części rozdziału.
Dotychczas przedstawiliśmy jedynie sposoby na rozszerzanie istniejących aplikacji i
zagwarantowanie, że programiści stosują najlepsze praktyki. Natomiast nie został poruszony
temat dodawania abstrakcji wysokiego poziomu. W tym miejscu do gry wchodzą tzw. definicje
zasobów niestandardowych (ang. custom resource definitions, CRD), które pozwalają na
dynamiczne dodawanie nowych zasobów do istniejącego klastra Kubernetes. Przykładowo za
pomocą CRD można dodać do klastra Kubernetes nowy zasób ReplicatedService. Gdy
programista tworzy nowy egzemplarz ReplicatedService, wówczas wykorzystuje Kubernetes
do przygotowania odpowiedniego zasobu Deployment i Service. Dlatego też egzemplarz
ReplicatedService to wygodna dla programisty abstrakcja pozwalająca na zastosowanie
często używanego wzorca. Definicje zasobów niestandardowych zwykle są implementowane za
pomocą pętli sterującej, która została zdefiniowana w klastrze i przeznaczona do zarządzania
typami nowych zasobów.
Wrażenia użytkownika podczas rozszerzania
Kubernetes
Dodawanie nowych zasobów do klastra to doskonały sposób na dostarczanie nowych
możliwości. Aby jednak w pełni je wykorzystać, bardzo często warto także polepszyć wrażenia
użytkownika podczas pracy z Kubernetes. Jednym z rozwiązań w tym zakresie jest rozszerzenie
powłoki Kubernetes.
Ogólnie rzecz biorąc, dostęp do Kubernetes odbywa się z użyciem narzędzia powłoki kubectl.
Na szczęście zostało ono opracowane z myślą o jego rozszerzeniu. Wtyczki dla kubectl to
zwykłe pliki binarne o nazwach w rodzaju kubectl-foo, gdzie foo to nazwa wtyczki. Gdy w
powłoce wydajesz polecenie kubectl foo ..., następuje wywołanie pliku binarnego
odpowiedniej wtyczki. Za pomocą wtyczek dla kubectl można zdefiniować nowe możliwości
przeznaczone do obsługi nowych zasobów, które zostały dodane do klastra. Możesz
zaimplementować dowolną funkcjonalność i jednocześnie wykorzystać znajomość narzędzi
powłoki kubectl. To szczególnie cenne, ponieważ oznacza, że programiści nie muszą poznawać
nowych zestawów narzędzi. Podobnie zyskujesz możliwość stopniowego wprowadzania
koncepcji natywnych dla Kubernetes, gdy programiści będą zdobywali coraz większą wiedzę z
zakresu tej technologii.
Rozważania projektowe podczas
budowania platformy
Niezliczona liczba platform została opracowana w celu zwiększenia produktywności
programisty. Biorąc pod uwagę możliwość obserwowania wszystkich miejsc, w których te
platformy odniosły sukces lub poniosły porażkę, można wymienić pewien zestaw wzorców i
wyciągnąć pewne wnioski na podstawie doświadczenia innych programistów. W tym
podrozdziale przedstawimy wybrane wskazówki, które powinny Ci pomóc w osiągnięciu
sukcesu budowanej przez Ciebie platformy i uniknięciu ślepego zaułka.
Obsługa eksportowania do obrazu kontenera
Wielu projektantów podczas budowania platformy koncentruje się na prostocie i pozwala
użytkownikowi na dostarczenie kodu (mamy tutaj na myśli np. funkcję w podejściu FaaS, czyli
function as a service) bądź też natywnego pakietu (np. pliku JAR dla kodu w języku Java)
zamiast pełnego obrazu kontenera. Takie podejście ma swoje zalety, ponieważ pozwala
użytkownikowi poruszać się w granicach doskonale znanych narzędzi programistycznych i
wypracowanego stylu pracy. Platforma zajmuje się za programistę konteneryzacją aplikacji.
Jednak problem z takim podejściem pojawia się, gdy programista napotyka ograniczenia
środowiska programistycznego, które otrzymał. Być może potrzebuje określonej wersji
środowiska uruchomieniowego języka programowania, aby usunąć pewien błąd. Ewentualnie
może wymagać dodatkowego pakietu zasobów lub plików wykonywalnych, które nie są częścią
struktury automatycznej konteneryzacji aplikacji.
Niezależnie od powodu dojście do ściany jest przykrym doświadczeniem dla programisty,
ponieważ oznacza, że musi się dowiedzieć nieco więcej na temat sposobów pakowania aplikacji,
gdy tak naprawdę chciał ją tylko odrobinę rozbudować w celu usunięcia błędu lub
zaimplementowania nowej funkcjonalności.
Jednak wcale nie musi tak być. Jeżeli obsługiwana jest możliwość eksportu środowiska
programistycznego platformy do ogólnego kontenera, wówczas programista korzystający z
danej platformy nie musi zaczynać wszystkiego od początku i uczyć się wszystkiego na temat
kontenerów. Zamiast tego otrzymuje pełny, działający obraz kontenera przedstawiający
aktualny stan aplikacji (np. obraz kontenera zawierający niezbędną funkcjonalność i środowisko
uruchomieniowe węzła). Biorąc pod uwagę ten punkt wyjścia, można wprowadzić niewielkie
modyfikacje mające na celu dostosowanie obrazu kontenera do własnych potrzeb. Taka
stopniowa degradacja i przyrostowe poznawanie znacznie ułatwiają drogę od wysokiego
poziomu platformy do niskiego poziomu infrastruktury, a tym samym bardzo poprawia ogólną
użyteczność platformy, ponieważ jej używanie nie wiąże się z wprowadzeniem zbyt wysokich
progów dla programistów.
Obsługa istniejących mechanizmów dla usług i
wykrywania usług
Z platformami łączy się także inny ciekawy fakt: ewoluują i łączą się z innymi systemami. Wielu
programistów może być niezwykle zadowolonych i produktywnych podczas pracy ze swoją
platformą, ale żadna z rzeczywistych aplikacji nie będzie obejmowała jednocześnie budowanej
przez Ciebie platformy, niskiego poziomu aplikacji Kubernetes, a także innych platform.
Połączenia ze starymi bazami danych lub aplikacjami typu open source utworzonymi dla
Kubernetes zawsze będą częścią wystarczająco ogromnej aplikacji.
Z powodu wymaganej łączności krytyczne znaczenie ma to, aby podstawowe komponenty i
mechanizm odkrywania usług były używane i udostępniane przez każdą budowaną przez Ciebie
platformę. Nie wyważaj otwartych drzwi w zakresie sposobu działania platformy, ponieważ
dojdziesz wówczas do ściany i utworzysz rozwiązanie, które nie będzie mogło się komunikować
ze światem.
Jeżeli aplikacje zdefiniowane na swojej platformie udostępnisz w postaci usług Kubernetes,
każda aplikacja znajdująca się w klastrze będzie miała możliwość korzystania z Twoich
aplikacji, niezależnie od tego, czy zostaną one uruchomione na platformie wyższego poziomu.
Podobnie, jeśli używasz serwerów DNS Kubernetes do wykrywania usług, wówczas będziesz w
stanie nawiązywać połączenia z wysokopoziomowej platformy aplikacji z innymi aplikacjami
uruchomionymi w klastrze, nawet jeśli nie zostały zdefiniowane na tej platformie. Kusząca może
być perspektywa opracowania czegoś łatwiejszego w użyciu, ale wzajemne połączenia między
poszczególnymi platformami to często spotykany wzorzec projektowy dla każdej aplikacji w
pewnym wieku oraz o określonym poziomie skomplikowania. Zawsze będziesz żałował decyzji o
zbudowaniu zamkniętego rozwiązania.
Najlepsze praktyki dotyczące tworzenia
platform dla aplikacji
Wprawdzie
Kubernetes
oferuje
potężne
narzędzia
przeznaczone
do
zarządzania
oprogramowaniem, ale zarazem zapewnia nieco mniejsze możliwości programistom w zakresie
tworzenia aplikacji. Dlatego też często trzeba tworzyć platformy na podstawie aplikacji, aby w
ten sposób zapewnić programistom większą produktywność i/lub ułatwić pracę z Kubernetes.
Podczas tworzenia wspomnianych platform warto się stosować do wymienionych tutaj
najlepszych praktyk.
Używaj kontrolerów dopuszczenia w celu ograniczenia i modyfikowania wywołań API do
klastra. Taki kontroler może zweryfikować i odrzucić niepoprawne zasoby Kubernetes.
Modyfikujący kontroler dopuszczenia może automatycznie zmodyfikować API zasobu w
celu dodania nowego kontenera przyczepy lub wprowadzenia innych zmian, o których
użytkownik nawet nie będzie wiedział.
Używaj wtyczek dla narzędzia powłoki kubectl w celu poprawy wrażeń użytkownika
podczas pracy z Kubernetes. Polega to na dodawaniu nowych narzędzi do już istniejącego
i doskonale znanego narzędzia powłoki. W rzadkich sytuacjach znacznie
odpowiedniejszym rozwiązaniem będzie użycie wbudowanego narzędzia.
Podczas tworzenia platform na podstawie Kubernetes dokładnie zastanów się nad tym, kto
korzysta z platformy i jak ewoluują potrzeby użytkowników. Celem jest niewątpliwie
uproszczenie niektórych rozwiązań i ułatwienie ich stosowania. To jednak może
doprowadzić do uwięzienia użytkowników i sprawić, że nie będą mogli zrobić wszystkiego,
co by chcieli, bez wcześniejszej modyfikacji wszystkiego na zewnątrz Twojej platformy, co
niewątpliwie będzie frustrującym doświadczeniem i zarazem źródłem porażki tej
platformy.
Podsumowanie
Kubernetes to fantastyczne narzędzie, pozwalające uprościć wdrożenie oprogramowania i jego
działanie. Mimo to nie zawsze jest środowiskiem najbardziej przyjaznym programiście ani też
nie zawsze zapewnia mu największą produktywność. Dlatego często tworzy się na podstawie
Kubernetes platformy wysokiego poziomu, aby w ten sposób zaoferować typowemu
programiście coś znacznie odpowiedniejszego i użyteczniejszego. W tym rozdziale
przedstawiliśmy kilka różnych podejść w zakresie tworzenia wspomnianych systemów
wysokiego poziomu. Zaprezentowaliśmy również krótkie omówienie podstawowych możliwości
oferowanych przez Kubernetes w zakresie rozszerzenia infrastruktury. Na koniec poznałeś
wnioski i reguły zaczerpnięte z naszych obserwacji innych platform zbudowanych na
fundamencie Kubernetes. Mamy nadzieję, że te informacje będą Ci pomocne w trakcie pracy
nad własną platformą.
Rozdział 16. Zarządzanie
informacjami o stanie i
aplikacjami wykorzystującymi
te dane
We wczesnych dniach orkiestracji kontenerów zadania zwykle dotyczyły aplikacji
bezstanowych, które do przechowywania informacji o stanie w razie potrzeby używały
systemów zewnętrznych. Kontenery uznawano za rozwiązanie krótkotrwałe, a orkiestracja
trwałego magazynu danych niezbędnego do przechowywania informacji o stanie była w
najlepszym razie trudna. Z czasem wyraźnie zarysowała się potrzeba obsługi opartych na
kontenerach zadań przechowujących informacje o stanie. W pewnych przypadkach pojawiała
się również konieczność zapewnienia większej wydajności działania takiego rozwiązania. W
trakcie wielu iteracji nie tylko dostosowano technologię Kubernetes do obsługi woluminów
pamięci masowej zamontowanych w podach, ale również te woluminy, bezpośrednio zarządzane
przez Kubernetes, stały się ważnym komponentem w orkiestracji pamięci masowej dla
wymagających tego zadań.
Jeżeli możliwość zamontowania zewnętrznego woluminu w kontenerze byłaby wystarczająca,
wówczas w Kubernetes istniałoby o wiele więcej działających na dużą skalę aplikacji
przechowujących informacje o stanie. Rzeczywistość jest jednak taka, że montowanie woluminu
to łatwe zadanie w wielkim schemacie aplikacji przechowujących informacje o stanie.
Większość aplikacji wymagających informacji o stanie nawet po wystąpieniu awarii węzła to
skomplikowane silniki związane ze stanem danych, np. systemy relacyjnych baz danych,
rozproszone magazyny danych typu klucz-wartość bądź też skomplikowane systemy
zarządzania dokumentami. Taka klasa aplikacji wymaga większej koordynacji w tym, jak
komponenty aplikacji uruchomionej w klastrze komunikują się ze sobą i jak są identyfikowane,
a także w zakresie kolejności ich pojawiania się i znikania z systemu.
W tym rozdziale skoncentrujemy się na najlepszych praktykach dotyczących zarządzania
stanem, od prostych wzorców, takich jak zapis pliku w udziale sieciowym, po skomplikowane
systemy zarządzania danymi, takie jak MongoDB, MySQL i Kafka. Znalazł się w nim również
krótki punkt poświęcony nowemu wzorcowi dla skomplikowanych systemów, Operator, który
pozwala nie tylko na używanie obiektów Kubernetes, ale również na dodawanie logiki
biznesowej lub aplikacji do kontrolerów niestandardowych. To z kolei może pomóc w ułatwieniu
zadań związanych z zarządzaniem skomplikowanymi danymi.
Woluminy i punkty montowania
Nie każde zadanie wymagające obsługi stanu musi być skomplikowaną bazą danych lub usługą
kolejkowania o wysokiej przepustowości danych. Bardzo często zdarza się, że aplikacje
przenoszone do skonteneryzowanych zadań oczekują istnienia określonych katalogów, a także
uprawnień do odczytu i zapisu informacji w tych katalogach. Możliwość wstrzyknięcia danych
do woluminu, który może być odczytywany przez kontenery w podzie, została dokładnie
omówiona w rozdziale 5. Jednak dane montowane za pomocą zasobu ConfigMap lub Secret
zwykle są przeznaczone tylko do odczytu. W tym podrozdziale skoncentrujemy się na
przypisywaniu kontenerom woluminów, które mogą być zapisywane i będą w stanie przetrwać
awarię kontenera lub, jeszcze lepiej, awarię poda.
Każde ważne środowisko uruchomieniowe kontenerów, takie jak Docker, rkt, CRI-O i nawet
Singularity, pozwala na montowanie w kontenerze woluminów, które są mapowane na
zewnętrzne systemy pamięci masowej. W najprostszej postaci taki system może być pewnym
miejscem w pamięci, ścieżką dostępu do lokalizacji w hoście kontenera, a także zewnętrznym
systemem plików, takim jak NFS, Glusterfs, CIFS lub Ceph. Być może w tym miejscu
zastanawiasz się, do czego może być Ci potrzebne takie rozwiązanie. Użytecznym przykładem
będzie starsza aplikacja utworzona w taki sposób, aby informacje dotyczące jej działania były
zapisywane w lokalnym systemie plików. Istnieje wiele potencjalnych rozwiązań, np.
uaktualnienie kodu aplikacji w celu przekazywania informacji do urządzenia stdout lub stderr
w kontenerze przyczepy, aby dane dziennika zdarzeń mogły być strumieniowane na zewnątrz za
pomocą współdzielonego woluminu poda. Można również wykorzystać istniejące w hoście
narzędzie do rejestrowania danych, które umie odczytywać woluminy dla dzienników zdarzeń
zarówno hosta, jak i kontenera aplikacji. W tym ostatnim przypadku można skorzystać z punktu
montowania w kontenerze, z użyciem właściwości hostPath w Kubernetes, jak pokazaliśmy w
poniższym fragmencie kodu.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-webserver
spec:
replicas: 3
selector:
matchLabels:
app: nginx-webserver
template:
metadata:
labels:
app: nginx-webserver
spec:
containers:
- name: nginx-webserver
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: hostvol
mountPath: /usr/share/nginx/html
volumes:
- name: hostvol
hostPath:
path: /home/webcontent
Najlepsze praktyki dotyczące woluminów
Spróbuj ograniczyć używanie woluminów do podów wymagających wielu kontenerów
współdzielących dane, np. stosujących wzorzec adaptera lub ambasadora. Dla
wymienionych typów wzorców współdzielenia używaj właściwości emptyDir.
Używaj właściwości emptyDir, gdy dostęp do danych odbywa się za pomocą agentów
opartych na węźle lub usług.
Spróbuj zidentyfikować wszelkie usługi zapisujące na dysku lokalnym dzienniki zdarzeń o
krytycznym znaczeniu dla aplikacji. Jeżeli to możliwe, zmień tę lokalizację na urządzenie
stdout i stderr, a następnie wykorzystaj istniejący w Kubernetes system agregacji
dzienników zdarzeń, który będzie strumieniował te dzienniki zdarzeń, zamiast używać
mapowania woluminu.
Pamięć masowa w Kubernetes
Dotychczas omówione przykłady pokazywały proste mapowanie woluminu na kontener w
podzie, co przedstawia jedynie podstawowe możliwości silnika kontenerów. Prawdziwie potężną
możliwością w Kubernetes jest zarządzanie pamięcią masową i montowaniem woluminów.
Dzięki temu można stosować znacznie bardziej dynamiczne scenariusze, w których pody mogą
być tworzone i usuwane według potrzeb, a pamięć masowa używana przez poda będzie
odpowiednio dostosowywana. Do zarządzania pamięcią masową dla podów Kubernetes stosuje
dwa oddzielne API: PersistentVolume i PersistentVolumeClaim.
API PersistentVolume
W przypadku API PersistentVolume pamięć masową najlepiej jest traktować jako dysk, który
zawiera wszystkie woluminy montowane w podzie. Omawiane API zapewnia obsługę tzw.
polityki oświadczeń, definiującej zasięg cyklu życiowego woluminu niezależnie od cyklu
życiowego poda, który korzysta z danego woluminu. Kubernetes może używać woluminów
zdefiniowanych dynamicznie lub statycznie. Aby możliwa była praca z dynamicznie tworzonymi
woluminami, w Kubernetes musi istnieć zasób o nazwie StorageClass. Obiekty omawianego
API mogą być tworzone w klastrze z użyciem różnych typów i klas oraz jedynie wtedy, gdy
wartość PersistentVolumeClaims odpowiada wartości PersistentVolume przypisanej do poda.
Sam wolumin jest obsługiwany przez przeznaczoną do tego celu wtyczkę. Istnieje wiele wtyczek
bezpośrednio obsługiwanych w Kubernetes, a każda z nich ma różne parametry konfiguracyjne
do dostosowania.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv001
labels:
tier: "silver"
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /tmp
server: 172.17.0.2
API PersistentVolumeClaims
API PersistentVolumeClaims pozwala nadać Kubernetes definicję wymagań zasobu dla pamięci
masowej, która będzie używana przez poda. Następnie pod będzie odwoływał się do
oświadczenia (ang. claim) i jeśli wartość persistentVolume będzie odpowiadała istniejącemu
żądaniu oświadczenia, wówczas nastąpi alokowanie danego woluminu dla konkretnego poda.
Absolutnym minimum jest zdefiniowanie wielkości pamięci pasowej i trybu dostępu, choć
można również zdefiniować określony zasób StorageClass. Selektory także mogą być używane
w celu dopasowywania obiektów API PersistentVolume spełniających określone kryteria.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
storageClass: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
selector:
matchLabels:
tier: "silver"
Przedstawione tutaj oświadczenie spowoduje dopasowanie utworzonego wcześniej obiektu
PersistentVolume, ponieważ nazwa klasy pamięci masowej, selektor, wielkość pamięci
masowej i tryb dostępu są takie same.
Kubernetes spowoduje dopasowanie obiektu API PersistentVolume i oświadczenia, a następnie
połączy je ze sobą. Aby użyć woluminu, w kodzie pod.spec trzeba odwołać się do nazwy
oświadczenia, jak pokazaliśmy w kolejnym fragmencie kodu.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-webserver
spec:
replicas: 3
selector:
matchLabels:
app: nginx-webserver
template:
metadata:
labels:
app: nginx-webserver
spec:
containers:
- name: nginx-webserver
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: hostvol
mountPath: /usr/share/nginx/html
volumes:
- name: hostvol
persistentVolumeClaim:
claimName: my-pvc
Klasy pamięci masowej
Zamiast samodzielnie definiować egzemplarze PersistentVolume z wyprzedzeniem,
administrator może zdecydować się na utworzenie obiektów StorageClass definiujących
wtyczkę pamięci masowej do użycia, określone opcje montowania i parametry, które będą
używane przez wszystkie egzemplarze PersistentVolume tej klasy. To następnie pozwala na
zdefiniowanie oświadczenia z odpowiednią klasą do użycia, a Kubernetes dynamicznie utworzy
egzemplarz PersistentVolume na podstawie parametrów i opcji StorageClass.
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: nfs
provisioner: cluster.local/nfs-client-provisioner
parameters:
archiveOnDelete: True
Kubernetes pozwala również operatorom na tworzenie domyślnych klas pamięci masowej za
pomocą wtyczki DefaultStorageClass. Jeżeli została ona włączona w API serwera, wówczas
nastąpi zdefiniowanie domyślnego egzemplarza StorageClass i dowolnego egzemplarza
PersistentVolumeClaims, który nie definiuje wyraźnie zasobu StorageClass. Część
dostawców chmury dostarcza domyślną klasę pamięci masowej przeznaczoną do mapowania na
najtańszą wersję pamięci masowej możliwej do użycia w egzemplarzach oferowanych przez
dostawcę chmury.
Interfejs pamięci masowej kontenera i FlexVolume
Często określane mianem „out-of-tree”, wtyczki woluminów, CSI (ang. container storage
interface) i FlexVolume, pozwalają dostawcom pamięci masowej tworzyć własne wtyczki. Dzięki
temu nie muszą oni czekać, aż dana funkcjonalność pojawi się bezpośrednio w bazie kodu
Kubernetes.
Wtyczki CSI i FlexVolume zostały przez operatory wdrożone w klastrach Kubernetes jako
rozszerzenia i mogą być uaktualniane przez dostawców pamięci masowej, gdy zajdzie potrzeba
udostępnienia nowej funkcjonalności.
Cel CSI został w następujący sposób określony w dokumencie zamieszczonym w serwisie
GitHub
na
stronie
https://github.com/container-storageinterface/spec/blob/master/spec.md#objective:
Celem jest zdefiniowanie przemysłowego standardu Container Storage Interface
umożliwiającego dostawcom pamięci masowej opracowywanie wtyczek, które następnie
będą działały w wielu różnych systemach orkiestracji kontenerów.
Interfejs FlexVolume zaliczał się do tradycyjnych metod używanych w celu dodawania kolejnych
funkcjonalności dla dostawców pamięci masowej. Wymagane jest zainstalowanie określonych
sterowników we wszystkich węzłach klastra, który będzie używał tego interfejsu. W zasadzie
mamy do czynienia z rozwiązaniem instalowanym w hostach tworzących klaster. Ten ostatni
komponent jest największą przeszkodą dla użycia interfejsu FlexVolume, zwłaszcza przez
dostawców usług zarządzanych, ponieważ uzyskanie dostępu do węzłów jest źle widziane, a do
węzłów głównych — praktycznie niemożliwe. Wtyczka CSI rozwiązuje ten problem przez
udostępnienie tej samej funkcjonalności, a także dzięki łatwości użycia podczas wdrażania poda
w klastrze.
Najlepsze praktyki dotyczące pamięci masowej w
Kubernetes
Wzorce projektowe aplikacji natywnych chmur próbują wymuszać jak najczęstsze stosowanie
projektów aplikacji bezstanowych. Jednak coraz większy zasięg usług opartych na kontenerach
sprawił, że coraz częściej trzeba stosować trwałe magazyny danych. Przedstawione tutaj
najlepsze praktyki związane z pamięcią masową w Kubernetes pomogą w opracowaniu
efektywnego podejścia, które pozwoli na dostarczenie wymaganej implementacji magazynu
danych w projekcie aplikacji.
Jeżeli to możliwe, należy włączyć wtyczkę DefaultStorageClass i zdefiniować domyślną
klasę magazynu danych. W aplikacji znajduje się wiele plików Helm w formacie chart,
które wymagają obiektu API PersistentVolume i mają domyślną klasę pamięci masowej
pozwalającą na instalację aplikacji bez konieczności wprowadzania w niej zbyt wielu
modyfikacji.
Podczas projektowania architektury klastra tradycyjnego rozwiązania lub dostawcy
chmury pod uwagę należy wziąć strefę i możliwości połączenia między warstwami
obliczeń i danych. Trzeba przy tym wykorzystać poprawne etykiety dla węzłów i obiektów
API PersistentVolume, a także zapewnić, że dane zostaną umieszczone jak najbliżej
zadania. Zdecydowanie nie chcesz, aby pod znajdujący się w węźle strefy A próbował
zamontować wolumin dołączony do węzła w strefie B.
Bardzo dokładnie zastanów się nad tym, które zadania wymagają umieszczenia na dysku
informacji o stanie. Czy to można obsłużyć za pomocą usługi zewnętrznej, takiej jak
system bazy danych, lub też — w przypadku oferty dostawcy chmury — za pomocą
hostingowanej usługi o API spójnym z obecnie używanym API, np. MongoDB lub MySQL
jako usługi?
Trzeba określić, ile wysiłku będzie kosztować zmodyfikowanie kodu aplikacji do postaci w
znacznie mniejszym stopniu zależnej od informacji o stanie.
Wprawdzie Kubernetes monitoruje i montuje woluminy w trakcie szeregowania zadań
przez mechanizm zarządcy procesów, ale jeszcze nie obsługuje nadmiarowości i tworzenia
kopii zapasowej danych, które są przechowywane w tych woluminach. Specyfikacja CSI
została dodana do API i jest przeznaczona dla producentów rozwiązań, aby umożliwić
stosowanie natywnych technologii migawek, jeśli nie zapewnia ich używany magazyn
danych.
Trzeba zweryfikować poprawność cyklu życiowego danych przechowywanych w
magazynie danych. Domyślnie zdefiniowana polityka powoduje dynamiczne tworzenie
obiektów API PersistentVolume, a woluminy są usuwane z magazynu danych po usunięciu
poda. Dane wrażliwe lub możliwe do wykorzystania w analizie śledczej również powinny
być uwzględnione przez zdefiniowaną politykę.
Aplikacje obsługujące informacje o stanie
Wprost przeciwnie do powszechnego przekonania, Kubernetes od samego początku obsługuje
aplikacje wymagające informacji o stanie, np. za pomocą technologii MySQL, Kafka czy
Cassandra. Jednak w pierwszych latach programiści musieli zmagać się z poziomem jego
skomplikowania, więc stosowano go tylko do małych zadań, a i tak trzeba było włożyć dużo
pracy, aby zapewnić np. możliwość jego skalowania lub niezawodność działania.
Aby w pełni poznać różnice o krytycznym znaczeniu, należy wiedzieć, jak typowy zasób
ReplicaSet planuje pody i jak nimi zarządza, a także jak każdy z nich może być szkodliwy dla
tradycyjnych aplikacji wymagających informacji o stanie.
Pody w zasobie ReplicaSet są skalowane w górę i otrzymują losowo wybrane nazwy.
Pody w zasobie ReplicaSet są skalowane w dół w dowolny sposób.
Pody w zasobie ReplicaSet nigdy nie są wywoływane bezpośrednio za pomocą ich nazw
lub adresów IP, ale przez ich powiązanie z usługą.
Pody w zasobie ReplicaSet mogą być w dowolnym momencie ponownie uruchamiane i
przenoszone do innych węzłów.
Pody w zasobie ReplicaSet mają obiekty API PersistentVolume mapowane i łączone tylko
za pomocą oświadczenia. Jednak każdy nowy pod z nową nazwą może w razie potrzeby
przejąć to oświadczenie.
Jeżeli masz tylko podstawową wiedzę z zakresu systemów zarządzania danymi klastra, możesz
natychmiast napotykać problemy związane z wymienionymi cechami charakterystycznymi
podów opartych na zasobie ReplicaSet. Wyobraź sobie sytuację, że pod ma aktualną kopię
zezwalającej na zapis informacji bazy danych, która nagle zostaje usunięta. W takiej sytuacji
będziemy mieli chaos w najczystszej postaci.
Większość neofitów świata Kubernetes przyjmuje założenie, że aplikacje obsługujące informacje
o stanie automatycznie są aplikacjami baz danych, i dlatego stawia znak równości między tymi
dwoma rodzajami aplikacji. Może tak być w tym znaczeniu, że Kubernetes nie ma informacji o
typie wdrażanej aplikacji. Dlatego też „nie wie”, że system bazy danych wymaga innego
procesu wyboru węzła głównego, który może lub nie może obsługiwać replikacji między
węzłami. Może również nie wiedzieć, że w ogóle nie ma do czynienia z systemem
bazodanowym. W tym miejscu do gry wchodzi zasób StatefulSet.
Zasób StatefulSet
Zasób StatefulSet ułatwia uruchamianie systemów aplikacji oczekujących znacznie bardziej
niezawodnego zachowania węzła/poda. Jeżeli spojrzysz na listę typowych cech
charakterystycznych poda w zasobie ReplicaSet, wówczas sposób działania zasobu
StatefulSet wyda się wręcz odwrotny. Pierwotna specyfikacja pojawiła się w Kubernetes 1.3 i
nosiła nazwę PetSets. Została wprowadzona w odpowiedzi na krytykę związaną z zarządzaniem
aplikacjami obsługującymi informacje o stanie, np. skomplikowanymi systemami zarządzania
danymi, oraz ich planowaniem.
Pody w zasobie StatefulSet są skalowane w górę i mają przypisywane sekwencyjne
nazwy. Wraz ze skalowaniem zbioru w górę pody otrzymują nazwy porządkowe i
domyślnie nowy pod musi być w pełni dostępny online (przekazanie opcji dotyczących
istnienia poda i jego dostępności), zanim będzie mógł być dodany następny pod.
Pody w zasobie StatefulSet są skalowane w dół w odwrotnej kolejności.
Dostęp do podów w zasobie StatefulSet może się odbywać pojedynczo z użyciem nazwy
kryjącej się za usługą typu headless.
Pody w zasobie StatefulSet wymagające punktu montowania woluminu muszą używać
zdefiniowanego szablonu PersistentVolume. Woluminy używane przez pody w zasobie
StatefulSet nie są usuwane podczas usuwania tego zasobu.
Specyfikacja zasobu StatefulSet jest podobna do specyfikacji Deployment, z wyjątkiem
deklaracji Service i szablonu PersistentVolume. Najpierw zostanie utworzona usługa typu
headless definiująca usługę, do której pody będą uzyskiwały dostęp. Usługa typu headless jest
taka sama jak zwykła usługa, choć nie przeprowadza operacji związanych z mechanizmem
równoważenia obciążenia.
apiVersion: v1
kind: Service
metadata:
name: mongo
labels:
name: mongo
spec:
ports:
- port: 27017
targetPort: 27017
clusterIP: None # To powoduje utworzenie usługi typu headless.
selector:
role: mongo
Definicja StatefulSet będzie wyglądała jak Deployment, choć z kilkoma zmianami.
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: "mongo"
replicas: 3
template:
metadata:
labels:
role: mongo
environment: test
spec:
terminationGracePeriodSeconds: 10
containers:
- name: mongo
image: mongo:3.4
command:
- mongod
- "--replSet"
- rs0
- "--bind_ip"
- 0.0.0.0
- "--smallfiles"
- "--noprealloc"
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-persistent-storage
mountPath: /data/db
- name: mongo-sidecar
image: cvallance/mongo-k8s-sidecar
env:
- name: MONGO_SIDECAR_POD_LABELS
value: "role=mongo,environment=test"
volumeClaimTemplates:
- metadata:
name: mongo-persistent-storage
annotations:
volume.beta.kubernetes.io/storage-class: "fast"
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 2Gi
Operatory
Zasób StatefulSet zdecydowanie odegrał ważną rolę we wprowadzeniu skomplikowanych
systemów obsługujących informacje o stanie jako zadań możliwych do wykonywania w
Kubernetes. Jak wcześniej wspomnieliśmy, jedynym poważnym problemem jest to, że
Kubernetes nie ma pełnych informacji o zadaniu uruchomionym w zasobie StatefulSet.
Wszystkie pozostałe skomplikowane operacje — np. tworzenie kopii zapasowej, zapewnienie
odporności na awarie, rejestracja węzła głównego, rejestracja nowej repliki i uaktualnienia —
muszą być przeprowadzane dość regularnie, więc wymagają dokładnego przemyślenia przed
ich wykonaniem w StatefulSet.
Na wczesnym etapie rozwoju Kubernetes inżynierowie SRE (ang. site reliability engineers)
CoreOS utworzyli dla Kubernetes nową klasę oprogramowania natywnej chmury, nazwanego
operatorami. Początkowym zamysłem była hermetyzacja danych uruchomionej aplikacji w
konkretnym kontrolerze, który rozszerza Kubernetes. Wyobraź sobie budowę opartego na
kontrolerze zasobu StatefulSet rozwiązania, które umożliwi wdrażanie, skalowanie,
uaktualnianie, tworzenie kopii zapasowej oraz ogólnie wykonywanie operacji konserwacyjnych
w oprogramowaniu typu Cassandra lub Kafka. Pierwsze operatory powstały dla
oprogramowania etcd i Prometheus i początkowo używały bazy danych do przechowywania
wskaźników. Poprawne utworzenie obiektów Prometheus i etcd, wykonanie ich kopii zapasowej
i przywrócenie ich konfiguracji może być obsługiwane przez operator. To w zasadzie nowe
obiekty zarządzane w Kubernetes, podobnie jak pody i obiekty Deployment.
Aż do niedawna operatory były jednorazowymi narzędziami utworzonymi przez inżynierów SRE
lub producentów oprogramowania dla konkretnych aplikacji. W połowie 2018 roku firma
RedHat opracowała Operator Framework, czyli zestaw narzędzi zawierających menedżera
cyklu życiowego SDK, i moduły, które zapewnią obsługę kolejnych funkcjonalności, takich jak
pomiary, rynek oprogramowania i funkcje typu rejestru. Operatory nie są przeznaczone
wyłącznie dla aplikacji przechowujących informacje o stanie, ale ze względu na niestandardową
logikę kontrolera są znacznie lepiej dopasowane do skomplikowanych usług danych i systemów
obsługujących informacje o stanie.
Operatory to technologia wciąż rozwijana w świecie Kubernetes. Zdobyła uznanie wielu
producentów systemów zarządzania danymi, dostawców usług chmury i inżynierów SRE na
całym świecie, którzy chcą wykorzystać część swojej wiedzy podczas uruchamiania w
Kubernetes skomplikowanych systemów rozproszonych. Uaktualnioną listę dostępnych
operatorów znajdziesz w witrynie OperatorHub pod adresem https://operatorhub.io/.
Najlepsze praktyki dotyczące zasobu StatefulSet i
operatorów
Ogromne aplikacje rozproszone wymagające informacji o stanie oraz prawdopodobnie
skomplikowanych operacji zarządzania i konfiguracji będą czerpały korzyść z oferowanego
przez Kubernetes zasobu StatefulSet i operatorów. Operatory nadal są rozwijane i mają
stojącą za nimi społeczność, więc przedstawione tutaj najlepsze praktyki zostały oparte na
możliwościach istniejących w czasie, gdy ta książka powstawała.
Decyzja o użyciu zasobu StatefulSet powinna zostać podjęta rozsądnie, ponieważ
aplikacje wykorzystujące informacje o stanie zwykle wymagają znacznie bardziej
zaawansowanego zarządzania niż to, które oferuje orkiestrator (we wcześniejszej części
rozdziału przedstawiliśmy informacje o potencjalnych rozwiązaniach tego problemu w
przyszłych wersjach Kubernetes).
Usługa typu headless dla zasobu StatefulSet nie jest tworzona automatycznie i musi
zostać utworzona w trakcie wdrożenia, aby zapewnić poprawne adresowanie podów w
poszczególnych węzłach.
Gdy aplikacja wymaga nadawania nazw porządkowych i niezawodnego skalowania, nie
zawsze będzie to oznaczało przypisywanie obiektów API PersistentVolume.
Jeżeli węzeł klastra przestanie reagować na żądania, wówczas wszystkie pody będące
częścią zasobu StatefulSet nie zostaną automatycznie usunięte. Zamiast tego po upływie
pewnego czasu przejdą do stanu Terminating lub Unknown. Jedynym sposobem na
usunięcie takiego poda jest usunięcie obiektu węzła z klastra, ponowne rozpoczęcie
działania przez kubelet lub wymuszenie usunięcia poda przez operator. Wymuszona
operacja usunięcia powinna być ostatnią deską ratunku. Należy zachować dużą
ostrożność, aby węzeł, w którym zostały usunięte pody, nie przeszedł do trybu online,
ponieważ wówczas w klastrze będą znajdowały się dwa pody o takich samych nazwach.
Usunięcie poda można wymusić poleceniem kubectl delete pod nginx-0 --graceperiod=0 --force.
Nawet po wymuszonej operacji usunięcia poda może się on znajdować w stanie Unknown,
więc poprawka w API serwera spowoduje usunięcie wpisu i to, że kontroler zasobu
StatefulSet utworzy nowy egzemplarz usuniętego poda: kubectl patch pod nginx-0 p '{"meta data":{"finalizers":null}}'.
Jeżeli uruchamiasz skomplikowany system danych z pewnym procesem wyboru węzła
głównego lub procesem potwierdzenia replikacji danych, skorzystaj z zaczepu preStop w
celu poprawnego zamknięcia wszelkich połączeń, wymuszenia wyboru węzła głównego
lub weryfikacji synchronizacji danych przed usunięciem poda. Nie zapomnij o zamknięciu
poda w elegancki sposób.
Gdy aplikacja wymagająca obsługi informacji o stanie jest skomplikowanym systemem
zarządzania danymi, wówczas warto rozważyć ustalenie, czy istnieje operator, który może
pomóc w zarządzaniu cyklami życiowymi bardziej skomplikowanych komponentów
aplikacji. Jeśli aplikacja została opracowana wewnątrz firmy, warto sprawdzić, czy
użytecznym rozwiązaniem będzie opracowanie jej w postaci operatora w celu ułatwienia
zarządzania nią. Zapoznaj się z SDK operatorów CoreOS — odpowiednie informacje na
ten temat znajdziesz na stronie https://coreos.com/operators/.
Podsumowanie
Wiele organizacji szuka możliwości umieszczenia w kontenerach swoich aplikacji
przechowujących informacje o stanie oraz pozostawienia ich bez zmian. Gdy coraz więcej i
więcej aplikacji natywnej chmury jest uruchamianych w opartych na Kubernetes rozwiązaniach
oferowanych przez dostawców chmury, waga danych staje się problemem. Aplikacje używające
informacji o stanie mają większe wymagania, choć w rzeczywistości uruchamianie ich w
klastrze odbywa się szybciej dzięki wprowadzeniu zasobu StatefulSet i operatorów.
Mapowanie woluminów na kontenery pozwala operatorom na abstrakcję podsystemu pamięci
masowej od dowolnego sposobu tworzenia aplikacji. Zarządzanie aplikacjami wymagającymi
informacji o stanie, np. systemami baz danych w Kubernetes, nadal jest skomplikowane w
systemach rozproszonych i wymaga ostrożnej orkiestracji z użyciem natywnych obiektów
Kubernetes: podów, zasobów ReplicaSet, Deployment i StatefulSet. Na szczęście użycie
operatorów zawierających dane o aplikacji wbudowanych jako natywne API Kubernetes może
pomóc w przeniesieniu wymienionych systemów do klastrów produkcyjnych.
Rozdział 17. Sterowanie
dopuszczeniem i autoryzacja
Kontrolowanie dostępu do API Kubernetes ma krytyczne znaczenie w zagwarantowaniu, że
klaster nie tylko jest bezpieczny, ale również może być używany w charakterze medium do
przekazywania zasad i zarządzeń dla użytkowników, zadań oraz komponentów klastra
Kubernetes. Z tego rozdziału dowiesz się, jak za pomocą wielokrotnie już wspomnianych we
wcześniejszej części książki kontrolerów dopuszczenia i modułów autoryzacji można włączać
określoną funkcjonalność, a także jak dostosować je do własnych potrzeb, aby spełniały
określone kryteria.
Na rysunku 17.1 pokazaliśmy, gdzie i jak można stosować sterowanie dopuszczeniem i
autoryzację. Ten rysunek pokazuje sposób obsługi od początku do końca żądania kierowanego
do API serwera Kubernetes, aż do chwili zapisania obiektu w pamięci masowej (o ile ten obiekt
zostanie zaakceptowany).
Rysunek 17.1. Sposób obsługi żądania API
Sterowanie dopuszczeniem
Czy kiedykolwiek zastanawiałeś się, jak przestrzenie nazw są tworzone automatycznie po
zdefiniowaniu zasobu w jeszcze nieistniejącej przestrzeni nazw? Być może zastanawiałeś się,
jak wybierana jest domyślna klasa pamięci masowej. Te zmiany są możliwe dzięki istnieniu mało
znanej funkcjonalności określanej mianem kontrolera dopuszczenia. W tym podrozdziale
przekonasz się, jak można wykorzystać te kontrolery do implementacji w imieniu użytkownika
najlepszych praktyk Kubernetes po stronie serwera, a także jak zastosować sterowanie
dopuszczeniem do określenia sposobu używania klastra Kubernetes.
Czym jest kontroler dopuszczenia?
Jeżeli kontroler dopuszczenia został wymieniony na ścieżce dostępu do żądań API serwera,
można z niego korzystać na wiele różnych sposobów. Najczęściej spotykany sposób użycia
kontrolera dopuszczenia może być zaliczony do jednej z trzech wymienionych tutaj grup.
Polityka i zarządzenia
Kontroler dopuszczenia pozwala wymusić stosowanie polityki w celu spełnienia wymagań
biznesowych, np.:
Tylko wewnętrzny mechanizm równoważenia obciążenia w chmurze może być stosowany
w przestrzeni nazw dev.
Wszystkie kontenery w podzie muszą mieć nałożone ograniczenia dotyczące zasobów.
Wszystkie zasoby powinny mieć predefiniowane standardowe etykiety i adnotacje, by
mogły być odkrywane przez istniejące narzędzia.
Cały przychodzący ruch sieciowy może używać jedynie protokołu HTTPS. Więcej
informacji na temat używania zaczepów sieciowych dopuszczenia w tym kontekście
znajdziesz w rozdziale 11.
Zapewnienie bezpieczeństwa
Kontroler dopuszczenia pozwala wymusić spójne stosowanie w klastrze reguł zapewnienia
bezpieczeństwa.
Kanonicznym
przykładem
jest
tutaj
kontroler
dopuszczenia
PodSecurityPolicy, który pozwala zachować kontrolę nad dotyczącymi bezpieczeństwa
właściwościami w specyfikacji poda. Przykładowo może uniemożliwić stosowanie
kontenerów uprzywilejowanych lub wykorzystanie określonych ścieżek dostępu z systemu
plików hosta. Za pomocą zaczepów sieciowych dopuszczenia możesz wymusić stosowanie
znacznie dokładniejszych lub samodzielnie zdefiniowanych reguł.
Zarządzanie zasobami
Kontroler dopuszczenia pozwala na weryfikację w celu zapewnienia najlepszych praktyk
dla użytkowników klastra, np.:
Zagwarantowanie, że wszystkie żądania przychodzące mają w pełni kwalifikowane nazwy
domen (ang. fully qualified domain names, FQDN) z określonym prefiksem.
Zagwarantowanie, że żądania przychodzące nie nakładają się na siebie.
Wszystkie kontenery w podzie muszą mieć ograniczenia zasobów.
Typy kontrolerów dopuszczenia
Mamy dwie klasy kontrolerów dopuszczenia: standardowe i dynamiczne. Kontrolery
standardowe są wkompilowane w API serwera i dostarczane w postaci wtyczek z każdym
wydaniem Kubernetes. Te kontrolery muszą być skonfigurowane podczas uruchamiania
serwera. Natomiast kontrolery dynamiczne są konfigurowane w trakcie działania Kubernetes i
są opracowywane poza podstawową bazą kodu Kubernetes. Jedynym typem dynamicznego
sterowania dopuszczeniem jest zaczep sieciowy dopuszczenia, który otrzymuje żądania za
pomocą wywołań zwrotnych HTTPS.
Technologia Kubernetes jest dostarczana z ponad 30 kontrolerami dopuszczenia, które można
włączyć za pomocą następującej opcji API serwera:
--enable-admission-plugins
Wiele z funkcjonalności dostarczanej z Kubernetes zależy od włączenia określonych
standardowych kontrolerów dopuszczenia i dlatego zaleca się stosowanie pewnego zbioru
domyślnego:
--enable-admissionplugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWe
bho
ok,Priority,ResourceQuota,PodSecurityPolicy
Pełną listę kontrolerów dopuszczenia Kubernetes i ich funkcjonalności znajdziesz w oficjalnej
dokumentacji Kubernetes.
Prawdopodobnie zwróciłeś uwagę na następujące elementy znajdujące się na liście zalecanych
kontrolerów dopuszczenia przeznaczonych do włączenia: MutatingAdmissionWebhook,
ValidatingAdmissionWebhook. Te standardowe kontrolery dopuszczenia nie implementują
żadnej logiki sterowania dopuszczeniem, a zamiast tego są używane do konfiguracji
działającego w klastrze punktu końcowego zaczepu sieciowego w celu przekazania obiektu
żądania sterującego dopuszczeniem.
Konfiguracja zaczepu sieciowego dopuszczenia
Jak wcześniej wspomnieliśmy, jedną z podstawowych zalet zaczepów sieciowych dopuszczenia
jest to, że są konfigurowane dynamicznie. Trzeba poznać sposób efektywnego konfigurowania
zaczepów sieciowych dopuszczenia ze względu na pewne implikacje i kompromisy w zakresie
trybów spójności i awarii.
Następny fragment kodu przedstawia manifest zasobu ValidatingWebhookConfiguration. Ten
manifest jest używany do zdefiniowania weryfikującego zaczepu sieciowego dopuszczenia. W
kodzie znajdują się dokładne informacje o sposobie działania jego poszczególnych fragmentów.
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: ## Nazwa zasobu.
webhooks:
- name: ## Nazwa zaczepu sieciowego dopuszczenia, która zostanie
wyświetlona użytkownikowi, ## gdy nastąpi odrzucenie żądania.
clientConfig:
service:
namespace: ## Przestrzeń nazw, w której znajduje się pod zaczepu
sieciowego dopuszczenia.
name: ## Nazwa usługi używanej w celu nawiązania połączenia z
zaczepem sieciowym dopuszczenia.
path: ## Adres URL zaczepu sieciowego.
caBundle: ## Certyfikat w formacie PEM z paczką CA, używany do
weryfikacji certyfikatu serwera ## zaczepu sieciowego.
rules: ## Opis operacji, które API serwera musi wykonywać w zasobach lub
podzasobach w danym ## zaczepie sieciowym.
- operations:
- ## Określona operacja wywołująca w API serwera wykonanie żądania do
zaczepu sieciowego ## (np. utworzenie, uaktualnienie, usunięcie, nawiązanie
połączenia).
apiGroups:
- ""
apiVersions:
- "*"
resources:
- ## Określone zasoby wymienione za pomocą nazw (np. deployments,
services, ingresses).
failurePolicy: ## Zdefiniowanie sposobu obsługi problemów związanych z
uzyskaniem dostępu lub ## nierozpoznanych błędów. To musi być wartość Ignore
lub Fail.
Spójrz również na manifest zasobu MutatingWebhookConfiguration. Ten manifest definiuje
modyfikujący zaczep sieciowy dopuszczenia. W kodzie znajdują się dokładne informacje o
sposobie działania jego poszczególnych fragmentów.
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: ## Nazwa zasobu.
webhooks:
- name: ## Nazwa zaczepu sieciowego dopuszczenia, która zostanie
wyświetlona użytkownikowi, ## gdy nastąpi odrzucenie żądania.
clientConfig:
service:
namespace: ## Przestrzeń nazw, w której znajduje się pod zaczepu
sieciowego dopuszczenia.
name: ## Nazwa usługi używanej w celu nawiązania połączenia z
zaczepem sieciowym dopuszczenia.
path: ## Adres URL zaczepu sieciowego.
caBundle: ## Certyfikat w formacie PEM z paczką CA, używany do
weryfikacji certyfikatu serwera ## zaczepu sieciowego.
rules: ## Opis operacji, które API serwera musi wykonywać w zasobach lub
podzasobach w danym ## zaczepie sieciowym.
- operations:
- ## Określona operacja wywołująca w API serwera wykonanie żądania do
zaczepu sieciowego ## (np. utworzenie, uaktualnienie, usunięcie, nawiązanie
połączenia).
apiGroups:
- ""
apiVersions:
- "*"
resources:
- ## Określone zasoby wymienione za pomocą nazw (np. deployments,
services, ingresses).
failurePolicy: ## Zdefiniowanie sposobu obsługi problemów związanych z
uzyskaniem dostępu lub ## nierozpoznanych błędów. To musi być wartość Ignore
lub Fail.
Być może zauważyłeś, że oba zasoby są identyczne, z wyjątkiem właściwości kind. Istnieje
jednak pewna różnica w backendzie: MutatingWebhookConfiguration pozwala zaczepowi
sieciowemu dopuszczenia na zwrot zmodyfikowanego obiektu żądania, podczas gdy
ValidatingWebhookConfiguration
nie
umożliwia
tego.
Mimo
to
zdefiniowanie
MutatingWebhookConfiguration i przeprowadzenie weryfikacji jest akceptowalne. Trzeba
uwzględnić pewne kwestie związane z zapewnieniem bezpieczeństwa, a ponadto należy
stosować regułę najmniejszych uprawnień.
W tym miejscu prawdopodobnie zadajesz sobie następujące pytanie: „Co się stanie,
jeśli
zdefiniuję
egzemplarz
ValidatingWebhookConfiguration
lub
MutatingWebhookConfiguration z właściwością zasobu, gdy obiektem reguły będzie
ValidatingWebhookConfiguration lub MutatingWebhookConfiguration?”. Dobrą
wiadomością
jest
to,
że
ValidatingWebhookConfiguration
i
MutatingWebhookConfiguration nigdy nie zostaną wywołane w żądaniu sterowania
dopuszczeniem
dla
obiektów
ValidatingWebhookConfiguration
i
MutatingWebhookConfiguration. Istnieje ku temu dobry powód: nie chcesz
przypadkowo umieścić klastra w stanie określanym jako niemożliwy do odzyskania.
Najlepsze praktyki dotyczące sterowania
dopuszczeniem
Skoro poznałeś potężne możliwości kontrolerów dopuszczenia, zapoznaj się teraz z kilkoma
najlepszymi praktykami, które pozwolą w pełni je wykorzystać.
Kolejność wtyczek sterowania dopuszczeniem nie ma znaczenia. We wczesnych wersjach
Kubernetes kolejność tych wtyczek była związana z operacjami przetwarzania, więc
zdecydowanie miała znaczenie. W obecnie obsługiwanych wersjach Kubernetes kolejność
wtyczek sterowania dopuszczeniem określana za pomocą opcji API serwera (--enableadmission-plugins) nie ma już znaczenia. Mimo to kolejność odgrywa małą rolę w
przypadku zaczepów sieciowych i dlatego ważne jest poznanie sposobu obsługi żądania w
takich przypadkach. Przyjęcie lub odrzucenie żądania ma postać operacji logicznego I,
więc jeśli którykolwiek zaczep sieciowy odrzuci żądanie, całe żądanie zostanie odrzucone,
a użytkownik otrzyma komunikat o błędzie. Trzeba zwrócić uwagę na to, że modyfikujący
kontroler dopuszczenia zawsze jest wykonywany przed weryfikującym kontrolerem
dopuszczenia. Jeżeli się nad tym zastanowić, to ma sens — prawdopodobnie nie chcesz
weryfikować obiektu, który zostanie następnie zmodyfikowany. Na rysunku 17.2
pokazaliśmy sposób obsługi żądania za pomocą zaczepu sieciowego dopuszczenia.
Rysunek 17.2. Sposób obsługi żądania API za pomocą zaczepów sieciowych dopuszczenia
Nie modyfikuj tych samych właściwości. Konfiguracja wielu modyfikujących zaczepów
sieciowych dopuszczenia również rodzi pewne problemy. Nie ma sposobu na
zdefiniowanie kolejności, w jakiej żądanie będzie przetwarzane przez poszczególne
zaczepy sieciowe dopuszczenia, więc jest bardzo ważne, aby te kontrolery nie
modyfikowały tych samych właściwości, ponieważ efektem może być nieoczekiwane
zachowanie. Jeżeli zaczepów sieciowych dopuszczenia jest wiele, zalecane jest
skonfigurowanie tych weryfikujących, aby potwierdzić, że manifest ostatecznego zasobu
jest zgodny z oczekiwaniami. W ten sposób masz gwarancję jego wykonania po zaczepach
sieciowego dopuszczenia.
Nieudane otwarcie i nieudane zamknięcie. Przypomnij sobie użycie właściwości
failurePolicy podczas konfiguracji zaczepów sieciowych, zarówno modyfikującego, jak i
weryfikującego. Wymienione właściwości definiują, jak API serwera powinno działać w
przypadku, gdy zaczepy sieciowe dopuszczenia mają dostęp do problemów lub napotykają
nieznane błędy. Tej właściwości można przypisać wartość Ignore lub Fail. Wartość
Ignore w zasadzie oznacza, że przetwarzanie żądania będzie kontynuowane, natomiast
wartość Fail powoduje odrzucenie całego żądania. Wprawdzie to może wydawać się
oczywiste, ale warto rozważyć implikacje wynikające z przypisania każdej z wymienionych
wartości. Zignorowanie zaczepu sieciowego dopuszczenia o znaczeniu krytycznym może
doprowadzić do zdefiniowania polityki, w której logika biznesowa opiera się na jej
niezastosowaniu do zasobu bez informowania o tym użytkownika.
Potencjalnym rozwiązaniem chroniącym przed tym będzie zgłoszenie komunikatu
ostrzeżenia, gdy API serwera zarejestruje brak możliwości dotarcia do określonego
zaczepu sieciowego dopuszczenia. Wartość Fail może mieć jeszcze poważniejsze
konsekwencje z powodu odrzucenia każdego żądania, gdy zaczep sieciowy
dopuszczenia napotka jakiekolwiek problemy. Aby się przed tym chronić, można
zdefiniować zasięg dla reguł i zagwarantować, że tylko żądania określonego zasobu
będą dotyczyły zaczepu. Jako użytkownik współdzielonego środowiska nigdy nie
powinieneś mieć zdefiniowanych żadnych reguł, które będą dotyczyły wszystkich
zasobów klastra.
Jeżeli samodzielnie utworzyłeś zaczep sieciowy dopuszczenia, musisz pamiętać o
bezpośrednim wpływie na żądania użytkownika lub systemu, zanim Twój zaczep podejmie
decyzję i udzieli odpowiedzi. Wszystkie wywołania zaczepów sieciowych dopuszczenia są
skonfigurowane z 30-sekundowym czasem utraty ważności, po którego upływie zostanie
wykonana operacja zdefiniowana przez właściwość failurePolicy. Nawet jeśli Twój
zaczep sieciowy dopuszczenia potrzebuje kilku sekund na zezwolenie lub odrzucenie
żądania, to nadal może mieć negatywny wpływ na wrażenia użytkownika pracującego z
danym klastrem. Unikaj stosowania skomplikowanej logiki lub stosowania systemów
zewnętrznych, takich jak baza danych, podczas przetwarzania logiki zezwolenia lub
odrzucenia żądania.
Stosuj zasięg dla zaczepów sieciowych dopuszczenia. Istnieje opcjonalna właściwość
NamespaceSelector, pozwalająca na stosowanie zasięgu przestrzeni nazw, w której może
działać zaczep sieciowy dopuszczenia. Domyślnie wartością tej właściwości jest pusty ciąg
tekstowy, co oznacza dopasowanie wszystkiego. Wartość tej właściwości można
wykorzystać do dopasowania etykiet przestrzeni nazw za pomocą właściwości
matchLabels. Zachęcamy, aby zawsze stosować właściwość NamespaceSelector,
ponieważ pozwala ona na wyraźne wskazywanie przestrzeni nazw.
Przestrzeń nazw kube-system została zarezerwowana i jest dostępna we wszystkich
klastrach Kubernetes. To właśnie w niej działają wszystkie usługi na poziomie systemu.
Nigdy nie powinieneś uruchamiać żadnych zaczepów sieciowych dopuszczenia względem
tej przestrzeni nazw, co można osiągnąć przez użycie właściwości NamespaceSelector i
niedopasowanie w niej przestrzeni nazw kube-system. Powinieneś to rozważyć również
dla wszystkich przestrzeni nazw na poziomie systemu, które są niezbędne do działania
klastra.
Konfiguracje zaczepów sieciowych dopuszczenia zabezpieczaj za pomocą kontroli dostępu
na podstawie roli użytkownika (ang. role-based access control, RBAC). Skoro dowiedziałeś
się wiele o właściwościach w konfiguracji zaczepu sieciowego dopuszczenia,
prawdopodobnie zastanawiasz się nad naprawdę prostym sposobem na złamanie dostępu
do klastra. Nie ulega wątpliwości, że utworzenie egzemplarzy
MutatingWebhookConfiguration i ValidatingWebhookConfiguration to operacja na
najwyższym poziomie klastra, więc musi być odpowiednio zabezpieczona za pomocą
mechanizmu RBAC. Jeżeli tego nie dopilnujesz, skutkiem może być włamanie do klastra
lub nawet gorzej — atak polegający na wstrzyknięciu danych do zadania wykonywanego w
klastrze.
Nie przekazuj żadnych danych wrażliwych. Zaczep sieciowy dopuszczenia jest w zasadzie
rodzajem „czarnego pudełka”, które akceptuje egzemplarz AdmissionRequest i
przekazuje egzemplarz AdmissionResponse. Sposób przechowywania i przetwarzania
żądania pozostaje nieznany dla użytkownika. Warto więc zastanowić się nad treścią
żądania przekazywanego do zaczepu sieciowego dopuszczenia. W danych poufnych
Kubernetes lub egzemplarzach ConfigMap mogą się znajdować informacje wrażliwe,
wymagające ściśle określonego sposobu przechowywania i współdzielenia. Udostępnienie
tych informacji zaczepowi sieciowemu dopuszczenia może doprowadzić do ich ujawnienia.
Dlatego też powinieneś ograniczać zasięg reguł zasobu do minimalnej liczby zasobów
niezbędnych do weryfikacji lub modyfikacji.
Autoryzacja
O autoryzacji bardzo często myślimy w kontekście następującego pytania: „Czy użytkownik
będzie miał możliwość przeprowadzenia danych akcji na tych zasobach?”. W Kubernetes
autoryzacja każdego żądania jest przeprowadzana po uwierzytelnianiu, ale jeszcze przed
operacją dopuszczenia. Z tego podrozdziału dowiesz się, jak można skonfigurować różne
moduły autoryzacji, i lepiej poznasz sposoby, na jakie można tworzyć odpowiednią politykę dla
klastra. Na rysunku 17.3 pokazaliśmy sposób obsługi autoryzacji podczas przetwarzania
żądania.
Rysunek 17.3. Sposób obsługi żądania API z uwzględnieniem modułów autoryzacji
Moduły autoryzacji
Moduły autoryzacji są odpowiedzialne za udzielenie dostępu lub jego odmowę. Określają, czy
udzielić dostępu, na podstawie polityki, która musi być wyraźnie zdefiniowana. W przeciwnym
razie wszystkie żądania będą niejawnie odrzucone.
Kubernetes w wersji 1.15 jest standardowo dostarczany z następującymi modułami autoryzacji:
ABAC (ang. attribute-based access control)
Pozwala na konfigurację polityki autoryzacji za pomocą plików lokalnych.
RBAC (ang. role-based access control)
Pozwala na konfigurację polityki autoryzacji za pomocą API Kubernetes (więcej informacji
na ten temat znajdziesz w rozdziale 4.).
Webhook
Pozwala na obsługę autoryzacji żądania za pomocą zdalnego punktu końcowego REST.
Node
Specjalizowany moduł autoryzacji, który zajmuje się autoryzowaniem żądań z kubeletów.
Wymienione moduły są konfigurowane przez administratora klastra za pomocą następującej
opcji API serwera: --authorize-mode. Istnieje możliwość konfiguracji wielu modułów i ich
sprawdzania po kolei. W przeciwieństwie do kontrolerów dopuszczenia, jeśli jeden moduł
autoryzacji zezwoli na wykonanie żądania, wówczas będzie ono przetworzone. Komunikat o
błędzie zostanie wyświetlony użytkownikowi tylko w przypadku, gdy wszystkie moduły
autoryzacji odrzucą żądanie.
ABAC
Spójrz na definicję polityki w kontekście użycia modułu autoryzacji ABAC. Przedstawiony tutaj
fragment kodu pozwala użytkownikowi mary na uzyskanie dostępu w trybie tylko do odczytu do
poda w przestrzeni nazw kube-system.
apiVersion: abac.authorization.kubernetes.io/v1beta1
kind: Policy
spec:
user: mary
resource: pods
readonly: true
namespace: kube-system
Jeżeli użytkownik mary wykona przedstawione tutaj żądanie, zostanie ono odrzucone, ponieważ
mary nie ma dostępu do podów w przestrzeni nazw demo-app.
apiVersion: authorization.k8s.io/v1beta1
kind: SubjectAccessReview
spec:
resourceAttributes:
verb: get
resource: pods
namespace: demo-app
Ten przykład wprowadził nową grupę API o nazwie authorization.k8s.io. Udostępnia ona
API autoryzacji serwera usługom zewnętrznym i oferuje wymienione poniżej zasoby, które
sprawdzają się doskonale podczas debugowania.
SelfSubjectAccessReview
Kontrola dostępu dla bieżącego użytkownika.
SubjectAccessReview
Podobnie jak w przypadku SelfSubjectAccessReview, ale dotyczy dowolnego użytkownika.
LocalSubjectAccessReview
Podobnie jak w przypadku SubjectAccessReview, ale dotyczy konkretnej przestrzeni nazw.
SelfSubjectRulesReview
Zwraca listę działań, które użytkownik może wykonać w danej przestrzeni nazw.
Naprawdę świetną cechą jest możliwość wykonywania zapytań do wymienionych API przez
utworzenie zasobów, z których zwykle się korzysta. Powróćmy na chwilę do poprzedniego
przykładu i zobaczmy, jak można użyć SelfSubjectAccessReview. Wartość właściwości w
wygenerowanych danych wyjściowych wskazuje na możliwość wykonania tego żądania.
$ cat << EOF | kubectl create -f - -o yaml
apiVersion: authorization.k8s.io/v1beta1
kind: SelfSubjectAccessReview
spec:
resourceAttributes:
verb: get
resource: pods
namespace: demo-app
EOF
apiVersion: authorization.k8s.io/v1beta1
kind: SelfSubjectAccessReview
metadata:
creationTimestamp: null
spec:
resourceAttributes:
namespace: demo-app
resource: pods
verb: get
status:
allowed: true
Faktycznie oprogramowanie Kubernetes jest dostarczane z dodatkami wbudowanymi w
narzędzie powłoki kubectl, dzięki którym takie zadanie można wykonywać jeszcze szybciej.
Polecenie kubectl auth can-i działa w ten sposób, że wykonuje zapytanie do tego samego
API, które zostało użyte w poprzednim przykładzie.
$ kubectl auth can-i get pods --namespace demo-app
Yes
Po użyciu danych uwierzytelniających użytkownika z uprawnieniami administratora za pomocą
tego samego polecenia można sprawdzić także możliwości innych użytkowników:
$ kubectl auth can-i get pods --namespace demo-app --as mary
Yes
RBAC
Stosowana w Kubernetes kontrola dostępu na podstawie roli użytkownika została dokładnie
omówiona w rozdziale 4.
Webhook
Użycie modułu autoryzacji zaczepu sieciowego pozwala administratorowi klastra na
konfigurację zewnętrznego punktu końcowego REST, do którego zostanie oddelegowany proces
autoryzacji. Dzięki temu proces przebiega poza klastrem i jest dostępny poprzez adres URL.
Konfiguracja punktu końcowego REST jest umieszczona w pliku w głównym systemie plików
oraz konfigurowana w serwerze API za pomocą --authorization-webhook-configfile=NAZWA_PLIKU.
Po
skonfigurowaniu
serwer
API
będzie
przekazywał
obiekty
SubmitAccessReview jako część treści żądania do zaczepu sieciowego autoryzacji, który z kolej
przetworzy i zwróci obiekt z odpowiednią właściwością.
Najlepsze praktyki dotyczące autoryzacji
Przed wprowadzeniem zmian w modułach autoryzacji skonfigurowanych w klastrze rozważ
stosowanie przedstawionych tu najlepszych praktyk.
Biorąc pod uwagę to, że polityki ABAC trzeba umieścić w systemie plików każdego węzła i
je synchronizować, ogólnie odradzamy stosowanie ABAC w klastrach składających się z
wielu systemów głównych. To samo można powiedzieć o module Webhook, ponieważ
konfiguracja jest oparta na pliku i obecności odpowiedniej opcji. Co więcej, zmiany
wprowadzone w plikach tych polityk wymagają ponownego uruchomienia serwera API. To
w praktyce oznacza przerwanie działa klastra składającego się z jednego serwera
głównego lub niespójną konfigurację w klastrze zawierającym wiele serwerów głównych.
Biorąc pod uwagę te szczegóły, zalecamy stosowanie modułu RBAC jedynie podczas
autoryzacji użytkownika, ponieważ reguły są konfigurowane i przechowywane w
Kubernetes.
Moduły Webhook oferują potężne możliwości, choć zarazem mogą być bardzo
niebezpieczne. Skoro każde żądanie jest przedmiotem procesu autoryzacji, awaria usługi
Webhook będzie miała katastrofalne konsekwencje dla klastra. Dlatego też zalecamy
niestosowanie zewnętrznych modułów autoryzacji, o ile nie jesteś bardzo doświadczony i
potrafisz świetnie poradzić sobie w razie awarii klastra, gdy usługa stanie się
niedostępna.
Podsumowanie
W tym rozdziale zostały omówione podstawowe zagadnienia związane ze sterowaniem
dopuszczeniem i autoryzacją, a ponadto przedstawiliśmy najlepsze praktyki dotyczące
wymienionych obszarów. Wykorzystaj te umiejętności do wypracowania najlepszej konfiguracji
sterowania dopuszczeniem i autoryzacji, która pozwoli na dostosowanie do własnych potrzeb
polityk niezbędnych do funkcjonowania Twojego klastra.
Rozdział 18. Zakończenie
Największą zaletą Kubernetes jest modułowość i ogólność. Niemalże każdy rodzaj aplikacji, jaki
chciałbyś wdrożyć, będzie pasował do Kubernetes, a wdrożenie aplikacji ogólnie będzie
możliwe niezależnie od tego, jakiego rodzaju modyfikacje trzeba będzie wprowadzić w
systemie.
Oczywiście, modułowość i ogólność wiążą się z pewnym kosztem, który w tym przypadku
wyraża się poziomem skomplikowania. Jeśli chcesz w pełni wykorzystać możliwości Kubernetes,
które pozwalają na to, aby opracowanie aplikacji, jej wdrożenie i zarządzanie nią było procesem
zarówno łatwiejszym, jak i bardziej niezawodnym, musisz poznać sposób działania jego API i
komponentów.
Równie ważne jest poznanie, jak Kubernetes łączy się z wieloma innymi systemami
zewnętrznymi oraz współdziała z systemami baz danych i ciągłego wdrażania, aby można było
wykorzystać go w rzeczywistych projektach.
Ta książka pozwoliła Ci zapoznać się z konkretnymi, rzeczywistymi przykładami dotyczącymi
określonych zagadnień, z którymi prawdopodobnie zetkniesz się niezależnie od tego, czy jesteś
początkującym użytkownikiem Kubernetes, czy też zaawansowanym administratorem. Jeżeli
dopiero poznajesz nowe dla Ciebie zagadnienia, które pragniesz opanować do perfekcji, lub
jedynie chcesz sprawdzić, jak inni poradzili sobie z danym problemem, to mamy nadzieję, że
przedstawiony materiał Ci w tym pomógł. Ufamy, że dzięki tej książce zdobędziesz odpowiednie
umiejętności i nabierzesz pewności siebie na tyle, aby w pełni wykorzystać możliwości
oferowane przez Kubernetes. Dziękujemy, że ją przeczytałeś, i nie możemy się doczekać, aż
spotkamy Cię w prawdziwym świecie.
O autorze
Brendan Burns jest wybitnym inżynierem w Microsoft Azure oraz współzałożycielem projektu
open source Kubernetes. Od ponad dekady tworzy aplikacje działające w chmurze.
Eddie Villalba jest inżynierem oprogramowania w oddziale Microsoft Commercial Software
Engineering, koncentruje się na Kubernetes i chmurze typu open source. Pomógł wielu
użytkownikom zaadaptować Kubernetes w ich aplikacjach.
Dave Strebel to architekt natywnej chmury Microsoft Azure zajmujący się Kubernetes i
chmurą typu open source. Jest głęboko zaangażowany w projekt Kubernetes, pomaga zespołowi
odpowiedzialnemu za wydania Kubernetes i kieruje projektem SIG Azure.
Lachlan Evenson jest głównym menedżerem programu w zespole kontenerów w Microsoft
Azure. Wielu osobom pomógł w rozpoczęciu pracy z Kubernetes na szkoleniach, które
przeprowadził, jak i dzięki wystąpieniom podczas różnych konferencji.
Kolofon
Zwierzęciem, które znalazło się na okładce książki Najlepsze praktyki w Kubernetes, jest
kaczka krzyżówka (Anas platyrhynchos). To rodzaj kaczki, która w poszukiwaniu pożywienia nie
nurkuje, lecz jedynie zanurza przednią część ciała i odżywia się na powierzchni wody.
Poszczególne gatunki z rodzaju Anas różnią się zasięgiem występowania, a także sposobem
życia i zachowania. Kaczki krzyżówki często krzyżują się z innymi gatunkami kaczek, co
zaowocowało powstaniem mieszańców międzygatunkowych w pełni zdolnych do rozrodu.
Młode kaczki krzyżówki są zagniazdownikami, co oznacza, że po wykluciu stają się w pełni
samodzielne i potrafią pływać. Zaczynają latać między trzecim a czwartym miesiącem życia.
Pełną dojrzałość osiągają po 14 miesiącach, a ich przeciętna długość życia wynosi 3 lata.
Kaczka krzyżówka to średniej wielkości kaczka, nieco cięższa od większości kaczek właściwych.
Długość ciała dorosłych osobników wynosi 50 – 65 cm, rozpiętość skrzydeł to 76 – 102 cm, a
masa ciała waha się w zakresie 870 – 1800 g u samców i 735 – 1320 g u samic. Upierzenie
kaczek krzyżówek jest żółto-czarne. W wieku około 6 miesięcy zaznacza się dymorfizm płciowy,
czyli możliwe jest rozróżnienie osobników męskich i żeńskich na podstawie ich ubarwienia.
Samce, zwane kaczorami, mają opalizujące na zielono ubarwienie głowy, białą szyję, opalizującą
na brązowo pierś, szaro-brązowe skrzydła i żółto-pomarańczowe nogi. Samica ma stonowane
ubarwienie w kolorze nakrapianego brązu.
Krzyżówki mają szeroki wachlarz siedlisk — występują zarówno na półkuli północnej, jak i
południowej. Spotykane są w akwenach słodko- i słonowodnych, od jezior poprzez rzeki aż po
morskie wybrzeża. Kaczki krzyżówki zamieszkujące półkulę północną często migrują, a zimą
przemieszczają się na południe. Pożywienie krzyżówek jest bardzo zróżnicowane i obejmuje
rośliny, nasiona, korzenie, a także ślimaki, bezkręgowce i skorupiaki.
Często się zdarza, że tzw. pasożyty lęgowe atakują gniazda kaczek krzyżówek. Te pasożyty to
gatunki innych ptaków, które składają jaja w gnieździe krzyżówki. Jeśli jaja pasożyta
przypominają jaja kaczki, wtedy kaczka je akceptuje i wysiaduje razem z własnymi.
Kaczki krzyżówki muszą się bronić przed różnymi drapieżnikami, zwłaszcza lisami i ptakami
drapieżnymi, takimi jak sokoły i orły. Są również atakowane przez sumy i szczupaki. W walce o
terytorium przeciwnikami krzyżówek są wrony, łabędzie i gęsi. Kaczki są znane także ze snu
jednopółkulowego, podczas którego jedna półkula śpi snem głębokim (odpowiadające jej oko
pozostaje zamknięte), a druga czuwa (odpowiadające jej oko pozostaje otwarte). Jest to
powszechne zjawisko wśród ptaków wodnych, pozwalające im na obronę przed drapieżnikami.
Wiele zwierząt występujących na okładkach książek wydawnictwa O’Reilly jest zagrożonych
wyginięciem. Wszystkie są niezwykle ważne dla świata.
Ilustracja na okładce autorstwa Jose Marzana została oparta na czarno-białej grafice
pochodzącej z dzieła The Animal World.
Spis treści
Wprowadzenie
Dla kogo jest przeznaczona ta książka?
Dlaczego napisaliśmy tę książkę?
Poruszanie się po książce
Konwencje zastosowane w książce
Użycie przykładowych kodów
Podziękowania
Rozdział 1. Konfiguracja podstawowej usługi
Ogólne omówienie aplikacji
Zarządzanie plikami konfiguracyjnymi
Tworzenie usługi replikowanej za pomocą wdrożeń
Najlepsze praktyki dotyczące zarządzania obrazami kontenera
Tworzenie replikowanej aplikacji
Konfiguracja zewnętrznego przychodzącego ruchu sieciowego HTTP
Konfigurowanie aplikacji za pomocą zasobu ConfigMap
Zarządzanie uwierzytelnianiem za pomocą danych poufnych
Wdrożenie prostej bezstanowej bazy danych
Utworzenie za pomocą usług mechanizmu równoważenia obciążenia TCP
Przekazanie przychodzącego ruchu sieciowego do serwera pliku statycznego
Parametryzowanie aplikacji za pomocą menedżera pakietów Helm
Najlepsze praktyki dotyczące wdrożenia
Podsumowanie
Rozdział 2. Sposób pracy programisty
Cele
Tworzenie klastra programistycznego
Konfiguracja klastra współdzielonego przez wielu programistów
Przygotowywanie zasobów dla użytkownika
Tworzenie i zabezpieczanie przestrzeni nazw
Zarządzanie przestrzeniami nazw
Usługi na poziomie klastra
Umożliwienie pracy programistom
Konfiguracja początkowa
Umożliwienie aktywnego programowania
Umożliwienie testowania i debugowania
Najlepsze praktyki dotyczące konfiguracji środowiska programistycznego
Podsumowanie
Rozdział 3. Monitorowanie i rejestrowanie danych w Kubernetes
Wskaźniki kontra dzienniki zdarzeń
Techniki monitorowania
Wzorce monitorowania
Ogólne omówienie wskaźników Kubernetes
cAdvisor
Wskaźniki serwera
kube-state-metrics
Które wskaźniki powinny być monitorowane?
Narzędzia do monitorowania
Monitorowanie Kubernetes za pomocą narzędzia Prometheus
Ogólne omówienie rejestrowania danych
Narzędzia przeznaczone do rejestrowania danych
Rejestrowanie danych za pomocą stosu EFK
Ostrzeganie
Najlepsze praktyki dotyczące monitorowania, rejestrowania danych i ostrzegania
Monitorowanie
Rejestrowanie danych
Ostrzeganie
Podsumowanie
Rozdział 4. Konfiguracja, dane poufne i RBAC
Konfiguracja za pomocą zasobu ConfigMap i danych poufnych
ConfigMap
Dane poufne
Najlepsze praktyki dotyczące API zasobu ConfigMap i danych poufnych
Najlepsze praktyki dotyczące danych poufnych
RBAC
Krótkie wprowadzenie do mechanizmu RBAC
Podmiot
Reguła
Rola
Zasób RoleBinding
Najlepsze praktyki dotyczące mechanizmu RBAC
Podsumowanie
Rozdział 5. Ciągła integracja, testowanie i ciągłe wdrażanie
System kontroli wersji
Ciągła integracja
Testowanie
Kompilacja kontenera
Oznaczanie tagiem obrazu kontenera
Ciągłe wdrażanie
Strategie wdrażania
Testowanie w produkcji
Stosowanie inżynierii chaosu i przygotowania
Konfiguracja ciągłej integracji
Konfiguracja ciągłego wdrażania
Przeprowadzanie operacji uaktualnienia
Prosty eksperyment z inżynierią chaosu
Najlepsze praktyki dotyczące technik ciągłej integracji i ciągłego wdrażania
Podsumowanie
Rozdział 6. Wersjonowanie, wydawanie i wdrażanie aplikacji
Wersjonowanie aplikacji
Wydania aplikacji
Wdrożenia aplikacji
Połączenie wszystkiego w całość
Najlepsze praktyki dotyczące wersjonowania, wydawania i wycofywania wdrożeń
Podsumowanie
Rozdział 7. Rozpowszechnianie aplikacji na świecie i jej wersje robocze
Rozpowszechnianie obrazu aplikacji
Parametryzacja wdrożenia
Mechanizm równoważenia obciążenia związanego z ruchem sieciowym w globalnie wdrożonej
aplikacji
Niezawodne wydawanie oprogramowania udostępnianego globalnie
Weryfikacja przed wydaniem oprogramowania
Region kanarkowy
Identyfikacja typów regionów
Przygotowywanie wdrożenia globalnego
Gdy coś pójdzie nie tak
Najlepsze praktyki dotyczące globalnego wdrożenia aplikacji
Podsumowanie
Rozdział 8. Zarządzanie zasobami
Zarządca procesów w Kubernetes
Predykaty
Priorytety
Zaawansowane techniki stosowane przez zarządcę procesów
Podobieństwo i brak podobieństwa podów
nodeSelector
Wartość taint i tolerancje
Zarządzanie zasobami poda
Żądanie zasobu
Ograniczenia zasobów i jakość usługi poda
PodDisruptionBudget
Dostępność minimalna
Dostępne maksimum
Zarządzanie zasobami za pomocą przestrzeni nazw
ResourceQuota
LimitRange
Skalowanie klastra
Skalowanie ręczne
Skalowanie automatyczne
Skalowanie aplikacji
Skalowanie za pomocą HPA
HPA ze wskaźnikami niestandardowymi
Vertical Pod Autoscaler
Najlepsze praktyki dotyczące zarządzania zasobami
Podsumowanie
Rozdział 9. Sieć, bezpieczeństwo sieci i architektura Service Mesh
Reguły działania sieci w Kubernetes
Wtyczki sieci
Kubenet
Najlepsze praktyki dotyczące pracy z Kubenet
Wtyczka zgodna ze specyfikacją CNI
Najlepsze praktyki dotyczące pracy z wtyczkami zgodnymi ze specyfikacją CNI
Usługi w Kubernetes
Typ usługi ClusterIP
Typ usługi NodePort
Typ usługi ExternalName
Typ usługi LoadBalancer
Ingress i kontrolery Ingress
Najlepsze praktyki dotyczące usług i kontrolerów Ingress
Polityka zapewnienia bezpieczeństwa sieci
Najlepsze praktyki dotyczące polityki sieci
Architektura Service Mesh
Najlepsze praktyki dotyczące architektury Service Mesh
Podsumowanie
Rozdział 10. Bezpieczeństwo poda i kontenera
API PodSecurityPolicy
Włączenie zasobu PodSecurityPolicy
Anatomia zasobu PodSecurityPolicy
Wyzwania związane z zasobem PodSecurityPolicy
Rozsądne polityki domyślne
Wiele mozolnej pracy
Czy programiści są zainteresowani poznawaniem zasobu PodSecurityPolicy?
Debugowanie jest uciążliwe
Czy opierasz się na komponentach, które są poza Twoją kontrolą?
Najlepsze praktyki dotyczące zasobu PodSecurityPolicy
Następne kroki związane z zasobem PodSecurityPolicy
Izolacja zadania i API RuntimeClass
Używanie API RuntimeClass
Implementacje środowiska uruchomieniowego
Najlepsze praktyki dotyczące izolacji zadań i API RuntimeClass
Pozostałe rozważania dotyczące zapewnienia bezpieczeństwa poda i kontenera
Kontrolery dopuszczenia
Narzędzia do wykrywania włamań i anomalii
Podsumowanie
Rozdział 11. Polityka i zarządzanie klastrem
Dlaczego polityka i zarządzanie są ważne?
Co odróżnia tę politykę od innych?
Silnik polityki natywnej chmury
Wprowadzenie do narzędzia Gatekeeper
Przykładowe polityki
Terminologia stosowana podczas pracy z Gatekeeper
Ograniczenie
Rego
Szablon ograniczenia
Definiowanie szablonu ograniczenia
Definiowanie ograniczenia
Replikacja danych
UX
Audyt
Poznanie narzędzia Gatekeeper
Następne kroki podczas pracy z narzędziem Gatekeeper
Najlepsze praktyki dotyczące polityki i zarządzania
Podsumowanie
Rozdział 12. Zarządzanie wieloma klastrami
Do czego potrzebujesz wielu klastrów?
Kwestie do rozważenia podczas projektowania architektury składającej się z wielu klastrów
Zarządzanie wieloma wdrożeniami klastrów
Wzorce wdrażania i zarządzania
Podejście GitOps w zakresie zarządzania klastrami
Narzędzia przeznaczone do zarządzania wieloma klastrami
Federacja Kubernetes
Najlepsze praktyki dotyczące zarządzania wieloma klastrami
Podsumowanie
Rozdział 13. Integracja usług zewnętrznych z Kubernetes
Importowanie usług do Kubernetes
Pozbawiona selektora usługa dla stabilnego adresu IP
Oparte na rekordzie CNAME usługi dla stabilnych nazw DNS
Podejście oparte na aktywnym kontrolerze
Eksportowanie usług z Kubernetes
Eksportowanie usług za pomocą wewnętrznych mechanizmów równoważenia obciążenia
Eksportowanie usług za pomocą usługi opartej na NodePort
Integracja komputerów zewnętrznych z Kubernetes
Współdzielenie usług między Kubernetes
Narzędzia opracowane przez podmioty zewnętrzne
Najlepsze praktyki dotyczące nawiązywania połączeń między klastrami a usługami
zewnętrznymi
Podsumowanie
Rozdział 14. Uczenie maszynowe w Kubernetes
Dlaczego Kubernetes doskonale sprawdza się w połączeniu z uczeniem maszynowym?
Sposób pracy z zadaniami uczenia głębokiego
Uczenie maszynowe dla administratorów klastra Kubernetes
Trenowanie modelu w Kubernetes
Wytrenowanie pierwszego modelu w Kubernetes
Trenowanie rozproszone w Kubernetes
Ograniczenia dotyczące zasobów
Sprzęt specjalizowany
Planowanie zasobów
Biblioteki, sterowniki i moduły jądra
Pamięć masowa
Przechowywanie zbioru danych i jego rozproszenie między węzły robocze podczas trenowania
modelu
Punkty kontrolne i zapisywanie modeli
Sieć
Protokoły specjalizowane
Obawy użytkowników zajmujących się analizą danych
Najlepsze praktyki dotyczące wykonywania w Kubernetes zadań związanych z uczeniem
maszynowym
Podsumowanie
Rozdział 15. Tworzenie wzorców aplikacji wysokiego poziomu na podstawie Kubernetes
Podejścia w zakresie tworzenia abstrakcji wysokiego poziomu
Rozszerzanie Kubernetes
Rozszerzanie klastrów Kubernetes
Wrażenia użytkownika podczas rozszerzania Kubernetes
Rozważania projektowe podczas budowania platformy
Obsługa eksportowania do obrazu kontenera
Obsługa istniejących mechanizmów dla usług i wykrywania usług
Najlepsze praktyki dotyczące tworzenia platform dla aplikacji
Podsumowanie
Rozdział 16. Zarządzanie informacjami o stanie i aplikacjami wykorzystującymi te dane
Woluminy i punkty montowania
Najlepsze praktyki dotyczące woluminów
Pamięć masowa w Kubernetes
API PersistentVolume
API PersistentVolumeClaims
Klasy pamięci masowej
Interfejs pamięci masowej kontenera i FlexVolume
Najlepsze praktyki dotyczące pamięci masowej w Kubernetes
Aplikacje obsługujące informacje o stanie
Zasób StatefulSet
Operatory
Najlepsze praktyki dotyczące zasobu StatefulSet i operatorów
Podsumowanie
Rozdział 17. Sterowanie dopuszczeniem i autoryzacja
Sterowanie dopuszczeniem
Czym jest kontroler dopuszczenia?
Typy kontrolerów dopuszczenia
Konfiguracja zaczepu sieciowego dopuszczenia
Najlepsze praktyki dotyczące sterowania dopuszczeniem
Autoryzacja
Moduły autoryzacji
ABAC
RBAC
Webhook
Najlepsze praktyki dotyczące autoryzacji
Podsumowanie
Rozdział 18. Zakończenie
O autorze
Kolofon
Download