Professional Documents
Culture Documents
Najlepsze praktyki w Kubernetes. - Liz Rice, Brenda Burns, Eddie Villalba
Najlepsze praktyki w Kubernetes. - Liz Rice, Brenda Burns, Eddie Villalba
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
http://helion.pl/user/opinie/naprak_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Poleć książkę
Kup w wersji papierowej
Oceń książkę
Księgarnia internetowa
Lubię to! » nasza społeczność
Wprowadzenie
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.
Kursywa
Wskazuje na nowe pojęcia, adresy URL i e-mail, nazwy plików, rozszerzenia plików itd.
Wskazuje tekst, który powinien być zastąpiony wartościami podanymi przez użytkownika
bądź wynikającymi z kontekstu.
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.
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.
journal/
frontend/
redis/
fileserver/
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.
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.
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
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.
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
...
# 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.
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.
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ść.
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)
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.
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
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.
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
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. frontend-
production/ 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).
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
...
...
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
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.
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.
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]
if err != nil {
panic(err)
keyDer := x509.MarshalPKCS1PrivateKey(key)
keyBlock := pem.Block{
Bytes: keyDer,
if err != nil {
panic(err)
pem.Encode(keyFile, &keyBlock)
keyFile.Close()
commonName := user
emailAddress := "someone@myco.com"
org := "My Co, Inc."
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},
if err != nil {
panic(err)
csr := x509.CertificateRequest{
RawSubject: asn1,
EmailAddresses: []string{emailAddress},
SignatureAlgorithm: x509.SHA256WithRSA,
if err != nil {
panic(err)
panic(err)
}
pem.Encode(csrFile, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes:bytes})
csrFile.Close()
#!/bin/bash
csr_name="my-client-csr"
name="${1:-my-user}"
csr="${2}"
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
name: ${csr_name}
spec:
groups:
- system:authenticated
- digital signature
- key encipherment
- client auth
EOF
echo
echo
echo "Porządkowanie."
echo
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.
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'
namespace: my-namespace
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: edit
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: myuser
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.
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.
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
{
...
"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.
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.
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.
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
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 Gregga metoda USE oznacza koncentrację na trzech
wymienionych tutaj obszarach:
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:
Idea tej metody została zaczerpnięta ze standardu czterech złotych reguł firmy Google:
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).
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.
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ą kube-
state-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-state-
metrics/tree/master/docs.
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.
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.
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
Node Exporter
Alertmanager
Pozwala na konfigurowanie i przekazywanie ostrzeżeń do systemów zewnętrznych.
Grafana
Spójrz na serwer Prometheusa i zobacz, jak można wykonywać pewne zapytania związane z
pobieraniem wskaźników Kubernetes.
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.
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 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.
Ś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.
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:
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-controller-
manager.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ń.
Elastic Stack,
Datadog,
Sumo Logic,
Sysdig,
Usługi dostawcy chmury (GCP Stackdriver, Microsoft Azure Monitor for containers i Amazon
CloudWatch).
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 apply -f
https://raw.githubusercontent.com/dstrebel/kbp/master/elasticsearch-
operator.yaml -n logging
$ 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:
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.
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.
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.
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.
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.
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 --from-
literal=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
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.
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.
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
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"
}
}
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"]
- 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 }}
[...]
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.
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:
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:
metadata:
name: tiller
namespace: myapp-prod
rules:
- apiGroups: ["", "batch", "extensions", "apps"]
resources: ["*"]
verbs: ["*"]
EOF
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
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:
Zaprezentujemy również przykładowe techniki CI i CD, na które składają się wymienione tutaj
zadania:
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
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.
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.
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.
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.
apiVersion: v1
metadata:
name: frontend
spec:
replicas: 3
template:
spec:
containers:
- name: frontend
image: brendanburns/frontend:v1
strategy:
type: RollingUpdate
rollingUpdate:
kind: Deployment
apiVersion: v1
metadata:
name: frontend
spec:
replicas: 3
template:
spec:
containers:
- name: frontend
image: brendanburns/frontend:v1
livenessProbe:
# ...
readinessProbe:
httpGet:
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.
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.
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.
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”.
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.
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).
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.
$ 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
--serviceaccount=default:drone
Kolejnym krokiem jest pobranie tokena dla serviceaccount:
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
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.
publish:
image: plugins/docker
dockerfile: ./frontend/Dockerfile
context: ./frontend
repo: dstrebel/frontend
tags: [latest, v2]
secrets: [ docker_username, docker_password ]
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ą:
image: dstrebel/drone-kubectl-helm
secrets: [ kubernetes_server, kubernetes_cert, kubernetes_token ]
kubectl: "get deployment frontend"
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.
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.
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.
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:
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
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gb-mysql
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
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
---
apiVersion: batch/v1
kind: Job
metadata:
name: db-backup
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
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.
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ę.
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.).
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ć.
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.
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 zakończy się sukcesem. Tak znaczny
wzrost zużycia poziomu zasobów będzie miał negatywny wpływ na jakość i dostępność aplikacji.
W zależności od okoliczności można kontynuować operacje umieszczenia aplikacji w
środowisku produkcyjnym i jednocześnie postarać się zrozumieć, skąd wzięła się zmiana w
poziomie zużycia zasobó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.
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.
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.
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ą.
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.
Ogólnie rzecz biorąc, wartość taint i tolerancja są używane w wymienionych tutaj sytuacjach:
Istnieje wiele różnych typów wartości taint wpływających na zarządcę procesów i uruchomione
kontenery:
NoSchedule
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
NodeCondition
Ta wartość oznacza wartością taint węzeł, gdy spełnia on określony warunek.
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: FailedScheduling 10s (x10 over 2m) default-scheduler 0/2 nodes
are
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.
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:
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.
Events:
Type Reason Age From Message
Warning FailedScheduling 27s (x2 over 27s) default-scheduler 0/3 nodes
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:
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.
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.
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
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.
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
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.
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.
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
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.
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.
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
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
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.
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.
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 kube-
proxy, 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.
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
kind: Service
apiVersion: v1
metadata:
name: prod-mongodb
namespace: prod
spec:
type: ExternalName
externalName: mymongodb.documents.azure.com
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
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
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:
- {}
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"
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
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
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ń.
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).
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
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.
AGE
replicaset.extensions/pause-delpoyment-67b77c4f69 1 0 0
41s
Możesz to potwierdzić przez wydanie polecenia dokładnie przedstawiającego zasób
ReplicaSet.
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
Pod Template:
Labels: app=pause
pod-template-hash=67b77c4f69
Containers:
pause:
Image: k8s.gcr.io/pause
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Events:
Type Reason Age From Message
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
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.
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
replicaset.extensions/pause-deployment-67b77c4f69 1 1 1
10s
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 pause-
privileged-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
AVAILABLE AGE
deployment.extensions/pause-privileged-deployment 0/1 0
0 37s
NAME DESIRED
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
Pod Template:
Labels: app=pause
pod-template-hash=6b7bcfb9b7
Containers:
pause:
Image: k8s.gcr.io/pause
Port: <none>
Mounts: <none>
Volumes: <none>
Conditions:
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.
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
deployment.extensions/pause-privileged-deployment 1/1 1
1 14s
NAME DESIRED
CURRENT READY AGE
replicaset.extensions/pause-privileged-deployment-658dc5569f 1
1 1 14s
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.
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
runtimeClassName: firecracker
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/)
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.
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ę.
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-policy-
agent/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.
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.
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
listKind: K8sAllowedReposList
plural: k8sallowedrepos
singular: k8sallowedrepos
validation:
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[_]
not any(satisfied)
}
Ten szablon ograniczenia składa się z trzech głównych komponentów.
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"
Specyfikacja
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
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.
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/prod-
repo-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
name: nginx
namespace: production
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 najlepsze praktyki dotyczące zarządzania wieloma klastrami
Kubernetes. Zagłębimy się w różnice między narzędziami oraz wzorcami operacyjnymi
przeznaczonymi do zarządzania wieloma klastrami, a także w szczegóły pokazujące różnice w
zarządzaniu wieloma klastrami i 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.
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-a-
cascading-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.
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.
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.
Projektem Kubernetes, którego rozwój warto śledzić, jest API Cluster (https://cluster-
api.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.
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:
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.
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.
$ 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
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.
$ 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.
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/kubernetes-
sigs/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.
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.
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
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 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
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.
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"
...
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.
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ć.
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.
Samoobsługa
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.
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:
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
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.
job.batch/mnist-demo created
mnist-demo 0/1 4s 4s
137] Your CPU supports instructions that this TensorFlow binary was not com-
2019-08-06 07:52:21.475416: I
tensorflow/core/common_runtime/gpu/gpu_device.cc:
pciBusID: d0c5:00:00.0
2019-08-06 07:52:21.475459: I
tensorflow/core/common_runtime/gpu/gpu_device.cc:
Extracting /tmp/tensorflow/input_data/train-images-idx3-ubyte.gz
Extracting /tmp/tensorflow/input_data/train-labels-idx1-ubyte.gz
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
...
Teraz możesz sprawdzić, czy trenowanie modelu już się zakończyło. W tym celu wystarczy
spojrzeć na stan zadań.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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
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.
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
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: nfs
provisioner: cluster.local/nfs-client-provisioner
parameters:
archiveOnDelete: True
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.
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.
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.
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
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.
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).
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
Zapewnienie bezpieczeństwa
Zarządzanie zasobami
--enable-admission-plugins
Class,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWe
bho
ok,Priority,ResourceQuota,PodSecurityPolicy
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: ## Nazwa zasobu.
webhooks:
clientConfig:
service:
namespace: ## Przestrzeń nazw, w której znajduje się pod zaczepu
sieciowego dopuszczenia.
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:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
webhooks:
service:
rules: ## Opis operacji, które API serwera musi wykonywać w zasobach lub
podzasobach w danym ## zaczepie sieciowym.
- operations:
apiGroups:
- ""
apiVersions:
- "*"
resources:
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.
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.
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
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.
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:
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-config-
file=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ą.
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