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 &lt;user-name&gt; 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