Professional Documents
Culture Documents
Kubernetes. Tworzenie natywnych aplikacji - Stefan Schimanski
Kubernetes. Tworzenie natywnych aplikacji - Stefan Schimanski
Kubernetes. Tworzenie natywnych aplikacji - Stefan Schimanski
Kubernetes
Tworzenie natywnych
aplikacji działających w
chmurze
Tytuł oryginału: Programming Kubernetes: Developing Cloud-Native Applications
© 2020 Helion SA
Authorized Polish translation of the English edition of Programming Kubernetes ISBN
9781492047100 © 2019 Michael Hausenblas and Stefan Schimanski
This translation is published and sold by permission of O’Reilly Media, Inc., which owns or
controls all rights to publish and sell the same.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by
any means, electronic or mechanical, including photocopying, recording or by any information
storage retrieval system, without permission from the Publisher.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu
niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą
kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym
lub innym powoduje naruszenie praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi
ich właścicieli.
Autor oraz Helion SA dołożyli wszelkich starań, by zawarte w tej książce informacje były
kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani
za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Helion
SA nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z
wykorzystania informacji zawartych w książce.
HELION SA
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Poleć książkę
Kup w wersji papierowej
Oceń książkę
Księgarnia internetowa
Lubię to! » nasza społeczność
Opinie na temat książki Kubernetes.
Tworzenie natywnych aplikacji
działających w chmurze
Książka Kubernetes. Tworzenie natywnych aplikacji działających w chmurze uzupełnia lukę w
ekosystemie związanym z platformą Kubernetes. Dostępnych jest wiele książek i dokumentacja
na temat uruchamiania klastrów w tej platformie, jednak wciąż pracujemy nad
wyeliminowaniem braków w obszarze programowania w Kubernetesie. Ta książka to bardzo
potrzebny i dobrze napisany podręcznik poświęcony programowaniu z użyciem i dla platformy
Kubernetes.
— Bryan Liles, starszy inżynier, VMware
Chciałbym, aby istniała taka książka, gdy zaczynałem pisać kontrolery dla platformy
Kubernetes. Zawiera ona obszerne i szczegółowe omówienie interfejsu programowania i
działania platformy Kubernetes, a także pisania stabilnego oprogramowania.
— Michael Gasch, architekt platformy aplikacji w zespole dyrektora technicznego w VMware
Jest to lektura obowiązkowa, jeśli chcesz rozszerzać Kubernetesa.
— Dimitris-Ilias Gkanatsios, Technical Evangelist, Microsoft Grecja
Rozszerzanie Kubernetesa to jedyny sposób na instalowanie złożonych aplikacji i zarządzanie
cyklem ich życia. W tej książce pokazane jest, jak tworzyć własne zasoby w Kubernetesie i jak
rozszerzać API Kubernetesa.
— Ahmed Belgana, inżynier ds. chmury, SAP
Przedmowa
Witaj w książce Kubernetes. Tworzenie natywnych aplikacji działających w chmurze.
Dziękujemy, że zdecydowałeś się spędzić z nami trochę czasu. Zanim przejdziemy do
szczegółów, warto szybko opisać kilka zagadnień porządkowych i organizacyjnych. Wyjaśniamy
tu też, po co napisaliśmy tę książkę.
Po co napisaliśmy tę książkę?
Obaj od początku 2015 r. bierzemy udział w rozwijaniu Kubernetesa, piszemy na temat tej
platformy, uczymy o niej i korzystamy z niej. Opracowaliśmy narzędzia i aplikacje dla
Kubernetesa, a także kilkakrotnie prowadziliśmy warsztaty na temat programowania dla tej
platformy i z jej użyciem. W pewnym momencie stwierdziliśmy: „Dlaczego nie napisać książki?”.
Dzięki temu jeszcze więcej osób mogłoby uczyć się — jednocześnie i we własnym tempie —
programować z użyciem platformy Kubernetes. A więc jesteśmy. Mamy nadzieję, że lektura tej
książki przyniesie Ci tyle samo przyjemności co nam jej pisanie.
Ekosystem
W ogólnym ujęciu ekosystem Kubernetesa jest jeszcze we wczesnej fazie rozwoju. Choć na
początku 2018 r. Kubernetes zyskał reputację standardowego narzędzia do zarządzania
kontenerami (i cyklem ich życia), nadal potrzebne są dobre praktyki pisania aplikacji
natywnych. Podstawowe cegiełki, takie jak klient client-go (http://bit.ly/2L5cUMu),
niestandardowe zasoby i natywne języki programowania dla chmury, są już gotowe. Jednak
duża część informacji ma postać wiedzy plemiennej rozproszonej po umysłach ludzi oraz
tysiącach kanałów w komunikatorze Slack i odpowiedzi w serwisie StackOverflow.
Zarządzanie pakietami
Narzędzia w tej książce często mają wiele zależności, które musisz uwzględnić, instalując
określone pakiety. Dlatego musisz znać system zarządzania pakietami używany w Twoim
komputerze. Może to być apt w systemach Ubuntu i Debian, yum w systemach CentOS i
RHEL, a port lub brew w systemach macOS. Niezależnie od używanego systemu upewnij
się, że wiesz, jak instalować, aktualizować i usuwać pakiety.
System Git
Git stał się standardowym narzędziem do kontroli wersji w środowisku rozproszonym. Jeśli
znasz już systemy CVS i SVN, ale nie używałeś Gita, powinieneś zapoznać się z tym
[1]
ostatnim. Dobrym punktem wyjścia będzie książka Version Control with Git Jona
Loeligera i Matthew McCullougha (wydawnictwo O’Reilly). Poza systemem Git znakomitym
narzędziem, które pomoże Ci tworzyć własne hostowane repozytoria, jest serwis GitHub
(http://github.com). Aby zapoznać się z serwisem GitHub, sprawdź oferowane w nim
szkolenia (https://services.github.com) i interaktywny samouczek (http://try.github.io).
Język Go
Kursywa
Oznacza nowe pojęcia, adresy URL, adresy e-mail, nazwy plików i rozszerzenia plików.
Stosowana dla instrukcji i innego tekstu, który powinien zostać wpisany bez modyfikacji
przez użytkownika.
Kursywa o stałej szerokości
Przeznaczona dla tekstu, który należy zastąpić wartościami podanymi przez użytkownika
lub wynikającymi z kontekstu.
Jeśli w książce znajduje się przykładowy kod, zwykle możesz wykorzystać go we własnych
programach i dokumentacji. Nie musisz prosić nas o pozwolenie, chyba że kopiujesz duże
części kodu. Na przykład napisanie programu z wykorzystaniem kilku fragmentów kodu z tej
książki nie wymaga pozwolenia. Jednak wymaga go sprzedaż lub dystrybucja płyty CD-ROM z
przykładami z książek wydawnictwa O’Reilly. Udzielenie odpowiedzi za pomocą cytatu z tekstu i
przykładowego kodu z książki nie wymaga pozwolenia, natomiast jest ono niezbędne przy
umieszczaniu dużych fragmentów kodu w dokumentacji produktów.
Będzie nam miło, gdy podasz tę książkę jako źródło informacji, nie jest to jednak konieczne.
Przy podawaniu źródła zwykle określa się tytuł, autora, wydawnictwo i numer ISBN. Oto
przykład: Kubernetes. Tworzenie natywnych aplikacji działających w chmurze, Michael
Hausenblas i Stefan Schimanski, O’Reilly, prawa autorskie 2019 Michael Hausenblas i Stefan
Schimanski.
Jeśli sądzisz, że planowany przez Ciebie sposób wykorzystania kodu wykracza poza zasady
dozwolonego użytku lub przedstawione tu uprawnienia, skontaktuj się z wydawnictwem
O’Reilly. Jego adres to permissions@oreilly.com.
Pliki manifestów Kubernetesa, przykładowy kod i inne skrypty używane w tej książce są
dostępne w serwisie GitHub (https://github.com/programming-kubernetes). Możesz sklonować
dostępne tam repozytoria, przejść do odpowiedniego rozdziału i receptury oraz zastosować
gotowy kod.
Podziękowania
Wielkie podziękowania składamy społeczności skupionej wokół Kubernetesa za opracowanie tak
fantastycznego oprogramowania i za bycie wspaniałymi ludźmi — otwartymi, uprzejmymi i
zawsze gotowymi do niesienia pomocy. Ponadto jesteśmy bardzo wdzięczni recenzentom
technicznym. Oto oni: Ahmed Belgana, Michael Gasch, Dimitris Gkanatsios, Mingding Han, Jess
Males, Max Neunhöffer, Ewout Prangsma i Adrien Trouillaud. Każdy z nich przekazał nam
bardzo cenne i praktyczne informacje oraz sprawił, że książka stała się bardziej zrozumiała i
przydatna dla czytelników. Dziękujemy za wysiłek i poświęcony czas.
Michael chce wyrazić swoją dogłębną wdzięczność swojej cudownej i wspierającej rodzinie:
niezwykle inteligentnej i zabawnej żonie, Anneliese, dzieciom: Saphirze, Ranji i Iannisowi, a
także psu Snoopy'emu, który jeszcze nie do końca przestał być szczeniakiem.
Stefan chce podziękować żonie, Clelii, za niezwykłe wsparcie i zachęty, gdy znów „pracował
nad książką”. Bez niej ta książka by nie powstała. Jeśli w tekście znajdziesz literówki, ich
dumnymi autorami są z dużym prawdopodobieństwem dwa koty: Nino i Kira.
1 Wyd. polskie: Kontrola wersji z systemem Git, Helion, 2014 — przyp. tłum.
Rozdział 1. Wprowadzenie
Programowanie w Kubernetesie może oznaczać dla różnych osób co innego. W tym rozdziale
najpierw określimy zakres i tematykę tej książki. Ponadto przedstawimy założenia dotyczące
używanego środowiska i wyjaśnimy, czego będziesz potrzebować, aby optymalnie wykorzystać
tę książkę. Zdefiniujemy, co rozumiemy przez programowanie dla Kubernetesa, czym są
natywne aplikacje dla Kubernetesa, a także — posługując się konkretnym przykładem — jakie
są ich cechy. Omówimy podstawy kontrolerów i operatorów oraz wyjaśnimy, jak działa
sterowana zdarzeniami warstwa kontroli w Kubernetesie. Gotowy? Zaczynajmy.
W tej książce „programowanie dla Kubernetesa” oznacza pisanie natywnych aplikacji dla
platformy Kubernetes, które bezpośrednio komunikują się z serwerem API, kierując zapytania o
stan zasobów i/lub aktualizując go. Nie mamy tu na myśli uruchamiania gotowych aplikacji
takich jak WordPress, Rocket Chat lub Twój ulubiony system CRM dla przedsiębiorstw (takie
narzędzia czasem są nazywane aplikacjami COTS — ang. commercially available off-the-shelf).
Ponadto w rozdziale 7. skupiamy się nie na kwestiach operacyjnych, ale głównie na etapie
pisania i testowania kodu. Tak więc w skrócie można stwierdzić, że ta książka dotyczy pisania
natywnych aplikacji działających w chmurze. Rysunek 1.1 może pomóc Ci lepiej to zrozumieć.
1. Możesz użyć aplikacji COTS, np. Rocket Chat, i uruchomić ją w Kubernetesie. Sama
aplikacja nie wie, że działa w Kubernetesie, i zwykle nie musi tego wiedzieć. To
Kubernetes kontroluje cykl życia aplikacji — znajduje węzeł, gdzie aplikacja jest
uruchamiana, pobiera obraz, uruchamia kontenery, sprawdza stan, montuje woluminy itd.
2. Możesz uruchomić w Kubernetesie niestandardową aplikację, którą napisałeś od podstaw.
Rozwijając ją, mogłeś myśleć o uruchamianiu jej w Kubernetesie, ale nie jest to konieczne.
Sytuacja wygląda tu podobnie jak dla aplikacji COTS.
3. W tej książce skupiamy się na natywnych aplikacjach dla chmury lub Kubernetesa, które
są w pełni świadome tego, że działają w Kubernetesie, a także wykorzystują w jakimś
stopniu API oraz zasoby Kubernetesa.
Przykład wprowadzający
Aby zademonstrować możliwości natywnych aplikacji dla Kubernetesa, załóżmy, że chcesz
zaimplementować narzędzie at, pozwalające zaplanować wykonanie polecenia w określonym
czasie (http://bit.ly/2L4VqzU).
Nowe narzędzie nazwiemy cnat (http://bit.ly/2RpHhON) (od ang. cloud-native at). Będzie ono
działać w następujący sposób: załóżmy, że chcesz wykonać polecenie echo „Natywne
aplikacje dla Kubernetesa są super!” o 2:00 w nocy 3 lipca 2020 r. W narzędziu cnat
można to będzie zrobić tak:
$ cat cnat-rocks-example.yaml
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
name: cnrex
spec:
schedule: “2019-07-03T02:00:00Z”
containers:
- name: shell
image: centos:7
command:
- “bin/bash”
- “-c”
Wzorce rozszerzania
Kubernetes to dający duże możliwości i z natury rozszerzalny system. Istnieje wiele sposobów
na modyfikowanie i/lub rozszerzanie Kubernetesa, na przykład używanie plików
konfiguracyjnych i opcji (http://bit.ly/2KteqbA) dla komponentów kontrolnych takich jak
kubelet lub serwer API Kubernetesa, a także stosowanie wielu zdefiniowanych punktów
rozszerzeń, którymi są:
Po zapoznaniu się z podstawami wzorców rozszerzeń Kubernetesa i zakresem tej książki pora
przejść do istoty mechanizmów kontroli Kubernetesa i ich rozszerzania.
Kontrolery i operatory
W tym podrozdziale poznasz kontrolery i operatory Kubernetesa oraz dowiesz się, jak działają.
Zgodnie ze słowniczkiem pojęć związanych z Kubernetesem (http://bit.ly/2IWGlxz) kontroler
zawiera pętlę sterowania. Obserwuje współdzielony stan klastra za pomocą serwera API i
wprowadza zmiany, próbując przejść od bieżącego stanu do stanu docelowego.
Kontrolery mogą operować podstawowymi zasobami, takimi jak instalacje lub usługi,
które zwykle są częścią menedżera kontrolera Kubernetesa (http://bit.ly/2WUAEVy) w
warstwie kontroli. Mogą też obserwować niestandardowe zasoby zdefiniowane przez
użytkownika i operować nimi.
Operatory to kontrolery obejmujące wiedzę operacyjną, np. z zakresu zarządzania cyklem
życia aplikacji, a także niestandardowe zasoby (zdefiniowane w rozdziale 4.).
Naturalne jest to, że ponieważ drugie z tych pojęć jest oparte na pierwszym, najpierw omówimy
kontrolery, a następnie opiszemy bardziej wyspecjalizowane operatory.
Pętla sterowania
Pętla sterowania na ogólnym poziomie wygląda tak:
Niezależnie od tego, jak skomplikowany lub prosty jest kontroler, trzy wymienione tu kroki
(wczytywanie stanu zasobów > modyfikowanie świata > aktualizowanie stanu zasobów)
pozostają takie same. Przyjrzyjmy się bliżej temu, jak te kroki są realizowane w kontrolerze w
Kubernetesie. Pętla sterowania jest pokazana na rysunku 1.2, gdzie widoczne są typowe
komponenty. Pośrodku znajduje się główna pętla kontrolera. Ta główna pętla działa nieustannie
w procesie kontrolera. Z kolei proces działa zwykle w podzie w klastrze.
Rysunek 1.2. Pętla sterowania w Kubernetesie
Jeśli chodzi o architekturę, kontroler zwykle używa następujących struktur danych (opisanych
szczegółowo w rozdziale 3.).
Informatory
Kolejki zadań
Kolejka zadań to komponent, który może być używany przez mechanizm obsługi zdarzeń do
obsługi kolejkowania zmian stanu i do ponawiania prób. W narzędziu client-go ten
mechanizm jest dostępny w pakiecie workqueue (http://bit.ly/2x7zyeK; zob. punkt „Kolejka
zadań”). Zasoby można ponownie umieszczać w kolejce po wystąpieniu błędów w trakcie
aktualizowania świata zewnętrznego lub zapisywania stanu (kroki 2. i 3. w pętli) lub gdy z
innych powodów trzeba ponownie przetworzyć zasób po upływie jakiegoś czasu.
Zdarzenia
Warstwa kontroli w Kubernetesie jest w dużym stopniu oparta na zdarzeniach i zasadzie
luźnego powiązania komponentów. W innych systemach rozproszonych do uruchamiania
operacji są używane wywołania RPC. W Kubernetesie jest inaczej. Kontrolery Kubernetesa
obserwują zmiany w obiektach Kubernetesa na serwerze API: operacje dodawania,
aktualizowania i usuwania. Gdy wystąpi takie zdarzenie, kontroler wykonuje logikę biznesową.
Na przykład aby uruchomić pod w instalacji, potrzebne jest współdziałanie wielu kontrolerów i
innych komponentów z warstwy kontroli:
Widać tu, że liczne niezależne pętle sterowania komunikują się wyłącznie dzięki modyfikacjom
obiektów na serwerze API i zdarzeń wywoływanych przez te zmiany za pomocą informatorów.
Zdarzenia są wysyłane z serwera API do informatorów w kontrolerach przy użyciu czujek (zob.
punkt „Czujki”), a dokładnie zdarzeń czujek przesyłanych połączeniami strumieniowymi.
Większość tych operacji jest niewidoczna dla użytkowników. Nawet mechanizm audytu serwera
API nie uwidacznia takich zdarzeń. Widoczne są tylko aktualizacje obiektów. Jednak kontrolery
często korzystają z danych z dziennika, gdy mają reagować na zdarzenia.
Jeśli chcesz dowiedzieć się więcej na temat zdarzeń, zapoznaj się z artykułem „Events, the DNA
of Kubernetes” (http://bit.ly/2MZwbl6) z bloga Michaela Gascha. Znajdziesz tam więcej
informacji i przykładów.
3m 3m 1 kube-controller-manager-
master.15932b6faba8e5ad Pod
3m 3m 1 kube-apiserver-master.15932b6fa3f3fbbc
Pod
3m 3m 1 etcd-master.15932b6fa8a9a776
Pod
…
2m 3m 2 weave-net-7nvnf.15932b73e61f5bc6
Pod
2m 3m 2 weave-net-7nvnf.15932b73efeec0b3
Pod
2m 3m 2 weave-net-7nvnf.15932b73e8f7d318
Pod
1. Przykład logiki sterowanej tylko zmianami, gdzie utracona może zostać druga zmiana
stanu.
2. Przykład logiki wyzwalanej zmianami, gdzie jednak przy przetwarzaniu zdarzenia zawsze
pobierany jest najnowszy stan (czyli poziom). Oznacza to, że logika jest wyzwalana
zmianami, ale sterowana poziomem.
3. Przykład logiki wyzwalanej zmianami i sterowanej poziomem z dodatkową
resynchronizacją.
W strategii nr 3 dodana została ciągła resynchronizacja (np. co pięć minut). Jeśli nie zostaną
zgłoszone nowe zdarzenia dotyczące podów, system uzgodni stan przynajmniej co pięć minut,
nawet jeśli aplikacja działa bardzo stabilnie i nie generuje wielu zdarzeń dotyczących podów.
Jeśli chcesz dowiedzieć się czegoś więcej na temat źródeł wyzwalaczy oraz powodów
zastosowania w Kubernetesie wyzwalaczy opartych na poziomach i uzgadniania stanu, zapoznaj
się z artykułem „Level Triggering and Reconciliation in Kubernetes” (http://bit.ly/2FmLLAW)
Jamesa Bowesa.
utilruntime.HandleError(
fmt.Errorf(“Nie można pobrać klucza dla %v %#v: %v”, rsc.Kind,
rs, err),
)
return nil
}
if diff < 0 {
diff *= -1
if diff > rsc.burstReplicas {
diff = rsc.burstReplicas
}
rsc.expectations.ExpectCreations(rsKey, diff)
klog.V(2).Infof(“Za mało replik dla %v %s/%s, potrzebnych %d,
tworzenie %d”,
controller.SlowStartInitialBatchSize,
func() error {
ref := metav1.NewControllerRef(rs, rsc.GroupVersionKind)
err := rsc.podControl.CreatePodsWithControllerRef(
rs.Namespace, &rs.Spec.Template, rs, ref,
)
if err != nil && errors.IsTimeout(err) {
return nil
}
return err
},
)
}
}
return err
} else if diff > 0 {
if diff > rsc.burstReplicas {
diff = rsc.burstReplicas
}
wg.Add(diff)
for _, pod := range podsToDelete {
go func(targetPod *v1.Pod) {
defer wg.Done()
if err := rsc.podControl.DeletePod(
rs.Namespace,
targetPod.Name,
rs,
); err != nil {
podKey := controller.PodKey(targetPod)
klog.V(2).Infof(“Nieudane usuwanie %v, zmniejszanie “ +
“oczekiwań dla %v %s/%s”,
podKey, rsc.Kind, rs.Namespace, rs.Name,
)
rsc.expectations.DeletionObserved(rsKey, podKey)
}
wg.Wait()
select {
return err
}
default:
}
}
return nil
}
Jednak zmiana stanu zasobu nie musi oznaczać, że dany zasób jest częścią klastra Kubernetesa.
Kontroler może więc modyfikować stan zasobów zlokalizowanych poza Kubernetesem, np. w
usłudze przechowywania danych w chmurze. Na przykład narzędzie AWS Service Operator
(http://bit.ly/2ItJcif) pozwala zarządzać zasobami w usłudze AWS. Między innymi możliwe jest
zarządzanie komorami S3. Oznacza to, że kontroler S3 nadzoruje zasób (komorę S3), która
istnieje poza Kubernetesem, a zmiany stanu odzwierciedlają konkretne etapy cyklu życia:
komora S3 jest tworzona i w określonym momencie usuwana.
W skrócie oznacza to, że jeśli (i kiedy) serwer API wykryje próbę współbieżnego zapisu, odrzuca
późniejszą z dwóch operacji zapisu. Następnie to klient (kontroler, program szeregujący,
narzędzie kubectl itd.) musi obsłużyć konflikt i ewentualnie ponowić próbę operacji zapisu.
Poniżej zilustrowano działanie optymistycznej współbieżności w Kubernetesie:
var err error
for retries := 0; retries < 10; retries++ {
foo, err = client.Get(“foo”, metav1.GetOptions{})
if err != nil {
break
<aktualizowanie-świata-i-inne>
_, err = client.Update(foo)
}
}
W tym kodzie pokazana jest pętla z ponawianiem prób, która w każdej iteracji pobiera
najnowszy obiekt foo, a następnie próbuje zaktualizować świat zewnętrzny i stan obiektu foo
zgodnie ze specyfikacją tego obiektu. Zmiany przed wywołaniem Update są wprowadzane w
trybie optymistycznym.
Obiekt foo zwracany przez wywołanie client.Get zawiera wersję zasobu (część zagnieżdżonej
struktury ObjectMeta; szczegóły znajdziesz w punkcie „ObjectMeta”), informującą system etcd
związany z operacją zapisu z wywołania client.Update, że inna jednostka w klastrze w
międzyczasie zapisała obiekt foo. Jeśli taki inny zapis miał miejsce, w pętli z ponawianiem prób
występuje błąd konfliktu wersji zasobu. To oznacza, że logika optymistycznej współbieżności
zawiodła. Wywołanie client.Update jest więc optymistyczne.
Przyjrzyj się konkretnemu przykładowi. Wyobraź sobie, że Twój klient nie jest jedyną jednostką
w klastrze modyfikującą pod. Istnieje też inna jednostka, kubelet, która nieustannie modyfikuje
jakieś pola, ponieważ kontener stale ulega awarii. Twój kontroler wczytuje najnowszy stan
poda:
kind: Pod
metadata:
name: foo
resourceVersion: 57
spec:
...
status:
...
Teraz załóżmy, że kontroler potrzebuje kilku sekund na wprowadzenie zmian w świecie
zewnętrznym. Po siedmiu sekundach kontroler próbuje zaktualizować wczytany pod — np.
zapisuje w nim adnotacje. W tym czasie kubelet wykrywa restart innego kontenera i
aktualizuje stan poda, aby to odzwierciedlić. Oznacza to, że wartość pola resourceVersion
rośnie do 58.
Obiekt przesłany przez kontroler w żądaniu aktualizacji ma pole resourceVersion: 57. Serwer
API próbuje ustawić w systemie etcd klucz dla poda o tej wartości. System etcd wykrywa
niedopasowanie wersji zasobu i zgłasza konflikt wersji 57 i 58. Aktualizacja kończy się
niepowodzeniem.
Podsumowanie tego przykładu jest takie, że programista kontrolera odpowiada za
implementacją strategii ponawiania prób i uwzględniania niepowodzenia optymistycznych
operacji. Nigdy nie wiadomo, jakie jeszcze jednostki modyfikują stan. Mogą to być inne
kontrolery lub kontrolery podstawowe, np. kontroler instalacji.
Oto istota tego fragmentu: błędy spowodowane konfliktami są w kontrolerach czymś zupełnie
normalnym. Zawsze powinieneś się ich spodziewać i obsługiwać je w kontrolowany sposób.
Należy zwrócić uwagę na to, że współbieżność optymistyczna doskonale współdziała z logiką
opartą na poziomach, ponieważ dzięki temu można bez dodatkowych zabiegów ponownie
uruchomić pętlę sterowania (zob. punkt „Wyzwalacze sterowane zmianami i sterowane
poziomem”). W następnym przebiegu pętla automatycznie wycofa optymistyczne zmiany
wprowadzone w nieudanej optymistycznej próbie i spróbuje wprowadzić najnowszy stan w
systemie.
Przejdźmy teraz do konkretnego rodzaju niestandardowych kontrolerów (powiązanego z
niestandardowymi zasobami) — do operatorów.
Operatory
Operatory to mechanizm Kubernetesa wprowadzony przez firmę CoreOS w 2016 r. W swoim
przełomowym wpisie na blogu „Introducing Operators: Putting Operational Knowledge into
Software” (http://bit.ly/2ZC4Rui) Brandon Philips, dyrektor techniczny firmy CoreOS,
zdefiniował operatory tak:
Inżynier SRE to osoba, która operuje aplikacją, pisząc oprogramowanie. Jest to inżynier
(programista), który wie, jak pisać oprogramowanie w dziedzinie powiązanej z konkretną
aplikacją. W wynikowym oprogramowaniu wbudowana jest wiedza z dziedziny działania
aplikacji.
[…]
Tę nową kategorię oprogramowania nazywamy operatorami. Operator to specyficzny dla
aplikacji kontroler, który rozszerza API Kubernetesa, aby tworzyć, konfigurować i
obsługiwać instancje złożonych aplikacji stanowych na rzecz użytkownika Kubernetesa.
Operator jest oparty na podstawowych mechanizmach Kubernetesa, zasobach i
kontrolerach, ale uwzględnia dziedzinę lub wiedzę specyficzną dla aplikacji, aby
automatyzować powtarzalne zadania.
W tej książce używamy operatorów zgodnie z opisem Philipsa. W bardziej formalnym ujęciu
operatory muszą spełniać trzy warunki (zob. też rysunek 1.5):
Podsumowanie
W pierwszym rozdziale zdefiniowaliśmy zakres książki i to, czego od Ciebie oczekujemy.
Wyjaśniliśmy, co oznacza tu programowanie dla Kubernetesa. Zdefiniowaliśmy też, czym są
natywne aplikacje dla Kubernetesa. W ramach przygotowań do dalszych przykładów
przedstawiliśmy też ogólne wprowadzenie do kontrolerów i operatorów.
Teraz gdy wiesz już, czego możesz oczekiwać od tej książki i jakie korzyści może przynieść Ci
jej lektura, pora przejść do szczegółów. W następnym rozdziale przyjrzymy się API Kubernetesa,
wewnętrznym mechanizmom serwera API, a także interakcjom z tym API z użyciem narzędzi
wiersza poleceń takich jak curl.
1 Więcej na ten temat dowiesz się z tekstu Megan OʼKeefe: „A Kubernetes Developer Workflow
for MacOS” (http://bit.ly/2WXfzu1), Medium, 24 stycznia 2019, a także z artykułu z bloga
Aleksa Ellisa: „Be KinD to yourself” (http://bit.ly/2XkK9C1), 14 grudnia 2018.
2 Źródło: „Omega: Flexible, Scalable Schedulers for Large Compute Clusters”
(http://bit.ly/2PjYZ59), Malte Schwarzkopf i inni, Google AI, 2013.
Rozdział 2. Podstawy API
Kubernetesa
W tym rozdziale omawiamy podstawy API Kubernetesa. Między innymi szczegółowo opisujemy
wewnętrzne mechanizmy serwera API, samo API, a także interakcje z API z poziomu wiersza
poleceń. Przedstawiamy też zagadnienia związane z API Kubernetesa, np. zasoby i rodzaje (ang.
kinds), a także grupy i wersjonowanie.
Serwer API
Kubernetes obejmuje zestaw węzłów (maszyn w klastrze) pełniących różne role. Ilustruje to
rysunek 2.1. Warstwa kontrolna w węźle nadrzędnym składa się z serwera API, menedżera
kontrolera i programu szeregującego. Serwer API to centralna jednostka zarządzająca i jedyny
komponent, który bezpośrednio komunikuje się z systemem etcd odpowiadającym za
składowanie danych w środowisku rozproszonym.
Obsługa API Kubernetesa. To API jest używane wewnątrz klastra przez komponenty
nadrzędne, węzły robocze i natywne aplikacje dla Kubernetesa. Z API korzystają też
zewnętrzne klienty, takie jak kubectl.
Pełnienie roli pośrednika dla komponentów klastra (takich jak panel kontrolny
Kubernetesa), strumieniowe przesyłanie dzienników, obsługa portów i udostępnianie sesji
kubectl exec.
Metoda HTTP GET służy do pobierania danych z określonego zasobu (np. danego poda) lub
z kolekcji zasobów (np. wszystkich podów z danej przestrzeni nazw).
Metoda HTTP POST jest przeznaczona do tworzenia zasobów, np. usług lub instalacji.
Metoda HTTP PUT służy do aktualizowania istniejących zasobów, np. do zmiany obrazu
kontenera w podzie.
Metoda HTTP PATCH wprowadza częściowe aktualizacje istniejących zasobów. Zapoznaj
się z punktem „Use a JSON merge patch to update a Deployment” (http://bit.ly/2Xpbi6I) w
dokumentacji Kubernetesa, aby dowiedzieć się więcej na temat dostępnych strategii i
skutków korzystania z nich.
Metoda HTTP DELETE służy do usuwania zasobów w nieodwracalny sposób.
Jeśli przyjrzysz się np. dokumentacji API Kubernetesa 1.14 (http://bit.ly/2IVevBG), znajdziesz
przykłady używania różnych metod HTTP. Na przykład aby wyświetlić pody z bieżącej
przestrzeni nazw (odpowiednik polecenia CLI kubectl -n PRZESTRZEŃ_NAZW get pods), należy
przesłać żądanie GET /api/v1/namespaces/PRZESTRZEŃ_NAZW/pods (rysunek 2.2).
Rysunek 2.2. Używanie interfejsu HTTP serwera API: wyświetlanie podów z określonej przestrzeni
nazw
Rodzaj jednostek. Każdy obiekt ma pole Kind (w formacie JSON zapisywane małą literą,
kind, w języku Go zapisywane jako Kind), informujące klienta takiego jak kubectl, że
obiekt reprezentuje np. pod. Są trzy kategorie rodzajów:
Obiekty reprezentują trwałą jednostkę w systemie, np. Pod lub Endpoints. Obiekty mają
nazwy, a wiele obiektów znajduje się w przestrzeniach nazw.
Listy, czyli kolekcje obejmujące jednostki jednego lub kilku rodzajów. Listy mają
ograniczony zbiór standardowych metadanych. Przykładowe listy to PodList i NodeList.
W odpowiedzi na wywołanie kubectl get pods dostajesz właśnie listy.
Rodzaje o specjalnym przeznaczeniu, używane dla określonych operacji na obiektach i dla
jednostek nietrwałych (takich jak /binding lub /scale). Na potrzeby wykrywania
informacji Kubernetes używa rodzajów APIGroup i APIResource. Dla wyników
oznaczających błąd używany jest rodzaj Status.
W programach dla Kubernetesa rodzaj bezpośrednio odpowiada typowi z języka Go. Dlatego
rodzaje, podobnie jak typy w języku Go, mają nazwy w liczbie pojedynczej rozpoczynające się
wielką literą.
Grupa API
Jest to kolekcja logicznie powiązanych rodzajów. Na przykład wszystkie obiekty wsadowe,
np. Job lub ScheduledJob, znajdują się w grupie API obiektów wsadowych.
Wersja
Każda grupa API może mieć (i zwykle ma) wiele wersji. Na przykład grupa najpierw
pojawia się w wersji v1alpha1, następnie jest promowana do wersji v1beta1, a ostatecznie
do wersji v1. Obiekt utworzony w jednej wersji (np. v1beta1) można pobrać w każdej z
obsługiwanych wersji. Serwer API przeprowadza bezstratną konwersję, aby zwracać
obiekty w żądanej wersji. Z perspektywy użytkownika klastra wersje to różne reprezentacje
tych samych obiektów.
Nie można powiedzieć, że „jeden obiekt w klastrze jest w wersji v1, a inny w
wersji v1beta1”. Zamiast tego każdy obiekt można zwrócić jako jego
reprezentację w wersji v1 lub v1beta1, zgodnie z żądaniem użytkownika
klastra.
Zasób
Zwykle zapisywany jest w liczbie mnogiej małą literą (np. pods) i oznacza zestaw punktów
końcowych HTTP (ścieżek) udostępniający operacje CRUD (ang. create, read, update,
delete, czyli tworzenie, wczytywanie, aktualizowanie, usuwanie) dotyczące obiektów
określonego typu w systemie. Często używane ścieżki to:
Każdy punkt końcowy zwykle zwraca i pobiera jednostki jednego rodzaju (PodList w
pierwszym przypadku i Pod w drugim). Jednak w innych sytuacjach (np. po błędach)
zwracany jest obiekt rodzaju Status.
Zasoby zawsze są powiązane z grupą API i wersją (razem tworzą identyfikator GVR — ang.
GroupVersionResource). Identyfikator GVR jednoznacznie definiuje ścieżkę HTTP. Konkretna
ścieżka, np. w przestrzeni nazw default, to /apis/batch/v1/namespaces/default/jobs. Na
rysunku 2.3 pokazany jest przykładowy identyfikator GVR dla zasobu Job z określonej
przestrzeni nazw.
Rysunek 2.3. API Kubernetesa — identyfikator GVR
W odróżnieniu od przykładowego identyfikatora GVR jobs zasoby z poziomu klastra, np. węzły i
przestrzenie nazw, nie mają w ścieżce członu $PRZESTRZEŃNAZW. Na przykład identyfikator GVR
dla zasobu nodes może wyglądać tak: /api/v1/nodes. Warto zauważyć, że przestrzenie nazw
pojawiają się w ścieżkach HTTP innych zasobów, jednak same też są zasobem (dostępnym za
pomocą ścieżki /api/v1/namespaces).
Podobnie jak zasoby mają identyfikatory GVR, tak wszystkie rodzaje mają określoną grupę API
oraz wersję i są identyfikowane za pomocą identyfikatorów GVK (ang. GroupVersionKind).
W kontekście globalnym przestrzeń zasobów API tworzy logicznie drzewo, w którym węzły
najwyższego poziomu to /api, /apis i niehierarchiczne punkty końcowe takie jak /healthz lub
/metrics. Przykładową grafikę ilustrującą przestrzeń zasobów API znajdziesz na rysunku 2.4.
Warto zauważyć, że kształt tej przestrzeni i ścieżki zależą od wersji Kubernetesa (choć z
upływem lat następuje stabilizacja).
Rysunek 2.4. Przykładowa przestrzeń API Kubernetesa
Poziom alfa (np. v1alpha1) jest zwykle domyślnie nieaktywny. Obsługa funkcji może
zostać wyłączona w dowolnym momencie bez ostrzeżenia. Takie wersje należy stosować
tylko w tymczasowych klastrach testowych.
Poziom beta (np. v2beta3) jest domyślnie aktywny, co oznacza, że kod jest dobrze
przetestowany. Jednak znaczenie obiektów może się zmienić, tak że staną się one
niekompatybilne z obiektami z późniejszych wersji beta i z wersji stabilnych.
Poziom stabilny (wersja ogólnie dostępna, np. v1) jest używany dla udostępnianego
oprogramowania przez wiele kolejnych wersji.
Przyjrzyj się teraz budowie przestrzeni API HTTP. Na najwyższym poziomie występuje podział
na grupę podstawową (wszystko w ścieżce /api/v1) i grupy nazwane w ścieżkach w formacie
/apis/$NAZWA/$WERSJA.
Istnieje też trzeci typ ścieżek HTTP udostępnianych przez serwer API. Nie są one ściśle
powiązane z zasobami i dotyczą jednostek dostępnych na poziomie klastra. Te ścieżki to np.
/metrics, /logs i /healthz. Oprócz tego serwer API obsługuje czujki. Oznacza to, że zamiast
odpytywać zasoby w ustalonych odstępach czasu, można dodać do określonych żądań człon ?
watch=true, a serwer API przejdzie w tryb używania czujek (http://bit.ly/2x5PnTl).
Specyfikacja opisuje oczekiwany stan zasobu, czyli coś, co należy podać za pomocą narzędzia
wiersza poleceń (np. kubectl) lub programowo w kodzie w języku Go. Status określa
zaobserwowany, aktualny stan zasobu i zależy od warstwy kontroli w postaci komponentów
podstawowych (takich jak menedżer kontrolera) lub własnego niestandardowego kontrolera
(zob. punkt „Kontrolery i operatory”). Na przykład w instalacji można określić, że chcesz, aby
zawsze działało 20 replik aplikacji. Kontroler instalacji (komponent menedżera kontrolera w
warstwie kontroli) wczytuje podaną przez Ciebie specyfikację instalacji i tworzy zbiór replik,
który następnie odpowiada za zarządzanie replikami — tworzy określoną liczbę podów, co
ostatecznie (z użyciem narzędzia kubelet) skutkuje uruchomieniem kontenerów w węzłach
roboczych. Jeśli któraś z replik ulegnie awarii, kontroler instalacji poinformuje o tym,
zmieniając status. Jest to deklaratywne zarządzanie stanem. Użytkownik deklaruje oczekiwany
stan, a Kubernetes zajmuje się pozostałymi zadaniami.
Z deklaratywnym zarządzaniem stanem w praktyce zapoznasz się w następnym podrozdziale,
gdy zaczniemy eksplorować API za pomocą wiersza poleceń.
kind: Deployment
metadata:
name: coredns
namespace: kube-system
...
spec:
template:
spec:
containers:
- name: coredns
image: 602401143452.dkr.ecr.us-east-
2.amazonaws.com/eks/coredns:v1.2.2
...
status:
replicas: 2
conditions:
- type: Available
status: “True”
lastUpdateTime: “2019-04-01T16:42:10Z”
...
W tym poleceniu kubectl widać, że w sekcji spec instalacji zdefiniowane są aspekty takie jak
używany obraz kontenera i liczba równolegle uruchamianych replik. Z kolei z sekcji status
można się dowiedzieć, ile replik działa w danym momencie.
$ curl http://127.0.0.1:8080/apis/batch/v1
{
“kind”: “APIResourceList”,
“apiVersion”: “v1”,
“groupVersion”: “batch/v1”,
“resources”: [
{
“name”: “jobs”,
“singularName”: “”,
“namespaced”: true,
“kind”: “Job”,
“verbs”: [
“create”,
“delete”,
“deletecollection”,
“get”,
“list”,
“patch”,
“update”,
“watch”
],
“categories”: [
“all”
]
},
{
“name”: “jobs/status”,
“singularName”: “”,
“namespaced”: true,
“kind”: “Job”,
“verbs”: [
“get”,
“patch”,
“update”
]
}
]
Nie musisz używać narzędzia curl i polecenia kubectl proxy, aby uzyskać
bezpośredni dostęp do API Kubernetesa za pomocą HTTP. Zamiast tego możesz
zastosować polecenie kubectl get --raw. Zastąp np. instrukcję curl
http://127.0.0.1:8080/apis/batch/v1 poleceniem kubectl get --raw
/apis/batch/v1.
Porównaj to z wersją v1beta1. Zauważ, że możesz pobrać listę obsługiwanych wersji dla grupy
API batch, używając ścieżki http://127.0.0.1:8080/apis/batch. A oto informacje o wersji
v1beta1:
$ curl http://127.0.0.1:8080/apis/batch/v1beta1
{
“kind”: “APIResourceList”,
“apiVersion”: “v1”,
“groupVersion”: “batch/v1beta1”,
“resources”: [
{
“name”: “cronjobs”,
“singularName”: “”,
“namespaced”: true,
“kind”: “CronJob”,
“verbs”: [
“create”,
“delete”,
“deletecollection”,
“get”,
“list”,
“patch”,
“update”,
“watch”
],
“shortNames”: [
“cj”
],
“categories”: [
“all”
]
},
{
“name”: “cronjobs/status”,
“singularName”: “”,
“namespaced”: true,
“kind”: “CronJob”,
“verbs”: [
“get”,
“patch”,
“update”
]
}
]
}
Widać tu, że wersja v1beta1 zawiera zasób cronjobs rodzaju CronJob. W czasie, gdy powstaje
ta książka, zadania crona nie zostały jeszcze promowane do wersji v1.
Jeśli chcesz wiedzieć, jakie zasoby API są obsługiwane w Twoim klastrze, a także poznać ich
rodzaje, ustalić, czy znajdują się w przestrzeni nazw, oraz poznać ich skrócone nazwy (głównie
do użytku z narzędziem kubectl w wierszu poleceń), możesz posłużyć się następującą
instrukcją:
$ kubectl api-resources
replicationcontrollers rc true
ReplicationController
resourcequotas quota true ResourceQuota
...
Poniżej pokazane jest powiązane polecenie, które może być bardzo przydatne przy ustalaniu
różnych wersji zasobu obsługiwanych w danym klastrze:
$ kubectl api-versions
admissionregistration.k8s.io/v1beta1
apiextensions.k8s.io/v1beta1
apiregistration.k8s.io/v1
apiregistration.k8s.io/v1beta1
appmesh.k8s.aws/v1alpha1
appmesh.k8s.aws/v1beta1
apps/v1
apps/v1beta1
apps/v1beta2
authentication.k8s.io/v1
authentication.k8s.io/v1beta1
authorization.k8s.io/v1
authorization.k8s.io/v1beta1
autoscaling/v1
autoscaling/v2beta1
autoscaling/v2beta2
batch/v1
batch/v1beta1
certificates.k8s.io/v1beta1
coordination.k8s.io/v1beta1
crd.k8s.amazonaws.com/v1alpha1
events.k8s.io/v1beta1
extensions/v1beta1
networking.k8s.io/v1
policy/v1beta1
rbac.authorization.k8s.io/v1
rbac.authorization.k8s.io/v1beta1
scheduling.k8s.io/v1beta1
storage.k8s.io/v1
storage.k8s.io/v1beta1
v1
Co się dzieje, gdy żądanie HTTP trafia do API Kubernetesa? Na ogólnym poziomie zachodzą
następujące interakcje:
h = WithTimeoutForNonLongRunningRequests(h, LongRunningFunc,
RequestTimeout)
h = WithWaitGroup(h, c.LongRunningFunc, c.HandlerChainWaitGroup)
h = WithRequestInfo(h, c.RequestInfoResolver)
h = WithPanicRecovery(h)
return h
}
Wszystkie potrzebne pakiety znajdują się w katalogu k8s.io/apiserver/pkg
(http://bit.ly/2LUzTdx). Oto opis wybranych metod:
WithPanicRecovery()
Odpowiada za przywracanie stanu i rejestrowanie paniki. Zdefiniowana w pliku
server/filters/wrap.go (http://bit.ly/2N0zfNB).
WithRequestInfo()
Dołącza wartość typu RequestInfo do kontekstu. Zdefiniowana w pliku
endpoints/filters/requestinfo.go (http://bit.ly/2KvKjQH).
WithWaitGroup()
Dodaje wszystkie krótkie żądania do grupy oczekujących. Używana do kontrolowanego
zamykania programu. Zdefiniowana w pliku server/filters/waitgroup.go
(http://bit.ly/2ItnsD6).
WithTimeoutForNonLongRunningRequests()
Zgłasza przekroczenie limitu czasu krótkotrwałych żądań (jest to większość żądań GET, PUT,
POST i DELETE), różniących się od długotrwałych, np. związanych z czujkami i pośrednikami.
Zdefiniowana w pliku server/filters/timeout.go (http://bit.ly/2KrKk8r).
WithCORS()
Zapewnia implementację mechanizmu CORS (https://enable-cors.org). CORS (ang. cross-
origin resource sharing) to mechanizm umożliwiający kodowi w JavaScripcie
zagnieżdżonemu na stronie HTML zgłaszanie żądań XMLHttpRequest do domen innych niż
źródłowa domena tego kodu. Ta metoda jest zdefiniowana w pliku server/filters/cors.go
(http://bit.ly/2L2A6uJ).
WithAuthentication()
Próbuje uwierzytelnić dane żądanie (jako użytkownik ludzki lub maszyna) i zapisuje
informacje o użytkowniku w podanym kontekście. Po powodzeniu z żądania usuwany jest
nagłówek HTTP Authorization. Jeśli uwierzytelnianie się nie powiedzie, zwracany jest kod
HTTP 401. Metoda jest zdefiniowana w pliku endpoints/filters/authentication.go
(http://bit.ly/2Fjzr4b).
WithAudit()
Dodaje do funkcji obsługi żądania informacje z dziennika kontroli dla wszystkich
przychodzących żądań. Wpisy z dziennika kontroli obejmują informacje takie jak źródłowy
adres IP żądania, użytkownik wywołujący operację i przestrzeń nazw żądania. Metoda
zdefiniowana jest w pliku admission/audit.go (http://bit.ly/2XpQN9U).
WithImpersonation()
Kontrolę dostępu
Przychodzące obiekty przechodzą przez łańcuch kontroli dostępu. Obejmuje on ok. 20
[1]
różnych wtyczek . Każda wtyczka może być używana na etapie modyfikacji (trzecie
pole na rysunku 2.5), na etapie sprawdzania poprawności (czwarte pole na rysunku)
lub na obu tych etapach.
Na etapie modyfikacji zmieniana może być treść przychodzącego żądania. Na przykład
polityka pobierania obrazów w zależności od konfiguracji procesu kontroli dostępu ma
wartość Always, IfNotPresent lub Never.
Drugi etap kontroli dostępu służy wyłącznie do sprawdzania poprawności. Sprawdzane
są np. ustawienia zabezpieczeń w podach lub dostępność przestrzeni nazw przed
utworzeniem obiektów w tej przestrzeni.
Sprawdzanie poprawności
Przychodzące obiekty są sprawdzane za pomocą rozbudowanej logiki dostępnej dla
wszystkich typów obiektów z systemu. Sprawdzane są np. formaty łańcuchów znaków
(aby mieć pewność, że w nazwach usług używane są tylko poprawne znaki zgodne z
nazwami DNS) lub to, czy wszystkie nazwy kontenerów w podzie są unikatowe.
Podsumowanie
W tym rozdziale najpierw opisaliśmy serwer API Kubernetesa jako czarną skrzynkę i
przyjrzeliśmy się jego interfejsowi HTTP. Dalej dowiedziałeś się, jak komunikować się z tą
czarną skrzynką za pomocą wiersza poleceń. Na koniec otworzyliśmy tę czarną skrzynkę i
zbadaliśmy jej wewnętrzne mechanizmy. Na tym etapie powinieneś już wiedzieć, jak działają
wewnętrzne mechanizmy serwera API i jak komunikować się z nim za pomocą narzędzia
wiersza poleceń kubectl, aby badać zasoby i operować nimi.
Repozytoria
Projekt Kubernetes obejmuje w repozytorium kubernetes w serwisie GitHub wiele repozytoriów systemu Git
dostępnych do użytku przez niezależnych programistów. Musisz zaimportować je do projektu, używając aliasu
domeny k8s.io/… (zamiast nazwy github.com/kubernetes/…). W kolejnych punktach omawiamy najważniejsze z
dostępnych repozytoriów.
Biblioteka klienta
Interfejs programowania Kubernetesa w języku Go składa się przede wszystkim z biblioteki k8s.io/client-go (aby
zachować zwięzłość, dalej jest ona nazywana client-go). Jest to typowa biblioteka klienta usług sieciowych
obsługująca wszystkie typy API oficjalnie dostępne w Kubernetesie. Można jej używać do wykonywania
standardowych operacji REST:
CREATE,
GET,
LIST,
UPDATE,
DELETE,
PATCH.
Wszystkie te operacje REST są zaimplementowane z użyciem interfejsu HTTP serwera API (zob. „Interfejs HTTP
serwera API”). Dostępna jest też operacja Watch, przeznaczona specjalnie dla API podobnych do API
Kubernetesa. Jest ona jedną z głównych różnic w porównaniu z innymi interfejsami API.
Biblioteka client-go (http://bit.ly/2RryyLM) jest dostępna w serwisie GitHub (zob. rysunek 3.1) i używana w
kodzie w języku Go za pomocą pakietu k8s.io/client-go. Jest udostępniana równolegle z wersjami Kubernetesa —
dla każdej wersji 1.x.y Kubernetesa istnieje wersja biblioteki client-go z analogicznym oznaczeniem
kubernetes-1.x.y.
Rysunek 3.1. Repozytorium client-go w serwisie GitHub
Ponadto istnieje schemat wersjonowania semantycznego. Na przykład wersja 9.0.0 biblioteki client-go jest
powiązana z wersją 1.12 Kubernetesa, wersja 10.0.0 biblioteki client-go jest powiązana z wersją 1.13
Kubernetesa itd. W przyszłości mogą pojawić się bardziej szczegółowo numerowane podwersje. Obok kodu
klienckiego dla obiektów API Kubernetesa biblioteka client-go zawiera też rozbudowany ogólny kod biblioteki.
Jest on używany także dla obiektów API zdefiniowanych przez użytkownika (zob. rozdział 4.). Na rysunku 3.1
pokazana jest lista dostępnych pakietów.
Choć wszystkie pakiety mają swoje zastosowania, większość kodu komunikująca się z API Kubernetesa używa
pakietu tools/clientcmd/ do konfigurowania klienta na podstawie pliku kubeconfig i pakietu kubernetes/ na
potrzeby samych klientów API Kubernetesa. Potrzebny kod zostanie przedstawiony wkrótce. Najpierw jednak
zakończmy ten krótki przegląd, omawiając inne ważne repozytoria i pakiety.
Typy języka Go są umieszczone w plikach types.go (np. k8s.io/api/core/v1/types.go). Ponadto istnieją też inne
pliki. Większość z nich jest automatycznie tworzona przez generator kodu.
import (
metav1 “k8s.io/apimachinery/pkg/apis/meta/v1”
“k8s.io/client-go/tools/clientcmd”
“k8s.io/client-go/kubernetes”
Ten kod importuje pakiet meta/v1, aby uzyskać dostęp do metody metav1.GetOptions. Ponadto importowany
jest pakiet clientcmd z biblioteki client-go, aby móc wczytywać i przetwarzać plik kubeconfig (z konfiguracją
klienta obejmującą nazwę serwera, dane uwierzytelniające itd.). Następnie importowany jest pakiet kubernetes
z biblioteki client-go ze zbiorami klientów dla zasobów Kubernetesa.
Plik kubeconfig domyślnie znajduje się w katalogu .kube/config w katalogu głównym użytkownika. Z tego miejsca
narzędzie kubectl pobiera dane uwierzytelniające dla klastrów Kubernetesa.
Plik kubeconfig jest wczytywany i przetwarzany za pomocą metody clientcmd.BuildConfigFromFlags. W tym
kodzie pomijamy obowiązkową obsługę błędów, jednak zmienna err standardowo zawierałaby np. błąd składni,
gdyby plik kubeconfig miał niewłaściwą składnię. Ponieważ w kodzie w Go błędy składni są częste, należałoby
odpowiednio go wykrywać:
config, err := clientcmd.BuildConfigFromFlags(“”, *kubeconfig)
if err != nil {
fmt.Printf(“Nie można wczytać pliku kubeconfig: %v\n”, err
os.Exit(1)
}
Za pomocą funkcji clientcmd.BuildConfigFromFlags można pobrać wartość typu rest.Config (ten typ
dostępny jest w pakiecie k8s.io/client-go/rest). Ta wartość jest przekazywana do funkcji
kubernetes.NewForConfig, aby utworzyć zbiór klientów Kubernetesa. Nazwa zbiór klientów wynika z tego, że
obejmuje on różne klienty dla wszystkich natywnych zasobów Kubernetesa.
Gdy w podzie w klastrze uruchamiasz plik binarny, narzędzie kubelet automatycznie montuje konto usługi w
kontenerze, używając ścieżki /var/run/secrets/kubernetes.io/serviceaccount. To konto jest używane zamiast
wspomnianego wcześniej pliku kubeconfig i może zostać łatwo przekształcone w wartość typu rest.Config za
pomocą metody rest.InClusterConfig(). Często spotykane jest połączenie wywołań rest.InClusterConfig()
i clientcmd.BuildConfigFromFlags() oraz korzystania ze zmiennej środowiskowej KUBECONFIG:
config, err := rest.InClusterConfig()
if err != nil {
// Rozwiązanie rezerwowe — użycie pliku kubeconfig.
kubeconfig = envvar
}
os.Exit(1)
}
}
W kolejnym przykładzie pobierana jest grupa podstawowa z wersji v1 (wywołanie clientset.Corev1()), a
następnie kod uzyskuje dostęp do poda „example” z przestrzeni nazw „book”:
pod, err := clientset.CoreV1().Pods(“book”).Get(“example”, metav1.GetOptions{})
Warto zauważyć, że tylko ostatnie wywołanie, Get, rzeczywiście wymaga dostępu do serwera. Wywołania CoreV1
i Pods jedynie pobierają klienta i ustawiają przestrzeń nazw na potrzeby późniejszego wywołania Get (często
stosowana nazwa tej techniki to wzorzec budowniczy; tu służy ona do budowania żądania).
Wywołanie Get przesyła żądanie HTTP GET ścieżki /api/v1/namespaces/book/pods/example z serwera zapisanego
w pliku kubeconfig. Jeśli serwer API Kubernetesa zwróci kod HTTP 200, w treści odpowiedzi znajdą się
zakodowane obiekty podów — albo w formacie JSON (jest do domyślny format przesyłania danych w bibliotece
client-go), albo w formacie protobuf.
Możesz włączyć format protobuf dla klientów natywnych zasobów Kubernetesa, modyfikując
konfigurację REST przed utworzeniem klienta na jej podstawie:
cfg, err := clientcmd.BuildConfigFromFlags(“”, *kubeconfig)
cfg.AcceptContentTypes = “application/vnd.kubernetes.protobuf,
application/json”
cfg.ContentType = “application/vnd.kubernetes.protobuf”
clientset, err := kubernetes.NewForConfig(cfg)
Wersjonowanie i kompatybilność
API Kubernetesa są wersjonowane. W poprzednim punkcie pokazane zostało, że pody znajdują się w wersji v1
grupy podstawowej. Grupa podstawowa obecnie istnieje w tylko jednej wersji. Istnieją jednak także inne grupy.
Na przykład grupa apps jest dostępna (w czasie, gdy powstaje ta książka) w wersjach v1, v1beta2 i v1beta1.
Jeśli przyjrzysz się pakietowi k8s.io/api/apps (http://bit.ly/2L1Nyio), znajdziesz w nim wszystkie obiekty API z
tych wersji. W pakiecie k8s.io/client-go/kubernetes/typed/apps (http://bit.ly/2x45Uab) znajdują się implementacje
klientów dla wszystkich tych wersji.
Wszystkie te wersje dotyczą tylko kodu klienckiego. Nie mówią nic o klastrze Kubernetesa ani o serwerze API.
Użycie klienta wymagającego wersji grupy API, której serwer API nie obsługuje, zakończy się niepowodzeniem.
Klienty mają zapisaną na stałe wersję, a autor aplikacji musi wybrać odpowiednią wersję grupy API, aby móc
komunikować się z używanym klastrem. W punkcie „Wersje API i gwarancje kompatybilności” dowiesz się więcej
na temat gwarancji kompatybilności grup API.
Drugi aspekt kompatybilności dotyczy metamechanizmów serwera API, z którym komunikuje się biblioteka
client-go. Dostępne są m.in. struktury z opcjami dla operacji CRUD, np. CreateOptions, GetOptions,
UpdateOptions i DeleteOptions. Innym ważnym typem jest ObjectMeta (opisany szczegółowo w punkcie
„ObjectMeta”), używany w każdym rodzaju. Wszystkie takie typy są często rozszerzane o nowe mechanizmy
(nazywane czasem mechanizmami API Machinery). W dokumentacji pól tych typów w Go w komentarzach
określone jest, czy dany mechanizm jest dostępny w wersji alfa, czy w wersji beta. Obowiązują tu te same
gwarancje kompatybilności API co dla innych pól API.
TypeMeta `json:”,inline”`
// Gdy dyrektywa dryRun jest używana, informuje, że modyfikacji nie należy utrwalać.
// Błędna lub nierozpoznana dyrektywa dryRun skutkuje błędem
Ostatnie pole, DryRun, zostało dodane w Kubernetesie 1.12 w wersji alfa i w Kubernetesie 1.13 w wersji beta
(domyślnie włączone). Nie jest rozpoznawane przez serwery API w starszych wersjach. W zależności od
mechanizmu przekazanie nierozpoznanej opcji może zostać zignorowane, a nawet skutkować odrzuceniem
żądania. Dlatego ważne jest, aby używać wersji biblioteki client-go, która nie odbiega zanadto od wersji
klastra.
Informacje o tym, w jakich wersjach dostępne są poszczególne pola, zawierają pliki źródłowe z
biblioteki k8s.io/api. Na przykład w Kubernetesie 1.13 znajdziesz je w gałęzi release-1.13
(http://bit.ly/2Yrhjgq). Pola w wersji alfa są opisane w ten sposób.
Dostępna jest też wygodniejsza w użyciu generowana dokumentacja API (http://bit.ly/2YrfiB2).
Zawiera ona jednak te same informacje co biblioteka k8s.io/api.
Warto też wspomnieć, że wiele mechanizmów w wersjach alfa i beta jest powiązanych z
bramkami (http://bit.ly/2RP5nmi; oto pierwotne źródło: http://bit.ly/2FPZPTT). Informacje o
mechanizmach można śledzić dzięki zgłoszeniom na stronie http://bit.ly/2YuHYcd.
Formalnie gwarantowana tabela obsługi różnych wersji biblioteki client-go w różnych klastrach jest
publikowana w pliku README tej biblioteki (http://bit.ly/2RryyLM). Zobacz tabelę 3.1.
✓: wersje biblioteki client-go i Kubernetesa mają te same mechanizmy i te same wersje grup API.
+: biblioteka client-go ma mechanizmy lub wersje grup API, które mogą być niedostępne w klastrze
Kubernetesa. Może to wynikać z dodania funkcji do biblioteki client-go lub usunięcia dawnych,
przestarzałych mechanizmów z Kubernetesa. Jednak wszystkie wspólne elementy (czyli większość API)
będą działać poprawnie.
–: wiadomo, że biblioteka client-go jest niekompatybilna z klastrami Kubernetesa w danej wersji.
Tabela 3.1. Kompatybilność biblioteki client-go z wersjami Kubernetesa
client-
go ✓ +– +– +– +– +– +–
6.0
client-
go +– ✓ +– +– +– +– +–
7.0
client-
go +– +– ✓ +– +– +– +–
8.0
client-
go +– +– +– ✓ +– +– +–
9.0
client-
go +– +– +– +– ✓ +– +–
10.0
client-
go +– +– +– +– +– ✓ +–
11.0
client-
go +– +– +– +– +– +– ✓
12.0
client-
go +– +– +– +– +– +– +–
HEAD
Wniosek z tabeli 3.1 jest taki, że biblioteka client-go jest obsługiwana w powiązanej z nią wersji klastra. Gdy
wersje nie pasują do siebie, programiści muszą starannie rozważyć, które mechanizmy i które grupy API można
stosować oraz czy są one obsługiwane w wersji klastra, z jaką komunikuje się aplikacja.
W tabeli 3.1 wymienione są wersje biblioteki client-go. W punkcie „Biblioteka klienta” pokrótce opisano, że w
bibliotece client-go formalnie używane jest wersjonowanie semantyczne — każde zwiększenie numeru
podwersji Kubernetesa (człon 13 w wersji 1.13.2) powoduje zwiększenie numeru wersji tej biblioteki. Dla
Kubernetesa 1.4 udostępniono bibliotekę client-go 1.0, a w czasie, gdy powstaje ta książka, dostępna jest
biblioteka client-go 12.0 udostępniona dla Kubernetesa 1.15.
Wersjonowanie semantyczne dotyczy tylko biblioteki client-go, a nie repozytorium API Machinery lub
repozytorium API. Dla tych ostatnich używane są wersje Kubernetesa, co ilustruje rysunek 3.4. W punkcie
„Vendoring” opisujemy, co to oznacza, jeśli chcesz zastosować vendoring i umieścić biblioteki k8s.io/client-go,
k8s.io/apimachinery oraz k8s.io/api w swoim projekcie.
v1alpha1, v1alpha2, v2alpha1 itd. to wersje alfa uznawane za niestabilne. To oznacza, że:
mogą zostać usunięte lub zmodyfikowane w dowolnym momencie bez zważania na kompatybilność;
dane mogą zostać usunięte lub zagubione albo stać się niedostępne po zmianie wersji Kubernetesa;
często są domyślnie wyłączone, jeśli administrator nie włączy ich ręcznie.
v1beta1, v1beta2, v2beta1 itd. to wersje beta. Są one krokiem na drodze do stabilności, co oznacza, że:
będą istniały w przynajmniej jeszcze jednej wersji Kubernetesa równolegle do powiązanej stabilnej
wersji API;
zwykle nie będą modyfikowane w sposób naruszający kompatybilność, jednak nie jest to bezwzględnie
gwarantowane;
obiekty przechowywane w wersji beta nie zostaną usunięte i nie staną się niedostępne;
wersje beta są często domyślnie włączone w klastrach, jednak może to zależeć od dystrybucji
Kubernetesa i dostawcy chmury.
v1, v2 itd. to stabilne, powszechnie dostępne API, które:
pozostaną dostępne;
będą kompatybilne.
Obok wersji grup API trzeba uwzględnić dwie inne ważne kwestie:
Wersje grup API dotyczą zasobów API jako całych jednostek, np. formatu podów lub usług. Obok wersji
grup API zasoby API mogą mieć pojedyncze pola wersjonowane niezależnie od grup. Na przykład pola w
stabilnych API mogą być oznaczone jako pola w wersji alfa w wewnątrzwierszowej dokumentacji w kodzie
w języku Go. Dla takich pól obowiązują te same reguły co opisane wcześniej zasady dla grup API. Oto
przykłady:
Pole w wersji alfa w stabilnym API może w dowolnym momencie stać się niekompatybilne,
spowodować utratę danych lub zostać usunięte. Na przykład pole ObjectMeta.Initializers, które
nigdy nie zostało promowane poza wersję alfa, zostanie w bliskiej przyszłości usunięte (w wersji 1.14
zostało uznane za wycofywane):
// WYCOFYWANE — initializers to pole w wersji alfa i zostanie
// To pole jest dostępne w wersji alfa i jest uwzględniane tylko przez serwery
// z włączonym mechanizmem TTLAfterFinished.
TTLSecondsAfterFinished *int32 `json:”ttlSecondsAfterFinished,omitempty”
}
Serwer API może w inny sposób traktować poszczególne pola. Jeśli bramka mechanizmu powiązana z
danym polem alfa jest wyłączona, niektóre takie pola są odrzucane, a inne — ignorowane. Jest to
udokumentowane w opisie pola (zob. pole TTLSecondsAfterFinished w poprzednim przykładzie).
Ponadto w dostępie do API istotne są wersje grup API. Serwer API przeprowadza „w locie” konwersję
między różnymi wersjami tego samego zasobu. Oznacza to, że możesz uzyskać dostęp do obiektów
utworzonych w jednej wersji (np. v1beta1) w każdej innej obsługiwanej wersji (np. v1) bez konieczności
wykonywania dodatkowych operacji w aplikacji. Jest to bardzo wygodne, jeśli chcesz pisać aplikacje zgodne
ze starszymi lub nowszymi wersjami oprogramowania.
Każdy obiekt zapisany systemie etcd jest przechowywany w określonej wersji. Domyślnie jest to
przechowywana wersja danego zasobu. Choć przechowywana wersja może się zmieniać w zależności
od wersji Kubernetesa, to w czasie, gdy powstaje ta książka, obiekt zapisany w systemie etcd nie jest
automatycznie aktualizowany. Dlatego administrator klastra musi zadbać o migrację w trakcie
aktualizowania klastrów Kubernetesa, zanim obsługa starszych wersji zostanie zarzucona. Nie istnieje
generyczny mechanizm migracji, a przebieg migracji zależy od dystrybucji Kubernetesa.
Jednak dla autora aplikacji tego rodzaju operacyjne zadania w ogóle nie powinny mieć znaczenia.
Konwersja „w locie” gwarantuje, że aplikacja będzie miała jednolity obraz obiektów klastra. Aplikacja
nie wykryje nawet, jaka przechowywana wersja jest używana. Przechowywane wersje są niewidoczne
dla kodu pisanego w Go.
Obiekty Kubernetesa w Go
W punkcie „Tworzenie i używanie klientów” zobaczyłeś, jak utworzyć klienta dla grupy podstawowej, aby
uzyskać dostęp do podów z klastra Kubernetesa. Teraz przyjrzymy się bliżej temu, czym jest pod (a także każdy
inny zasób Kubernetesa) w świecie języka Go.
[1]
Zasoby Kubernetesa (a precyzyjniej — obiekty) to instancje określonego rodzaju udostępniane przez serwer
API w formie struktur. W zależności od rodzaju dostępne są oczywiście różne pola. Jednak budowa wszystkich
zasobów jest taka sama.
Jeśli chodzi o system typów, obiekty Kubernetesa implementują interfejs runtime.Object języka Go. Ten
interfejs pochodzi z pakietu k8s.io/apimachinery/pkg/runtime i jest bardzo prosty:
// Interfejs Object musi być obsługiwany przez wszystkie typy API zarejestrowane w
schemacie.
// Ponieważ obiekty schematu mają być serializowane na potrzeby przesyłu,
// interfejs Object musi umożliwiać jednostkom serializującym podanie
// rodzaju, wersji i grupy obiektu. W sytuacjach, gdy obiekt nie ma być serializowany,
// można zwracać akcesor niewykonujący operacji.
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
W tym kodzie schema.ObjectKind (z pakietu k8s.io/apimachinery/pkg/runtime/schema) to następny prosty
interfejs:
// Wszystkie serializowane obiekty ze schematu kodują informacje o swoim typie.
// (lub nil, jeśli dany obiekt nie udostępnia lub nie zawiera pól z tymi danymi).
GroupVersionKind() GroupVersionKind
}
Oznacza to, że obiekt Kubernetes jest w Go strukturą danych, która:
Głęboka kopia to sklonowana wersja struktury danych, która nie współdzieli żadnej pamięci z pierwotnym
obiektem. Jest ona używana, gdy kod ma modyfikować obiekt bez wpływu na jego pierwotną wersją. W
poświęconym generowaniu kodu punkcie „Znaczniki globalne” szczegółowo opisano implementację głębokiego
kopiowania w Kubernetesie.
W prostych słowach można stwierdzić, że obiekt przechowuje swój typ i umożliwia klonowanie.
TypeMeta
Choć runtime.Object jest tylko interfejsem, warto wiedzieć, jak jest zaimplementowany. Obiekty Kubernetesa z
biblioteki k8s.io/api implementują getter i setter wartości typu schema.ObjectKind poprzez zagnieżdżenie
struktury metav1.TypeMeta z pakietu k8s.io/apimachinery/meta/v1:
// TypeMeta opisuje jeden obiekt w odpowiedzi lub żądaniu w API.
// Zawiera łańcuchy znaków reprezentujące typ obiektu i wersję schematu API.
// Kind to wartość typu string określająca zasób REST reprezentowany przez ten obiekt.
// Serwery mogą wywnioskować tę wartość na podstawie punktu końcowego,
// do którego klient kieruje żądania.
// Aktualizowanie jest niedozwolone.
}
Gdy używana jest ta struktura, deklaracja poda w Go wygląda tak:
// Pod to kolekcja kontenerów, które można uruchamiać w hoście. Ten zasób jest
// tworzony przez klienty i szeregowany do wykonania w hostach.
// +optional
metav1.ObjectMeta `json:”metadata,omitempty”`
// +optional
Spec PodSpec `json:”spec,omitempty”`
[2]
Ten kod odpowiada reprezentacji poda w formacie YAML, znanej wszystkim użytkownikom Kubernetesa :
apiVersion: v1
kind: Pod
metadata:
namespace: default
name: example
spec:
containers:
- name: hello
image: debian:latest
command:
- /bin/sh
args:
- -c
- echo “Witaj, świecie”; sleep 10000
Na zapleczu: zależności między typami języka Go, pakietami, rodzajami i nazwami grup
Możesz się zastanawiać, skąd klient zna rodzaj i grupę API, którymi ma uzupełnić pole typu TypeMeta. Jest
to tylko pozornie proste pytanie:
Wydaje się, że rodzajem jest tylko nazwa typu z języka Go, którą można pobrać z obiektu za pomocą
mechanizmu refleksji. Zwykle jest to prawdą (w ok. 99% sytuacji), jednak zdarzają się wyjątki. W
rozdziale 4. poznasz niestandardowe zasoby, dla których ta zasada nie obowiązuje.
Wydaje się, że grupa to tylko nazwa pakietu z języka Go (typy z grupy API apps są zadeklarowane w
pakiecie k8s.io/api/apps). Często jest to prawdą, ale nie zawsze. Dla grupy podstawowej nazwą jest pusty
łańcuch znaków. Ponadto typy z grupy rbac.authorization.k8s.io znajdują się w pakiecie
k8s.io/api/rbac, a nie w k8s.io/api/rbac.authorization.k8s.io.
Właściwa odpowiedź na pytanie o to, jak uzupełnić pole typu TypeMeta, wymaga uwzględnienia
schematów opisanych szczegółowo w punkcie „Schematy”.
Gdy będziesz uruchamiać przykłady z punktu „Tworzenie i używania klientów”, aby pobrać pod z klastra,
zauważ, że obiekt poda zwrócony przez klienta nie ma ustawionego rodzaju ani wersji. W aplikacjach opartych
na bibliotece client-go obowiązuje zwyczaj, że te pola w obiektach w pamięci są puste; zostają one uzupełnione
dopiero wtedy, gdy wartości są serializowane do formatu JSON lub protobuf. Operację tę wykonuje
automatycznie klient (a bardziej precyzyjnie — mechanizm serializowania uwzględniający wersje).
Tak więc aplikacje oparte na bibliotece client-go sprawdzają typy z języka Go, aby ustalić, jaki obiekt jest
używany. W innych platformach (np. w Operator SDK; zob. punkt „Operator SDK”) może to wyglądać inaczej.
ObjectMeta
Obok pola typu TypeMeta większość obiektów z najwyższego poziomu ma też pole typu metav1.ObjectMeta,
także z pakietu k8s.io/apimachinery/pkg/meta/v1:
}
W formatach JSON i YAML te pola należą do kategorii metadata. Na przykład dla opisywanego wcześniej poda
pole typu metav1.ObjectMeta zawiera następujące dane:
metadata:
namespace: default
name: example
Zwykle pole to obejmuje wszystkie informacje z poziomu meta, np. nazwę, przestrzeń nazw, wersję zasobów (nie
należy mylić jej z wersją grupy API), kilka znaczników czasu, a także znane etykiety i adnotacje. Dokładne
omówienie pól z typu ObjectMeta znajdziesz w punkcie „Budowa typu”.
Wersje zasobów zostały opisane wcześniej w punkcie „Współbieżność optymistyczna”. W kodzie używającym
biblioteki client-go wersja zasobu prawie nigdy nie jest wczytywana ani zapisywana. Jest to jednak jedno z pól
w Kubernetesie, dzięki którym cały system może działać. Pole resourceVersion należy do typu ObjectMeta,
ponieważ każdy obiekt z zagnieżdżoną wartością tego typu odpowiada kluczowi z systemu etcd, z którego
pochodzi wartość pola resourceVersion.
W systemie jest tylko kilka wyjątków od konwencji związanej ze stosowaniem sekcji spec i status. Nie używa się
ich np. dla punktów końcowych w grupie podstawowej lub typów związanych z kontrolą dostępu opartą na rolach
(np. dla typu ClusterRole).
Zbiory klientów
We wprowadzającym przykładzie z punktu „Tworzenie i używanie klientów” pokazaliśmy, że wywołanie
kubernetes.NewForConfig(config) zwraca zbiór klientów. Zbiór klientów zapewnia dostęp do klientów dla
różnych grup API i zasobów. Wywołanie kubernetes.NewForConfig(config) z pakietu k8s.io/client-
go/kubernetes daje dostęp do wszystkich grup API i zasobów zdefiniowanych w bibliotece k8s.io/api, czyli do
prawie całego zbioru zasobów udostępnianych przez serwer API Kubernetesa; kilka wyjątków to typy
APIServices (reprezentuje zagregowane serwery API) i CustomResourceDefinition (zob. rozdział 4.).
W rozdziale 5. wyjaśniamy, jak zbiory klientów są generowane na podstawie typów API (z biblioteki k8s.io/api w
tym scenariuszu). W niezależnych projektach, gdzie występują niestandardowe API, używane są nie tylko zbiory
klientów z Kubernetesa. Jednak wszystkie zbiory klientów mają coś wspólnego — konfigurację w formacie REST,
zwracaną m.in. przez wywołanie clientcmd.BuildConfigFromFlags(„”, *kubeconfig) (tak jak w przykładzie).
Discovery() discovery.DiscoveryInterface
AppsV1() appsv1.AppsV1Interface
AppsV1beta1() appsv1beta1.AppsV1beta1Interface
AppsV1beta2() appsv1beta2.AppsV1beta2Interface
AuthenticationV1() authenticationv1.AuthenticationV1Interface
AuthenticationV1beta1() authenticationv1beta1.AuthenticationV1beta1Interface
AuthorizationV1() authorizationv1.AuthorizationV1Interface
AuthorizationV1beta1() authorizationv1beta1.AuthorizationV1beta1Interface
...
}
W przeszłości ten interfejs obejmował niewersjonowane metody, np. Apps() appsv1.AppsV1Interface. Jednak w
powiązanej z Kubernetesem 1.14 bibliotece client-go 11.0 zostały one uznane za wycofywane. Już
wspominaliśmy, że dobrą praktyką jest jednoznaczne określanie wersji grupy API używanej w aplikacji.
Każdy zbiór klientów zapewnia dostęp do klient odpowiedzialnego za wykrywanie informacji (jest on używany w
obiektach typu RESTMapper; zob. punkty „Odwzorowania REST” i „Używanie API w wierszu poleceń”).
Każda metoda o nazwie GrupaWersja (np. AppsV1beta1) pozwala uzyskać zasoby z danej grupy API. Oto
przykład:
type AppsV1beta1Interface interface {
RESTClient() rest.Interface
ControllerRevisionsGetter
DeploymentsGetter
StatefulSetsGetter
}
RESTClient zwraca generycznego klienta REST, a każdemu zasobowi odpowiada jeden interfejs:
DeploymentExpansion
}
W zależności od zasięgu zasobu — na poziomie klastra lub przestrzeni nazw — akcesor (tu jest nim
DeploymentGetter) może, ale nie musi przyjmować argumentu namespace.
Typ DeploymentInterface daje dostęp do wszystkich operacji obsługiwanych przez zasób. Większość tych
operacji jest oczywista. Dalej opisano te, które wymagają dodatkowych objaśnień.
Domyślnie metoda UpdateStatus() jest generowana przez narzędzie client-gen (zob. punkt „Znaczniki dla
generatora client-gen”). Istnienie tej metody nie gwarantuje, że dany zasób obsługuje podzasoby. To zastrzeżenie
jest ważne w kontekście pracy z zasobami CRD w punkcie „Podzasoby”.
...
// +optional
FieldSelector string `json:”fieldSelector,omitempty”`
...
}
Czujki
Typ Watch zapewnia interfejs ze zdarzeniami dotyczącymi wszystkich zmian (dodania, usuwania i modyfikacji) w
obiektach. Zwracana wartość typu watch.Interface z pakietu k8s.io/apimachinery/pkg/watch wygląda tak:
// Zwraca kanał, do którego kierowane będą wszystkie zdarzenia. Jeśli wystąpi błąd lub
const (
// Obiekt to:
// * jeśli Type ma wartość Added lub Modified: nowy stan obiektu.
Object runtime.Object
}
Choć kuszące jest bezpośrednie używanie tego interfejsu, jest to odradzane. W zamian zaleca się używanie
informatorów (zob. punkt „Informatory i buforowanie”). Informatory łączą pokazany interfejs zdarzeń z buforem
umożliwiającym indeksowane wyszukiwanie. Jest to zdecydowanie najczęstszy sposób używania czujek. Na
zapleczu informatory najpierw wywołują funkcję List dla klienta, aby uzyskać zbiór wszystkich obiektów (jako
punkt wyjścia do utworzenia bufora), a następnie obserwują te obiekty, by aktualizować bufor. Informatory we
właściwy sposób obsługują błędy — przywracają stan po awariach sieci lub innych problemach z klastrem.
Rozszerzanie klientów
DeploymentExpansion to pusty interfejs. Służy do dodawania niestandardowych operacji klientów, jednak
obecnie jest bardzo rzadko używany w Kubernetesie. Zamiast tego stosowany jest generator klientów, który
pozwala dodawać niestandardowe metody w deklaratywny sposób (zob. punkt „Znaczniki dla generatora client-
gen”).
Warto zauważyć, że metody z typu DeploymentInterface ani nie oczekują poprawnych informacji w polach Kind
i APIVersion z typu TypeMeta, ani nie ustawiają wartości tych pól w wywołaniach Get() i List() (zob. też punkt
„TypeMeta”). Te pola są zapełniane rzeczywistymi wartościami tylko w trakcie przesyłu danych.
Opcje klientów
Warto przyjrzeć się różnym opcjom, jakie można ustawić w trakcie tworzenia zbioru klientów. Z uwagi przed
punktem „Wersjonowanie i kompatybilność” dowiedziałeś się, że obiekty natywnych typów Kubernetesa można
przesyłać w formacie protobuf. Ten format jest bardziej wydajny niż JSON (zarówno jeśli chodzi o pamięć, jak i
ze względu na obciążenie procesora w kliencie i na serwerze), dlatego zaleca się stosowanie go.
W trakcie debugowania i w celu poprawy czytelności wskaźników często warto rozróżniać klienty korzystające z
serwera API. W tym celu można ustawić pole agenta użytkownika w konfiguracji REST. Wartość domyślna to
binary/version (os/arch) kubernetes/commit. Na przykład narzędzie kubectl ma agenta użytkownika
kubectl/v1.14.0 (darwin/amd64) kubernetes/d654b49. Jeśli ten wzorzec jest niewystarczający, można go
dostosować do potrzeb:
cfg.UserAgent = fmt.Sprintf(
...
// Jeśli podane jest 0, tworzony klient RESTClient używa wartości DefaultBurst: 10.
Burst int
Wartość domyślna pola QPS to 5 (żądań na sekundę), a wartość domyślna pola burst — 10.
Dla limitu czasu oczekiwania nie ma wartości domyślnej (przynajmniej nie w konfiguracji REST klienta.
Domyślnie serwer API Kubernetesa stosuje limit czasu oczekiwania 60 sekund dla wszystkich żądań, które nie są
długotrwałe. Długotrwałe żądania mogą pochodzić od czujek lub być kierowane do podzasobów takich jak /exec,
/portforward lub /proxy.
Informatory i buforowanie
Interfejs klienta opisany w punkcie „Zbiory klientów” obejmuje operację Watch, udostępniającą interfejs zdarzeń
reagujący na zmiany (dodawanie, usuwanie i aktualizowanie obiektów). Informator zapewnia wysokopoziomowy
interfejs programowania związany z najczęstszym zastosowaniem czujek: obsługą bufora w pamięci i szybkim,
indeksowanym wyszukiwaniem obiektów w pamięci na podstawie nazw lub innych właściwości.
Kontroler, który komunikuje się z serwerem API za każdym razem, gdy potrzebuje obiektu, powoduje wysokie
obciążenie w systemie. Rozwiązaniem tego problemu są informatory korzystające z bufora w pamięci.
Informatory potrafią też reagować na modyfikacje obiektów w czasie bliskim rzeczywistemu i nie wymagają
żądań związanych z odpytywaniem.
Na rysunku 3.5 pokazany jest wzorzec informatora. Informatory:
Informatory udostępniają też zaawansowaną obsługę błędów. Gdy długotrwałe połączenie z czujką zostanie
zerwane, przywracają stan, zgłaszając inne żądanie do czujki. Kontynuują obsługę strumienia zdarzeń bez utraty
żadnych z nich. Jeśli przestój jest długi, a serwer API traci zdarzenia, ponieważ system etcd usunął je z bazy
przed udanym przetworzeniem nowego żądania do czujki, informator ponownie zgłasza żądanie pobrania (ang.
relist) wszystkich obiektów.
Obok ponownego żądania pobrania obowiązuje konfigurowalny okres resynchronizacji służący do uzgodnienia
zawartości bufora w pamięci z logiką biznesową. Po każdym upływie tego czasu dla wszystkich obiektów
wywoływane są zarejestrowane funkcje obsługi zdarzeń. Zwykle stosowane wartości są podawane w minutach
(np. 10 lub 30 minut).
Resynchronizacja odbywa się w całości w pamięci i nie powoduje przesłania żądania na serwer.
W przeszłości było inaczej, jednak zmieniono używaną technikę (http://bit.ly/2FmeMge),
ponieważ obsługa błędów w mechanizmie czujek została usprawniona w wystarczającym
stopniu, aby ponowne żądanie pobrania obiektów stało się zbędne.
Wszystkie te zaawansowane i sprawdzone w praktyce mechanizmy obsługi błędów są dobrym powodem do tego,
by używać informatorów, zamiast tworzyć niestandardowy kod i bezpośrednio używać metody Watch() klienta.
Informatory są stosowane w wielu miejscach w samym Kubernetesie i są jednym z głównych rozwiązań
architektury API Kubernetesa.
Wprawdzie zaleca się stosowanie informatorów zamiast odpytywania, niemniej i tak powodują one obciążenie
serwera API. Jeden program binarny powinien tworzyć tylko jednego informatora dla danego identyfikatora GVR.
Aby ułatwić współdzielenie informatorów, można je tworzyć za pomocą fabryki współdzielonych informatorów.
Fabryka współdzielonych informatorów umożliwia współdzielenie w aplikacji informatorów dla tego samego
zasobu. Oznacza to, że w różnych pętlach sterowania można używać na zapleczu tego samego połączenia z
czujką z serwera API. Na przykład kube-controller-manager (jeden z głównych komponentów klastra
Kubernetesa; zob. punkt „Serwer API”) ma wysoką, dwucyfrową liczbę kontrolerów. Jednak dla każdego zasobu
(np. dla podów) w procesie działa tylko jeden informator.
Zawsze używaj fabryki współdzielonych informatorów do ich tworzenia. Nie próbuj ręcznie
generować informatorów. Dodatkowe koszty korzystania z fabryki są bardzo małe, a złożony
kontroler binarny, który nie używa informatorów współdzielonych, zapewne będzie nawiązywał
wiele połączeń z czujkami dotyczących tego samego zasobu.
Jeśli zaczniesz od konfiguracji REST (zob. punkt „Tworzenie i używanie klientów”), łatwo utworzysz fabrykę
współdzielonych informatorów dla zbioru klientów. Informatory dla standardowych zasobów Kubernetesa są
generowane przez generator kodu i udostępniane w pakiecie k8s.io/client-go/informers jako część biblioteki
client-go:
import (
...
“k8s.io/client-go/informers”
)
...
podInformer := informerFactory.Core().V1().Pods()
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(new interface{}) {...},
})
informerFactory.Start(wait.NeverStop)
informerFactory.WaitForCacheSync(wait.NeverStop)
Warto też zauważyć, że można dodać wiele funkcji obsługi zdarzeń. Sama fabryka współdzielonych informatorów
istnieje tylko dlatego, że tak często jest potrzebna w plikach binarnych kontrolera z wieloma pętlami sterowania,
z których każda instaluje funkcje obsługi zdarzeń dodające obiekty do własnej kolejki zadań.
Interfejs zdarzeń w czujkach na zapleczu powoduje zwykle pewne opóźnienia. W systemie, dla którego
zastosowano odpowiednie planowanie wydajności, te opóźnienia nie są duże. Oczywiście dobrą praktyką jest
pomiar opóźnień z użyciem wskaźników. Jednak opóźnienia i tak występują, dlatego logikę aplikacji trzeba tak
zaprojektować, aby nie szkodziły one działaniu kodu.
To, jaki okres między operacjami resynchronizacji będzie właściwy, zależy od sytuacji. Na
przykład 30 sekund to dość krótki czas. W wielu sytuacjach dobrym wyborem jest kilka minut (a
nawet do 30). W najgorszym scenariuszu oznacza to, że naprawienie usterki w kodzie (np. utraty
sygnału z powodu nieprawidłowej obsługi błędów) za pomocą uzgadniania stanu zajmie 30
minut.
Warto też zauważyć, że ostatni wiersz w przykładowym kodzie, wywołanie Get(„client-go”), wymaga tylko
dostępu do pamięci (a nie do serwera API). Obiekty w magazynie danych w pamięci nie mogą być bezpośrednio
modyfikowane. Zamiast tego trzeba użyć zbioru klientów, aby uzyskać dostęp do zasobów na potrzeby zapisu.
Informator otrzymuje wtedy zdarzenia z serwera API i aktualizuje przechowywany w pamięci stan.
func NewFilteredSharedInformerFactory(
tweakListOptions internalinterfaces.TweakListOptionsFunc
) SharedInformerFactor
Informatory są jedną z cegiełek do tworzenia kontrolerów. W rozdziale 6. zobaczysz, jak wygląda typowy
kontroler oparty na bibliotece client-go. Obok klientów i informatorów trzecią podstawową cegiełką jest tu
kolejka zadań. Przyjrzyjmy się jej teraz.
Kolejka zadań
Kolejka zadań jest strukturą danych. Można dodawać do niej elementy i pobierać je z niej w kolejności
zdefiniowanej w kolejce. Formalnie struktura tego rodzaju jest nazywana kolejką priorytetową. Biblioteka
client-go na potrzeby tworzenia kontrolerów udostępnia rozbudowaną implementację takich kolejek w pakiecie
k8s.io/client-go/util/workqueue (http://bit.ly/2IV0JPz).
Ściśle rzecz biorąc, ten pakiet udostępnia kilka wersji kolejek przeznaczony do różnych celów. Interfejs bazowy
implementowany we wszystkich tych wersjach wygląda tak:
Add(item interface{})
Len() int
Done(item interface{})
ShutDown()
ShuttingDown() bool
}
Tu funkcja Add(item) dodaje element, funkcja Len() zwraca długość kolejki, a Get() zwraca element o
najwyższym priorytecie (i blokuje operacje do czasu, gdy element stanie się dostępny). Każdy element zwrócony
przez funkcję Get() wymaga wywołania Done(item) po zakończeniu przetwarzania elementu przez kontroler. W
trakcie przetwarzania elementu kolejne wywołania Add(item) powodują jedynie oznaczenie elementu jako
zmodyfikowanego, po czym zostaje on ponownie dodany po wywołaniu Done(item).
Ten generyczny interfejs jest typem bazowym dla następujących typów kolejek:
}
RateLimitingInterface ogranicza częstotliwość dodawania elementów do kolejki; jest to interfejs
pochodny od DelayingInterface:
type RateLimitingInterface interface {
DelayingInterface
Forget(item interface{})
Najciekawsza jest tu metoda Forget(item). Zeruje ona mechanizm wydłużania odczekiwania dla danego
elementu. Zwykle jest wywoływana po udanym przetworzeniu elementu.
W terminologii związanej z API Machinery nie występuje pojęcie typ. Zamiast niego używane jest określenie
rodzaj.
Rodzaje
Rodzaje są podzielone na grupy API i wersjonowane, co opisaliśmy już w punkcie „Terminologia związana z API”.
Dlatego ważnym pojęciem w repozytorium API Machinery są identyfikatory GVK (ang. GroupVersionKind).
W Go każdy identyfikator GVK odpowiada jednemu typowi języka Go. Z kolei typ z języka Go może odpowiadać
kilku identyfikatorom GVK.
Rodzaje formalnie nie są powiązane jeden do jednego ze ścieżkami HTTP. Wiele rodzajów ma punkty końcowe
HTTP REST używane do dostępu do obiektów danego rodzaju. Istnieją jednak rodzaje bez żadnego punktu
końcowego HTTP (np. używany do wywoływania webhooków admission.k8s.io/v1beta1.AdmissionReview;
http://bit.ly/2XJXBQD). Ponadto niektóre rodzaje są zwracane przez wiele punktów końcowych (m.in.
meta.k8s.io/v1.Status, http://bit.ly/31Ktjvz, zwracany przez wszystkie punkty końcowe w celu informowania o
statusie, np. o błędach, gdy wywołanie nie zwraca innych obiektów).
Zasoby
Obok rodzajów występują też zasoby, co opisaliśmy w punkcie „Terminologia związana z API”. Zasoby też są
grupowane i wersjonowane oraz mają identyfikatory GVR (ang. GroupVersionResource).
Każdy identyfikator GVR odpowiada jednej (bazowej) ścieżce HTTP. Identyfikatory GVR służą do identyfikowania
punktów końcowych REST w API Kubernetesa. Na przykład identyfikator GVR apps/v1.deployments jest
powiązany z punktem końcowym /apis/apps/v1/namespaces/przestrzeńnazw/ deployments.
Biblioteki klientów wykorzystują to powiązanie do tworzenia ścieżek HTTP na potrzeby dostępu do zasobów o
określonych identyfikatorach GVR.
Nazwy zasobów zwyczajowo są zapisywane małą literą i w liczbie mnogiej (zwykle odpowiadają liczbie mnogiej
nazwy powiązanego rodzaju). Te nazwy muszą być zgodne z formatem ścieżek DNS (RFC 1035). Nie jest to
zaskoczeniem, ponieważ zasoby bezpośrednio odpowiadają ścieżkom HTTP.
Odwzorowania REST
Odwzorowanie z identyfikatora GVK na identyfikator GVR jest nazywane odwzorowaniem REST.
RESTMapper to interfejs z języka Go (http://bit.ly/2Y7wYS8) umożliwiający zażądanie identyfikatora GVR na
podstawie identyfikatora GVK:
RESTMapping(gk schema.GroupKind, versions ...string) (*RESTMapping, error)
Resource schema.GroupVersionResource.
GroupVersionKind schema.GroupVersionKind
// jeśli wyszukiwanie nie dotyczy wersji. Gdy podana jest wersja, funkcja zwraca
We fragmentach identyfikatora GVR nie wszystkie pola są podane. Załóżmy, że wpisałeś polecenie kubectl get
pods. Brakuje w nim grupy i wersji. Jednak RESTMapper, mając wystarczające informacje, nadal może
odwzorować to polecenie na rodzaj v1 Pods.
We wcześniejszym przykładzie instalacji RESTMapper, który zna dane instalacje (wkrótce dowiesz się, co to
oznacza), odwzoruje nazwę apps/v1.Deployment (http://bit.ly/2IujaLU) na zasób apps/v1.deployments o zasięgu
przestrzeni nazw.
Istnieje wiele różnych implementacji interfejsu RESTMapper. Najważniejsza z nich przeznaczona dla aplikacji
klienckich to oparty na wykrywaniu informacji typ DeferredDiscoveryRESTMapper (http://bit.ly/2XroxUq) z
pakietu k8s.io/client-go/restmapper. Używa ona mechanizmu wykrywania informacji z serwera API Kubernetesa
do dynamicznego tworzenia odwzorowań REST. Działa także dla zasobów innych niż podstawowe (np. dla
zasobów niestandardowych).
Schemat
Ostatnim pojęciem, które należy zaprezentować w związku z systemem typów Kubernetesa, jest schemat
(http://bit.ly/2N1PGJB). Schematy są dostępne w pakiecie k8s.io/apimachinery/pkg/runtime.
Schemat łączy świat języka Go z niezależnym od implementacji światem identyfikatorów GVK. Głównym
zadaniem schematu jest odwzorowywanie typów z języka Go na możliwe identyfikatory GVK:
func (s *Scheme) ObjectKinds(obj Object) ([]schema.GroupVersionKind, bool, error)
W punkcie „Obiekty Kubernetesa w Go” opisaliśmy, że obiekt może zwrócić grupę i rodzaj za pomocą metody
GetObjectKind() schema.ObjectKind. Jednak grupa i rodzaj zwykle nie są podane, dlatego metoda ta jest
prawie nieprzydatna do identyfikowania obiektów.
Zamiast tego schemat określa dla danego obiektu typ z Go, używając mechanizmu refleksji, i odwzorowuje ten
typ na zarejestrowane dla niego identyfikatory GVK. Aby ta technika działała, typy z Go muszą oczywiście zostać
zarejestrowane w schemacie:
Rysunek 3.6. Schemat łączący typy danych z języka Go z identyfikatorami GVK, konwersjami i defaulterami
Dla typów podstawowych Kubernetesa dostępny jest predefiniowany schemat w zbiorze klientów client-go
(http://bit.ly/2FkXDn2) w pakiecie k8s.io/client-go/kubernetes/scheme. Wstępnie zarejestrowane są w nim
wszystkie typy podstawowe. Każdy zbiór klientów wygenerowany przez generator kodu z narzędzia client-gen
(zob. rozdział 5.) zawiera pakiet pomocniczy scheme z wszystkimi typami z wszystkich grup i wersji z danego
zbioru klientów.
Omówienie schematu kończy przegląd zagadnień związanych z repozytorium API Machinery. Jeśli masz
zapamiętać tylko jedną rzecz związaną z tymi zagadnieniami, niech będzie to rysunek 3.7.
Rysunek 3.7. API Machinery w pigułce — od typów języka Go przez identyfikatory GVK i GVR do ścieżek HTTP
Vendoring
W tym rozdziale zobaczyłeś, że biblioteki k8s.io/client-go, k8s.io/api i k8s.io/apimachinery są podstawą w
programowaniu dla Kubernetesa w języku Go. W Go stosowana jest technika vendoringu do dołączania tych
bibliotek w repozytorium z kodem źródłowym niezależnych aplikacji.
Technika vendoringu wciąż jest rozwijana przez społeczność użytkowników języka Go. W czasie, gdy powstaje ta
książka, popularnych jest kilka narzędzi do vendoringu, np. godeps, dep i glide. Jednocześnie w Go 1.12
wprowadzono obsługę modułów języka Go, które w przyszłości zapewne staną się standardową metodą
vendoringu w społeczności użytkowników Go. Mechanizm ten nie jest jednak jeszcze gotowy do zastosowania w
ekosystemie Kubernetesa.
Obecnie w większości projektów używane są narzędzia dep i glide. W samym Kubernetesie w projekcie
github.com/kubernetes/kubernetes w pracach nad wersją 1.15 zdecydowano się zastosować moduły języka Go.
Poniższy opis jest adekwatny dla wszystkich narzędzi do vendoringu.
W repozytoriach k8s.io/∗ źródłem informacji o obsługiwanych wersjach zależności jest plik Godeps/Godeps.json.
Należy podkreślić, że wybór zależności niewymienionych w tym pliku może sprawić, iż biblioteka nie będzie
działać.
W punkcie „Biblioteka klienta” znajdziesz więcej informacji na temat publikowanych znaczników z bibliotek
k8s.io/client-go, k8s.io/api i k8s.io/apimachinery oraz kompatybilności między tymi znacznikami.
glide
W projektach, gdzie używane jest narzędzie glide, można wczytywać plik Godeps/Godeps.json przy każdej
zmianie zależności. Okazało się, że jest to dość stabilne rozwiązanie. Programista musi tylko zadeklarować
właściwą wersję biblioteki k8s.io/client-go, a glide wybierze odpowiednią wersję bibliotek k8s.io/apimachinery,
k8s.io/api i innych zależności.
W niektórych projektach w serwisie GitHub plik glide.yaml wygląda tak:
package: github.com/book/example
import:
- package: k8s.io/client-go
version: v10.0.0
...
Wtedy polecenie glide install -v spowoduje pobranie biblioteki k8s.io/client-go i zależności do lokalnego
pakietu vendor/. Opcja -v powoduje tu usunięcie katalogów vendor/ z dodawanych bibliotek. Jest to potrzebne do
naszych celów.
Jeśli aktualizujesz projekt do nowej wersji biblioteki client-go i w tym celu zmodyfikujesz plik glide.yaml,
polecenie glide update -v pobierze odpowiednie nowe wersje zależności.
dep
Narzędzie dep jest uważane za bardziej rozbudowane i zaawansowane od narzędzia glide. Przez długi czas
uznawano je za następcę narzędzia glide w ekosystemie Kubernetesa i wydawało się, że dep stanie się tym
najważniejszym narzędziem do vendoringu w języku Go. W czasie, gdy powstaje ta książka, przyszłość nie jest
jeszcze jasna, ale wydaje się, że należy ona do modułów języka Go.
Przy korzystaniu z biblioteki client-go trzeba mieć świadomość kilku ograniczeń narzędzia dep:
To oznacza, że wybór zależności dla biblioteki client-go będzie prawdopodobnie błędny po zaktualizowaniu
wersji tej biblioteki w pliku Godep.toml. Jest to niekorzystne, ponieważ programista musi bezpośrednio (i zwykle
ręcznie) zadeklarować wszystkie zależności.
Oto działający i spójny plik Godep.toml:
[[constraint]]
name = “k8s.io/api”
version = “kubernetes-1.13.0”
[[constraint]]
name = “k8s.io/apimachinery”
version = “kubernetes-1.13.0”
[[constraint]]
name = “k8s.io/client-go”
version = “10.0.0”
[prune]
go-tests = true
unused-packages = true
[[override]]
name = “k8s.io/api”
version = “kubernetes-1.13.0”
[[override]]
name = “k8s.io/apimachinery”
version = “kubernetes-1.13.0”
[[override]]
name = “k8s.io/client-go”
version = “10.0.0”
Moduły języka Go
Moduły języka Go są przyszłością zarządzania zależnościami w tym języku. Ich wstępna obsługa została
wprowadzona w Go 1.11 (http://bit.ly/2FmBp3Y), a stabilna wersja pojawiła się w Go 1.12. Po ustawieniu
zmiennej środowiskowej GO111MODULE=on dla modułów języka Go działają różne polecenia, np. go run i go get.
W Go 1.13 będzie to ustawienie domyślne.
Moduły języka Go są oparte na pliku go.mod z katalogu głównego projektu. Oto fragment pliku go.mod z
projektu github.com/programming-kubernetes/pizza-apiserver z rozdziału 8.:
module github.com/programming-kubernetes/pizza-apiserver
require (
...
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe
k8s.io/client-go v2.0.0-alpha.0.0.20190307161346-7621a5ebb88b+incompatible
k8s.io/klog v0.2.1-0.20190311220638-291f19f84ceb
W bibliotece client-go v11.0.0 (odpowiada ona Kubernetesowi 1.14) i w starszych jej wersjach nie ma
bezpośredniej obsługi modułów języka Go. Można jednak używać takich modułów razem z bibliotekami
Kubernetesa, co pokazano w poprzednim przykładzie.
Jeśli jednak w bibliotece client-go lub w innych repozytoriach Kubernetesa nie ma pliku go.mod (jest tak
przynajmniej do wersji 1.15 Kubernetesa), trzeba ręcznie wybrać odpowiednie wersje modułów. Oznacza to, że
będziesz potrzebować kompletnej listy wszystkich zależności pasujących do wersji zależności wymienionych w
pliku Godeps/Godeps.json z biblioteki client-go.
Zwróć też uwagę na mało czytelne wersje z poprzedniego przykładu. Są to pseudowersje uzyskane na podstawie
istniejących znaczników (jeśli nie ma znaczników, jako przedrostek używana jest wersja v0.0.0). Co gorsza, w
pliku można podać wersje ze znacznikami, ale polecenia modułów języka Go po następnym ich wywołaniu
zastąpią wersje pseudowersjami.
W bibliotece client-go v12.0.0 (powiązanej z Kubernetesem 1.15) udostępniany jest plik go.mod, a
wycofywana jest obsługa wszystkich pozostałych narzędzi do vendoringu (zob. dokument opisujący to
rozwiązanie — http://bit.ly/2IZ9MPg). Udostępniany plik go.mod obejmuje wszystkie zależności, a w pliku
go.mod w projekcie nie trzeba już ręcznie wymieniać wszystkich pośrednio potrzebnych zależności. W przyszłych
wersjach schemat tworzenia znaczników może zostać zmodyfikowany, aby wyeliminować okropne pseudowersje i
zastąpić je poprawnymi znacznikami wersji semantycznych. Jednak w czasie, gdy powstaje ta książka, takie
rozwiązanie nie zostało jeszcze w pełni zaimplementowane i nie podjęto też decyzji o jego wprowadzeniu.
Podsumowanie
W tym rozdziale skoncentrowaliśmy się na interfejsie programowania Kubernetesa w języku Go. Omówiliśmy
dostęp do API Kubernetesa znanych typów podstawowych, reprezentujących obiekty API dostępne w każdym
klastrze Kubernetesa.
To kończy omówienie podstaw API Kubernetesa i reprezentacji tego API w języku Go. Jesteś gotów do przejścia
do niestandardowych zasobów, które są jedną z podstaw działania operatorów.
2 Polecenie kubectl explain pod pozwala skierować do serwera API zapytanie o schemat obiektu (z
dokumentacją pól włącznie).
Rozdział 4. Używanie
niestandardowych zasobów
W tym rozdziale przedstawimy niestandardowe zasoby. Jest to jeden z głównych mechanizmów
rozszerzania używanych w ekosystemie Kubernetesa.
Niestandardowe zasoby są używane w małych, wewnętrznych obiektach konfiguracyjnych i nie
są powiązane z logiką kontrolera. Definiuje się je czysto deklaratywnie. Jednak odgrywają one
podstawową rolę w wielu rozbudowanych projektach programistycznych opartych na
Kubernetesie, których autorzy chcą zapewnić interfejs podobny do natywnego API
Kubernetesa. Dotyczy to np. systemów service mesh takich jak Istio, Linkerd 2.0 i AWS App
Mesh, które wszystkie są oparte na niestandardowych zasobach.
Pamiętasz punkt „Przykład wprowadzający” z rozdziału 1.? Podstawą tego przykładu jest
niestandardowy zasób o następującej postaci:
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
name: example-at
spec:
schedule: “2019-07-03T02:00:00Z”
status:
phase: “pending”
Niestandardowe zasoby są dostępne w każdym klastrze Kubernetesa od wersji 1.7. Są
zapisywane w tej samej instancji systemu etcd co główne zasoby API Kubernetes i
udostępniane przez ten sam serwer API Kubernetesa. Na rysunku 4.1 pokazane jest, że żądania
niestandardowych zasobów są kierowane do serwera apiextensions-apiserver, który
udostępnia zasoby tworzone za pomocą definicji CRD, jeśli nie są to:
Definicja CRD sama jest zasobem Kubernetesa. Opisuje niestandardowe zasoby dostępne w
klastrze. W tym przykładzie definicja CRD niestandardowego zasobu wygląda tak:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ats.cnat.programming-kubernetes.info
spec:
group: cnat.programming-kubernetes.info
names:
kind: At
listKind: AtList
plural: ats
singular: at
scope: Namespaced
subresources:
status: {}
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
Nazwa definicji CRD (tu jest to ats.cnat.programming-kubernetes.info) to nazwa zasobu w
liczbie mnogiej z dołączoną nazwą grupy. Ta definicja tworzy niestandardowy zasób rodzaju At
w grupie API cnat.programming-kubernetes.info jako zasób ats z przestrzeni nazw.
Jeśli ta definicja CRD zostanie użyta w klastrze, narzędzie kubectl automatycznie wykryje
zasób, a użytkownik będzie mógł uzyskać do niego dostęp w następujący sposób:
NAME CREATED AT
ats.cnat.programming-kubernetes.info 2019-04-01T14:03:33Z
Wykrywanie informacji
Na zapleczu narzędzie kubectl używa wykrywania informacji z serwera API, aby poznać nowe
zasoby. Przyjrzyjmy się teraz dokładnie procesowi wykrywania.
... Accept:
application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json
NAME AGE
example-at 43s
$ http localhost:8080/apis/
{
“groups”: [{
“name”: “at.cnat.programming-kubernetes.info”,
“preferredVersion”: {
“groupVersion”: “cnat.programming-kubernetes.info/v1”,
“version”: “v1alpha1“
},
“versions”: [{
“groupVersion”: “cnat.programming-kubernetes.info/v1alpha1”,
“version”: “v1alpha1”
}]
}, ...]
}
$ http localhost:8080/apis/cnat.programming-kubernetes.info/v1alpha1
“apiVersion”: “v1”,
“groupVersion”: “cnat.programming-kubernetes.info/v1alpha1”,
“kind”: “APIResourceList”,
“resources”: [{
“kind”: “At”,
“name”: “ats”,
“namespaced”: true,
}, ...]
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: nazwa
spec:
group: nazwa grupy
names:
validation: # Opcjonalne
openAPIV3Schema: schemat OpenAPI # Opcjonalne
subresources: # Opcjonalne
scale: # Opcjonalne
niestandardowego zasobu
do składowania obiektu
- ...
Wiele z tych pól jest opcjonalnych lub ma wartość domyślną. Te pola zostaną opisane
szczegółowo w następnych punktach.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ats.cnat.programming-kubernetes.info
spec:
group: cnat.programming-kubernetes.info
names:
kind: At
listKind: AtList
plural: ats
singular: at
scope: Namespaced
subresources:
status: {}
validation:
openAPIV3Schema:
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
schedule:
type: string
type: object
status:
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: At
listKind: AtList
plural: ats
singular: at
conditions:
- lastTransitionTime: “2019-03-17T09:44:21Z”
message: no conflicts found
reason: NoConflicts
status: “True”
type: NamesAccepted
- lastTransitionTime: null
status: “True”
type: Established
storedVersions:
- v1alpha1
Widać tu, że jeśli pole z nazwą jest puste, w specyfikacji używane są wartości domyślne, a
nazwy w statusie zostają opisane jako zaakceptowane. Ponadto ustawiane są następujące
warunki:
Zaawansowane mechanizmy
niestandardowych zasobów
W tym podrozdziale opisane są zaawansowane mechanizmy niestandardowych zasobów, np.
sprawdzanie poprawności i podzasoby.
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
type: object
properties:
schedule:
type: string
pattern: “^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])...”
command:
type: string
required:
- schedule
- command
status:
type: object
properties:
phase:
type: string
required:
- metadata
- apiVersion
- kind
- spec
[1]
W tym schemacie określone jest, że wartość to obiekt w formacie JSON . Oznacza to, że
używana jest mapa łańcuchów znaków, a nie wycinek lub wartość taka jak liczba. Ponadto
obiekt ten ma (oprócz właściwości metadata, kind i apiVersion, pośrednio zdefiniowanych dla
niestandardowych zasobów) dwie dodatkowe właściwości: spec i status.
Każda z tych właściwości też jest obiektem w formacie JSON. Wymagane pola właściwości spec
to łańcuchy znaków schedule i command. Pole schedule musi pasować do wzorca dat w
formacie ISO (przedstawionego tu za pomocą wyrażenia regularnego). Opcjonalna właściwość
status zawiera pole tekstowe phase.
Ręczne tworzenie schematów OpenAPI może być żmudne. Na szczęście trwają prace nad tym,
aby znacznie uprościć ten proces dzięki generowaniu kodu. W projekcie Kubebuilder (zob.
punkt „Kubebuilder”) opracowano narzędzie crd-gen. Jest ono dostępne w repozytorium
sigs.k8s.io/controller-tools (http://bit.ly/2J00kvi) i rozbudowywane krok po kroku, aby było
przydatne także w innych kontekstach. Generator crd-schema-gen (http://bit.ly/31N0eQf) to
fork narzędzia crd-gen rozwijany w tym kierunku.
$ kubectl api-resources
NAME SHORTNAMES APIGROUP NAMESPACED KIND
bindings true Binding
...
Narzędzie kubectl poznaje krótkie nazwy za pomocą wykrywania informacji (zob. punkt
„Wykrywanie informacji”). Oto przykład:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ats.cnat.programming-kubernetes.info
spec:
...
shortNames:
- at
Następnie za pomocą polecenia kubectl get at można wyświetlić wszystkie niestandardowe
zasoby cnat z danej przestrzeni nazw.
Niestandardowe zasoby (podobnie jak wszystkie inne) mogą należeć do kategorii. Najczęściej
używana jest kategoria all, np. w poleceniu kubectl get all, które wyświetla wszystkie
dostępne dla użytkownika zasoby klastra, np. pody i usługi.
Niestandardowe zasoby zdefiniowane w klastrze można dodać do istniejącej lub nowej
kategorii, używając pola categories:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ats.cnat.programming-kubernetes.info
spec:
...
categories:
- all
Przy tych ustawieniach polecenie kubectl get all wyświetli także niestandardowy zasób cnat
z danej przestrzeni nazw.
Wyświetlane kolumny
Narzędzie kubectl z interfejsu CLI używa wyświetlania po stronie serwera, aby generować
dane wyjściowe polecenia kubectl get. To oznacza, że kieruje do serwera API zapytania o
wyświetlane kolumny i wartości z każdego wiersza.
Także niestandardowe zasoby obsługują wyświetlanie kolumn po stronie serwera. Używane jest
tu pole additionalPrinterColumns. Człon additional (czyli dodatkowe) wynika z tego, że
pierwsza kolumna zawsze zawiera nazwę obiektu. Dodatkowe kolumny są definiowane w
następujący sposób:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ats.cnat.programming-kubernetes.info
spec:
additionalPrinterColumns: (optional)
- name: nazwa kolumn dla kubectl
additionalPrinterColumns: # (opcjonalne)
- name: schedule
type: string
JSONPath: .spec.schedule
- name: command
type: string
JSONPath: .spec.command
- name: phase
type: string
JSONPath: .status.phase
Teraz narzędzie kubectl wyświetli zasób cnat w następujący sposób:
Podzasoby
O podzasobach pokrótce wspomnieliśmy w punkcie „Podzasoby status — UpdateStatus”.
Podzasoby to specjalne punkty końcowe HTTP, w których przyrostek jest dodawany do ścieżki
HTTP zwykłego zasobu. Na przykład standardowa ścieżka HTTP poda to
/api/v1/namespace/przestrzeńnazw/pods/nazwa. Pody mają wiele podzasobów, np. /logs,
/portforward, /exec i /status. Ścieżki HTTP takich podzasobów to:
/api/v1/namespace/przestrzeńnazw/pods/nazwa/logs
/api/v1/namespace/przestrzeńnazw/pods/nazwa/portforward
/api/v1/namespace/przestrzeńnazw/pods/nazwa/exec
/api/v1/namespace/przestrzeńnazw/pods/nazwa/status
Dla punktów końcowych podzasobów używany jest inny protokół niż dla punktu końcowego
głównego zasobu.
W czasie, gdy powstaje ta książka, niestandardowe zasoby obsługują dwa podzasoby: /scale i
/status. Oba są opcjonalne, co oznacza, że trzeba je bezpośrednio aktywować w definicji CRD.
Podzasób status
Podzasób /status służy do oddzielania podanej przez użytkownika specyfikacji instancji
niestandardowego zasobu od określanego przez kontroler statusu. Głównym celem tego
rozwiązania jest rozdział uprawnień:
Kontrola dostępu oparta na rolach nie pozwala podawać reguł na takim poziomie
szczegółowości. Reguły zawsze są podawane na poziomie zasobu. Podzasób /status rozwiązuje
ten problem, ponieważ zapewnia dwa punkty końcowe, które same są zasobami. Każdy z nich
można niezależnie kontrolować za pomocą reguł kontroli dostępu opartej na rolach. Ten
mechanizm można nazwać podziałem na status i specyfikację. Oto przykład zastosowania
reguły tylko dla podzasobu /status zasobu ats (nazwa „ats” odpowiada głównemu zasobowi):
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: ...
rules:
- apiGroups: [“”]
resources: [“ats/status”]
verbs: [“update”, “patch”]
Zasoby (także niestandardowe), które mają podzasób /status, działają w nowy sposób. Dotyczy
to także punktu końcowego głównego zasobu.
Warto zauważyć, że w żądaniu aktualizacji przesyłane są zwykle zarówno pole spec, jak i
status, jednak w treści żądania można byłoby pominąć jedno z nich.
Zwróć też uwagę na to, że punkt końcowy /status ignoruje wszystko poza statusem, w tym
zmiany metadanych, np. etykiet i adnotacji.
Rozdział na status i specyfikację w niestandardowym zasobie można włączyć w następujący
sposób:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
spec:
subresources:
status: {}
...
Zauważ, że do pola status w tym fragmencie w formacie YAML przypisany jest pusty obiekt. W
ten sposób można ustawić wartość pola, które nie ma innych właściwości. Jeśli użyjesz tylko
kodu:
subresources:
status:
to przy sprawdzaniu poprawności wystąpi błąd, ponieważ w formacie YAML pole status będzie
miało wartość null.
...
versions:
- name: v1alpha1
served: true
storage: true
- name: v1beta1
served: true
subresources:
status: {}
Ten kod aktywuje podzasób /status dla wersji v1beta1, ale już nie dla wersji v1alpha1.
Podzasób scale
Drugim podzasobem dostępnym dla niestandardowych zasobów jest /scale. Podzasób ten jest
[2]
(uzyskanym w wyniku projekcji) widokiem zasobu, pozwalającym wyświetlać i modyfikować
wartości tylko w replice. Jest to dobrze znany podzasób dla zasobów takich jak instalacje i
zbiory replik Kubernetesa, które — co oczywiste — można skalować w obie strony.
Polecenie kubectl scale wykorzystuje podzasób /scale. Na przykład poniższy kod modyfikuje
liczbę replik w danej instancji:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
spec:
subresources:
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
labelSelectorPath: .status.labelSelector
...
Ten kod sprawia, że zaktualizowana liczba replik jest zapisywana w polu spec.replicas i
zwracana z tego pola w żądaniach GET.
Za pomocą podzasobu /status nie można zmodyfikować selektora etykiet (można go tylko
wczytać). Ten selektor ma zapewniać kontrolerowi informacje potrzebne do zliczania
określonych obiektów. Na przykład kontroler ReplicaSet zlicza pody zgodne z danym
selektorem.
Selektor etykiet jest opcjonalny. Jeśli działanie niestandardowego zasobu nie jest zgodne z
selektorami etykiet, nie podawaj ścieżki JSON dla takiego selektora.
W przykładowej instrukcji kubectl scale --replicas=3 ... wartość 3 jest zapisywana w polu
spec.replicas. Oczywiście można tu zastosować także inne proste ścieżki JSON.
Przykładowymi sensownymi nazwami pól mogłyby być spec.instances lub spec.size (zależy
to od kontekstu).
namespace: przestrzeń-nazw-niestandardowego-zasobu
uid: uid-niestandardowego-zasobu
resourceVersion: wersja-zasób-niestandardowego-zasobu
creationTimestamp: czas-utworzenia-niestandardowego-zasobu
spec:
replicas: 3
status:
replicas: 2
selector: “environment = production”
Współbieżność optymistyczna działa tak samo dla głównego zasobu i podzasobu /scale. Oznacza
to, że zapis głównego zasobu może powodować konflikty z zapisem podzasobu /scale i w drugą
stronę.
Klient dynamiczny
Klient dynamiczny z pakietu k8s.io/client-go/dynamic (http://bit.ly/2Y6eeSK) nie uwzględnia
znanych identyfikatorów GVK. Nie używa nawet żadnych typów języka Go innych niż
unstructured.Unstructured (http://bit.ly/2WYZ6oS), który opakowuje funkcję json.Unmarshal i
jej dane wyjściowe.
Klient dynamiczny nie używa ani schematu, ani obiektów typu RESTMapper. To oznacza, że
programista musi ręcznie określić wszystkie informacje o typach, podając zasoby (zob. punkt
„Zasoby”) w postaci identyfikatora GVR:
schema.GroupVersionResource{
Group: “apps”,
Version: “v1”,
Resource: “deployments”,
}
Jeśli dostępna jest konfiguracja klienta REST („Tworzenie i używanie klientów”), dynamicznego
klienta można utworzyć w jednym wierszu:
Musisz znać zasięg zasobu (z poziomu przestrzeni nazw lub klastra). Dla
zasobów z poziomu klastra wystarczy pominąć człon Namespace(namespace).
Dostępne są również typizowane wersje funkcji, które rzutują wartość na dany typ i zwracają
błąd, gdy taka operacja zakończy się niepowodzeniem:
func NestedBool(obj map[string]interface{}, fields ...string) (bool, bool,
error)
func NestedFloat64(obj map[string]interface{}, fields ...string)
Klienty typizowane
Klienty typizowane nie używają generycznych struktur danych podobnych do
map[string]interface{}. Zamiast tego korzystają z rzeczywistych typów języka Go, które są
różne i specyficzne dla poszczególnych identyfikatorów GVK. Te klienty są znacznie łatwiejsze
w użyciu, znacznie zwiększają bezpieczeństwo ze względu na typ, a także poprawiają spójność i
czytelność kodu. Ich wadą jest to, że oferują mniejszą elastyczność, ponieważ przetwarzane
typy muszą być znane w czasie kompilacji, a klienty są generowane, co zwiększa złożoność.
Budowa typu
Rodzaje są reprezentowane jako struktury języka Go. Zwykle struktura ma nazwę identyczną z
nazwą rodzaju (choć technicznie nie jest to wymagane) i jest umieszczona w pakiecie
odpowiadającym grupie i wersji z danego identyfikatora GVK. Często stosowaną konwencją jest
uwzględnianie identyfikatora GVK grupa/wersja.Rodzaj w pakiecie języka Go:
pkg/apis/grupa/wersja
i definiowanie struktury Rodzaj języka Go w pliku types.go.
Każdy typ języka Go odpowiadający identyfikatorowi GVK obejmuje strukturę TypeMeta z
pakietu k8s.io/apimachinery/pkg/apis/meta/v1 (http://bit.ly/2Y5HdWT). Struktura TypeMeta
zawiera tylko pola Kind i ApiVersion:
Dostępnych jest też wiele dodatkowych pól. Gorąco zachęcamy do lektury rozbudowanej
dokumentacji internetowej (http://bit.ly/2IutNyh), ponieważ daje ona dobry obraz
podstawowych możliwości obiektów Kubernetesa.
Typy najwyższego poziomu z Kubernetesa (mające zagnieżdżone struktury TypeMeta i
ObjectMeta oraz, w tym kontekście, utrwalane w systemie etcd) wyglądają bardzo podobnie do
siebie w tym sensie, że zwykle mają pola spec i status. Przyjrzyj się przykładowej instalacji z
pliku k8s.io/kubernetes/apps/v1/types.go (http://bit.ly/2RroTFb):
type Deployment struct {
metav1.TypeMeta `json:”,inline”`
metav1.ObjectMeta `json:”metadata,omitempty”`
// +k8s:deepcopy-gen=package
// +groupName=cnat.programming-kubernetes.info
package v1alpha1
Plik register.go zawiera funkcje pomocnicze do rejestrowania w schemacie (zob. punkt
„Schemat”) typów języka Go dla niestandardowych zasobów:
package wersja
import (
metav1 “k8s.io/apimachinery/pkg/apis/meta/v1”
“k8s.io/apimachinery/pkg/runtime”
“k8s.io/apimachinery/pkg/runtime/schema”
group “repozytorium/pkg/apis/grupa”
)
Version: “wersja”,
}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
package versioned
import (
discovery “k8s.io/client-go/discovery”
rest “k8s.io/client-go/rest”
flowcontrol “k8s.io/client-go/util/flowcontrol”
cnatv1alpha1 “.../cnat/cnat-client-go/pkg/generated/clientset/versioned/
)
*discovery.DiscoveryClient
cnatV1alpha1 *cnatv1alpha1.CnatV1alpha1Client
}
}
Zbiór klientów jest reprezentowany za pomocą interfejsu Interface i zapewnia dostęp do
interfejsu klienta grupy API dla każdej wersji. W przykładowym kodzie takim interfejsem jest
CnatV1alpha1Interface:
type CnatV1alpha1Interface interface {
RESTClient() rest.Interface
AtsGetter
}
“k8s.io/client-go/tools/clientcmd”
client “github.com/.../cnat/cnat-client-
go/pkg/generated/clientset/versioned”
)
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags(“”, *kubeconfig)
clientset, err := client.NewForConfig(config)
ats := clientset.CnatV1alpha1Interface().Ats(“default”)
book, err := ats.Get(“kubernetes-programming”, metav1.GetOptions{})
Widać tu, że mechanizmy generowania kodu umożliwiają zaprogramowanie logiki
niestandardowych zasobów w taki sam sposób jak dla podstawowych zasobów Kubernetesa.
Dostępne są także narzędzia wysokopoziomowe, np. informatory (zob. omówienie narzędzia
informer-gen w rozdziale 5.).
corev1 “k8s.io/api/core/v1”
metav1 “k8s.io/apimachinery/pkg/apis/meta/v1”
“k8s.io/client-go/kubernetes/scheme”
“k8s.io/client-go/tools/clientcmd”
runtimeclient “sigs.k8s.io/controller-runtime/pkg/client”
)
corev1 “k8s.io/api/core/v1”
metav1 “k8s.io/apimachinery/pkg/apis/meta/v1”
“k8s.io/client-go/kubernetes/scheme”
“k8s.io/client-go/tools/clientcmd”
runtimeclient “sigs.k8s.io/controller-runtime/pkg/client”
cnatv1alpha1 “github.com/.../cnat/cnat-
kubebuilder/pkg/apis/cnat/v1alpha1”
)
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags(“”, *kubeconfig)
crScheme := runtime.NewScheme()
cnatv1alpha1.AddToScheme(crScheme)
Podsumowanie
W tym rozdziale przedstawiliśmy niestandardowe zasoby — podstawowy mechanizm
rozszerzania używany w ekosystemie Kubernetesa. Na tym etapie powinieneś dobrze rozumieć
ich możliwości i ograniczenia, a także znać dostępne klienty.
Pora przejść do generowania kodu służącego do zarządzania takimi zasobami.
1 Nie pomyl tutaj obiektów Kubernetesa z obiektami w formacie JSON. Te ostatnie to inna
nazwa map z łańcuchami znaków używanych w formacie JSON i w OpenAPI.
2 „Uzyskany w wyniku projekcji” oznacza tu tyle, że obiekt scale jest projekcją głównego
zasobu (w tym sensie, że wyświetlane są tylko określone pola, a wszystko pozostałe jest
ukrywane).
Rozdział 5. Automatyzowanie
generowania kodu
Z tego rozdziału dowiesz się, jak w projektach w języku Go używać generatorów kodu dla
Kubernetesa, aby tworzyć niestandardowe zasoby w naturalny sposób. Generatory kodu są
używane w wielu miejscach w implementacji natywnych zasobów Kubernetesa. Tu użyjemy tych
samych generatorów.
Na bardzo wczesnym etapie rozwoju Kubernetesa trzeba było przerabiać coraz większe ilości
kodu wraz z dodawaniem do systemu nowych zasobów. Generatory znacznie uprościły
konserwację takiego kodu. Już dawno utworzono bibliotekę Gengo (http://bit.ly/2L9kwNJ), a
później na jej podstawie kolekcję generatorów k8s.io/code-generator (http://bit.ly/2Kw8I8U)
przeznaczonych do użytku w kodzie spoza Kubernetesa. Tych generatorów będziemy używać w
następnych podrozdziałach dla niestandardowych zasobów.
Wywoływanie generatorów
Generatory kodu są zwykle wywoływane w prawie ten sam sposób we wszystkich projektach
kontrolerów. Różne są tylko pakiety, nazwy grup i wersje API. Wywołanie skryptu k8s.io/code-
generator/generate-groups.sh lub skryptu powłoki Bash takiego jak hack/update-codegen.sh to
najłatwiejszy sposób na dodanie na poziomie systemu budowania generatora kodu dla typów
języka Go odpowiadających niestandardowym zasobom (zob. powiązane z książką repozytorium
w serwisie GitHub — http://bit.ly/2J0s2YL).
github.com/programming-kubernetes/cnat/cnat-client-go/pkg/generated
github.com/programming-kubernetes/cnat/cnat-client-go/pkg/apis \
cnat:v1alpha1 \
--output-base “${GOPATH}/src” \
--go-header-file “hack/boilerplate.go.txt”
W tym kodzie all oznacza wywołanie wszystkich czterech standardowych generatorów kodu
dla niestandardowych zasobów:
deepcopy-gen
lister-gen
Tworzy dla niestandardowych zasobów listery, które udostępniają warstwę bufora tylko do
odczytu na potrzeby żądań GET i LIST.
conversion-gen
defaulter-gen
Odpowiada za ustawianie wartości domyślnych w wybranych polach.
Niektóre generatory, np. deepcopy-gen, tworzą pliki bezpośrednio w pakietach grupy API. Te
pliki mają nazwy zgodne ze standardowym schematem (z przedrostkiem zz_generated.), dzięki
czemu łatwo jest je wykluczać w systemie kontroli wersji (np. za pomocą pliku .gitignore), choć
w większości projektów generowane pliki są jednak dodawane do takiego systemu, ponieważ
[1]
narzędzia języka Go związane z generatorami kodu nie są w pełni dopracowane .
$ hack/update-codegen.sh
// +jakiś-znacznik
// +inny-znacznik=wartość
// +znacznik-w-drugim-bloku-komentarza
// +znacznik-w-pierwszym-bloku-komentarza
type Foo struct {
}
Znaczniki globalne
Znaczniki globalne są zapisywane w pliku doc.go pakietu. Typowy plik
pkg/apis/grupa/wersja/doc.go wygląda tak:
// +k8s:deepcopy-gen=package
// +groupName=cnat.programming-kubernetes.info
package v1alpha1
Pierwszy wiersz tego pliku nakazuje generatorowi deepcopy-gen domyślne tworzenie metod do
głębokiego kopiowania dla każdego typu z danego pakietu. Jeśli masz typy, w których głębokie
kopiowanie nie jest potrzebne, pożądane, a nawet możliwe, możesz wyłączyć tworzenie takich
metod za pomocą lokalnego znacznika // +k8s:deepcopy-gen=false. Jeżeli nie włączysz
generowania metod do głębokiego kopiowania na poziomie pakietu, będziesz musiał aktywować
proces generowania ich w każdym typie, w którym takie metody są potrzebne. Służy do tego
znacznik // +k8s:deepcopy-gen=true.
Gdy używany jest znacznik // +groupName, generator klientów (zob. punkt „Klienty typizowane
tworzone za pomocą generatora client-gen”) wygeneruje klienta z użyciem poprawnej ścieżki
HTTP /apis/foo.project.example.com. Oprócz znacznika +groupName istnieje też znacznik
+groupGoName, definiujący niestandardowy identyfikator języka Go (dla nazw zmiennych i
typów), który będzie używany zamiast nazwy nadrzędnego pakietu. Generatory domyślnie
używają dla identyfikatorów nazwy nadrzędnego pakietu pisanej wielką literą. W naszym
przykładzie będzie to Cnat. Lepszym identyfikatorem byłoby CNAt (od ang. Cloud Native At). Za
pomocą znacznika // +groupGoName=CNAt można zastosować taki identyfikator zamiast Cnat
(choć w przykładzie nie wykorzystaliśmy tej możliwości i pozostawiliśmy identyfikator Cnat), a
wynik wygenerowany przez narzędzie client-gen będzie wyglądał wtedy tak:
Discovery() discovery.DiscoveryInterface
CNatV1() atv1alpha1.CNatV1alpha1Interface
Znaczniki lokalne
Znaczniki lokalne są zapisywane albo bezpośrednio nad typem API, albo w drugim bloku
komentarza nad nim. Oto główne typy z pliku types.go z przykładowego projektu cnat
(http://bit.ly/31QosJw):
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type At struct {
metav1.TypeMeta `json:”,inline”`
metav1.ObjectMeta `json:”metadata,omitempty”`
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
metav1.TypeMeta `json:”,inline”`
metav1.ListMeta `json:”metadata,omitempty”`
Gdyby w pakiecie z typami API istniała struktura pomocnicza (zwykle jest to odradzane, aby
zachować przejrzystość pakietów z typami API), konieczne byłoby wyłączenie dla niej
generowania metod do obsługi głębokiego kopiowania. Oto przykład:
// +k8s:deepcopy-gen=false
//
// Helper to struktura pomocnicza, a nie typ API.
...
runtime.Object i DeepCopyObject
Istnieje specjalny znacznik związany z głębokim kopiowaniem, który wymaga dodatkowych
objaśnień:
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Tło historyczne
Przed wersją 1.8 Kubernetesa schemat (zob. punkt „Schemat”) przechowywał referencje
do specyficznych dla typów funkcji do obsługi głębokiego kopiowania. Używana wtedy
była implementacja głębokiego kopiowania oparta na refleksji. Oba te mechanizmy były
źródłami wielu skomplikowanych i trudnych do wykrycia błędów. Dlatego w Kubernetesie
przestawiono się na statyczne głębokie kopiowanie z użyciem metody DeepCopyObject z
interfejsu runtime.Object.
Metoda DeepCopyObject() nie robi nic oprócz wywoływania wygenerowanej metody DeepCopy.
Sygnatura tej ostatniej zależy od typu (DeepCopy() *T zależy od typu T). Sygnatura tej
pierwszej to zawsze DeepCopyObject() runtime.Object:
func (in *T) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
Zdarza się, że także inne interfejsy wymagają głębokiego kopiowania. Załóżmy, że typ API ma
pole typu interfejsowego Foo:
}
Wiesz już, że typy API muszą umożliwiać głębokie kopiowanie. Dlatego także pole Foo musi być
uwzględnione przy głębokim kopiowaniu. Jak zrobić to w generyczny sposób (bez rzutowania
typów), nie dodając metody DeepCopyFoo() Foo do interfejsu Foo?
type Foo interface {
...
DeepCopyFoo() Foo
}
W takiej sytuacji można posłużyć się omawianym znacznikiem:
// +k8s:deepcopy-gen:interfaces=<package>.Foo
type FooImplementation struct {
...
}
W kodzie źródłowym Kubernetesa ten znacznik jest używany także w kilku innych miejscach
poza typem runtime.Object:
// +k8s:deepcopy-gen:interfaces=…/pkg/registry/rbac/reconciliation.RuleOwner
// +k8s:deepcopy-
gen:interfaces=…/pkg/registry/rbac/reconciliation.RoleBinding
Ten znacznik informuje generator client-gen, że należy utworzyć klienta dla danego typu (ten
znacznik nigdy nie jest domyślnie włączony). Warto zauważyć, że nie musisz (a nawet nie
możesz) stosować go do typów List obiektów API.
Generator klientów musi wybrać odpowiednią ścieżkę HTTP — z przestrzenią nazw lub bez niej.
Dla zasobów z poziomu klastra trzeba używać następującego znacznika:
// +genclient:nonNamespaced
Domyślnie generowany jest klient z poziomu przestrzeni nazw. Konieczne jest dopasowanie do
ustawienia zasięgu w manifeście definicji CRD. Dla klientów o specjalnym przeznaczeniu
przydatne może być też precyzyjne kontrolowanie tego, które metody HTTP są dostępne.
Można to zrobić za pomocą kilku znaczników. Oto przykład:
// +genclient:noVerbs
// +genclient:onlyVerbs=create,delete
// +genclient:skipVerbs=get,list,create,update,patch,delete,watch
// +genclient:method=Create,verb=create,
// result=k8s.io/apimachinery/pkg/apis/meta/v1.Status
Pierwsze trzy powinny być oczywiste. Ostatni wymaga jednak pewnych objaśnień.
Typ, nad którym znajduje się ten znacznik, będzie obsługiwał tylko operację tworzenia i nie
będzie zwracał samego typu API; zamiast tego zwracana będzie wartość typu metav1.Status.
W przypadku zasobów niestandardowych takie rozwiązanie nie ma sensu, jednak dla
napisanych w Go serwerów API udostępnianych przez użytkowników (zob. rozdział 8.) takie
zasoby mogą istnieć (i zdarza się to w praktyce).
Znacznik // +genclient:method= często stosuje się, aby dodać metodę do skalowania zasobu.
W punkcie „Podzasób scale” opisaliśmy, jak włączyć zasób /scale dla niestandardowych
zasobów. Znaczniki wymienione poniżej pozwalają utworzyć w klientach potrzebne metody:
// +genclient:method=GetScale,verb=get,subresource=scale,\
// result=k8s.io/api/autoscaling/v1.Scale
// +genclient:method=UpdateScale,verb=update,subresource=scale,\
//
input=k8s.io/api/autoscaling/v1.Scale,result=k8s.io/api/autoscaling/v1.Scale
Pierwszy z tych znaczników tworzy getter GetScale. Drugi pozwala wygenerować setter
UpdateScale.
Nie wahaj się też zaglądać do kodu generatorów. Dokumentacja generatora deepcopy-gen jest
dostępna w pliku main.go (http://bit.ly/2x9HmN4), a dokumentację generatora client-gen
znajdziesz w dokumentacji dla programistów pracujących nad Kubernetesem
(http://bit.ly/2WYNlns). Obecnie nie istnieje dodatkowa dokumentacja dla generatorów
informer-gen i lister-gen, choć w pliku generate-groups.sh pokazane jest, jak wywoływać
każdy z nich (http://bit.ly/31MeSHp).
Podsumowanie
W tym rozdziale pokazaliśmy, jak używać generatorów kodu Kubernetesa dla niestandardowych
zasobów. Po omówieniu tego zagadnienia przejdziemy do narzędzi z wyższych poziomów
abstrakcji — do rozwiązań służących do tworzenia niestandardowych kontrolerów i operatorów,
które umożliwiają skupienie się na logice biznesowej.
Czynności wstępne
W tym rozdziale podstawowym przykładem używanym do prezentacji różnych rozwiązań będzie
projekt cnat (polecenie at natywne dla chmury, opisane w punkcie „Przykład wprowadzający”).
Jeśli chcesz równolegle wykonywać opisywane tu czynności, zwróć uwagę na następujące
założenia:
1. Masz zainstalowaną i odpowiednio skonfigurowaną wersję 1.12 (lub nowszą) języka Go.
2. Posiadasz dostęp do klastra Kubernetesa w wersji 1.12 (lub nowszej) — albo lokalnie, np.
za pomocą narzędzia kind lub k3d, albo zdalnie, z użyciem wybranego dostawcy usług w
chmurze. Ponadto masz narzędzie kubectl skonfigurowane na potrzeby dostępu do tego
klastra.
3. Sklonowałeś nasze repozytorium GitHub (http://bit.ly/2N3R6U4) za pomocą polecenia git
clone. To repozytorium zawiera kompletny i działający kod źródłowy oraz niezbędne
polecenia pokazane w dalszych punktach. Warto zauważyć, że tu pokazujemy, jak
opracować rozwiązanie od podstaw. Jeśli chcesz zobaczyć same efekty prac, to zamiast
samodzielnie wykonywać omawiane kroki, możesz sklonować repozytorium i wywołać
tylko polecenia instalujące definicje CRD i niestandardowe zasoby oraz uruchamiające
niestandardowy kontroler.
Po omówieniu kwestii porządkowych pora przejść do pisania operatorów. W tym rozdziale
omówimy narzędzia sample-controller, Kubebuilder i Operator SDK.
Gotów poznać Go? Bierzmy się za niego (gra słów zamierzona).
Może zwróciłeś uwagę na to, że jako bazowy adres URL w tej książce używany
jest człon k8s.io. Zaczęliśmy korzystać z niego w rozdziale 3. Warto
przypomnieć, że jest to alias nazwy kubernetes.io, a w kontekście zarządzania
pakietami języka Go nazwa ta odpowiada repozytorium
github.com/kubernetes. Człon k8s.io nie zapewnia automatycznych
przekierowań. Dlatego np. nazwa k8s.io/sample-controller oznacza, że
powinieneś użyć repozytorium github.com/kubernetes/sample-controller
(http://bit.ly/2UppsTN).
Przygotowania
Najpierw wywołaj polecenie go get k8s.io/sample-controller, aby pobrać kod źródłowy i
zależności do systemu. Powinieneś używać ścieżki $GOPATH/src/k8s.io/sample-\controller.
$ go build -o cnat-controller .
NAME CREATED AT
foos.samplecontroller.k8s.io 2019-05-29T12:16:57Z
W zależności od używanej dystrybucji Kubernetesa możesz zobaczyć także wiele innych
definicji CRD. Jednak dostępna powinna być przynajmniej pozycja foos.samplecontroller.k8s.io.
foo.samplecontroller.k8s.io/example-foo created
deployment.extensions/example-foo 1/1 1 1
67s
NAME AGE
foo.samplecontroller.k8s.io/example-foo 67s
Logika biznesowa
Aby rozpocząć implementowanie logiki biznesowej, najpierw zmień nazwę istniejącego katalogu
pkg/apis/samplecontroller na pkg/apis/cnat, a następnie utwórz własną definicję CRD i
niestandardowy zasób:
$ cat artifacts/examples/cnat-crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ats.cnat.programming-kubernetes.info
spec:
group: cnat.programming-kubernetes.info
version: v1alpha1
names:
kind: At
plural: ats
scope: Namespaced
$ cat artifacts/examples/cnat-example.yaml
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
labels:
controller-tools.k8s.io: “1.0”
name: example-at
spec:
schedule: “2019-04-12T10:12:00Z”
$ ./hack/update-codegen.sh
pkg/apis/cnat/v1alpha1/zz_generated.deepcopy.go,
pkg/generated/∗.
W pliku types.go zwróć uwagę na trzy stałe reprezentujące trzy fazy działania zasobu At. Do
czasu zaplanowanego wykonania operacji zasób znajduje się w stanie PENDING, do czasu
ukończenia operacji ten stan to RUNNING, a ostatecznie zasób przechodzi do stanu DONE:
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
const (
PhasePending = “PENDING”
PhaseRunning = “RUNNING”
PhaseDone = “DONE”
c.workqueue.AddRateLimited(key)
c.workqueue.AddAfter(key, when)
} else {
c.workqueue.Forget(obj)
Teraz gdy już dobrze rozumiesz ogólne działanie tego podejścia, pora przejść do logiki
biznesowej. Najważniejsza jest w niej funkcja syncHandler(), w której zaimplementowana
będzie logika biznesowa naszego niestandardowego kontrolera. Oto sygnatura tej funkcji:
// syncHandler porównuje aktualny stan z oczekiwanym i próbuje sprawić, aby
[1]
W funkcji syncHandler() zaimplementowane są następujące przejścia między stanami :
// Jeśli żaden etap nie jest ustawiony, domyślnie ustawiany jest status
początkowy — pending.
if instance.Status.Phase == “” {
instance.Status.Phase = cnatv1alpha1.PhasePending
case cnatv1alpha1.PhasePending:
d, err := timeUntilSchedule(instance.Spec.Schedule)
if err != nil {
utilruntime.HandleError(fmt.Errorf(“błąd przetwarzania harmonogramu:
%v”, err))
// Błąd przy odczycie harmonogramu — ponowne zakolejkowanie żądania:
return d, nil
}
klog.Infof(
instance.Status.Phase = cnatv1alpha1.PhaseRunning
case cnatv1alpha1.PhaseRunning:
pod := newPodForCR(instance)
pod.ObjectMeta.OwnerReferences = append(pod.ObjectMeta.OwnerReferences,
*owner)
// Próba sprawdzenia, czy pod już istnieje. Jeśli nie istnieje (co jest
// oczekiwane), należy na podstawie specyfikacji utworzyć pod na potrzeby
zadania:
found, err := c.kubeClientset.CoreV1().Pods(pod.Namespace).
Get(pod.Name, metav1.GetOptions{})
if err != nil && errors.IsNotFound(err) {
}
klog.Infof(“instancja %s: uruchomiono pod: name=%s”, key, pod.Name)
found.Status.Phase == corev1.PodSucceeded {
klog.Infof(
“instancja %s: kontener zakończył pracę: przyczyna=%q
komunikat=%q”,
key, found.Status.Reason, found.Status.Message,
)
instance.Status.Phase = cnatv1alpha1.PhaseDone
} else {
case cnatv1alpha1.PhaseDone:
klog.Infof(“instancja %s: status: DONE”, key)
if err != nil {
return time.Duration(0), err
kubeClientset kubernetes.Interface,
cnatClientset clientset.Interface,
atInformer informers.AtInformer,
podInformer corev1informer.PodInformer) *Controller {
utilruntime.Must(cnatscheme.AddToScheme(scheme.Scheme))
klog.V(4).Info(“Tworzenie obiektu do rozgłaszania zdarzeń”)
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(klog.Infof)
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{
Interface: kubeClientset.CoreV1().Events(“”),
})
source := corev1.EventSource{Component: controllerAgentName}
rateLimiter := workqueue.DefaultControllerRateLimiter()
controller := &Controller{
kubeClientset: kubeClientset,
cnatClientset: cnatClientset,
atLister: atInformer.Lister(),
atsSynced: atInformer.Informer().HasSynced,
podLister: podInformer.Lister(),
podsSynced: podInformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(rateLimiter,
“Ats”),
recorder: recorder,
}
AddFunc: controller.enqueueAt,
UpdateFunc: func(old, new interface{}) {
controller.enqueueAt(new)
},
})
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.enqueuePod,
UpdateFunc: func(old, new interface{}) {
controller.enqueuePod(new)
},
})
return controller
}
Potrzebne są też dwie funkcje pomocnicze, aby to rozwiązanie zadziałało. Jedna z nich oblicza
czas do momentu zaplanowanego uruchomienia polecenia:
layout := “2006-01-02T15:04:05Z”
s, err := time.Parse(layout, schedule)
if err != nil {
return time.Duration(0), err
}
return s.Sub(now), nil
}
labels := map[string]string{
“app”: cr.Name,
}
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name + “-pod”,
Namespace: cr.Namespace,
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: “busybox”,
Image: “busybox”,
Command: strings.Split(cr.Spec.Command, “ “),
},
},
RestartPolicy: corev1.RestartPolicyOnFailure,
},
}
}
Te dwie funkcje pomocnicze i zaprezentowany tu podstawowy przepływ logiki biznesowej
wykorzystamy ponownie w funkcji syncHandler() w dalszej części rozdziału, dlatego
koniecznie dokładnie zapoznaj się z tymi funkcjami.
Warto zauważyć, że w zasobie At pod jest zasobem pomocniczym, a kontroler musi zadbać o
usunięcie podów (w przeciwnym razie występuje ryzyko powstania osieroconych podów).
Kontroler sample-controller jest dobrym narzędziem do nauczenia się, jak budować takie
komponenty. Jednak zwykle celem jest skoncentrowanie się na logice biznesowej, a nie na
szablonowym kodzie. W tym obszarze dostępne są dwa powiązane projekty: Kubebuilder i
Operator SDK. Przyjrzyjmy się każdemu z nich i temu, jak przy ich użyciu zaimplementować
narzędzie cnat.
Kubebuilder
Kubebuilder (http://bit.ly/2I8w9mz), którego właścicielem i opiekunem jest grupa SIG (ang.
Special Interest Group) API Machinery w społeczności skupionej wokół Kubernetesa, to zestaw
bibliotek umożliwiających łatwe i wydajne tworzenie operatorów. Najlepszym źródłem do
dokładnego zapoznania się z Kubebuilderem jest dostępna w internecie książka na temat tego
narzędzia (https://book.kubebuilder.io). Opisane są w niej komponenty Kubebuildera i sposoby
jego używania. Jednak tu skoncentrujemy się na tym, jak za pomocą Kubebuildera
zaimplementować operator dla naszego narzędzia cnat (http://bit.ly/2RpHhON; zob. też
odpowiedni katalog w repozytorium Git — http://bit.ly/2Iv6pAS).
$ dep version
dep:
version : v0.5.1
go version : go1.12
go compiler : gc
platform : darwin/amd64
features : ImportDuringSolve=false
$ kustomize version
Version: {KustomizeVersion:v2.0.3
GitCommit:a6f65144121d1955266b0cd836ce954c04122dc8
BuildDate:2019-03-18T22:15:21+00:00 GoOs:darwin GoArch:amd64}
$ kubebuilder version
Version: version.Version{
KubeBuilderVersion:”1.0.8”,
KubernetesVendor:”1.13.1”,
GitCommit:”1adf50ed107f5042d7472ba5ab50d5e1d357169d”,
Przygotowania
Aby utworzyć początkową wersję operatora cnat, użyjemy polecenia init (zauważ, że w
zależności od środowiska jego wykonywanie może zająć kilka minut):
$ kubebuilder init \
--domain programming-kubernetes.info \
--license apache2 \
y
dep ensure
Running make...
make
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
.
├── Dockerfile
├── Gopkg.lock
├── Gopkg.toml
├── Makefile
├── PROJECT
├── bin
│ └── manager
├── cmd
│ └── manager
│ └── main.go
├── config
│ ├── crds
│ ├── default
│ │ ├── kustomization.yaml
│ │ ├── manager_auth_proxy_patch.yaml
│ │ ├── manager_image_patch.yaml
│ │ └── manager_prometheus_metrics_patch.yaml
│ ├── manager
│ │ └── manager.yaml
│ └── rbac
│ ├── auth_proxy_role.yaml
│ ├── auth_proxy_role_binding.yaml
│ ├── auth_proxy_service.yaml
│ ├── rbac_role.yaml
│ └── rbac_role_binding.yaml
├── cover.out
├── hack
│ └── boilerplate.go.txt
└
└── pkg
├── apis
│ └── apis.go
├── controller
│ └── controller.go
└── webhook
└── webhook.go
13 directories, 22 files
Następnie należy utworzyć API (czyli niestandardowy kontroler), używając polecenia create
api. Wykonywanie tego polecenia powinno zająć mniej czasu, ale też może chwilę potrwać:
$ kubebuilder create api \
--group cnat \
--version v1alpha1 \
--kind At
Running make...
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
go vet ./pkg/... ./cmd/...
│ ├── kustomization.yaml
│ ├── manager_auth_proxy_patch.yaml
│ ├── manager_image_patch.yaml
│ └── manager_prometheus_metrics_patch.yaml
├── manager
│ └── manager.yaml
├── rbac
│ ├── auth_proxy_role.yaml
│ ├── auth_proxy_role_binding.yaml
│ ├── auth_proxy_service.yaml
│ ├── rbac_role.yaml
│ └── rbac_role_binding.yaml
└── samples
└── cnat_v1alpha1_at.yaml
pkg/
├── apis
│ ├── addtoscheme_cnat_v1alpha1.go
│ ├── apis.go
│ └── cnat
│ ├── group.go
│ └── v1alpha1
│ ├── at_types.go
│ ├
│ ├── at_types_test.go
│ ├── doc.go
│ ├── register.go
│ ├── v1alpha1_suite_test.go
│ └── zz_generated.deepcopy.go
├── controller
│ ├── add_at.go
│ ├── at
│ │ ├── at_controller.go
│ │ ├── at_controller_suite_test.go
│ │ └── at_controller_test.go
│ └── controller.go
└── webhook
└── webhook.go
11 directories, 27 files
Zwróć uwagę na pojawienie się pliku cnat_v1alpha1_at.yaml z definicją CRD w katalogu
config/crds/, a także pliku cnat_v1alpha1_at.yaml (tak, to ta sama nazwa; ten plik reprezentuje
przykładową instancję niestandardowego zasobu opartego na wspomnianej definicji CRD) w
katalogu config/samples/. Ponadto w katalogu pkg/ pojawiło się kilka nowych plików.
Najważniejsze z nich to apis/cnat/v1alpha1/at_types.go i controller/at/at_controller.go. Wkrótce
je zmodyfikujemy.
Następnie należy utworzyć w Kubernetesie specjalną przestrzeń nazw, cnat, i ustawić ją jako
domyślną przestrzeń nazw. Kontekst należy ustawić w następujący sposób (dobrą praktyką jest
używanie specjalnej przestrzeni nazw zamiast przestrzeni nazw default):
$ kubectl create ns cnat && \
kubectl config set-context $(kubectl config current-context) --
namespace=cnat
{“level”:”info”,”ts”:1559152740.0550249,”logger”:”entrypoint”,
“msg”:”setting up client for manager”}
{“level”:”info”,”ts”:1559152740.057556,”logger”:”entrypoint”,
“msg”:”setting up manager”}
{“level”:”info”,”ts”:1559152740.1396701,”logger”:”entrypoint”,
“msg”:”Registering Components.”}
{“level”:”info”,”ts”:1559152740.1397,”logger”:”entrypoint”,
“msg”:”setting up scheme”}
{“level”:”info”,”ts”:1559152740.139773,”logger”:”entrypoint”,
“msg”:”Setting up controller”}
{“level”:”info”,”ts”:1559152740.139831,”logger”:”kubebuilder.controller”,
“msg”:”Starting EventSource”,”controller”:”at-controller”,
“source”:”kind source: /, Kind=”}
{“level”:”info”,”ts”:1559152740.139929,”logger”:”kubebuilder.controller”,
“msg”:”Starting EventSource”,”controller”:”at-controller”,
“source”:”kind source: /, Kind=”}
{“level”:”info”,”ts”:1559152740.139971,”logger”:”entrypoint”,
“msg”:”setting up webhooks”}
{“level”:”info”,”ts”:1559152740.13998,”logger”:”entrypoint”,
“msg”:”Starting the Cmd.”}
{“level”:”info”,”ts”:1559152740.244628,”logger”:”kubebuilder.controller”,
“msg”:”Starting Controller”,”controller”:”at-controller”}
{“level”:”info”,”ts”:1559152740.344791,”logger”:”kubebuilder.controller”,
...
{“level”:”info”,”ts”:1559153311.659829,”logger”:”controller”,
“msg”:”Creating Deployment”,”namespace”:”cnat”,”name”:”at-sample-
deployment”}
{“level”:”info”,”ts”:1559153311.678407,”logger”:”controller”,
“msg”:”Updating Deployment”,”namespace”:”cnat”,”name”:”at-sample-
deployment”}
{“level”:”info”,”ts”:1559153311.6839428,”logger”:”controller”,
“msg”:”Updating Deployment”,”namespace”:”cnat”,”name”:”at-sample-
deployment”}
{“level”:”info”,”ts”:1559153311.693443,”logger”:”controller”,
“msg”:”Updating Deployment”,”namespace”:”cnat”,”name”:”at-sample-
deployment”}
{“level”:”info”,”ts”:1559153311.7023401,”logger”:”controller”,
“msg”:”Updating Deployment”,”namespace”:”cnat”,”name”:”at-sample-
deployment”}
{“level”:”info”,”ts”:1559153332.986961,”logger”:”controller”,#
“msg”:”Updating Deployment”,”namespace”:”cnat”,”name”:”at-sample-
deployment”}
To informuje, że ogólna konfiguracja zakończyła się powodzeniem! Teraz, po przygotowaniu
szkieletu i udanym uruchomieniu operatora cnat, możemy przejść do podstawowego zadania:
implementowania logiki biznesowej narzędzia cnat za pomocą Kubebuildera.
Logika biznesowa
Na początek zmodyfikujemy pliki config/crds/cnat_v1alpha1_at.yaml (http://bit.ly/2N1jQNb) i
config/samples/cnat_v1alpha1_at.yaml (http://bit.ly/2Xs1F7c) zgodnie z definicjami CRD z
projektu cnat i wartościami niestandardowych zasobów. Ponownie użyjemy tu struktur z
punktu „Wzorowanie się na projekcie sample-controller”.
PhasePending = “PENDING”
PhaseRunning = “RUNNING”
PhaseDone = “DONE”
)
}
// AtStatus definiuje zaobserwowany status zasobu rodzaju At.
type AtStatus struct {
// Phase reprezentuje status harmonogramu. Do czasu wykonania polecenia
// status to PENDING, następnie status to DONE.
if errors.IsNotFound(err) {
// Nie znaleziono obiektu żądania. Mógł zostać usunięty po
// żądaniu uzgodnienia stanu. Należy zwrócić sterowanie i nie
kolejkować ponownie żądania:
}
// Pora utworzyć główny blok z sekcjami case i zaimplementować
// diagram stanów PENDING -> RUNNING -> DONE.
switch instance.Status.Phase {
case cnatv1alpha1.PhasePending:
reqLogger.Info(“Etap: PENDING”)
// Dopóki nie wykonano polecenia, trzeba sprawdzać, czy nie
// nadszedł czas na jego wykonanie:
reqLogger.Info(“Sprawdzanie harmonogramu”, “Target”,
instance.Spec.Schedule)
// Sprawdzanie, czy nadszedł już czas na wykonanie polecenia.
// Dokładność czasu to dwie sekundy:
d, err := timeUntilSchedule(instance.Spec.Schedule)
if err != nil {
// do zaplanowanego czasu.
return reconcile.Result{RequeueAfter: d}, nil
}
reqLogger.Info(“Nadszedł czas!”, “Gotowy do wykonania”,
instance.Spec.Command)
instance.Status.Phase = cnatv1alpha1.PhaseRunning
case cnatv1alpha1.PhaseRunning:
reqLogger.Info(“Etap: RUNNING”)
pod := newPodForCR(instance)
}
found := &corev1.Pod{}
nsName := types.NamespacedName{Name: pod.Name, Namespace:
pod.Namespace}
err = r.Get(context.TODO(), nsName, found)
// Próba sprawdzenia, czy pod już istnieje. Jeśli pod nie istnieje
// (co jest oczekiwane), należy utworzyć zgodny ze specyfikacją
jednorazowy pod:
if err != nil && errors.IsNotFound(err) {
reqLogger.Info(“Etap: DONE”)
return reconcile.Result{}, nil
default:
reqLogger.Info(“NOP”)
Warto zauważyć, że wywołanie Update na końcu kodu operuje na podzasobie /status (zob. punkt
„Podzasób status”), a nie na całym niestandardowym zasobie. Oznacza to, że stosujemy tu
zalecaną praktykę rozdziału na status i specyfikację.
Po utworzeniu niestandardowego zasobu example-at można obejrzeć dane wyjściowe lokalnie
uruchomionego operatora:
$ make run
...
{“level”:”info”,”ts”:1555063897.488535,”logger”:”controller”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063897.488621,”logger”:”controller”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063897.4886441,”logger”:”controller”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T10:12:00Z”}
{“level”:”info”,”ts”:1555063897.488703,”logger”:”controller”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 10:12:00 +0000 UTC with a diff of 22.511336s”}
{“level”:”info”,”ts”:1555063907.489264,”logger”:”controller”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063907.489402,”logger”:”controller”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063907.489428,”logger”:”controller”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T10:12:00Z”}
{“level”:”info”,”ts”:1555063907.489486,”logger”:”controller”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 10:12:00 +0000 UTC with a diff of 12.510551s”}
{“level”:”info”,”ts”:1555063917.490178,”logger”:”controller”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063917.4902349,”logger”:”controller”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063917.490247,”logger”:”controller”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T10:12:00Z”}
{“level”:”info”,”ts”:1555063917.490278,”logger”:”controller”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 10:12:00 +0000 UTC with a diff of 2.509743s”}
{“level”:”info”,”ts”:1555063927.492718,”logger”:”controller”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063927.49283,”logger”:”controller”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063927.492857,”logger”:”controller”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T10:12:00Z”}
{“level”:”info”,”ts”:1555063927.492915,”logger”:”controller”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
{“level”:”info”,”ts”:1555063927.626236,”logger”:”controller”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063927.626303,”logger”:”controller”,
“msg”:”Etap: RUNNING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063928.07445,”logger”:”controller”,
“msg”:”Uruchomiono pod”,”namespace”:”cnat”,”at”:”example-at”,
“name”:”example-at-pod”}
{“level”:”info”,”ts”:1555063928.199562,”logger”:”controller”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063928.199645,”logger”:”controller”,
“msg”:”Etap: DONE”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063937.631733,”logger”:”controller”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555063937.631783,”logger”:”controller”,
“msg”:”Etap: DONE”,”namespace”:”cnat”,”at”:”example-at”}
...
Aby sprawdzić, czy nasz niestandardowy kontroler wykonał swoje zadanie, wywołaj następujące
polecenie:
$ kubectl get at,pods
NAME AGE
at.cnat.programming-kubernetes.info/example-at 11m
$ export IMG=quay.io/pk/cnat:v1
$ make docker-build
$ make docker-push
Teraz przejdźmy do narzędzia Operator SDK, które współdzieli z Kubebuilderem część kodu
bazowego i API.
Operator SDK
Aby ułatwić budowanie aplikacji dla Kubernetesa, firmy CoreOS i Red Hat opracowały
platformę Operator Framework. Częścią tej platformy jest narzędzie Operator SDK
(http://bit.ly/2KtpK7D), które umożliwia programistom tworzenie operatorów bez szczegółowej
wiedzy na temat API Kubernetesa.
Operator SDK zapewnia narzędzia do budowania i testowania operatorów oraz tworzenia
pakietów z nimi. Choć to SDK oferuje rozbudowane możliwości (przede wszystkim w obszarze
testowania), tu skupimy się na implementowaniu operatora narzędzia cnat
(http://bit.ly/2RpHhON) z użyciem tego SDK. Zobacz też powiązany katalog w naszym
repozytorium Git (http://bit.ly/2FpCtE9).
Zacznijmy od podstaw — zainstaluj narzędzie Operator SDK (http://bit.ly/2ZBQlCT) i sprawdź,
czy dostępne są wszystkie zależności:
$ dep version
dep:
version : v0.5.1
$ operator-sdk --version
operator-sdk version v0.6.0
Przygotowania
Teraz możemy wygenerować szkielet operatora dla narzędzia cnat:
Te polecenia generują potrzebny szablonowy kod, a także liczne funkcje pomocnicze, np.
przeznaczone do głębokiego kopiowania funkcje DeepCopy(), DeepCopyInto() i
DeepCopyObject().
Teraz możesz zastosować automatycznie wygenerowaną definicję CRD w klastrze Kubernetesa:
$ kubectl apply -f deploy/crds/cnat_v1alpha1_at_crd.yaml
Uruchom lokalnie nasz niestandardowy kontroler narzędzia cnat. Od tego momentu może on
zacząć przetwarzanie żądań:
$ OPERATOR_NAME=cnatop operator-sdk up local --namespace “cnat”
INFO[0000] Running the operator locally.
INFO[0000] Using namespace cnat.
{“level”:”info”,”ts”:1555041531.871706,”logger”:”cmd”,
“msg”:”Go Version: go1.12.1”}
{“level”:”info”,”ts”:1555041531.871785,”logger”:”cmd”,
“msg”:”Go OS/Arch: darwin/amd64”}
{“level”:”info”,”ts”:1555041531.8718028,”logger”:”cmd”,
“msg”:”Version of operator-sdk: v0.6.0”}
{“level”:”info”,”ts”:1555041531.8739321,”logger”:”leader”,
“msg”:”Trying to become the leader.”}
{“level”:”info”,”ts”:1555041531.8743382,”logger”:”leader”,
“msg”:”Skipping leader election; not running in a cluster.”}
{“level”:”info”,”ts”:1555041536.1611362,”logger”:”cmd”,
“msg”:”Registering Components.”}
{“level”:”info”,”ts”:1555041536.1622112,”logger”:”kubebuilder.controller”,
“msg”:”Starting EventSource”,”controller”:”at-controller”,
“msg”:”Starting Controller”,”controller”:”at-controller”}
{“level”:”info”,”ts”:1555041540.280784,”logger”:”kubebuilder.controller”,
“msg”:”Starting workers”,”controller”:”at-controller”,”worker count”:1}
Niestandardowy kontroler pozostanie w tym stanie do czasu utworzenia niestandardowego
zasobu ats.cnat.programming-kubernetes.info. Zróbmy to:
$ cat deploy/crds/cnat_v1alpha1_at_cr.yaml
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
name: example-at
spec:
schedule: “2019-04-11T14:56:30Z”
command: “echo SUPER”
$ kubectl get at
NAME AGE
at.cnat.programming-kubernetes.info/example-at 54s
Logika biznesowa
W logice biznesowej w operatorze należy zaimplementować dwie rzeczy:
// +k8s:openapi-gen=true
type AtStatus struct {
// Phase reprezentuje stan harmonogramu. Do czasu wykonania polecenia
stan
// to PENDING, po wykonaniu polecenia stan to DONE.
Phase string `json:”phase,omitempty”`
}
W pliku at_controller.go należy zaimplementować diagram stanów dla trzech etapów z
przejściami ze stanu PENDING do RUNNING i z RUNNING do DONE.
{“level”:”info”,”ts”:1555044934.0237482,”logger”:”controller_at”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:
“example-at”,”Target”:”2019-04-12T04:56:00Z”}
{“level”:”info”,”ts”:1555044934.02382,”logger”:”controller_at”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 04:56:00 +0000 UTC with a diff of 25.976236s”}
{“level”:”info”,”ts”:1555044934.148148,”logger”:”controller_at”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044934.148224,”logger”:”controller_at”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044934.148243,”logger”:”controller_at”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T04:56:00Z”}
{“level”:”info”,”ts”:1555044934.1482902,”logger”:”controller_at”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 04:56:00 +0000 UTC with a diff of 25.85174s”}
{“level”:”info”,”ts”:1555044944.1504588,”logger”:”controller_at”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044944.150568,”logger”:”controller_at”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044944.150599,”logger”:”controller_at”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T04:56:00Z”}
{“level”:”info”,”ts”:1555044944.150663,”logger”:”controller_at”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 04:56:00 +0000 UTC with a diff of 15.84938s”}
{“level”:”info”,”ts”:1555044954.385175,”logger”:”controller_at”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044954.3852649,”logger”:”controller_at”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044954.385288,”logger”:”controller_at”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T04:56:00Z”}
{“level”:”info”,”ts”:1555044954.38534,”logger”:”controller_at”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 04:56:00 +0000 UTC with a diff of 5.614691s”}
{“level”:”info”,”ts”:1555044964.518383,”logger”:”controller_at”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044964.5184839,”logger”:”controller_at”,
“msg”:”Etap: PENDING”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044964.518566,”logger”:”controller_at”,
“msg”:”Sprawdzanie harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Target”:”2019-04-12T04:56:00Z”}
{“level”:”info”,”ts”:1555044964.5186381,”logger”:”controller_at”,
“msg”:”Zakończono przetwarzanie
harmonogramu”,”namespace”:”cnat”,”at”:”example-at”,
“Result”:”2019-04-12 04:56:00 +0000 UTC with a diff of -4.518596s”}
{“level”:”info”,”ts”:1555044964.5186849,”logger”:”controller_at”,
“msg”:”Nadszedł czas!”,”namespace”:”cnat”,”at”:”example-at”,
“Gotowy do wykonania”:”echo SUPER”}
{“level”:”info”,”ts”:1555044964.642559,”logger”:”controller_at”,
{“level”:”info”,”ts”:1555044966.038771,”logger”:”controller_at”,
“msg”:”Etap: DONE”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044966.708663,”logger”:”controller_at”,
“msg”:”=== uzgadnianie stanu At”,”namespace”:”cnat”,”at”:”example-at”}
{“level”:”info”,”ts”:1555044966.708749,”logger”:”controller_at”,
“msg”:”Etap: DONE”,”namespace”:”cnat”,”at”:”example-at”}
...
Widać tu trzy etapy działania operatora: stan PENDING do czasu 1555044964.518566, następnie
stan RUNNING, a na końcu stan DONE.
Aby sprawdzić poprawność funkcji w niestandardowym kontrolerze i wynik operacji, wpisz
następujące instrukcje:
$ kubectl get at,pods
NAME AGE
at.cnat.programming-kubernetes.info/example-at 23m
SUPER
Gdy skończysz tworzyć niestandardowy kontroler w trybie lokalnym (tak jak w tym
przykładzie), zapewne zechcesz wygenerować na jego podstawie obraz kontenera. Obraz
kontenera z tym niestandardowym kontrolerem można później wykorzystać np. w instalacji
Kubernetesa. Aby wygenerować obraz kontenera, możesz zastosować następujące polecenie:
$ operator-sdk build $REGISTRY/PROJECT/IMAGE
Oto kilka innych źródeł, gdzie znajdziesz więcej informacji i przykładów związanych z
narzędziem Operator SDK:
Inne podejścia
Oprócz opisanych już rozwiązań (lub w połączeniu z nimi) warto poznać następujące projekty,
biblioteki i narzędzia:
Metacontroller (https://metacontroller.app)
Podsumowanie
W tym rozdziale przyjrzeliśmy się różnym narzędziom umożliwiającym wydajniejsze pisanie
niestandardowych kontrolerów i operatorów. W przeszłości jedynym rozwiązaniem było
wzorowanie się na projekcie sample-controller. Jednak dzięki narzędziom Kubebuilder i
Operator SDK obecnie dostępne są dwie techniki, które pozwalają skupić się na logice
biznesowej niestandardowego kontrolera zamiast na szablonowym kodzie. Na szczęście te dwa
narzędzia współdzielą wiele API i dużo kodu, dlatego zmiana jednego na drugie nie powinna
nastręczać dużych trudności.
Pora zobaczyć, jak udostępniać efekty naszej pracy, czyli jak tworzyć pakiety i dystrybuować
napisane kontrolery.
1 Prezentujemy tu tylko istotne fragmenty. Sama funkcja zawiera dużo szablonowego kodu,
który nie jest istotny w kontekście omawianych zagadnień.
Rozdział 7. Udostępnianie
kontrolerów i operatorów
Gdy już zaznajomiłeś się z tworzeniem niestandardowych kontrolerów, pora przejść do
udostępniania swoich niestandardowych kontrolerów i operatorów w środowisku
produkcyjnym. W tym rozdziale omawiamy operacyjne aspekty kontrolerów i operatorów,
pokazujemy, jak tworzyć pakiety zawierające takie komponenty, a także przedstawiamy dobre
praktyki uruchamiania kontrolerów w środowisku produkcyjnym. Dowiesz się też, jak zadbać o
to, by Twoje punkty rozszerzeń nie zaszkodziły bezpieczeństwu lub wydajności klastra
Kubernetesa.
Pakowanie — trudności
W Kubernetesie zasoby są zdefiniowane z użyciem manifestów, zwykle w formacie YAML. Jest
to niskopoziomowy interfejs do deklarowania stanu zasobów. Jednak pliki manifestów mają
ograniczenia. Najważniejsze w kontekście pakowania aplikacji kontenerowych jest to, że
manifesty w formacie YAML są statyczne. Oznacza to, że wszystkie wartości w pliku YAML są
zapisane na stałe. Dlatego jeśli chcesz np. zmienić obraz kontenera w manifeście instalacji
(http://bit.ly/2WZ1uRD), musisz utworzyć nowy manifest.
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mycustomcontroller
spec:
replicas: 1
template:
metadata:
labels:
app: customcontroller
spec:
containers:
- name: thecontroller
image: example/controller:0.1.0
ports:
- containerPort: 9999
env:
- name: REGION
value: eu-west-1
Helm
Helm (https://helm.sh), nazywany przez jego autorów najlepszym menedżerem pakietów dla
Kubernetesa, został pierwotnie opracowany przez firmę Deis, obecnie jest projektem fundacji
CNCF (ang. Cloud Native Computing Foundation; https://www.cncf.io), a główni uczestnicy
projektu pracują dla firm Microsoft, Google i Bitnami (obecnie należącej do VMware).
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include “flagger.fullname” . }}
...
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
template:
metadata:
labels:
app.kubernetes.io/name: {{ template “flagger.name” . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
- name: flagger
securityContext:
readOnlyRootFilesystem: true
runAsUser: 10001
image: “{{ .Values.image.repository }}:{{ .Values.image.tag }}”
Aby zainstalować pakiet chart, można uruchomić polecenie helm install. Helm udostępnia
kilka sposobów wyszukiwania i instalowania pakietów chart, jednak najłatwiejszy polega na
użyciu jednego z oficjalnych stabilnych pakietów chart:
Released smiling-penguin
# Wyświetlanie uruchomionych aplikacji:
$ helm ls
# Usuwanie aplikacji:
$ helm delete smiling-penguin
Removed smiling-penguin
Aby spakować kontroler, musisz utworzyć pakiet chart narzędzia Helm i gdzieś go opublikować
— domyślnie w publicznym indeksowanym repozytorium udostępnianym w serwisie Helm Hub
(https://hub.helm.sh; zob. rysunek 7.1).
Rysunek 7.1. Na zrzucie z serwisu Helm Hub widać publicznie dostępne pakiety chart narzędzia
Helm
Aby znaleźć więcej informacji na temat tworzenia pakietów chart narzędzia Helm, w wolnym
czasie zapoznaj się z następującymi źródłami:
Znakomity artykuł „How to Create Your First Helm Chart” w witrynie Bitnami
(http://bit.ly/2ZIlODJ).
Tekst „Using S3 as a Helm Repository” (http://bit.ly/2KzwLDY) dotyczący przechowywania
pakietów chart narzędzia Helm wewnątrz organizacji.
Oficjalna dokumentacja narzędzia Helm: „The Chart Best Practices Guide”
(http://bit.ly/31GbayW).
Helm jest popularny po części z powodu łatwości użytkowania. Jednak zdaniem części osób
obecna architektura narzędzia Helm nie jest w pełni bezpieczna (http://bit.ly/2WXM5vZ). Dobra
wiadomość jest taka, że społeczność aktywnie pracuje nad rozwiązaniem wykrytych problemów.
Kustomize
Kustomize (https://kustomize.io) umożliwia deklaratywne modyfikowanie konfiguracji plików
manifestu w Kubernetesie zgodnie ze znanym API Kubernetesa. Narzędzie to udostępniono w
połowie 2018 r. (http://bit.ly/2L5Ec5f), a obecnie jest rozwijane jako projekt grupy SIG CLI
przez społeczność Kubernetesa.
imageTags:
- name: quay.io/programming-kubernetes/cnat-operator
newTag: 0.1.0
resources:
- cnat-controller.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: cnat-controller
spec:
replicas: 1
template:
metadata:
labels:
app: cnat
spec:
containers:
- name: custom-controller
image: quay.io/programming-kubernetes/cnat-operator
Zastosuj polecenie kustomize build, a uzyskasz następujące dane wyjściowe (bez
modyfikowania pliku cnat-controller.yaml!):
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: cnat-controller
spec:
replicas: 1
template:
metadata:
labels:
app: cnat
spec:
containers:
- name: custom-controller
image: quay.io/programming-kubernetes/cnat-operator:0.1.0
Dane wyjściowe z wywołania kustomize build można później wykorzystać np. w wywołaniu
kubectl apply, a wszystkie zmiany (http://bit.ly/2LbCDTr) zostaną automatycznie
uwzględnione.
Ponieważ w kubectl wbudowana jest obsługa narzędzia Kustomize, zapewne coraz więcej osób
będzie z niego korzystać. Warto zauważyć, że choć rozwiązuje ono niektóre problemy
(modyfikowanie wartości), inne obszary, np. sprawdzanie poprawności i aktualizacje, mogą
wymagać używania Kustomize razem z językami takimi jak CUE firmy Google
(http://bit.ly/32heAZl).
Aby podsumować temat pakowania, warto przyjrzeć się niektórym innym rozwiązaniom
stosowanym przez praktyków.
Narzędzia uniksowe
Do modyfikowania wartości w nieprzetworzonych manifestach Kubernetesa możesz używać
w skryptach powłoki różnych narzędzi wiersza poleceń, np. sed, awk i jq. Jest to popularne
podejście i, przynajmniej do czasu udostępnienia Helma, jedno z najczęściej stosowanych
rozwiązań. Istotnym powodem jego popularności jest to, że minimalizuje ilość zależność i
jest stosunkowo przenośne w środowiskach uniksowych.
ytt (https://get-ytt.io)
Ytt to następne narzędzie do obsługi szablonów w formacie YAML. W ytt używany jest
język, który jest zmodyfikowaną wersją języka Starlark, opracowanego do obsługi
konfiguracji w firmie Google (http://bit.ly/2NaqoJh). Ytt przetwarza semantycznie struktury
YAML, a jego twórcy kładą nacisk na możliwość ponownego wykorzystania kodu.
Ksonnet (https://ksonnet.io)
Jest to narzędzie do zarządzania konfiguracją z manifestów Kubernetesa, pierwotnie
opracowane w firmie Heptio (obecnie należącej do VMware). Ksonnet został uznany za
przestarzały i nie jest już aktywnie rozwijany, dlatego stosuj go na własne ryzyko.
Więcej o opisanych tu rozwiązaniach przeczytasz w artykule Jessego Suena „The State of
Kubernetes Configuration Management: An Unsolved Problem” (http://bit.ly/2N9BkXM).
Po ogólnym omówieniu metod pakowania kodu pora przyjrzeć się najlepszym praktykom
pakowania i udostępniania kontrolerów oraz operatorów.
Odpowiednio skonfiguruj kontrolę dostępu. Zdefiniuj specjalne konto usługi dla kontrolera
z minimalnymi uprawnieniami systemu RBAC. Więcej informacji znajdziesz w punkcie
„Odpowiednie uprawnienia”.
Rozważ zasięg niestandardowego kontrolera. Czy ma zarządzać niestandardowymi
zasobami z jednej przestrzeni nazw, czy z różnych przestrzeni? Zapoznaj się z
wypowiedziami Aleksa Ellisa na Twitterze (http://bit.ly/2ZHd5S7), gdzie opisane są wady i
zalety poszczególnych podejść.
Wykonaj testy i profilowanie dla kontrolera, aby ustalić generowane przez niego
obciążenie i skalowalność. Firma Red Hat opracowała szczegółowy zestaw wymogów wraz
z instrukcjami w poradniku dla autorów operatorów w serwisie OperatorHub
(http://bit.ly/2IEplx4).
Upewnij się, że definicje CRD i kontroler są dobrze udokumentowane, najlepiej za pomocą
wewnątrzwierszowej dokumentacji dostępnej w serwisie GoDoc (https://godoc.org). Za
wzór niech posłuży Ci projekt vank-vaults firmy Banzai Cloud (http://bit.ly/2XtfPVB).
Aby ułatwić sobie pracę, możesz zdefiniować rolę ClusterRole z niezbędnymi regułami
systemu RBAC i obiekt RoleBinding wiążący tę rolą z określoną przestrzenią nazw. Pozwala to
wykorzystać tę samą rolę w różnych przestrzeniach nazw, co wyjaśniono w punkcie Using
RBAC Authorization w dokumentacji (http://bit.ly/2LdVFsj).
$ ls -al rbac/
total 40
drwx------ 7 mhausenblas staff 224 12 Apr 09:52 .
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- apps
resources:
- deployments
resources:
- deployments/status
- cnat.programming-kubernetes.info
resources:
- ats
verbs: [“get”, “list”, “watch”, “create”, “update”, “patch”, “delete”]
- apiGroups:
- cnat.programming-kubernetes.info
resources:
- ats/status
verbs: [“get”, “update”, “patch”]
- apiGroups:
- admissionregistration.k8s.io
resources:
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
- “”
resources:
- secrets
verbs: [“get”, “list”, “watch”, “create”, “update”, “patch”, “delete”]
- apiGroups:
- “”
resources:
- services
verbs: [“get”, “list”, “watch”, “create”, “update”, “patch”, “delete”]
Gdy przyjrzysz się uprawnieniom wygenerowanym przez Kubebuildera dla wersji v1,
[2]
prawdopodobnie będziesz zaskoczony . My byliśmy. Zgodnie z najlepszymi praktykami
kontroler — jeśli nie zachodzą specjalne okoliczności — nie powinien mieć możliwości:
Kubebuilder oczywiście nie potrafi precyzyjnie zrozumieć, jak działa kod kontrolera. Dlatego
nie jest zaskoczeniem, że wygenerowane reguły systemu RBAC pozwalają na zdecydowanie
zbyt wiele. Zalecamy dokładne sprawdzanie uprawnień i ograniczanie ich do minimum na
podstawie przedstawionej listy kontrolnej.
kind: ClusterRoleBinding
metadata:
creationTimestamp: null
name: manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: default
namespace: system
Więcej o najlepszych praktykach dotyczących systemu RBAC i związanych z nim narzędziach
znajdziesz w serwisie RBAC.dev (https://rbac.dev). Jest to witryna poświęcona systemowi RBAC
w Kubernetesie. Teraz przejdźmy do zagadnień związanych z testami i wydajnością
niestandardowych kontrolerów.
Najlepszą praktyką jest używanie takich testów w procesie ciągłej integracji. Oznacza to, że
należy od początku automatyzować proces budowania niestandardowego kontrolera oraz
testowania i pakowania kodu. Jeśli chcesz przyjrzeć się przykładowej konfiguracji, zapoznaj się
ze znakomitym tekstem Marko Mudrinića „Spawning Kubernetes Clusters in CI for Integration
and E2E tests” (http://bit.ly/2FwN1RU).
Rejestrowanie zdarzeń
Upewnij się, że zapewniona jest wystarczająca ilość rejestrowanych informacji, aby umożliwić
rozwiązywanie problemów (http://bit.ly/2WXD85D). Jak zwykle w środowisku kontenerowym
rejestrowane informacje są przesyłane do standardowego wyjścia (stdout), gdzie mogą być
używane albo w postaci zagregowanej, albo na poziomie podów z użyciem polecenia kubectl
logs. Zagregowane dane mogą być udostępniane za pomocą rozwiązań specyficznych dla
dostawców chmury, np. narzędzia Stackdriver w chmurze Google Cloud lub narzędzia
CloudWatch w usłudze AWS. Dostępne są też niestandardowe zestawy rozwiązań, np.
Elasticsearch-Logstash-Kibana i Elasticsearch-Fluentd-Kibana. Receptury z tego obszaru
znajdziesz w książce Kubernetes Cookbook (http://bit.ly/2FTgJzk) Sébastiena Goasguena i
Michaela Hausenblasa (O’Reilly).
“ts”:1555063927.492718,
“logger”:”controller”,
“ts”:1555063927.49283,
“logger”:”controller”,
“msg”:”Etap: PENDING” }
{ “level”:”info”,
“ts”:1555063927.492857,
“logger”:”controller”,
“msg”:”Sprawdzanie harmonogramu” }
{ “level”:”info”,
“ts”:1555063927.492915,
“logger”:”controller”,
“msg”:”Zakończono przetwarzanie harmonogramu” }
reqLogger.Info(“Etap: PENDING”)
// Jeśli polecenie nie zostało jeszcze wykonane, trzeba sprawdzić, czy
// nie nadszedł czas jego uruchomienia:
reqLogger.Info(“Sprawdzanie harmonogramu”, “Target”,
instance.Spec.Schedule)
fmt.Sprintf(“%v”, d))
if d > 0 {
// Nie nadszedł jeszcze czas na wykonanie polecenia. Należy poczekać do
zaplanowanego czasu.
return reconcile.Result{RequeueAfter: d}, nil
}
reqLogger.Info(“Nadszedł czas!”, “Gotowy do wykonania”,
instance.Spec.Command)
instance.Status.Phase = cnatv1alpha1.PhaseRunning
Ten fragment kodu w języku Go pozwala zobaczyć, co należy rejestrować, a przede wszystkim
kiedy używać wywołań regLogger.Info i reqLogger.Error.
Po omówieniu podstaw rejestrowania zdarzeń pora przejść do powiązanego zagadnienia —
wskaźników!
Jeśli używasz systemu service mesh (np. opartych na pośredniku Envoy — https://envoy.com —
Istio i App Mesh lub narzędzia Linkerd), instrumentacja zwykle jest od razu gotowa lub
wymaga tylko niewielkiej pracy (konfiguracji). W przeciwnym razie musisz zastosować
odpowiednie biblioteki, np. dostępne w Prometheusu (http://bit.ly/2xb2qmv), aby udostępnić we
własnym kodzie odpowiednie wskaźniki. W tym kontekście możesz zapoznać się z nowym
projektem Service Mesh Interface (SMI; https://smi-spec.io), powstałym na początku 2019 r.,
który ma zapewniać dla systemów service mesh standardowy interfejs oparty na
niestandardowych zasobach i kontrolerach.
Inną przydatną funkcją dostępną w Kubernetesie za pomocą serwera API są audyty
(http://bit.ly/2O4WBkL). Umożliwiają one rejestrowanie sekwencji operacji wpływających na
klaster. W polityce audytowania można stosować różne strategie — od nierejestrowania zdarzeń
po rejestrowanie metadanych na temat zdarzeń, treści żądań i treści odpowiedzi. Możesz też
używać na zapleczu prostego mechanizmu rejestrowania zdarzeń lub zastosować webhooki do
zintegrowania rozwiązania z niezależnymi systemami.
Podsumowanie
W tym rozdziale skupiliśmy się na tym, jak przygotować operatory do użytku w środowisku
produkcyjnym. Omówiliśmy tu operacyjne aspekty kontrolerów i operatorów, w tym pakowanie,
zabezpieczenia i wydajność.
W ten sposób omówiliśmy podstawy pisania i użytkowania niestandardowych kontrolerów i
operatorów Kubernetesa. Teraz przejdziemy do następnego sposobu rozszerzania Kubernetesa
— tworzenia niestandardowego serwera API.
Scenariusze stosowania
niestandardowych serwerów API
Niestandardowy serwer API można zastosować zamiast definicji CRD. Może on robić wszystko
to, co definicje CRD i zapewnia prawie nieograniczoną elastyczność. Oczywiście wiąże się to z
kosztami w postaci złożoności zarówno prac programistycznych, jak i eksploatacji.
Przyjrzyj się niektórym ograniczeniom definicji CRD z czasu, gdy powstaje ta książka (stabilną
wersją jest obecnie Kubernetes 1.14). Definicje CRD:
Przykład — pizzeria
Aby dowiedzieć się, jak implementowane są niestandardowe serwery API, w tym podrozdziale
przyjrzymy się przykładowemu projektowi — niestandardowemu serwerowi API, który
implementuje API pizzerii. Zapoznaj się teraz z wymaganiami.
Pizza
Typ pizzy sprzedawanej w pizzerii.
Dodatki to zasoby z poziomu klastra, przechowujące tylko liczbę zmiennoprzecinkową
oznaczającą cenę jednostkową danego dodatku. Instancja tego rodzaju jest bardzo prosta:
apiVersion: restaurant.programming-kubernetes.info/v1alpha1
kind: Topping
metadata:
name: mozzarella
spec:
cost: 1.0
apiVersion: restaurant.programming-kubernetes.info/v1alpha1
kind: Pizza
metadata:
name: margherita
spec:
toppings:
- mozzarella
- pomidory
Lista dodatków jest uporządkowana (jak każda lista w formacie YAML lub JSON), jednak jeśli
chodzi o semantykę typu, kolejność dodatków nie ma znaczenia. Klient niezależnie od
kolejności dodatków otrzyma tę samą pizzę. Na liście dozwolone mają być powtórzenia, aby
umożliwić zamówienie np. pizzy z dodatkowym serem.
Wszystkie te rzeczy można łatwo zaimplementować za pomocą definicji CRD. Teraz dodajmy
[3]
wymagania wykraczające poza podstawowe możliwości definicji CRD :
W specyfikacji pizzy dozwolone mają być tylko te dodatki, dla których istnieje powiązany
obiekt typu Topping.
Załóżmy też, że początkowo udostępniliśmy to API w wersji v1alpha1, jednak ostatecznie
stwierdziliśmy, że chcemy utworzyć inną reprezentację dodatków w wersji v1beta1 tego
samego API.
Oznacza to, że chcemy mieć dwie wersje tego API i móc płynnie przechodzić między nimi.
Dlatego gorąco zachęcamy do zapoznania się z tym rozdziałem, nawet jeśli nie planujesz
tworzyć niestandardowego serwera API. Możliwe, że prezentowane tu techniki w przyszłości
będą dostępne także w definicjach CRD. Wtedy przyda Ci się wiedza z zakresu
niestandardowych serwerów API.
Architektura — agregowanie
Zanim przejdziemy to technicznych szczegółów implementacji, chcemy na ogólnym poziomie
opisać architekturę niestandardowych serwerów API w kontekście klastra Kubernetesa.
Główny serwer API Kubernetesa, kube-apiserver, zawsze jest pierwszym punktem kontaktu
dla narzędzia kubectl i innych klientów API. Grupy API udostępniane przez niestandardowy
serwer API są przekazywane przez proces kube-apiserver do procesu niestandardowego
serwera API. Oznacza to, że proces kube-apiserver wie wszystko o niestandardowych
serwerach API i udostępnianych przez nie grupach API, aby móc kierować do tych serwerów
odpowiednie żądania.
Przyjrzyj się teraz dokładniej ścieżkom żądań kierowanych do niestandardowego serwera API,
ale przychodzących do gniazda TCP serwera API Kubernetesa (zob. rysunek 8.1):
Usługi API
Aby serwer API Kubernetesa znał grupy API udostępniane przez niestandardowy serwer API, w
grupie API apiregistration.k8s.io/v1 trzeba utworzyć jeden obiekt APIService. Takie
obiekty zwracają tylko grupy API i ich wersje. Nie podają zasobów ani innych szczegółowych
informacji:
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: nazwa
spec:
group: nazwa-grupy-API
version: wersja-grupy-API
service:
namespace: przestrzeń-nazw-usługi-niestandardowego-serwera-API
name: -usługa-serwera-API
caBundle: zestaw-certyfikatów-w-formacie-base64
insecureSkipTLSVerify: wartość-logiczna
groupPriorityMinimum: 2000
versionPriority: 20
Możesz używać dowolnej nazwy, ale dla przejrzystości warto stosować takie, które określają
nazwę i wersję grupy API, np. nazwa-grupy-wersja.
Usługą może być zwykła usługa ClusterIP (http://bit.ly/2X0zEEu) z klastra. Może to być także
usługa ExternalName z określoną nazwą DNS dla niestandardowych serwerów API spoza
klastra. W obu sytuacjach trzeba używać portu 443. W czasie, gdy powstaje ta książka, żadne
inne porty usług nie są obsługiwane. Jednak dzięki odwzorowaniu docelowego portu usługi
można zastosować dla podów niestandardowego serwera API dowolny wyższy (najlepiej
niezarejestrowany) port, dlatego nie jest to istotne ograniczenie.
Zestaw certyfikatów (caBundle) jest używany, by serwer API Kubernetesa mógł zaufać usłudze,
z którą się komunikuje. Warto zauważyć, że żądania kierowane do API mogą zawierać poufne
dane. Aby uniknąć ataków typu man-in-the-middle, należy skonfigurować pole caBundle i nie
aktywować niebezpiecznej opcji insecureSkipTLSVerify. Jest to ważne przede wszystkim w
klastrach produkcyjnych, gdzie używany jest mechanizm wymiany certyfikatów.
W obiekcie typu APIService ustawiane są też dwa priorytety. Ich działanie jest dość
skomplikowane, a opis znajdziesz w dokumentacji języka Go dla typu APIService:
// GroupPriorityMininum to minimalny priorytet, jaki powinna mieć dana grupa.
Wyższy priorytet oznacza, że
// Zauważ, że dla innych wersji tej grupy podane mogą być wyższe wartości
//
// wewnątrz grupy, może być mała, zwykle ok. 10. Gdy priorytety wersji są
takie same,
// Jeśli ten łańcuch znaków jest „w stylu Kubernetesa”, dana wersja znajdzie
się przed wersjami
// bez przyrostka takiego jak beta lub alpha), następnie według wersji,
// a potem według podwersji. Oto przykładowa posortowana lista wersji:
Drugi priorytet wyznacza kolejność wersji, aby zdefiniować preferowaną wersję do użytku w
dynamicznych klientach.
Oto lista wartości GroupPriorityMinimum dla natywnych grup API Kubernetesa:
Dlatego zastosowanie wartości 2000 dla API podobnych do platform PaaS oznacza, że będą one
[4]
umieszczone na końcu listy .
Kolejność grup API jest istotna w procesie odwzorowywania REST w narzędziu kubectl (zob.
punkt „Odwzorowania REST”). To oznacza, że ta kolejność ma wpływ na pracę użytkowników.
Jeśli występują niezgodne nazwy zasobów lub krótkie nazwy, wybierany jest zasób z najwyższą
wartością GroupPriorityMinimum.
Ponadto kolejność oparta na priorytetach może być używana także w wyjątkowej sytuacji
zastępowania wersji grupy API za pomocą niestandardowego serwera API. Możesz np. (z
dowolnych przyczyn) zastąpić natywną grupę API Kubernetesa zmodyfikowaną grupą,
ustawiając dla niestandardowej usługi API niższą wartość GroupPriorityMinimum niż wartości
z pokazanej tabeli.
Warto przypomnieć, że serwer API Kubernetesa nie musi znać listy zasobów ani na potrzeby
służących do wykrywania informacji punktów końcowych /apis i apis/nazwa-grupy, ani na
potrzeby przekazywania żądań. Lista zasobów jest zwracana tylko przez trzeci punkt końcowy
do wykrywania informacji: /apis/nazwa-grupy/wersja. Jednak, co opisaliśmy w poprzednim
punkcie, ten punkt końcowy jest obsługiwany przez zagregowany niestandardowy serwer API, a
nie przez agregator kube-aggregator.
metadata:
name: extension-apiserver-authentication
namespace: kube-system
data:
client-ca-file: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
requestheader-allowed-names: ‘[“aggregator”]’
requestheader-client-ca-file: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
requestheader-extra-headers-prefix: ‘[“X-Remote-Extra-”]’
requestheader-group-headers: ‘[“X-Remote-Group”]’
requestheader-username-headers: ‘[“X-Remote-User”]’
Na podstawie tych informacji zagregowany niestandardowy serwer API z ustawieniami
domyślnymi uwierzytelni:
Delegowana autoryzacja
Po zakończeniu uwierzytelniania konieczna jest autoryzacja każdego żądania. Autoryzacja jest
oparta na liście nazw użytkowników i grup. Domyślnym mechanizmem autoryzacji w
Kubernetesie jest RBAC.
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
spec:
resourceAttributes:
group: apps
resource: deployments
verb: create
namespace: default
version: v1
name: example
user: michael
groups:
- system:authenticated
- admins
- authors
Serwer API Kubernetesa odbiera taki obiekt ze zagregowanego niestandardowego serwera API,
sprawdza reguły RBAC w klastrze i podejmuje decyzję, po czym zwraca obiekt rodzaju
SubjectAccessReview z ustawionym polem statusu. Oto przykład:
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
status:
allowed: true
denied: false
generated/informers/externalversions”
)
o := &CustomServerOptions{
RecommendedOptions: genericoptions.NewRecommendedOptions(
defaultEtcdPathPrefix,
apiserver.Codecs.LegacyCodec(v1alpha1.SchemeGroupVersion),
genericoptions.NewProcessInfo(“pizza-apiserver”, “pizza-
apiserver”),
),
}
return o
}
return &RecommendedOptions{
Etcd: NewEtcdOptions(storagebackend.NewDefaultConfig(prefix,
codec)),
SecureServing: sso.WithLoopback(),
Authentication: NewDelegatingAuthenticationOptions(),
Authorization: NewDelegatingAuthorizationOptions(),
Audit: NewAuditOptions(),
Features: NewFeatureOptions(),
CoreAPI: NewCoreAPIOptions(),
ExtraAdmissionInitializers:
func(c *server.RecommendedConfig) ([]admission.PluginInitializer,
error) {
Webhook: NewWebhookOptions(),
}
W razie potrzeby wszystkie te pola można zmodyfikować. Na przykład, jeśli potrzebny jest
niestandardowy port domyślny, ustaw pole
RecommendedOptions.SecureServing.SecureServingOptions.BindPort.
Opcje można uzupełnić w celu ustawienia wartości domyślnych, które nie powinny pojawiać się
w systemie pomocy dotyczącym flag, ale które są niezbędne do uzyskania kompletnego zestawu
opcji.
Opcje są przekształcane na konfigurację serwera przez metodę Config()
(*apiserver.Config, error). Początkowo używana jest rekomendowana konfiguracja
domyślna, a następnie stosowane są do niej opcje:
if err != nil {
return nil,
fmt.Errorf(“Błąd tworzenia samodzielnie podpisywanego certyfikatu:
%v”, err)
}
serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
err = o.RecommendedOptions.ApplyTo(serverConfig, apiserver.Scheme);
if err != nil {
return nil, err
}
config := &apiserver.Config{
GenericConfig: serverConfig,
ExtraConfig: apiserver.ExtraConfig{},
}
return config, nil
}
Tworzona tu konfiguracja zawiera uruchamiane struktury danych. Oznacza to, że konfiguracje
to obiekty czasu wykonywania programu — w odróżnieniu od opcji, które odpowiadają flagom.
Wiersz o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts tworzy
samodzielnie podpisywane certyfikaty potrzebne, jeśli użytkownik nie przekazał flag
reprezentujących wcześniej wygenerowane certyfikaty.
Funkcja genericapiserver.NewRecommendedConfig zwraca rekomendowaną konfigurację
domyślną, a RecommendedOptions.ApplyTo modyfikuje ją na podstawie flag (i innych podanych
opcji).
Struktura z konfiguracją w projekcie pizza-apiserver to nakładka na obiekt typu
RecommendedConfig dla naszego przykładowego niestandardowego serwera API:
type ExtraConfig struct {
ExtraConfig ExtraConfig
}
GenericConfig genericapiserver.CompletedConfig
ExtraConfig *ExtraConfig
}
Jeśli potrzebny jest dodatkowy stan dla uruchomionego niestandardowego serwera API, należy
go umieścić w strukturze ExtraConfig.
Podobnie jak dla struktur z opcjami, tak i dla konfiguracji istnieje metoda Complete()
CompletedConfig ustawiająca wartości domyślne. Ponieważ dla konfiguracji niezbędne jest
wywołanie Complete(), często jest ono wymuszane za pomocą systemu plików poprzez dodanie
nieeksportowanego typu danych completedConfig. Pomysł polega na tym, że jedynie
wywołanie Complete() może przekształcić strukturę Config w completedConfig:
func (cfg *Config) Complete() completedConfig {
c := completedConfig{
cfg.GenericConfig.Complete(),
&cfg.ExtraConfig,
}
c.GenericConfig.Version = &version.Info{
Major: “1”,
Minor: “0”,
}
return completedConfig{&c}
}
Uzupełnioną konfigurację można przekształcić w strukturę czasu wykonania CustomServer,
używając konstruktora New():
genericapiserver.NewEmptyDelegate(),
)
if err != nil {
return nil, err
}
s := &CustomServer{
GenericAPIServer: genericServer,
}
return s, nil
}
Warto zauważyć, że celowo pominęliśmy tu kod instalacji API. Wrócimy do niego w punkcie
„Instalowanie API” (dowiesz się tam, jak w trakcie rozruchu połączyć rejestry ze standardowym
serwerem API). Rejestr implementuje API i semantykę składowania danych dla grupy API.
Omówienie tego procesu dla grupy API dla pizzerii znajdziesz w punkcie „Rejestr i strategia”.
Ostatecznie można uruchomić obiekt typu CustomServer, wywołując metodę Run(stopCh <-
chan struct{}) error. W tym przykładzie jest ona wywoływana w metodzie Run obiektu opcji.
Oznacza to, że metoda CustomServerOptions.Run:
tworzy konfigurację,
uzupełnia konfigurację,
tworzy obiekt typu CustomServer,
wywołuje metodę CustomServer.Run.
server.GenericAPIServer.AddPostStartHook(“start-pizza-apiserver-
informers”,
func(context genericapiserver.PostStartHookContext) error {
config.GenericConfig.SharedInformerFactory.Start(context.StopCh)
o.SharedInformerFactory.Start(context.StopCh)
return nil
},
)
return server.GenericAPIServer.PrepareRun().Run(stopCh)
}
Funkcja PrepareRun() podłącza specyfikację OpenAPI i może wykonywać także inne operacje
potrzebne po zainstalowaniu API. Po tym wywołaniu metoda Run uruchamia sam serwer.
Wykonywanie metody zostaje wtedy zablokowane do czasu zamknięcia kanału stopCh.
W tym przykładzie podłączany jest też haczyk pouruchomieniowy (ang. post-start hook) start-
pizza--apiserver-informers. Jak wskazuje na to nazwa, ten haczyk jest wywoływany, gdy
serwer HTTPS już działa i oczekuje na żądania. Tu ten haczyk uruchamia fabryki
współdzielonych informatorów.
Zauważ, że nawet lokalne wewnątrzprocesowe informatory dla zasobów z niestandardowego
serwera API komunikują się z interfejsem hosta lokalnego z użyciem protokołu HTTPS. Dlatego
należy je uruchamiać wtedy, gdy serwer już działa i oczekuje w porcie HTTPS na żądania.
Warto też zauważyć, że punkt końcowy /healthz zwraca informację o powodzeniu tylko po
udanym wykonaniu wszystkich haczyków pouruchomieniowych.
Cały kod przygotowujący środowisko jest już gotowy. W projekcie pizza-apiserver cały ten
kod jest opakowany w polecenie cobra:
// NewCommandStartCustomServer to funkcja obsługi polecenia ‚start master’ z
wiersza poleceń
o := *defaults
cmd := &cobra.Command{
Short: “Uruchamianie niestandardowego serwera API”,
Long: “Uruchamianie niestandardowego serwera API”,
return err
}
if err := o.Run(stopCh); err != nil {
return err
}
return nil
},
}
flags := cmd.Flags()
o.RecommendedOptions.AddFlags(flags)
return cmd
}
Po utworzeniu funkcji NewCommandStartCustomServer metoda main() procesu jest prosta:
func main() {
logs.InitLogs()
defer logs.FlushLogs()
stopCh := genericapiserver.SetupSignalHandler()
klog.Fatal(err)
}
}
Zwróć uwagę przede wszystkim na wywołanie SetupSignalHandler. Konfiguruje ono obsługę
sygnałów uniksowych. Sygnały SIGINT (zgłaszany po wciśnięciu kombinacji Ctrl+C w oknie
terminala) i SIGKILL powodują tu zamknięcie kanału stopu. Kanał stopu jest przekazywany do
działającego niestandardowego serwera API, który zostaje zamknięty po zamknięciu tego
kanału. Dlatego główna pętla po otrzymaniu jednego z tych sygnałów inicjuje zamykanie
serwera. Procedura zamykania jest kontrolowana, ponieważ przed zamknięciem serwera
kończone są wykonywane żądania (domyślnie mają one 60 sekund na zakończenie pracy). Kod
gwarantuje też, że wszystkie żądania są przesyłane do kodu zaplecza odpowiedzialnego za
audyt i że żadne dane związane z audytem nie zostają pominięte. Gdy wszystkie te działania
zostaną zakończone, wywołanie cmd.Execute() zwraca sterowanie, a proces kończy pracę.
Pierwsze uruchomienie
Wszystko jest już gotowe do pierwszego uruchomienia niestandardowego serwera API. Jeśli
konfiguracja klastra znajduje się w pliku ~/.kube/config, możesz użyć jej na potrzeby
delegowanego uwierzytelniania i delegowanej autoryzacji:
$ cd $GOPATH/src/github.com/programming-kubernetes/pizza-apiserver
$ etcd &
$ curl -k https://localhost:443/healthz
ok
Można też wyświetlić punkt końcowy odpowiedzialny za wykrywanie informacji, jednak na razie
wynik takiego wywołania nie jest interesujący. Nie utworzyliśmy API, dlatego wyświetlana lista
jest pusta:
$ curl -k https://localhost:443/apis
{
“kind”: “APIGroupList”,
“groups”: []
}
Każdy serwer API udostępnia wiele zasobów i ich wersji (zob. rysunek 2.3). Niektóre zasoby
mają wiele wersji. Aby umożliwić korzystanie z wielu wersji zasobu, serwer API musi
dokonywać konwersji między wersjami.
W celu uniknięcia wykładniczego wzrostu liczby potrzebnych konwersji między wersjami
serwery API używają wersji wewnętrznej w implementacji logiki API. Wersja wewnętrzna jest
czasem nazywana wersją osiową (ang. hub version), ponieważ jest swoistą osią, na którą i z
której konwertowane są wszystkie pozostałe wersje (zob. rysunek 8.4). Wewnętrzna logika API
jest implementowana tylko raz dla wersji centralnej.
Na rysunku 8.5 pokazane jest, jak serwery API używają wersji wewnętrznej w cyklu życia
żądania kierowanego do API:
Rysunek 8.5. Konwersja obiektów API w cyklu życia żądania
Na rysunku 8.6 pokazano nie tylko konwersje, ale też ustawianie wartości domyślnych (ang.
defaulting). Jest to proces uzupełniania nieokreślonych wartości pól. Ustawianie wartości
domyślnych jest ściśle powiązane z konwersją i zawsze jest stosowane do wersji zewnętrznej
przesyłanej w żądaniu od użytkownika, z systemu etcd lub z webhooka kontroli dostępu, ale
nigdy w momencie konwersji z wersji centralnej na wersję zewnętrzną.
Logika ustawiania wartości domyślnych może się zmieniać w cyklu życia serwera API. Wyobraź
sobie, że dodałeś do typu nowe pole. Użytkownik może mieć zapisane na dysku dawne obiekty.
Takie obiekty mogą się też znajdować w systemie etcd. Jeśli nowe pole ma wartość domyślną,
zostanie ona ustawiona, gdy dawne, zapisane obiekty będą przesyłane na serwer API lub gdy
użytkownik pobierze jeden z dawnych obiektów z systemu etcd. Będzie się wtedy wydawać, że
nowe pole istniało od zawsze, podczas gdy w rzeczywistości to proces ustawiania wartości
domyślnych na serwerze API przypisze do pola wartość w trakcie przetwarzania żądania.
Konwersja jest niezbędna, jeśli chodzi o działanie serwera API. Bardzo istotne
jest też, aby wszystkie konwersje (w jedną i drugą stronę) były poprawne ze
względu na konwersję powrotną. Poprawność ze względu na konwersję
powrotną oznacza, że w grafie wersji (zob. rysunek 8.4) można dokonać
konwersji w jedną, a następnie w drugą stronę, rozpoczynając od dowolnych
wartości, i nigdy nie skutkuje to utratą informacji. Konwersje muszą więc być
wzajemnie jednoznaczne i działać w modelu jeden do jednego. Możliwe musi
być np. przejście od losowego (ale poprawnego) obiektu z wersji v1 do
wewnętrznej wersji centralnej, a następnie ponownie do wersji v1. Wynikowy
obiekt musi być równoważny pierwotnemu.
Zapewnienie poprawności ze względu na konwersję powrotną często wymaga
starannego zastanowienia. Prawie zawsze wpływa to na projekt nowych wersji
API, a także na rozszerzanie dawnych typów, aby można było w nich zapisać
informacje przechowywane w nowych wersjach.
Krótko mówiąc: zapewnianie poprawności ze względu na konwersję powrotną
jest trudne — czasem bardzo. Zobacz punkt „Testowanie konwersji
powrotnych”, aby się dowiedzieć, jak skutecznie testować konwersję powrotną.
v1beta1.SchemeGroupVersion,
v1alpha1.SchemeGroupVersion,
))
}
Ponieważ używanych jest kilka wersji, trzeba zdefiniować priorytety. Kolejność wersji będzie
używana do ustalenia domyślnej wersji składowania zasobu. Była też istotna przy wyborze
wersji w wewnętrznych klientach (zwracających obiekty z wersji wewnętrznej; zob. uwagę
„Wersjonowane klienty a dawne klienty wewnętrzne”). Jednak klienty wewnętrzne są
wycofywane. W przyszłości nawet w kodzie serwera API używany będzie klient wersji
zewnętrznych.
Konwersje
Konwersja polega na wzięciu obiektu z jednej wersji i przekształceniu go na obiekt z innej
wersji. Konwersja jest implementowana z użyciem funkcji konwersji. Część takich funkcji jest
pisana ręcznie (zwyczajowo są one umieszczane w pliku pkg/apis/nazwa-
grupy/wersja/conversion.go), a inne są automatycznie generowane przez generator
conversion-gen (http://bit.ly/31RewiP; te funkcje zwyczajowo są umieszczane w pliku
pkg/apis/nazwa-grupy/wersja/zz_generated.conversion.go).
Konwersja jest inicjowana za pomocą schematu (zob. punkt „Schemat”) i metody Convert(), do
której przekazywane są obiekt źródłowy in i obiekt docelowy out:
func (s *Scheme) Convert(in, out interface{}, context interface{}) error
Parametr context jest opisany tak:
// …opcjonalne pole, które jednostki wywołujące mogą używać do przekazywania
// informacji do funkcji konwersji.
Ten parametr jest używany tylko w wyjątkowych sytuacjach. Zwykle ma wartość nil. Dalej w
rozdziale przyjrzymy się zasięgowi funkcji konwersji. Zobaczysz tam, jak uzyskać dostęp do
kontekstu w funkcjach konwersji.
Aby można było przeprowadzić konwersję, w schemacie znane muszą być typy API z języka Go,
powiązane z nimi identyfikatory GVK i funkcje konwersji dla tych identyfikatorów GVK.
Generator conversion-gen rejestruje wygenerowane funkcje konwersji za pomocą lokalnego
obiektu do budowania schematu. W naszym przykładowym niestandardowym serwerze API plik
zz_generated.conversion.go rozpoczyna się tak:
func init() {
localSchemeBuilder.Register(RegisterConversions)
}
a.(*Topping),
b.(*restaurant.Topping),
scope,
)
},
); err != nil {
return err
}
...
return nil
}
...
Funkcja Convert_v1alpha1_Topping_To_restaurant_Topping() jest wygenerowana.
Przyjmuje obiekt z wersji v1alpha1 i przekształca go na obiekt wersji wewnętrznej.
})
}
return nil
}
Oczywiście żaden generator kodu nie jest na tyle inteligentny, aby zrozumieć zamiary
użytkownika definiującego różne typy.
Warto zauważyć, że w trakcie konwersji obiekt źródłowy nigdy nie może być modyfikowany.
Jednak zupełnie normalne i wysoce zalecane (często z powodu wydajności) jest ponowne
wykorzystywanie źródłowych struktur danych w docelowym obiekcie, jeśli typy tych struktur są
ze sobą zgodne.
Jest to tak ważne, że powtarzamy to w ostrzeżeniu. Ewentualne modyfikacje obiektów mają
wpływ nie tylko na implementację konwersji, ale też na jednostki uruchamiające konwersję i
korzystające z efektów konwersji.
Choć współdzielenie struktur danych związane jest z ryzykiem, pozwala w wielu sytuacjach
uniknąć niepotrzebnego alokowania pamięci. Generator kodu porównuje struktury źródłowe i
docelowe oraz używa pakietu unsafe języka Go do przekształcania za pomocą prostej konwersji
typów wskaźników do struktur z tym samym układem pamięci. Ponieważ typ reprezentujący
pizzę w wersji wewnętrznej i w wersji v1beta1 ma ten sam układ pamięci, otrzymujemy
następującą funkcję:
func autoConvert_restaurant_PizzaSpec_To_v1beta1_PizzaSpec(
in *restaurant.PizzaSpec,
out *PizzaSpec,
s conversion.Scope,
) error {
out.Toppings = *(*[]PizzaTopping)(unsafe.Pointer(&in.Toppings))
return nil
}
Na poziomie języka maszynowego jest to pusta instrukcja, dlatego funkcja działa błyskawicznie.
Pozwala to uniknąć alokowania pamięci dla wycinka i kopiowania kolejnych elementów z
argumentu in do out.
Warto też wspomnieć o trzecim argumencie funkcji konwersji — o zasięgu konwersji
(conversion.Scope).
Zasięg konwersji zapewnia dostęp do wielu wartości z poziomu meta. Można np. uzyskać
dostęp do wartości context przekazanej do metody Convert(in, out interface{}, context
interface{}) error schematu:
s.Meta().Context
Można też uruchomić konwersję podobiektów (za pomocą wywołania s.Convert) lub
przeprowadzić konwersję bez uwzględniania zarejestrowanych funkcji konwersji (przy użyciu
wywołania s.DefaultConvert).
Jednak w większości scenariuszy konwersja nie wymaga używania zasięgu. Dla uproszczenia
zasięg można ignorować, chyba że natrafisz na skomplikowaną sytuację, w której potrzebny jest
kontekst wykraczający poza obiekty źródłowy i docelowy.
Wyobraź sobie, że jest rok 2014 i używasz bardzo starej wersji Kubernetesa. Pole
restartPolicy zostało właśnie dodane do systemu w jego najnowszej ówcześnie wersji. Po
zaktualizowaniu klastra systemie etcd znajduje się pod bez pola restartPolicy. Wywołanie
kubectl get pod wczyta dawny pod z systemu etcd, a kod do ustawiania wartości domyślnych
ustawi wartość domyślną Always. Z perspektywy użytkownika dawny pod w magiczny sposób
uzyskuje nowe pole restartPolicy.
Wróć do rysunku 8.6 i zobacz, gdzie obecnie ustawiane są wartości domyślne w procesie
obsługi żądania w Kubernetesie. Zauważ, że operacja ta jest wykonywana tylko dla typów z
wersji zewnętrznych, a nie dla typów z wersji wewnętrznej.
Teraz przyjrzyj się kodowi odpowiedzialnemu za ustawianie wartości domyślnych. Proces ten
(podobnie jak konwersje) jest inicjowany przez kod z biblioteki k8s.io/apiserver za pomocą
schematu. Dlatego w schemacie trzeba zarejestrować funkcje ustawiające wartości domyślne
dla niestandardowych typów.
Większość kodu do ustawiania wartości domyślnych jest generowana (też podobnie jak w
przypadku konwersji) za pomocą pliku binarnego defaulter-gen (http://bit.ly/2J108vK). Ten
generator analizuje typy API i tworzy funkcje ustawiające wartości domyślne w pliku
pkg/apis/nazwa-grupy/wersja/zz_generated.defaults.go. Wygenerowany kod domyślnie nie robi
nic innego oprócz wywoływania funkcji ustawiających wartości domyślne podstruktur.
Własną logikę ustawiania wartości domyślnych możesz zdefiniować za pomocą wzorca nazw dla
funkcji ustawiających takie wartości — SetDefaultsRodzaj:
func SetDefaultsRodzaj(obj *Typ) {
...
}
Ponadto — inaczej niż dla konwersji — trzeba ręcznie zarejestrować wygenerowaną funkcję w
lokalnym obiekcie do budowania schematu:
func init() {
localSchemeBuilder.Register(RegisterDefaults)
}
Tu funkcja RegisterDefaults jest generowana w pakiecie pkg/apis/nazwa-
grupy/wersja/zz_generated.defaults.go.
W kodzie do ustawiania wartości domyślnych niezwykle istotna jest wiedza o tym, czy pole
zostało ustawione przez użytkownika, czy nie. W wielu sytuacjach nie jest to oczywiste.
Język Go ma wartości zerowe dla wszystkich typów i używa ich, jeśli pole nie występuje w
przekazanych danych w formacie JSON lub protobuf. Załóżmy, że pole logiczne foo ma wartość
domyślną true. Wartością zerową tego pola jest false. Niestety, nie jest jasne, czy wczytana
wartość false została ustawiona przez użytkownika, czy jest wartością zerową dla pól
logicznych.
Aby uniknąć takich sytuacji, dla typów API w języku Go często trzeba posłużyć się typem
wskaźnikowym (w omawianym przykładzie jest to *bool). Wartość false podana przez
użytkownika skutkuje różnym od nil wskaźnikiem typu logicznego do wartości false. Jeśli
użytkownik podał wartość true, powstaje różny od nil wskaźnik typu logicznego do wartości
true. Jeżeli nie została podana żadna wartość, wskaźnik jest równy nil. Te sytuacje można
wykrywać w kodzie do ustawiania wartości domyślnych:
func SetDefaultsRodzaj(obj *Typ) {
if obj.Foo == nil {
x := true
obj.Foo = &x
}
}
Ten kod zapewnia oczekiwaną semantykę „foo domyślnie przyjmuje wartość true”.
...
“k8s.io/apimachinery/pkg/api/apitesting/roundtrip”
restaurantfuzzer “github.com/programming-kubernetes/pizza-
apiserver/pkg/apis/
restaurant/fuzzer”
)
}
Wewnętrznie wywołanie RoundTripTestForAPIGroup instaluje grupę API w tymczasowym
schemacie za pomocą funkcji Install. Następnie tworzy losowe obiekty dla wersji
wewnętrznej, używając przekazanego fuzzera, i konwertuje te obiekty do wybranej wersji
zewnętrznej i z powrotem do wersji wewnętrznej. Wynikowe obiekty muszą być
odpowiednikami obiektów pierwotnych. Takie testy są powtarzane setki lub tysiące razy dla
wszystkich wersji zewnętrznych.
Fuzzer to funkcja zwracająca wycinek z funkcjami generującymi losowe obiekty typów (ang.
randomizer) z wersji wewnętrznych i ich podtypów. W tym przykładzie fuzzer znajduje się w
pliku pkg/apis/restaurant/fuzzer/fuzzer.go i zwraca funkcję generującą losowe obiekty ze
specyfikacją:
// Funcs zwraca fuzzery dla grupy API dla pizzerii.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(s *restaurant.PizzaSpec, c fuzz.Continue) {
{“pomidory”, 1},
}
}
seen := map[string]bool{}
for i := range s.Toppings {
// Quantity ma być liczbą dodatnią, ale nie za dużą.
s.Toppings[i].Quantity = 1 + c.Intn(10)
// Usuwanie powtórzeń.
for {
if !seen[s.Toppings[i].Name] {
break
}
s.Toppings[i].Name = c.RandString()
}
seen[s.Toppings[i].Name] = true
}
},
}
}
Jeśli nie zostanie podana funkcja generująca losowe obiekty, używana biblioteka
github.com/google/gofuzz (http://bit.ly/2KJrb27) spróbuje uzyskać takie obiekty, ustawiając
losowe wartości dla typów bazowych i rekurencyjnie przetwarzając wskaźniki, struktury, mapy i
wycinki, ostatecznie wywołując niestandardowe funkcje generujące losowe obiekty (jeśli
programista je podał).
W trakcie pisania funkcji generującej losowe obiekty jednego z typów wygodnie jest najpierw
wywołać funkcję c.FuzzNoCustom(s). Ustawia ona losowe wartości dla podanego obiektu s, a
dodatkowo wywołuje niestandardowe funkcje dla podstruktur, ale już nie dla samego s.
Następnie programista może ograniczyć i poprawić losowe wartości, aby obiekt był prawidłowy.
Ważne jest, aby fuzzery było możliwie ogólne i generowały jak najwięcej
poprawnych obiektów. Jeśli fuzzer ma zbyt wiele ograniczeń, pokrycie danych
testami będzie niskie. W trakcie rozwijania Kubernetesa w wielu sytuacjach nie
wykryto błędów regresji, ponieważ używane fuzzery były niskiej jakości.
Fuzzer musi uwzględniać tylko obiekty, które są poprawne i które odpowiadają
obiektom, jakie można zdefiniować w wersjach zewnętrznych. Często trzeba
ograniczyć losowe wartości ustawiane przez funkcję c.FuzzNoCustom(s) w
taki sposób, aby losowe obiekty były prawidłowe. Na przykład łańcuch znaków
z adresem URL nie musi umożliwiać konwersji powrotnej dla przypadkowych
wartości, jeśli w procesie sprawdzania poprawności nieprawidłowe łańcuchy
znaków są odrzucane.
Sprawdzanie poprawności
Sprawdzanie poprawności przychodzących obiektów ma miejsce zaraz po deserializacji,
ustawieniu wartości domyślnych i konwersji do wersji wewnętrznej. Na pokazanym wcześniej
rysunku 8.5 widać, że sprawdzanie poprawności jest wykonywane między modyfikującymi
wtyczkami kontroli dostępu, a sprawdzającymi poprawność wtyczkami kontroli dostępu, na
długo przed wykonaniem logiki tworzącej lub modyfikującej obiekty.
To oznacza, że sprawdzanie poprawności trzeba zaimplementować raz — dla wersji
wewnętrznej (nie trzeba tego robić dla wszystkich wersji zewnętrznych). Ma to tę zaletę, że —
co oczywiste — zmniejsza ilość pracy związanej z implementacją i gwarantuje spójność między
wersjami. Z drugiej strony błędy sprawdzania poprawności nie dotyczą wersji zewnętrznych.
Zdarza się to w zasobach Kubernetesa, jednak w praktyce nie stanowi to istotnego problemu.
W tym punkcie opiszemy implementowanie funkcji do sprawdzania poprawności. Łączenie ich z
niestandardowym serwerem API (wywoływanie sprawdzania poprawności w strategii, która
konfiguruje generyczny rejestr) omówimy w następnym punkcie. Rysunek 8.5 jest więc nieco
mylący, ale za to przejrzysty.
Na razie wystarczy przyjrzeć się punktowi wejścia do procesu sprawdzania poprawności w
strategii:
func (pizzaStrategy) Validate(
ctx context.Context, obj runtime.Object,
) field.ErrorList {
pizza := obj.(*restaurant.Pizza)
return validation.ValidatePizza(pizza)
}
Ten kod wywołuje funkcję do sprawdzania poprawności ValidateRodzaj(obj *Rodzaj)
field.ErrorList z pakietu do sprawdzania poprawności dla grupy API
pkg/apis/grupa/validation.
Funkcja do sprawdzania poprawności zwraca listę błędów. Zwykle są one zapisywane w tym
samym stylu — zwracane wartości są dołączane do listy błędów w trakcie rekurencyjnego
przetwarzania typu z użyciem jednej funkcji do sprawdzania poprawności dla każdej struktury:
return allErrs
}
s.Toppings[i].Name,
“nie może być puste.”,
))
} else {
if prevNames[s.Toppings[i].Name] {
allErrs = append(allErrs, field.Invalid(
fldPath.Child(“toppings”).Index(i).Child(“name”),
s.Toppings[i].Name,
“musi być unikatowe.”,
))
}
prevNames[s.Toppings[i].Name] = true
}
}
return allErrs
}
Warto zwrócić uwagę na określanie ścieżki do pola z użyciem wywołań Child i Index. Ta
ścieżka jest zapisana w formacie JSON i wyświetlana po wystąpieniu błędów.
oldPizza := old.(*restaurant.Pizza)
return validation.ValidatePizzaUpdate(objPizza, oldPizza)
}
Takie funkcje można wykorzystać do upewniania się, że modyfikowane są wyłącznie pola, które
nie są przeznaczone tylko do odczytu. Kod sprawdzający poprawność w trakcie modyfikacji
często wywołuje zwykłe funkcje do sprawdzania poprawności, a dodatkowo przeprowadza
kontrolę związaną z modyfikacjami.
Rejestr i strategia
Zobaczyłeś już, jak definiowane i sprawdzane są typy API. Następny krok to
zaimplementowanie logiki REST dla tych typów API. Na rysunku 8.7 pokazane jest, że rejestr to
centralny element implementacji grupy API. Generyczny kod obsługi żądań REST z biblioteki
k8s.io/apiserver kieruje wywołania do tego rejestru.
Rysunek 8.7. Składowanie zasobów i rejestr generyczny
Rejestr generyczny
Logika REST jest zwykle implementowana w rejestrze generycznym. Jest to — zgodnie z nazwą
— generyczna implementacja interfejsów rejestru z pakietu k8s.io/apiserver/pkg/registry/rest.
W rejestrze generycznym zaimplementowane jest domyślna obsługa żądań REST dla
„normalnych” zasobów. Prawie wszystkie zasoby Kubernetesa korzystają z tej implementacji.
Tylko dla kilku — dla tych, które nie utrwalają obiektów, np. SubjectAccessReview; zob. punkt
„Delegowana autoryzacja” — stosowane są niestandardowe implementacje.
W pliku k8s.io/apiserver/pkg/registry/rest/rest.go znajdziesz wiele interfejsów luźno
odpowiadających operacjom HTTP i niektórym mechanizmom API. Jeśli dany interfejs jest
zaimplementowany w rejestrze, kod punktu końcowego API udostępnia określone funkcje REST.
Ponieważ w rejestrze generycznym zaimplementowanych jest większość interfejsów z pakietu
k8s.io/apiserver/pkg/registry/rest, zasoby używające tego rejestru obsługują wszystkie
domyślne operacje HTTP z Kubernetesa (zob. punkt „Interfejs HTTP serwera API”). Oto lista
zaimplementowanych interfejsów wraz z opisem z dokumentacji go doc z kodu źródłowego
Kubernetesa:
CollectionDeleter
Obiekt, który potrafi usunąć kolekcję zasobów REST.
Creater
Obiekt, który potrafi utworzyć instancję obiektu REST.
CreaterUpdater
Obiekt składowania danych, który musi obsługiwać operacje tworzenia i modyfikacji.
Exporter
Obiekt, który wie, jak przygotować zasoby REST na potrzeby eksportu.
Getter
Obiekt, który potrafi pobrać zasób REST o podanej nazwie.
GracefulDeleter
Obiekt, który wie, jak przekazać opcje usuwania, aby umożliwić opóźnione usuwanie
obiektu REST.
Lister
Obiekt, który potrafi pobrać zasoby pasujące do podanych kryteriów dotyczących pola i
etykiety.
Patcher
Obiekt składowania danych, który obsługuje pobieranie i modyfikacje.
Scoper
Wymagany obiekt określający zasięg zasobu.
Updater
Obiekt, który potrafi zmodyfikować instancję obiektu REST.
Watcher
Obiekt, który powinien być implementowany przez wszystkie obiekty składowania danych
mające umożliwiać obserwowanie zmian za pomocą API Watch.
Przyjrzyj się jednemu z tych interfejsów, Creater:
// Creater to obiekt, który potrafi utworzyć instancję obiektu REST.
type Creater interface {
// New zwraca pusty obiekt, który można zastosować razem z funkcją Create
// po umieszczeniu w nim danych z żądania.
// Obiekt musi być typu wskaźnikowego, aby można go użyć w wywołaniu
Codec.DecodeInto([]byte,
// runtime.Object).
New() runtime.Object
Strategia
Rejestr generyczny można w pewnym stopniu zmodyfikować, używając obiektu nazywanego
strategią. Strategia udostępnia wywołania zwrotne do mechanizmów takich jak sprawdzanie
poprawności (zob. punkt „Sprawdzanie poprawności”).
Strategia implementuje interfejsy REST strategii podane tu wraz z ich opisami z dokumentacji
go doc (definicje znajdziesz w pakiecie k8s.io/apiserver/pkg/registry/rest):
RESTCreateStrategy
Definiuje podstawowe sprawdzanie poprawności, akceptowane dane wejściowe i sposób
generowania nazw na potrzeby tworzenia obiektu zgodnie z konwencjami z API
Kubernetesa.
RESTDeleteStrategy
Definiuje usuwanie obiektu zgodnie z konwencjami z API Kubernetesa.
RESTGracefulDeleteStrategy
Musi być zaimplementowany w rejestrze obsługującym kontrolowane usuwanie.
GarbageCollectionDeleteStrategy
Musi być zaimplementowany w rejestrze, w którym jednostki zależne mają być domyślnie
osierocane.
RESTExportStrategy
func NewREST(
scheme *runtime.Scheme,
optsGetter generic.RESTOptionsGetter,
) (*registry.REST, error) {
strategy := NewStrategy(scheme)
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &restaurant.Pizza{} },
NewListFunc: func() runtime.Object { return &restaurant.PizzaList{}
},
PredicateFunc: MatchPizza,
DefaultQualifiedResource: restaurant.Resource(“pizzas”),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
options := &generic.StoreOptions{
RESTOptions: optsGetter,
AttrFunc: GetAttrs,
}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
return ®istry.REST{store}, nil
}
Ta funkcja tworzy obiekt rejestru generycznego typu genericregistry.Store i ustawia kilka
pól. Liczne z tych pól są opcjonalne. Jeśli programista ich nie ustawi, funkcja
store.CompleteWithOptions przypisze do nich wartości domyślne.
Instancja niestandardowej strategii najpierw jest tworzona przez konstruktor NewStrategy, a
następnie łączona z rejestrem na potrzeby użytkowania operatorów create, update i delete.
Ponadto funkcja z pola NewFunc tworzy nową instancję obiektu, a funkcja z pola NewListFunc
tworzy nową listę obiektów. Funkcja z pola PredicateFunc przekształca selektor (który może
zostać przekazany w żądaniu listy) na funkcję predykatową i filtruje obiekty czasu wykonania.
Zwrócony obiekt to rejestr REST. W przykładowym projekcie (http://bit.ly/2Rxcv6G) jest to tylko
prosta nakładka na obiekt rejestru generycznego pozwalająca uzyskać własny typ:
type REST struct {
*genericregistry.Store
}
Teraz wszystko jest już gotowe do utworzenia instancji API i powiązania jej z niestandardowym
serwerem API. W następnym punkcie zobaczysz, jak na tej podstawie utworzyć funkcję obsługi
żądań HTTP.
Instalowanie API
Aby aktywować API na serwerze API, trzeba wykonać dwa kroki:
1. Wersję API trzeba zainstalować w schemacie z typami API (oraz funkcjami do konwersji i
ustawiania wartości domyślnych) serwera.
2. Wersję API trzeba zainstalować w multiplekserze żądań HTTP na serwerze.
Pierwszy krok jest zwykle wykonywany centralnie w funkcjach init w ramach rozruchu
serwera API. W przykładowym niestandardowym serwerze API dzieje się to w pliku
pkg/apiserver/apiserver.go, w którym zdefiniowane są obiekty serverConfig i CustomServer
(zob. punkt „Wzorzec opcji i konfiguracji oraz szablonowy kod potrzebny do uruchomienia
serwera”):
import (
...
“k8s.io/apimachinery/pkg/runtime”
“k8s.io/apimachinery/pkg/runtime/serializer”
“github.com/programming-kubernetes/pizza-
apiserver/pkg/apis/restaurant/install”
)
var (
Scheme = runtime.NewScheme()
Codecs = serializer.NewCodecFactory(Scheme)
)
Następnie dla każdej grupy API, która ma być udostępniana, należy wywołać funkcję
Install():
func init() {
install.Install(Scheme)
}
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
)
}
To powoduje zarejestrowanie w globalnym schemacie naszych typów API, w tym funkcji do
konwersji i ustawiania wartości domyślnych. Oznacza to, że pusty schemat z rysunku 8.3 ma
teraz wszystkie informacje o naszych typach.
Drugi krok polega na dodaniu grupy API do multipleksera żądań HTTP. Kod generycznego
serwera API zagnieżdżony w strukturze CustomServer zawiera metodę
InstallAPIGroup(apiGroupInfo *APIGroupInfo) error, która konfiguruje dla grupy API cały
potok obsługi żądań.
Jedyne, co musisz zrobić, to przekazać poprawnie zapełnioną strukturę typu APIGroupInfo.
Zrobimy to w konstruktorze New() (*CustomServer, error) typu completedConfig:
// New zwraca nową instancję typu CustomServer na podstawie podanej
konfiguracji.
func (c completedConfig) New() (*CustomServer, error) {
genericServer, err := c.GenericConfig.New(“pizza-apiserver”,
genericapiserver.NewEmptyDelegate())
if err != nil {
return nil, err
s := &CustomServer{
GenericAPIServer: genericServer,
}
apiGroupInfo :=
genericapiserver.NewDefaultAPIGroupInfo(restaurant.GroupName,
Scheme, metav1.ParameterCodec, Codecs)
v1alpha1storage := map[string]rest.Storage{}
pizzaRest := pizzastorage.NewREST(Scheme,
c.GenericConfig.RESTOptionsGetter)
v1alpha1storage[“pizzas”] = customregistry.RESTInPeace(pizzaRest)
toppingRest := toppingstorage.NewREST(
Scheme, c.GenericConfig.RESTOptionsGetter,
)
v1alpha1storage[“toppings”] = customregistry.RESTInPeace(toppingRest)
apiGroupInfo.VersionedResourcesStorageMap[“v1alpha1”] = v1alpha1storage
v1beta1storage := map[string]rest.Storage{}
pizzaRest = pizzastorage.NewREST(Scheme,
c.GenericConfig.RESTOptionsGetter)
v1beta1storage[“pizzas”] = customregistry.RESTInPeace(pizzaRest)
apiGroupInfo.VersionedResourcesStorageMap[“v1beta1”] = v1beta1storage
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
return s, nil
}
Obiekt apiGroupInfo zawiera referencje do rejestru generycznego, który zmodyfikowaliśmy w
punkcie „Rejestr i strategia” z użyciem strategii. Dla każdej wersji grupy i zasobu za pomocą
zaimplementowanych konstruktorów tworzona jest instancja rejestru.
Sam rejestr jest niezależny od wersji, ponieważ używa obiektów wersji wewnętrznej (zob. punkt
8.5). Dlatego dla każdej wersji wywoływany jest ten sam konstruktor rejestru.
Wywołanie InstallAPIGroup powoduje uzyskanie kompletnego niestandardowego serwera API
gotowego do udostępniania naszej niestandardowej grupy API (zob. rysunek 8.7).
Po wszystkich tych żmudnych przygotowaniach pora zobaczyć nasze nowe grupy API w akcji. W
tym celu uruchomimy serwer w sposób pokazany w punkcie „Pierwsze uruchomienie”. Jednak
tym razem lista wykrytych informacji nie jest pusta — zawiera nowo zarejestrowany zasób:
$ curl -k https://localhost:443/apis
{
“kind”: “APIGroupList”,
“groups”: [
{
“name”: “restaurant.programming-kubernetes.info”,
“versions”: [
{
“groupVersion”: “restaurant.programming-kubernetes.info/v1beta1”,
“version”: “v1beta1”
},
{
“groupVersion”: “restaurant.programming-kubernetes.info/v1alpha1”,
“version”: “v1alpha1”
}
],
“preferredVersion”: {
“groupVersion”: “restaurant.programming-kubernetes.info/v1beta1”,
“version”: “v1beta1”
},
“serverAddressByClientCIDRs”: [
{
“clientCIDR”: “0.0.0.0/0”,
“serverAddress”: “:443”
}
]
}
]
}
W ten sposób prawie osiągnęliśmy cel, jakim jest udostępnianie API pizzerii. Podłączyliśmy
wersje grupy API, dodaliśmy konwersje, a sprawdzanie poprawności działa prawidłowo.
Brakuje jednak kodu sprawdzającego, czy podany dodatek do pizzy istnieje w klastrze. Można
byłoby umieścić taki kod w funkcjach do sprawdzania poprawności. Jednak zwyczajowo są to
funkcje do sprawdzania poprawności formatu. Są statyczne i nie wymagają do działania innych
zasobów.
Bardziej złożone sprawdzanie poprawności jest implementowane w kodzie kontroli dostępu.
Jest to temat następnego punktu.
Kontrola dostępu
Każde żądanie po unmarshallingu, ustawieniu wartości domyślnych i konwersji na typy
wewnętrzne przechodzi przez łańcuch wtyczek kontroli dostępu (zob. rysunek 8.2). Żądania
przechodzą kontrolę dostępu dwukrotnie:
Ta sama wtyczka może zarówno modyfikować żądania, jak i sprawdzać ich poprawność. Dlatego
może być wywoływana przez mechanizm kontroli dostępu dwukrotnie:
Oprócz tego łańcuch wtyczek (ich kolejność) jest taki sam w obu etapach kontroli dostępu.
Wtyczki są zawsze włączane lub wyłączane dla obu etapów.
Wtyczki kontroli dostępu — przynajmniej te zaimplementowane w języku Go w sposób opisany
w tym rozdziale — współdziałają z typami z wersji wewnętrznej. Z kolei webhooki kontroli
dostępu (zob. punkt „Webhooki kontroli dostępu”) są oparte na typach z wersji zewnętrznych i
wymagają konwersji przy przesyłaniu żądań do webhooka i z powrotem (gdy używane są
webhooki modyfikujące).
Po wszystkich tych teoretycznych rozważaniach pora przejść do kodu.
Implementacja
Wtyczka kontroli dostępu to typ implementujący:
GetObjectConvertor() runtime.ObjectConvertor
}
Atrybuty przekazane do wtyczki (w funkcji Admit, Validate lub w obu tych funkcjach)
zawierają wszystkie potrzebne do zaimplementowania zaawansowanych testów informacje,
jakie można pobrać z żądania:
// Attributes to interfejs używany przez kontroler AdmissionController do
pobierania
// informacji o żądaniu, które służą do podejmowania decyzji z zakresu
kontroli dostępu.
type Attributes interface {
GetResource() schema.GroupVersionResource
// GetSubresource zwraca nazwę żądanego podzasobu. Jest to
GetSubresource() string
// GetOperation zwraca wykonywaną operację.
GetOperation() Operation
GetObject() runtime.Object
GetOldObject() runtime.Object
// GetKind zwraca typ przetwarzanego obiektu (np. Pod).
GetKind() schema.GroupVersionKind
// GetUserInfo zwraca informacje o użytkowniku zgłaszającym dane żądanie.
GetUserInfo() user.Info
}
Na etapie modyfikowania (czyli w implementacji metody Admit(a Attributes) error) można
zmieniać atrybuty, a bardziej precyzyjnie — obiekt zwrócony przez funkcję GetObject()
runtime.Object.
Odrzucenie żądania jest sygnalizowane zwróceniem błędu różnego od nil przez funkcję Admit
lub Validate.
...
}
Wtyczki kontroli dostępu zawsze powinny najpierw sprawdzać identyfikator GVK przekazanego
obiektu:
o ObjectInterfaces,
) error {
// Interesują nas tylko pizze.
if a.GetKind().GroupKind() != restaurant.Kind(“Pizza”) {
return nil
}
...
}
Podobnie powinien wyglądać kod przy sprawdzaniu poprawności:
a admission.Attributes,
o ObjectInterfaces,
) error {
// Interesują nas tylko pizze.
if a.GetKind().GroupKind() != restaurant.Kind(“Pizza”) {
return nil
}
...
}
_ admission.ObjectInterfaces,
) error {
return nil
}
if !d.WaitForReady() {
return admission.NewForbidden(a, fmt.Errorf(“Niegotowy”))
obj := a.GetObject()
pizza := obj.(*restaurant.Pizza)
return admission.NewForbidden(
a,
}
return nil
Rejestrowanie
Wtyczki kontroli dostępu trzeba zarejestrować. Służy do tego funkcja Register:
“PizzaTopping”,
},
)
}
Ta funkcja jest dodawana do listy wtyczek w obiekcie typu RecommendedOptions (zob. punkt
„Wzorzec opcji i konfiguracji oraz szablonowy kod potrzebny do uruchomienia serwera”):
func (o *CustomServerOptions) Complete() error {
pizzatoppings.Register(o.RecommendedOptions.Admission.Plugins)
o.RecommendedOptions.Admission.RecommendedPluginOrder =
append(oldOrder, “PizzaToppings”)
return nil
kind: AdmissionConfiguration
apiVersion: apiserver.k8s.io/v1alpha1
plugins:
- name: CustomAdmissionPlugin
path: custom-admission-plugin.yaml
Inna możliwość to wewnątrzwierszowe podawanie konfiguracji, aby cała konfiguracja kontroli
dostępu znajdowała się w jednym miejscu:
kind: AdmissionConfiguration
apiVersion: apiserver.k8s.io/v1alpha1
plugins:
- name: CustomAdmissionPlugin
configuration:
wewnątrzwierszowa-niestandardowa-konfiguracja-w-formacie-yaml
Podłączanie zasobów
Wtyczki kontroli dostępu często potrzebują do działania klientów, informatorów lub innych
zasobów. Potrzebne zasoby można podłączyć za pomocą inicjalizatorów wtyczek.
Istnieje wiele standardowych inicjalizatorów wtyczek. Jeśli dana wtyczka ma być przez nie
wywoływana, musi implementować określone interfejsy z wywołaniami zwrotnymi (więcej na
ten temat dowiesz się z pakietu k8s.io/apiserver/pkg/admission/initializer):
// WantsExternalKubeClientSet definiuje funkcję udostępniającą obiekt
ClientSet
// wtyczkom kontroli dostępu, które potrzebują tego obiektu.
admission.InitializationValidator
}
// WantsExternalKubeInformerFactory definiuje funkcję udostępniającą obiekt
InformerFactory
SetExternalKubeInformerFactory(informers.SharedInformerFactory)
admission.InitializationValidator
SetAuthorizer(authorizer.Authorizer)
admission.InitializationValidator
SetScheme(*runtime.Scheme)
admission.InitializationValidator
}
Jeśli zaimplementujesz wybrane z tych funkcji, wtyczka będzie wywoływana w trakcie rozruchu
serwera, aby uzyskać dostęp np. do zasobów Kubernetesa lub globalnego schematu serwera
API.
}
Standardowe inicjalizatory są bardzo przydatne, jednak potrzebujemy dostępu do informatora
dotyczącego dodatków do pizzy. Zobacz więc, jak dodać własne inicjalizatory. Inicjalizator
obejmuje:
Implementację interfejsu z rodziny Wants* (np. WantsRestaurantInformerFactory), który
powinien być zaimplementowany we wtyczce kontroli dostępu:
admission.InitializationValidator
}
plugin admission.Interface,
) {
if wants, ok := plugin.(WantsRestaurantInformerFactory); ok {
wants.SetRestaurantInformerFactory(i.informers)
}
}
Metoda Initialize() sprawdza tu, czy przekazana wtyczka implementuje powiązany
niestandardowy interfejs inicjalizatora Wants*. Jeśli tak jest, inicjalizator wywołuje
zaimplementowaną metodę dla danej wtyczki.
...
o.RecommendedOptions.ExtraAdmissionInitializers =
func(c *genericapiserver.RecommendedConfig) (
[]admission.PluginInitializer, error,
) {
client, err :=
clientset.NewForConfig(c.LoopbackClientConfig)
if err != nil {
return nil, err
informerFactory := informers.NewSharedInformerFactory(
client, c.LoopbackClientConfig.Timeout,
)
o.SharedInformerFactory = informerFactory
return []admission.PluginInitializer{
custominitializer.New(informerFactory),
}, nil
}
...
}
Ten kod tworzy klienta z pętlą zwrotną dla grupy API dla pizzerii, tworzy odpowiednią
fabrykę informatorów, zapisuje ją w opcjach o i zwraca inicjalizator wtyczki dodający tę
fabrykę.
Synchronizowanie informatorów
Jeśli informatory są używane we wtyczkach kontroli dostępu, zawsze należy najpierw
sprawdzić, czy są zsynchronizowane. Dopiero potem można ich używać w funkcjach
Admit() i Validate(). Do tego czasu należy odrzucać żądania, zgłaszając błąd
Forbidden.
Używając pomocniczej struktury Handler (opisanej w punkcie „Implementacja”), można
zsynchronizować informatory za pomocą funkcji Handler.WaitForReady():
if !d.WaitForReady() {
return admission.NewForbidden(
a, fmt.Errorf(“Brak gotowości do obsługi żądań”),
}
Aby w metodzie WaitForReady() uwzględnić metodę HasSynced() niestandardowego
informatora, należy dodać tę ostatnią do funkcji informujących o gotowości w
implementacji inicjalizatora. Oto przykład:
d.toppingLister = f.Restaurant().V1Alpha1().Toppings().Lister()
d.SetReadyFunc(f.Restaurant().V1Alpha1().Toppings().Informer().HasSynced)
}
Instalowanie niestandardowych
serwerów API
W punkcie „Usługi API” pokazaliśmy obiekt typu APIService służący do rejestrowania wersji
grupy API z niestandardowego serwera API za pomocą agregatora z serwera API Kubernetesa:
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: nazwa
spec:
group: nazwa-grupy-API
version: wersja-grupy-API
service:
namespace: przestrzeń-nazw-usług-niestandardowego-serwera-API
name: usługa-niestandardowego-serwera-API
caBundle: zestaw-certyfikatów-w-formacie-base64
insecureSkipTLSVerify: wartość-logiczna
groupPriorityMinimum: 2000
versionPriority: 20
Obiekt typu APIService wskazuje usługę. Zwykle jest to normalna usługa IP z klastra. Oznacza
to, że niestandardowy serwer API jest instalowany w klastrze z użyciem podów. Usługa
przekazuje żądania do tych podów.
Manifesty instalacji
Używane są tu następujące manifesty (znajdziesz je w przykładowym kodzie w serwisie GitHub;
http://bit.ly/2J6CVIz). Będą one częścią instalacji niestandardowej usługi API w klastrze:
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1alpha1.restaurant.programming-kubernetes.info
spec:
insecureSkipTLSVerify: true
group: restaurant.programming-kubernetes.info
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: api
namespace: pizza-apiserver
version: v1alpha1
i v1beta1:
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1alpha1.restaurant.programming-kubernetes.info
spec:
insecureSkipTLSVerify: true
group: restaurant.programming-kubernetes.info
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: api
namespace: pizza-apiserver
version: v1alpha1
apiVersion: v1
kind: Service
metadata:
name: api
namespace: pizza-apiserver
spec:
ports:
- port: 443
protocol: TCP
targetPort: 8443
selector:
apiserver: “true”
Obiekt typu Deployment (tak jak tutaj) lub DaemonSet dla podów niestandardowego
serwera API:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pizza-apiserver
namespace: pizza-apiserver
labels:
apiserver: “true”
spec:
replicas: 1
selector:
matchLabels:
apiserver: “true”
template:
metadata:
labels:
apiserver: “true”
spec:
serviceAccountName: apiserver
containers:
- name: apiserver
image: quay.io/programming-kubernetes/pizza-apiserver:latest
imagePullPolicy: Always
command: [“/pizza-apiserver”]
args:
- --etcd-servers=http://localhost:2379
- --cert-dir=/tmp/certs
- --secure-port=8443
- --v=4
- name: etcd
image: quay.io/coreos/etcd:v3.2.24
workingDir: /tmp
name: pizza-apiserver
spec: {}
Zagregowany serwer API często jest instalowany w węzłach zarezerwowanych dla podów
warstwy kontroli (nazywanych węzłami nadrzędnymi — ang. master). Wtedy warto zastosować
obiekt typu DaemonSet, aby uruchamiać jedną instancję niestandardowego serwera API na
jeden węzeł nadrzędny. To prowadzi do konfiguracji z wysoką dostępnością usługi. Warto
zauważyć, że serwery API są bezstanowe. Oznacza to, że można je łatwo instalować wiele razy i
nie wymaga to wyboru lidera.
Po utworzeniu pokazanych manifestów praca jest już prawie ukończona. Jednak, jak to często
bywa, bezpieczna instalacja wymaga dodatkowego zastanowienia. Może zauważyłeś, że pody
(zdefiniowane w pokazanej instalacji) używają niestandardowego konta usługi, apiserver.
Można je utworzyć za pomocą kolejnego manifestu:
kind: ServiceAccount
apiVersion: v1
metadata:
name: apiserver
namespace: pizza-apiserver
To konto usługi wymaga zestawu uprawnień, które można dodać za pomocą obiektów systemu
RBAC.
Obiekty można tworzyć tylko w istniejącej przestrzeni nazw i usuwać wraz z usunięciem
danej przestrzeni nazw. W tym celu serwer API musi pobierać, wyświetlać i obserwować
przestrzenie nazw.
Webhooków kontroli dostępu
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: aggregated-apiserver-clusterrole
rules:
- apiGroups: [“”]
resources: [“namespaces”]
verbs: [“get”, “watch”, “list”]
- apiGroups: [“admissionregistration.k8s.io”]
resources: [“mutatingwebhookconfigurations”,
“validatingwebhookconfigurations”]
kind: ClusterRoleBinding
metadata:
name: pizza-apiserver-clusterrolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: aggregated-apiserver-clusterrole
subjects:
- kind: ServiceAccount
name: apiserver
namespace: pizza-apiserver
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pizza-apiserver-auth-reader
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
subjects:
- kind: ServiceAccount
name: apiserver
namespace: pizza-apiserver
i z istniejącą rolą klastra systemu RBAC system:auth-delegator:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pizza-apiserver:system:auth-delegator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: apiserver
namespace: pizza-apiserver
$ cd artifacts/deployment
Wygląda to poprawnie. Spróbuj teraz wyświetlić pizze. Włącz rejestrowanie zdarzeń, aby
zobaczyć, czy nie wystąpiły błędy:
...
... GET https://localhost:58727/apis?timeout=32s
...
... GET https://localhost:58727/apis/restaurant.programming-kubernetes.info/
v1alpha1?timeout=32s
...
No resources found.
Wygląda to bardzo dobrze. Widać, że kubectl stosuje wykrywanie informacji, aby ustalić, czym
jest pizza. W tym celu kieruje do API restaurant.programming-kubernetes.info/v1beta1
zapytanie o listę pizz. Nie jest zaskoczeniem, że na razie żadne pizze nie są dostępne.
Oczywiście można to zmienić:
$ cd ../examples
pizza.restaurant.programming-kubernetes.info/margherita created
$ kubectl get pizza -o yaml margherita
apiVersion: restaurant.programming-kubernetes.info/v1beta1
kind: Pizza
metadata:
creationTimestamp: “2019-05-05T13:39:52Z”
name: margherita
namespace: default
resourceVersion: “6”
pizzas/margherita
uid: 42ab6e88-6f3b-11e9-8270-0e37170891d3
spec:
toppings:
- name: mozzarella
quantity: 1
- name: pomidory
quantity: 1
status: {}
Wszystko wygląda świetnie. Jednak pizza margherita była prosta. Wypróbujmy w praktyce
ustawianie wartości domyślnych, tworząc pizzę, dla której nie są podane żadne dodatki:
apiVersion: restaurant.programming-kubernetes.info/v1alpha1
kind: Pizza
metadata:
name: salami
spec:
Mechanizm ustawiania wartości domyślnych powinien przekształcić ten obiekt w pizzę salami z
dodatkiem w postaci salami. Sprawdźmy to:
$ kubectl create -f empty-pizza.yaml
pizza.restaurant.programming-kubernetes.info/salami created
$ kubectl get pizza -o yaml salami
apiVersion: restaurant.programming-kubernetes.info/v1beta1
kind: Pizza
metadata:
creationTimestamp: “2019-05-05T13:42:42Z”
name: salami
namespace: default
resourceVersion: “8”
pizzas/salami
uid: a7cb7af2-6f3b-11e9-8270-0e37170891d3
spec:
toppings:
- name: salami
quantity: 1
- name: mozzarella
quantity: 1
- name: pomidory
quantity: 1
status: {}
Certyfikaty i zaufanie
Obiekt typu APIService zawiera pole caBundle. Określa ono, na jakiej podstawie agregator (na
serwerze API Kubernetesa) może zaufać niestandardowemu serwerowi API. To pole zawiera
certyfikat (wraz z certyfikatami pośrednimi) używany do sprawdzania, czy zagregowany serwer
API ma zadeklarowaną tożsamość. W każdej poważnej instalacji w tym polu należy umieścić
odpowiedni zestaw certyfikatów.
Zaufanie w drugą stronę — ze strony niestandardowego serwera API dla serwera API
Kubernetesa — i wstępne uwierzytelnianie żądań są opisane w punkcie „Delegowane
uwierzytelnianie i obsługa zaufania”. W tym obszarze nie trzeba robić nic więcej.
Wróćmy do przykładu z pizzą. Aby rozwiązanie było bezpieczne, potrzebny jest certyfikat
serwera i klucz dla niestandardowego serwera API z instalacji. Obie te informacje umieścimy w
sekrecie serving-cert i zamontujemy w podzie w ścieżce /var/run/apiserver/serving-cert/tls.
{crt,key}. Następnie użyjemy pliku tls.crt jako certyfikatu w obiekcie typu APIService.
Wszystkie te elementy znajdziesz w przykładowym kodzie w serwisie GitHub
(http://bit.ly/2XxtJWP).
$ make
openssl req -new -x509 -subj “/CN=api.pizza-apiserver.svc”
......................++
................................................................++
rolebinding.rbac.authorization.k8s.io/pizza-apiserver-auth-reader unchanged
deployment.apps/pizza-apiserver configured
namespace/pizza-apiserver unchanged
clusterrolebinding.rbac.authorization.k8s.io/pizza-apiserver-
clusterrolebinding
unchanged
clusterrole.rbac.authorization.k8s.io/aggregated-apiserver-clusterrole
unchanged
serviceaccount/apiserver unchanged
service/api unchanged
secret/serving-cert created
apiservice.apiregistration.k8s.io/v1alpha1.restaurant.programming-
kubernetes.info
configured
apiservice.apiregistration.k8s.io/v1beta1.restaurant.programming-
kubernetes.info
configured
Zwróć uwagę na właściwą nazwę CN=api.pizza-apiserver.svc w certyfikacie. Serwer API
Kubernetesa przekazuje żądanie do usługi api/pizza-apiserver, dlatego w certyfikacie trzeba
umieścić nazwę DNS tej usługi.
kind: APIService
metadata:
name: v1alpha1.restaurant.programming-kubernetes.info
...
spec:
caBundle: LS0tLS1C...
group: restaurant.programming-kubernetes.info
groupPriorityMinimum: 1000
service:
name: api
namespace: pizza-apiserver
version: v1alpha1
versionPriority: 15
status:
conditions:
- lastTransitionTime: “2019-05-05T14:07:07Z”
status: “True”
type: Available
artifacts/deployment
Dane wyglądają zgodnie z oczekiwaniami. Flaga insecureSkipTLSVerify zniknęła, a w polu
caBundle zapisana jest wartość certyfikatu w formacie base64. Ponadto usługa nadal jest
dostępna.
Zobacz teraz, czy kubectl wciąż może kierować zapytania do API:
No resources found.
$ cd ../examples
topping.restaurant.programming-kubernetes.info/salami created
topping.restaurant.programming-kubernetes.info/pomidory created
Pizza margherita wróciła. Tym razem jest idealnie zabezpieczona. Złośliwy sprzedawca pizzy
nie ma szans na rozpoczęcie ataku man-in-the-middle. Buon appetito!
Można też zastosować system etcd z warstwy kontroli klastra (czyli z serwera kube-
apiserver). W zależności od środowiska (samodzielnie instalowane, lokalne lub usługi
hostowane takie jak GKE — Google Container Engine) takie rozwiązanie może być albo
akceptowalne, albo niemożliwe, ponieważ użytkownik nie ma dostępu do takiego klastra (jest
tak w GKE). Gdy to podejście jest akceptowalne, w niestandardowym serwerze API trzeba
zastosować ścieżkę różną od ścieżki z serwera API Kubernetes lub innych użytkowników
systemu etcd. W przykładowym niestandardowym serwerze API ta ścieżka wygląda tak:
const defaultEtcdPathPrefix =
“/registry/pizza-apiserver.programming-kubernetes.github.com”
o := &CustomServerOptions{
RecommendedOptions: genericoptions.NewRecommendedOptions(
defaultEtcdPathPrefix,
...
),
}
return o
}
Przedrostek ścieżki dla systemu etcd jest tu inny niż w ścieżkach dla serwera API Kubernetesa,
gdzie używane są inne nazwy grup API.
Istotne jest też, że dla systemu etcd można zastosować pośrednika. W projekcie etcdproxy-
controller (http://bit.ly/2Na2VrN) zaimplementowano ten mechanizm z użyciem wzorca
operator. Oznacza to, że pośredniki dla systemu etcd mogą być instalowane w klastrze
automatycznie i konfigurowane przy użyciu obiektów typu EtcdProxy.
Pośredniki systemu etcd automatycznie odwzorowują klucze, dlatego gwarantowane jest, że
przedrostki kluczy systemu etcd nie będą powodować konfliktów. Dzięki temu można
współużytkować klastry z systemem etcd w wielu zagregowanych serwerach API bez obaw o
to, że jeden z tych serwerów będzie wczytywał lub modyfikował dane innego serwera. Poprawia
to bezpieczeństwo w środowisku, w którym klastry ze współdzielonym systemem etcd są
niezbędne — np. z powodu ograniczonych zasobów lub w celu uniknięcia kosztów operacyjnych.
Na podstawie kontekstu trzeba wybrać jedną z dostępnych opcji. Zagregowane serwery API
mogą oczywiście korzystać na zapleczu także z innych systemów składowania danych —
przynajmniej teoretycznie, ponieważ zaimplementowanie interfejsów składowania danych z
biblioteki k8s.io/apiserver wymaga dużej ilości niestandardowego kodu.
Podsumowanie
Był to długi rozdział, ale dotarłeś do końca. Zdobyłeś wiele informacji na temat API w
Kubernetesie i ich implementacji.
To dużo materiału. Teraz znacznie lepiej rozumiesz, czym są API w Kubernetesie i jak są
implementowane. Mamy nadzieję, że zachęci Cię to do:
Wersjonowanie niestandardowych
zasobów
W rozdziale 8. zobaczyłeś, że zasoby są dostępne w różnych wersjach API. W przykładowym
niestandardowym serwerze API zasoby reprezentujące pizzę istnieją jednocześnie w wersjach
v1alpha1 i v1beta1 (zob. punkt „Przykład — pizzeria”). W niestandardowym serwerze API
każdy obiekt w żądaniu jest najpierw konwertowany z wersji używanego punktu końcowego API
na wersję wewnętrzną (zob. punkt „Typy wewnętrzne i konwersja”), a następnie konwertowany
z powrotem na wersję zewnętrzną na potrzeby składowania danych i zwracania odpowiedzi.
Mechanizm konwersji jest implementowany w funkcjach konwersji. Niektóre takie funkcje są
pisane ręcznie, inne generowane (zob. punkt „Konwersje”).
Wersjonowanie API to rozbudowany mechanizm dostosowywania i usprawniania API przy
zachowaniu zgodności ze starszymi klientami. Wersjonowanie odgrywa centralną rolę we
wszystkich komponentach Kubernetesa, gdzie pozwala promować API z wersji alfa do wersji
beta i ostatecznie do wersji ogólnie dostępnej. W tym procesie w API często zmienia się
struktura lub są one rozszerzane.
Przez długi czas wersjonowanie było mechanizmem dostępnym tylko za pomocą opisanych w
rozdziale 8. zagregowanych serwerów API. Każde rozbudowane API wymaga w pewnym
momencie wersjonowania, ponieważ naruszanie zgodności z konsumentami API jest
nieakceptowalne.
Na szczęście bardzo niedawno do Kubernetesa dodano wersjonowanie definicji CRD. W
Kubernetesie 1.14 ten mechanizm jest dostępny w wersji alfa, a w Kubernetesie 1.15
promowano go do wersji beta. Warto zauważyć, że konwersja wymaga strukturalnych
schematów sprawdzania poprawności w formacie OpenAPI v3 (zob. punkt „Sprawdzanie
poprawności niestandardowych zasobów”). Jednak narzędzia takie jak Kubebuilder i tak
generują schematy strukturalne. Szczegóły techniczne są opisane w punkcie „Schematy
strukturalne”.
kind: Pizza
metadata:
name: margherita
spec:
toppings:
- mozzarella
- tomato
Ten obiekt w wersji v1beta1 powinien mieć inną reprezentację wycinka z dodatkami:
apiVersion: restaurant.programming-kubernetes.info/v1beta1
kind: Pizza
metadata:
name: margherita
spec:
toppings:
- name: mozzarella
quantity: 1
- name: tomato
quantity: 1
W wersji v1alpha1 do reprezentowania dodatkowego sera służą powtórzenia dodatków,
natomiast w wersji v1beta1 ten sam efekt jest uzyskiwany za pomocą pola quantity dla
każdego dodatku. Kolejność dodatków nie ma tu znaczenia.
metadata:
name: pizzas.restaurant.programming-kubernetes.info
spec:
group: restaurant.programming-kubernetes.info
names:
kind: Pizza
listKind: PizzaList
plural: pizzas
singular: pizza
scope: Namespaced
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
schema: ...
- name: v1beta1
served: true
storage: false
schema: ...
W tej definicji CRD zdefiniowane są dwie wersje zasobu: v1alpha1 i v1beta1. Pierwsza z nich
jest skonfigurowana jako wersja składowania danych (zob. rysunek 9.1). Oznacza to, że każdy
obiekt zapisywany w systemie etcd jest najpierw konwertowany do wersji v1alpha1.
Rysunek 9.1. Konwersja i wersja składowania danych
Obecna definicja CRD pozwala utworzyć obiekt w wersji v1alpha1 i pobrać go w wersji
v1beta1, jednak oba punkty końcowe API zwracają ten sam obiekt. Nie tego oczekiwaliśmy,
jednak zaraz poprawimy kod.
kind: Pizza
metadata:
name: margherita
spec:
toppings:
- mozzarella
- tomato
apiVersion: restaurant.programming-kubernetes.info/v1beta1
kind: Pizza
metadata:
creationTimestamp: “2019-04-14T11:39:20Z”
generation: 1
name: margherita
namespace: pizza-apiserver
resourceVersion: “47959”
selfLink: /apis/restaurant.programming-kubernetes.info/v1beta1/namespaces/
pizza-apiserver/
pizzas/margherita
uid: f18427f0-5ea9-11e9-8219-124e4d2dc074
spec:
toppings:
- mozzarella
- tomato
Niestabilna — może zostać usunięta lub zmienić się w dowolnym momencie. Często
domyślnie jest nieaktywna.
v1beta1
v1
Wersja ogólnie dostępna jest wybierana jako pierwsza. Potem używane są wersje beta, a na
końcu wersje alfa. Numery wersji (podobnie jak numery podwersji) są porządkowane od
najwyższego do najniższego. Każda wersja definicji CRD, która nie pasuje do tego wzorca, jest
umieszczana na końcu (takie wersje są porządkowane alfabetycznie).
Dlatego tu wywołanie kubectl get pizza zwraca v1beta1, choć obiekt został utworzony w
wersji v1alpha1.
1. Klient żąda wersji (np. za pomocą naszego wywołania kuebectl get pizza margherita).
2. System etcd ma zapisany obiekt w jakiejś wersji.
3. Jeśli wersje nie pasują do siebie, składowany obiekt jest przesyłany w celu konwersji na
serwer z webhookiem. Webhook zwraca odpowiedź z przekształconym obiektem.
4. Przekształcony obiekt jest odsyłany do klienta.
Trzeba zaimplementować serwer webhooka. Wcześniej jednak przyjrzyj się API webhooka.
Serwer API Kubernetesa przesyła obiekt typu ConversionReview z grupy API
apiextensions.k8s.io/v1beta1:
metav1.TypeMeta `json:”,inline”`
Request *ConversionRequest
Response *ConversionResponse
Pole Request jest ustawiane w treści przesyłanej do webhooka. Pole Response jest ustawiane w
odpowiedzi.
...
Objects []runtime.RawExtension
}
W łańcuchu znaków DesiredAPIVersion używany jest standardowy format wersji API znany z
konstrukcji TypeMeta: grupa/wersja.
W polu Objects zapisana jest kolekcja obiektów. Używany jest wycinek, ponieważ dla jednego
żądania listy pizz webhook otrzymuje jedno żądanie konwersji, a w wycinku zapisywane są
wszystkie obiekty na potrzeby danego żądania listy.
...
Pole Result informuje serwer API Kubernetesa o tym, czy konwersja zakończyła się
powodzeniem.
Ale kiedy w procesie obsługi żądania wywoływany jest nasz webhook odpowiedzialny za
konwersję? Jakiego rodzaju obiektu wejściowego można oczekiwać? Aby lepiej to zrozumieć,
przyjrzyj się ogólnemu procesowi obsługi żądania z rysunku 9.3. Wszystkie zapełnione i
kreskowane kółka reprezentują tu konwersje w kodzie biblioteki k8s.io/apiserver.
Warto też zauważyć, że żądania PATCH powodują automatyczne ponawianie prób po konflikcie
(modyfikacje nie powodują ponawiania prób i bezpośrednio zwracają błędy do jednostki
wywołującej). Każde ponowienie próby obejmuje odczyt i zapis danych w systemie etcd
(kreskowane kółka na rysunku 9.3), co oznacza dwa wywołania webhooka w każdej takiej
iteracji.
Zanim zaczniesz implementować webhook, warto wyjaśnić, co webhook może robić, a czego
trzeba w nim unikać:
Dwa ostatnie punkty końcowe to webhooki kontroli dostępu, opisane szczegółowo w punkcie
„Webhooki kontroli dostępu”. Ten sam plik binarny z webhookiem służy do obsługi kontroli
dostępu i konwersji.
Członu v1beta1 z podanych ścieżek nie należy mylić z wersją v1beta1 grupy API dla pizzerii; tu
ten człon oznacza wersję grupy API apiextensions.k8s.io obsługiwaną w webhooku. W
[1]
przyszłości obsługiwana będzie wersja v1 API webhooka . Wtedy dodamy v1 jako następny
punkt końcowy, aby zapewnić obsługę dawnych (z dzisiejszej perspektywy) i nowych klastrów
Kubernetesa. W manifeście definicji CRD można podać, które wersje webhook obsługuje.
Przyjrzyjmy się teraz działaniu webhooka konwersji. Dalej szczegółowo opisujemy, jak
zainstalować webhook w rzeczywistym klastrze. Warto przypomnieć, że w Kubernetesie 1.14
webhooki konwersji nadal są dostępne w wersji alfa, dlatego trzeba je ręcznie aktywować za
pomocą bramki funkcji CustomResourceWebhookConversion. W Kubernetesie 1.15 te webhooki
są dostępne w wersji beta.
Bezpieczny kod serwera z biblioteki k8s.io/apiserver jest zgodny ze wzorcem opcji i konfiguracji
(zob. punkt „Wzorzec opcji i konfiguracji oraz szablonowy kod potrzebny do uruchomienia
serwera”). Bardzo łatwo jest dodać ten kod do własnego pliku binarnego:
*options.NewSecureServingOptions(),
}
o.SecureServing.ServerCert.PairName = “pizza-crd-webhook”
return o
}
type Options struct {
SecureServing options.SecureServingOptions
}
if err != nil {
return nil, err
c := &Config{}
return c, nil
}
W funkcji main w pliku binarnym tworzona jest instancja struktury Options, po czym do tej
instancji przypisywany jest zbiór flag:
opt := NewDefaultOptions()
fs := pflag.NewFlagSet(“pizza-crd-webhook”, pflag.ExitOnError)
globalflag.AddGlobalFlags(fs, “pizza-crd-webhook”)
opt.AddFlags(fs)
if err := fs.Parse(os.Args); err != nil {
panic(err)
}
if err != nil {
panic(err)
stopCh := server.SetupSignalHandler()
...
// Uruchamianie serwera.
restaurantInformers.Start(stopCh)
if doneCh, err := cfg.SecureServing.Serve(
handlers.LoggingHandler(os.Stdout, mux),
time.Second * 30, stopCh,
); err != nil {
panic(err)
} else {
<-doneCh
}
)
mux := http.NewServeMux()
mux.Handle(“/convert/v1beta1/pizza”, http.HandlerFunc(conversion.Serve))
mux.Handle(“/admit/v1beta1/pizza”,
http.HandlerFunc(admission.ServePizzaAdmit))
mux.Handle(“/validate/v1beta1/pizza”,
http.HandlerFunc(admission.ServePizzaValidation(restaurantInformers)))
restaurantInformers.Start(stopCh)
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieudany odczyt treści: %v”, err))
return
}
reviewGVK := gv.WithKind(“ConversionReview”)
obj, gvk, err := codecs.UniversalDeserializer().Decode(body, &reviewGVK,
&apiextensionsv1beta1.ConversionReview{})
if err != nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieudane dekodowanie treści: %v”, err))
return
}
review, ok := obj.(*apiextensionsv1beta1.ConversionReview)
if !ok {
responsewriters.InternalError(w, req,
}
if review.Request == nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwane żądanie nil”))
return
}
...
}
W tym kodzie używana jest fabryka kodeków codecs utworzona na podstawie schematu. W
schemacie trzeba umieścić typy z grupy apiextensions.k8s.io/v1beta1. Należy też dodać typy z
grupy API dla pizzerii. W przekazywanym obiekcie typu ConversionReview typ pizzy jest
zagnieżdżony w obiekcie typu runtime.RawExtension. Więcej na ten temat dowiesz się już
niedługo.
apiextensionsv1beta1 “k8s.io/apiextensions-
apiserver/pkg/apis/apiextensions/v1beta1”
“github.com/programming-kubernetes/pizza-crd/pkg/apis/restaurant/install”
...
)
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
func init() {
utilruntime.Must(apiextensionsv1beta1.AddToScheme(scheme))
install.Install(scheme)
}
Typ runtime.RawExtension to nakładka na obiekty podobne do obiektów Kubernetesa
zagnieżdżane w polu innego obiektu. Budowa tego typu jest bardzo prosta:
type RawExtension struct {
}
Typ runtime.RawExtension udostępnia też dwie specjalne metody do marshallingu danych w
formatach JSON i protobuf. Istnieje również specjalna logika do konwersji obiektów typu
runtime.Object „w locie”, w ramach konwersji na typy wersji wewnętrznej (czyli kod do
automatycznego kodowania i dekodowania obiektów).
Gdy używane są definicje CRD, nie istnieją typy wersji wewnętrznej, dlatego automatyczna
konwersja nie ma tu znaczenia. Pole RawExtension.Raw jest zapełniane wycinkiem z bajtami,
reprezentującym w formacie JSON obiekt pizzy przesyłany do webhooka w celu konwersji.
Dlatego trzeba odkodować ten wycinek. Warto ponownie zauważyć, że jeden obiekt typu
ConversionReview może przechowywać wiele obiektów, dlatego konieczne jest ich
przetwarzania w pętli:
// Konwersja obiektów.
review.Response = &apiextensionsv1beta1.ConversionResponse{
UID: review.Request.UID,
Result: metav1.Status{
Status: metav1.StatusSuccess,
},
}
var objs []runtime.Object
for _, in := range review.Request.Objects {
if in.Object == nil {
review.Response.Result = metav1.Status{
Message: err.Error(),
Status: metav1.StatusFailure,
}
break
}
}
if err != nil {
review.Response.Result = metav1.Status{
Message: err.Error(),
Status: metav1.StatusFailure,
}
break
}
objs = append(objs, obj)
}
Wywołanie convert dokonuje konwersji obiektu in.Object, używając oczekiwanej wersji API
jako wersji docelowej. Warto zauważyć, że natychmiast po wystąpieniu pierwszego błędu kod
wychodzi z pętli.
review.Response.ConvertedObjects =
append(review.Response.ConvertedObjects,
runtime.RawExtension{Object: obj},
)
}
}
out := &v1beta1.Pizza{
TypeMeta: in.TypeMeta,
ObjectMeta: in.ObjectMeta,
Status: v1beta1.PizzaStatus{
Cost: in.Status.Cost,
},
}
out.TypeMeta.APIVersion = apiVersion
idx := map[string]int{}
for _, top := range in.Spec.Toppings {
if i, duplicate := idx[top]; duplicate {
out.Spec.Toppings[i].Quantity++
continue
}
idx[top] = len(out.Spec.Toppings)
out.Spec.Toppings = append(out.Spec.Toppings,
v1beta1.PizzaTopping{
Name: top,
Quantity: 1,
})
}
return out, nil
case *v1beta1.Pizza:
if apiVersion != v1alpha1.SchemeGroupVersion.String() {
return nil, fmt.Errorf(“Niemożliwa konwersja %s na %s”,
v1beta1.SchemeGroupVersion, apiVersion)
}
klog.V(2).Infof(“Konwersja %s/%s z %s na %s”,
in.Namespace, in.Name, v1alpha1.SchemeGroupVersion, apiVersion)
out := &v1alpha1.Pizza{
TypeMeta: in.TypeMeta,
ObjectMeta: in.ObjectMeta,
Status: v1alpha1.PizzaStatus{
Cost: in.Status.Cost,
},
}
out.TypeMeta.APIVersion = apiVersion
out.Spec.Toppings = append(
out.Spec.Toppings, in.Spec.Toppings[i].Name)
}
}
default:
}
metadata:
name: pizzas.restaurant.programming-kubernetes.info
spec:
...
conversion:
strategy: Webhook
webhookClientConfig:
caBundle: zestaw-certyfikatów-w-formacie-base64
service:
namespace: pizza-crd
name: webhook
path: /convert/v1beta1/pizza
Zestaw certyfikatów musi pasować do certyfikatu serwera używanego przez webhook. W
przykładowym projekcie używamy pliku Makefile (http://bit.ly/2FukVac) do generowania
certyfikatów z użyciem protokołu OpenSSL i dołączamy je do manifestu za pomocą
zastępowania tekstu.
Warto zauważyć, że serwer API Kubernetesa przyjmuje, iż webhook obsługuje wszystkie podane
wersje definicji CRD. Ponadto dla jednej definicji CRD można podać tylko jeden taki webhook.
Powinno to jednak wystarczać, ponieważ definicje CRD i webhooki konwersji są zwykle
rozwijane przez ten sam zespół.
Zwróć też uwagę na to, że w obecnej wersji API apiextensions.k8s.io/v1beta1 należy używać
portu usługi 443. W usłudze można jednak odwzorować ten port na dowolny port używany
przez pody z webhookiem. W tym przykładzie odwzorowujemy port 443 na port 8443
obsługiwany przez plik binarny webhooka.
Konwersja w praktyce
Teraz gdy już rozumiesz, jak działa webhook konwersji i jak jest powiązany z klastrem, pora
zobaczyć, jak działa.
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
kubeadmConfigPatchesJson6902:
- group: kubeadm.k8s.io
version: v1beta1
kind: ClusterConfiguration
patch: |
- op: add
path: /apiServer/extraArgs
value: {}
- op: add
path: /apiServer/extraArgs/feature-gates
value: CustomResourceWebhookConversion=true
Następnie można utworzyć klaster:
$ cd pizza-crd
$ cd manifest/deployment
$ make
$ kubectl create -f ns.yaml
$ kubectl create -f pizza-crd.yaml
topping-crd.yaml
Definiuje niestandardowy zasób dodatków w tej samej grupie API. Ten zasób istnieje tylko
w wersji v1alpha1.
sa.yaml
Dodaje konto usługi webhook.
rbac.yaml
Definiuje role służące do wczytywania, wyświetlania i obserwowania dodatków.
rbac-bind.yaml
Wiąże role systemu RBAC z poprzedniego pliku z kontem usługi webhook.
service.yaml
Definiuje usługi webhook i odwzorowuje port 443 na 8443 z podów z webhookiem.
serving-cert-secret.yaml
Zawiera certyfikat serwera i klucz prywatny przeznaczone do użytku w podach z
webhookiem. Ten certyfikat jest używany bezpośrednio w zestawie certyfikatów w
przedstawionym wcześniej manifeście CRD reprezentującym pizzę.
deployment.yaml
Uruchamia pody z webhookami. W opcjach --tls-cert-file i --tls-private-key należy
użyć sekretu z certyfikatem serwera.
Teraz można wreszcie utworzyć pizzę margheritę:
$ cat ../examples/margherita-pizza.yaml
apiVersion: restaurant.programming-kubernetes.info/v1alpha1
kind: Pizza
metadata:
name: margherita
spec:
toppings:
- mozzarella
- tomato
$ kubectl create ../examples/margherita-pizza.yaml
pizza.restaurant.programming-kubernetes.info/margherita created
Obecnie, gdy gotowy jest już webhook konwersji, można pobrać ten sam obiekt w obu wersjach.
Najpierw bezpośrednio w wersji v1alpha1:
$ kubectl get pizzas.v1alpha1.restaurant.programming-kubernetes.info \
margherita -o yaml
apiVersion: restaurant.programming-kubernetes.info/v1alpha1
kind: Pizza
metadata:
creationTimestamp: “2019-04-14T21:41:39Z”
generation: 1
name: margherita
namespace: pizza-crd
resourceVersion: “18296”
pizzas/margherita
uid: 15c1c06a-5efe-11e9-9230-0242f24ba99c
spec:
toppings:
- mozzarella
- tomato
status: {}
Ten sam obiekt w wersji v1beta1 ma inną strukturę toppings:
$ kubectl get pizzas.v1beta1.restaurant.programming-kubernetes.info \
margherita -o yaml
apiVersion: restaurant.programming-kubernetes.info/v1beta1
kind: Pizza
metadata:
creationTimestamp: “2019-04-14T21:41:39Z”
generation: 1
name: margherita
namespace: pizza-crd
resourceVersion: “18296”
pizzas/margherita
uid: 15c1c06a-5efe-11e9-9230-0242f24ba99c
spec:
toppings:
- name: mozzarella
quantity: 1
- name: tomato
quantity: 1
status: {}
Jeśli pole spec.toppings ma wartość nil lub jest puste, domyślnie przypisywane są do
niego mozzarella, pomidory i salami.
Nieznane pola należy usuwać z niestandardowego zasobu w formacie JSON; nie są one
utrwalane w systemie etcd.
Pole spec.toppings musi zawierać tylko te dodatki, dla których istnieje odpowiedni
obiekt.
// +optional
Name string `json:”name,omitempty”`
// Namespace to przestrzeń nazw powiązana z żądaniem (jeśli taka
istnieje).
// +optional
}
Ten sam obiekt typu AdmissionReview jest używany dla obu typów webhooków kontroli
dostępu (modyfikujących i sprawdzających poprawność). Jedyna różnica polega na tym, że dla
webhooków modyfikujących obiekt typu AdmissionResponse może mieć pola patch i
patchType, które są uwzględniane na serwerze API Kubernetesa po otrzymaniu przez niego
odpowiedzi od webhooka. W odpowiedziach od webhooków sprawdzających poprawność te dwa
pola pozostają puste.
Najważniejszym polem dla naszych potrzeb jest tu Object, które — tak jak w pokazanym
webhooku konwersji — używa typu runtime.RawExtension do zapisania obiektu
reprezentującego pizzę.
Dostępny jest też dawny obiekt dla żądań modyfikacji. Moglibyśmy np. sprawdzać w nim pola,
które są przeznaczone tylko do odczytu, ale zostały w żądaniu zmodyfikowane. W naszym
przykładzie tego nie robimy, jednak w Kubernetesie często spotkasz się z kodem, w którym
zaimplementowana jest taka logika. Dotyczy to np. większości pól podów, ponieważ nie można
zmienić polecenia dla poda po jego utworzeniu.
W Kubernetesie 1.14 obiekt częściowej modyfikacji zwrócony przez modyfikujący webhook musi
być typu Patch z formatu JSON (zob. RFC 6902). Ten obiekt opisuje, jak należy zmodyfikować
obiekt w zgodzie z obowiązującym niezmiennikiem.
Warto zauważyć, że zalecaną praktyką jest końcowa ocena prawidłowości za pomocą webhooka
sprawdzającego poprawność każdej zmiany wprowadzanej przez webhook modyfikujący —
przynajmniej wtedy, gdy sprawdzane właściwości mają wpływ na działanie kodu. Wyobraź
sobie, że kilka modyfikujących webhooków zmienia te same pola obiektu. Nie masz wtedy
pewności, czy dane zmiany przetrwają do końca działania łańcucha modyfikujących webhooków
kontroli dostępu.
Obecnie dla modyfikujących webhooków nie jest używana kolejność inna niż alfabetyczna.
Trwają dyskusje nad tym, by w przyszłości to zmienić.
Kolejność działania webhooków sprawdzających poprawność oczywiście nie ma znaczenia, a
serwer API Kubernetesa wywołuje je równolegle, aby przyspieszyć pracę. Z kolei webhooki
modyfikujące zwiększają opóźnienie każdego żądania, które przetwarzają, ponieważ są
wywoływane sekwencyjnie.
Opóźnienie często wynosi ok. 100 ms (oczywiście jest to wysoce zależne od środowiska).
Dlatego sekwencyjne uruchamianie wielu webhooków skutkuje znacznym opóźnieniem, które
użytkownik odczuwa w trakcie tworzenia lub modyfikowania obiektów.
metadata:
name: restaurant.programming-kubernetes.info
webhooks:
- name: restaurant.programming-kubernetes.info
failurePolicy: Fail
sideEffects: None
admissionReviewVersions:
- v1beta1
rules:
- apiGroups:
- “restaurant.programming-kubernetes.info”
apiVersions:
- v1alpha1
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- pizzas
clientConfig:
service:
namespace: pizza-crd
name: webhook
path: /admit/v1beta1/pizza
caBundle: zestaw-certyfikatów
Ten kod rejestruje nasz webhook z początku rozdziału przeznaczony dla zasobów pizza-crd na
potrzeby obsługi kontroli dostępu z modyfikacjami do dwóch wersji zasobu pizza, grupy API
restaurant.programming-kubernetes.info oraz operacji HTTP CREATE i UPDATE (umożliwia
też częściowe modyfikacje).
Istnieją też inne sposoby na ograniczenie w konfiguracji webhooka listy pasujących zasobów.
Można m.in. zastosować selektor przestrzeni nazw (aby pominąć np. przestrzeń nazw warstwy
kontroli w celu uniknięcia problemów z rozruchem serwera) oraz bardziej zaawansowane
wzorce z symbolami wieloznacznymi i podzasobami.
Warto też zwrócić uwagę na tryb obsługi awarii — Fail lub Ignore. Określa on, co robić, jeśli
nie można uzyskać dostępu do webhooka lub gdy zawiedzie on z innego powodu.
review, ok := obj.(*admissionv1beta1.AdmissionReview)
if !ok {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwany identyfikator GVK: %s”, gvk))
return
}
if review.Request == nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwane żądanie nil”))
return
}
...
}
W webhooku sprawdzającym poprawność trzeba podłączyć informator (służący do sprawdzania,
czy w klastrze istnieją dodatki). Dopóki informator nie jest zsynchronizowany, zwracany jest
wewnętrzny błąd. Informator, który nie jest zsynchronizowany, zawiera niekompletne dane,
dlatego dodatki mogłyby zostać uznane za nieznane, a prawidłowa pizza — odrzucona:
func ServePizzaValidation(informers
restaurantinformers.SharedInformerFactory)
func (http.ResponseWriter, *http.Request)
{
toppingInformer :=
informers.Restaurant().V1alpha1().Toppings().Informer()
toppingLister := informers.Restaurant().V1alpha1().Toppings().Lister()
return
}
// Odczyt treści.
body, err := ioutil.ReadAll(req.Body)
if err != nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieudany odczyt treści: %v”, err))
return
return
}
review, ok := obj.(*admissionv1beta1.AdmissionReview)
if !ok {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwany identyfikator GVK: %s”, gvk))
return
}
if review.Request == nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwane żądanie nil”))
return
}
...
}
}
Podobnie jak w webhooku konwersji, tak i powyżej skonfigurowaliśmy schemat i fabrykę
kodeków, używając grupy API dla kontroli dostępu i grupy API dla pizzerii:
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
func init() {
utilruntime.Must(admissionv1beta1.AddToScheme(scheme))
install.Install(scheme)
}
Dzięki temu można odkodować zagnieżdżony obiekt pizzy (tym razem tylko jeden, bez
wycinków) z obiektu typu AdmissionReview:
// Dekodowanie obiektu.
if review.Request.Object.Object == nil {
var err error
review.Request.Object.Object, _, err =
codecs.UniversalDeserializer().Decode(review.Request.Object.Raw, nil,
nil)
if err != nil {
review.Response.Result = &metav1.Status{
Message: err.Error(),
Status: metav1.StatusFailure,
}
responsewriters.WriteObject(http.StatusOK, gvk.GroupVersion(),
codecs, review, w, req)
return
}
}
Następnie ma miejsce etap modyfikacji w procesie kontroli dostępu (ustawianie wartości
domyślnych pola spec.toppings dla obu wersji API):
orig := review.Request.Object.Raw
var bs []byte
case *v1beta1.Pizza:
// Domyślne dodatki.
if len(pizza.Spec.Toppings) == 0 {
pizza.Spec.Toppings = []v1beta1.PizzaTopping{
{“pomidory”, 1},
{“mozzarella”, 1},
{“salami”, 1},
}
}
bs, err = json.Marshal(pizza)
if err != nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwany błąd kodowania: %v”, err))
return
}
default:
review.Response.Result = &metav1.Status{
Message: fmt.Sprintf(“Nieoczekiwany typ %T”,
review.Request.Object.Object),
Status: metav1.StatusFailure,
}
responsewriters.WriteObject(http.StatusOK, gvk.GroupVersion(),
codecs, review, w, req)
return
}
Można też zastosować algorytmy konwersji z webhooka konwersji, a następnie
zaimplementować ustawianie wartości domyślnych tylko dla jednej z wersji. Oba podejścia są
akceptowalne, a to, które z nich będzie lepsze, zależy od sytuacji. Tu ustawianie wartości
domyślnych jest na tyle proste, że można je zaimplementować dwukrotnie.
Ostatni krok polega na obliczeniu częściowej modyfikacji — różnicy między pierwotnym
obiektem (zapisanym w zmiennej orig w formacie JSON) a nowym z ustawionymi wartościami
domyślnymi:
// Porównywanie wersji pierwotnej i wersji z ustawionymi wartościami
domyślnymi.
ops, err := jsonpatch.CreatePatch(orig, bs)
if err != nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwany błąd wyznaczania różnicy: %v”, err))
return
}
review.Response.Patch, err = json.Marshal(ops)
if err != nil {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieoczekiwany błąd kodowania częściowej modyfikacji: %v”,
err))
return
}
typ := admissionv1beta1.PatchTypeJSONPatch
review.Response.PatchType = &typ
review.Response.Allowed = true
Używamy tu biblioteki JSON-Patch (http://bit.ly/2IKxwIk; a dokładniej — odgałęzienie Matta
Bairda, http://bit.ly/2xfBIsN, z krytycznymi poprawkami — http://bit.ly/2XxKfWP), aby uzyskać
częściową modyfikację między pierwotnym obiektem orig a zmodyfikowanym obiektem bs. Oba
te obiekty są przekazywane jako wycinki z bajtami w formacie JSON. Inna możliwość to
bezpośrednie operowanie nietypizowanymi danymi w formacie JSON i ręczne uzyskanie
częściowej modyfikacji w formacie JSON. Wybór jednej z tych technik zależy od sytuacji.
Wygodne jest używanie biblioteki diff.
Następnie, tak jak w webhooku konwersji, należy zapisać odpowiedź za pomocą służącego do
tego obiektu, używając utworzonej wcześniej fabryki kodeków:
responsewriters.WriteObject(
http.StatusOK, gvk.GroupVersion(), codecs, review, w, req,
)
Webhook sprawdzający poprawność jest bardzo podobny, ale do sprawdzania istnienia obiektów
reprezentujących dodatki używa listera dodatków ze współdzielonego informatora:
switch pizza := review.Request.Object.Object.(type) {
case *v1alpha1.Pizza:
for _, topping := range pizza.Spec.Toppings {
_, err := toppingLister.Get(topping)
if err != nil && !errors.IsNotFound(err) {
responsewriters.InternalError(w, req,
fmt.Errorf(“Nieudane wyszukiwanie dodatku %q: %v”, topping,
err))
return
} else if errors.IsNotFound(err) {
review.Response.Result = &metav1.Status{
Message: fmt.Sprintf(“Nieznany dodatek %q “, topping),
Status: metav1.StatusFailure,
}
responsewriters.WriteObject(http.StatusOK, gvk.GroupVersion(),
codecs, review, w, req)
return
}
}
review.Response.Allowed = true
case *v1beta1.Pizza:
for _, topping := range pizza.Spec.Toppings {
_, err := toppingLister.Get(topping.Name)
}
responsewriters.WriteObject(http.StatusOK, gvk.GroupVersion(),
codecs, review, w, req)
return
}
}
review.Response.Allowed = true
default:
review.Response.Result = &metav1.Status{
pizza.restaurant.programming-kubernetes.info/margherita created
Sprawdźmy też, czy ustawianie wartości domyślnych działa zgodnie z oczekiwaniami. Chcemy
utworzyć pustą pizzę:
apiVersion: restaurant.programming-kubernetes.info/v1alpha1
kind: Pizza
metadata:
name: salami
spec:
Kod powinien w niej ustawić wartości domyślne dla pizzy salami — i tak się dzieje:
$ kubectl create -f ../examples/empty-pizza.yaml
pizza.restaurant.programming-kubernetes.info/salami created
$ kubectl get pizza salami -o yaml
apiVersion: restaurant.programming-kubernetes.info/v1beta1
kind: Pizza
metadata:
creationTimestamp: “2019-04-14T22:49:40Z”
generation: 1
name: salami
namespace: pizza-crd
resourceVersion: “23227”
uid: 962e2dda-5f07-11e9-9230-0242f24ba99c
spec:
toppings:
- name: pomidory
quantity: 1
- name: mozzarella
quantity: 1
- name: salami
quantity: 1
status: {}
Gotowe — pizza salami z wszystkimi oczekiwanymi dodatkami. Smacznego!
Przed końcem rozdziału warto przyjrzeć się wersji apiextensions.k8s.io/v1 grupy API
niestandardowych zasobów (jest to wersja ogólnie dostępna zamiast wersji beta). Ważne są w
niej schematy strukturalne.
Ściśle rzecz biorąc, definiowanie schematu nadal jest opcjonalne, a każda istniejąca definicja
CRD nadal będzie działać. Jednak bez schematu żadna z wymienionych funkcji nie będzie
dostępna dla danego niestandardowego zasobu.
Ponadto podany schemat musi być zgodny z określonymi regułami, aby było wiadomo, że
podane typy są zgodne z konwencjami obowiązującymi dla API Kubernetesa
(http://bit.ly/2Nfd9Hn). Takie schematy nazywamy strukturalnymi.
Schematy strukturalne
Schemat strukturalny to schemat sprawdzania poprawności z OpenAPI v3 (zob. punkt
„Sprawdzanie poprawności niestandardowych zasobów”), który spełnia trzy następujące
warunki:
1. Określa niepusty typ (za pomocą słowa kluczowego type z OpenAPI) dla korzenia, dla
każdego pola węzła obiektu (za pomocą słów kluczowych properties i
additionalProperties z OpenAPI) i dla każdego elementu węzła tablicy (za pomocą
słowa kluczowego items z OpenAPI). Wyjątkami są:
węzły z ustawieniem x-kubernetes-int-or-string: true,
węzły z ustawieniem x-kubernetes-preserve-unknown-fields: true.
2. Wszystkie pola w obiektach i elementy w tablicach, które są podane w sekcjach allOf,
anyOf, oneOf lub not, mają też w schemacie określone pole/element poza tymi logicznymi
sekcjami.
3. W schemacie w sekcjach allOf, anyOf, oneOf i not nie są ustawiane pola description,
type, default, additionProperties lub nullable. Wyjątkiem są dwa wzorce dla
ustawienia x-kubernetes-int-or-string: true (zob. punkt „IntOrString i
RawExtension”).
4. Jeśli podane jest pole metadata, dozwolone są jedynie ograniczenia dotyczące pól
metadata.name i metadata.generateName.
properties:
name:
type: string
pattern: “^a”
finalizers:
type: array
items:
type: string
pattern: “my-finalizer”
anyOf:
- properties:
bar:
type: integer
minimum: 42
required: [“bar”]
description: “obiekt foo bar”
Ten schemat nie jest strukturalny, ponieważ nie spełnia pewnych warunków:
type: string
pattern: “abc”
bar:
type: integer
metadata:
type: object
properties:
name:
type: string
pattern: “^a”
anyOf:
- properties:
bar:
minimum: 42
required: [“bar”]
Naruszenie reguł schematu strukturalnego jest zgłaszane w warunku NonStructural definicji
CRD.
Możesz sam się przekonać, że schemat z przykładu cnat z punktu „Sprawdzanie poprawności
niestandardowych zasobów” i schematy z przykładu z definicjami CRD dla pizzerii
(http://bit.ly/31MrFcO) rzeczywiście są strukturalne.
Okrajanie a zachowywanie nieznanych pól
W niestandardowych zasobach wszystkie (często sprawdzone) dane w formacie JSON są
zapisywane w takiej postaci jak w systemie etcd. To oznacza, że nieznane pola (jeśli w ogóle
używany jest schemat sprawdzania poprawności z OpenAPI v3) są zachowywane. Jest to różnica
w porównaniu z natywnymi zasobami Kubernetesa takimi jak pody. Jeśli użytkownik poda pole
spec.dowolnePole, będzie ono akceptowane w punkcie końcowych HTTPS serwera API, ale
usuwane (okrajane) przed zapisem danego poda w systemie etcd.
Jeżeli zdefiniowany jest schemat sprawdzania poprawności z OpenAPI v3 (albo w globalnym
polu spec.validation.openAPIV3Schema, albo dla każdej wersji), można włączyć okrajanie
(usuwanie nieznanych pól przy tworzeniu i modyfikowaniu obiektów), ustawiając wartość pola
spec.preserveUnknownFields na false.
[2]
Przyjrzyj się kodowi narzędzia cnat . Gdy używasz klastra z Kubernetesem 1.15, możesz
włączyć okrajanie:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ats.cnat.programming-kubernetes.info
spec:
...
preserveUnknownFields: false
Następnie można utworzyć instancję z nieznanym polem:
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
name: example-at
spec:
schedule: “2019-07-03T02:00:00Z”
command: echo “Witaj, świecie!”
someGarbage: 42
Jeśli pobierzesz ten obiekt za pomocą wywołania kubectl get at example-at, zobaczysz, że
pole someGarbage jest usuwane:
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
name: example-at
spec:
schedule: “2019-07-03T02:00:00Z”
command: echo “Witaj, świecie!”
Można powiedzieć, że pole someGarbage zostało przycięte.
W Kubernetesie 1.15 okrajanie jest dostępne w wersji apiextensions/v1beta1, ale domyślnie jest
wyłączone (pole spec.preserveUnknownFields domyślnie ma wartość true). W wersji
apiextensions/v1 nie będzie można tworzyć nowych definicji CRD z ustawieniem
spec.preserveUnknownFields: true.
Sterowanie okrajaniem
Gdy w definicji CRD znajduje się ustawienie spec.preserveUnknownField: false, okrajanie
jest włączone dla wszystkich zasobów niestandardowych danego typu we wszystkich wersjach.
Można jednak wyłączyć okrajanie dla poddrzewa w formacie JSON, używając ustawienia x-
kubernetes--preserve-unknown-fields: true w schemacie sprawdzania poprawności z
OpenAPI v3:
type: object
properties:
json:
x-kubernetes-preserve-unknown-fields: true
W polu json można zapisać dowolną wartość w formacie JSON, a żadne dane nie będą
okrajane.
Można też podać częściową specyfikację dozwolonych danych w formacie JSON:
type: object
properties:
json:
x-kubernetes-preserve-unknown-fields: true
type: object
description: dowolne dane w formacie JSON
W tym podejściu dozwolone są jedynie wartości typu object.
Okrajanie zostaje ponownie włączone dla każdej podanej właściwości (można też zastosować
słowo kluczowe additionalProperties):
type: object
properties:
json:
x-kubernetes-preserve-unknown-fields: true
type: object
properties:
spec:
type: object
properties:
foo:
type: string
bar:
type: string
Przy takich ustawieniach poniższa wartość:
json:
spec:
foo: abc
bar: def
something: x
status:
something: x
jest okrajana do następującej postaci:
json:
spec:
foo: abc
bar: def
status:
something: x
To oznacza, że pole something w podanym obiekcie spec jest okrajane (ponieważ istnieje
specyfikacja tego obiektu), ale zewnętrzne dane nie zostają przycięte. Nie istnieje specyfikacja
obiektu status, dlatego pole status.something nie jest okrajane.
IntOrString i RawExtension
Zdarza się, że schematy strukturalne nie dają wystarczających możliwości. Jest tak np. w
przypadku pola polimorficznego, cechującego się tym, że może być różnego typu. Wśród
natywnych typów API Kubernetesa tak działa typ IntOrString.
Można korzystać z typu IntOrString w definicjach CRD, używając w schemacie dyrektywy x-
kubernetes-int-or-string: true. Podobnie można zadeklarować obiekt typu
runtime.RawExtension, używając ustawienia x-kubernetes-embedded-object: true.
Oto przykład:
type: object
properties:
intorstr:
type: object
x-kubernetes-int-or-string: true
embedded:
x-kubernetes-embedded-object: true
x-kubernetes-preserve-unknown-fields: true
Wartości domyślne
W natywnych typach Kubernetesa często ustawiane są określone wartości domyślne.
Ustawianie wartości domyślnych dla typów CRD było możliwe tylko za pomocą modyfikujących
webhooków kontroli dostępu (zob. punkt „Webhooki kontroli dostępu”). Jednak w Kubernetesie
1.15 do definicji CRD dodano bezpośrednią obsługę ustawiania wartości domyślnych (zob.
dokument z projektem tego mechanizmu — http://bit.ly/2ZFH8JY). Służą do tego opisane w
poprzednim punkcie schematy z OpenAPI v3.
W Kubernetesie 1.15 nadal jest to funkcja w wersji alfa. Jest ona domyślnie
wyłączona, a do włączania służy bramka funkcji CustomResourceDefaulting.
Jednak po promocji do wersji beta, co zapewne nastąpi w Kubernetesie 1.16,
ustawianie wartości domyślnych w definicjach CRD stanie się powszechne.
Aby ustawić wartości domyślne wybranych pól, wystarczy podać odpowiednią wartość z
użyciem słowa kluczowego default w schemacie z OpenAPI v3. Jest to bardzo przydatne, gdy
dodajesz nowe pola do typu.
Zacznijmy od schematu z narzędzia cnat z punktu „Sprawdzanie poprawności
niestandardowych zasobów”. Załóżmy, że chcemy umożliwić podawanie obrazu kontenera, przy
czym domyślnie ma być używany obraz busybox. W tym celu do schematu z OpenAPI v3
dodamy pole image typu string i ustawimy dla niego wartość domyślną busybox:
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
type: object
properties:
schedule:
type: string
pattern: “^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])...”
command:
type: string
image:
type: string
default: “busybox”
required:
- schedule
- command
status:
type: object
properties:
phase:
type: string
required:
- metadata
- apiVersion
- kind
- spec
Jeśli użytkownik utworzy instancję bez podawania obrazu, wartość obrazu zostanie ustawiona
automatycznie:
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
name: example-at
spec:
schedule: “2019-07-03T02:00:00Z”
command: echo “Witaj, świecie!”
W trakcie tworzenia instancji ten kod zostanie automatycznie przekształcony w:
apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
name: example-at
spec:
schedule: “2019-07-03T02:00:00Z”
command: echo “Witaj, świecie!”
image: busybox
Jest to bardzo wygodne i znacznie ułatwia korzystanie z definicji CRD. Co więcej, wszystkie
dawne obiekty utrwalone w systemie etcd automatycznie otrzymają nowe pole, gdy będą
[3]
wczytywane z serwera API .
Warto zauważyć, że utrwalone obiekty z systemu etcd nie będą zastępowane (nie będzie
przeprowadzana automatyczna migracja). Wartości domyślne będą dodawane dopiero przy
odczycie, „w locie”, i utrwalane wyłącznie w momencie modyfikowania obiektu z innego
powodu.
Podsumowanie
Webhooki kontroli dostępu i konwersji sprawiają, że definicje CRD wkraczają na zupełnie nowy
poziom. Wcześniej niestandardowe zasoby były używane głównie w prostych, mało
zaawansowanych sytuacjach — często na potrzeby konfiguracji lub wewnętrznych aplikacji,
gdzie kompatybilność API nie była zbyt istotna.
Dzięki webhookom niestandardowe zasoby znacznie upodabniają się do zasobów natywnych
dzięki długiemu cyklowi życia i rozbudowanej semantyce. Zobaczyłeś, jak implementować
zależności między różnymi zasobami i jak konfigurować ustawianie wartości domyślnych dla
pól.
Na tym etapie zapewne masz wiele pomysłów dotyczących tego, gdzie wykorzystać poznane
funkcje w istniejących definicjach CRD. Jesteśmy ciekawi innowacji, jakie społeczność
użytkowników w przyszłości opracuje na podstawie tych funkcji.
Ogólne
Oficjalna dokumentacja Kubernetesa (https://kubernetes.io/docs/home).
Społeczność skupiona wokół Kubernetesa w serwisie GitHub (http://bit.ly/2LX2YF8).
Kanał client-go docs w przestrzeni roboczej Kubernetes Slack.
Kubernetes deep dive: API Server — część 1. (https://red.ht/2IJBDEk).
Kubernetes deep dive: API Server — część 2. (https://red.ht/2RAEv9s).
Kubernetes deep dive: API Server — część 3. (https://red.ht/2NaXgBD).
Kubernetes API Server, część 1. (http://bit.ly/2IKh0be).
The Mechanics of Kubernetes (http://bit.ly/2IV2lcb).
Dokumentacja go doc biblioteki k8s.io/api (https://godoc.org/k8s.io/api).
Książki
Kubernetes: Up and Running, wydanie drugie (https://oreil.ly/2SaANU4), Kelsey
Hightower i inni (O’Reilly).
Cloud Native DevOps with Kubernetes (https://oreil.ly/2BaE1iq), John Arundel i Justin
Domingus (O’Reilly).
Managing Kubernetes (https://oreil.ly/2wtHcAm), Brendan Burns i Craig Tracey (O’Reilly).
Kubernetes Cookbook (http://bit.ly/2FTgJzk), Sébastien Goasguen i Michael Hausenblas
(O’Reilly).
The Kubebuilder Book (https://book.kubebuilder.io).
Samouczki i przykłady
Kubernetes by Example (http://kubernetesbyexample.com).
The Katacoda Kubernetes Playground (http://bit.ly/31Sydqp).
Banzai Cloud Operator SDK (http://bit.ly/2ZG3OtA).
Operator Developer Guide (http://bit.ly/2Fx4zh4).
Artykuły
Writing a Kubernetes Operator in Golang (http://bit.ly/2Ei2hCr).
Stay Informed with Kubernetes Informers (http://bit.ly/2Y5OKYX).
Events, the DNA of Kubernetes (http://bit.ly/31Tvey8).
Kubernetes Events Explained (http://bit.ly/2XzwEOM).
Level Triggering and Reconciliation in Kubernetes (http://bit.ly/2FmLLAW).
Comparing Kubernetes Operator Pattern with Alternatives (http://bit.ly/2XxGEYO).
Kubernetes Operators (https://kubedex.com/operators).
Kubernetes Custom Resource, Controller and Operator Development Tools
(http://bit.ly/2FpO4Ug).
Demystifying Kubernetes Operators with the Operator SDK: Part 1
(http://bit.ly/2NbGRwZ).
Under the Hood of Kubebuilder Framework (http://bit.ly/2X2NpgX).
Best Practices for Building Kubernetes Operators and Stateful Apps
(http://bit.ly/2NdvQeJ).
Kubernetes Operator Development Guidelines (http://bit.ly/31P7rPC).
Mutating Webhooks with slok/kubewebhook (http://bit.ly/2RyScG1).
Repozytoria
kubernetes-client (http://bit.ly/2xfSrfT).
kubernetes/kubernetes (http://bit.ly/2SltTLP).
kubernetes/perf-tests (http://bit.ly/2X556g8).
cncf/apisnoop (http://bit.ly/32u5SqN).
open-policy-agent/gatekeeper (http://bit.ly/2LXCpiX).
stakater/Konfigurator (http://bit.ly/2JBX8HO).
ynqa/kubernetes-rust (https://github.com/ynqa/kubernetes-rust).
hossainemruz/k8s-initializer-finalizer-practice (http://bit.ly/30GzTSF).
munnerz/k8s-api-pager-demo (http://bit.ly/30Ep2IT).
m3db/m3db-operator (http://bit.ly/2XURVi2).
O autorach
Michael Hausenblas pracuje na stanowisku developer advocate w Amazon Web Services.
Należy do jednostki zespołu odpowiedzialnego za usługi kontenerowe skupiającej się na
bezpieczeństwie kontenerów. Michael dzieli się swoim doświadczeniem z zakresu natywnej
infrastruktury i natywnych aplikacji dla chmury, tworząc wersje demonstracyjne
oprogramowania, pisząc artykuły na blogach i książki, prowadząc prelekcje i współtworząc
otwarte oprogramowanie. Przed zatrudnieniem się w AWS pracował dla firm Red Hat,
Mesosphere i MapR oraz dla dwóch instytucji naukowych w Irlandii i Austrii.
Stefan Schimanski pracuje w firmie Red Hat jako główny inżynier oprogramowania w
obszarze technologii Go, Kubernetes i OpenShift. Koncentruje się na serwerze API
Kubernetesa, a przede wszystkim na implementowaniu definicji CRD, bibliotece API Machinery
i publikowaniu repozytoriów roboczych Kubernetesa: client-go, apimachinery, api i innych.
Przed przyjściem do Red Hat Stefan pracował w firmie Mesosphere nad technologiami
Marathon i Spark oraz produktami związanymi z Kubernetesem. Był też freelancerem i
konsultantem z zakresu wysoko dostępnych systemów rozproszonych. Wcześniej Stefan
prowadził badania w dziedzinie logiki matematycznej w zakresie matematyki konstruktywnej,
systemów typów i rachunku lambda.
Kolofon
Zwierzę na okładce książki Kubernetes to samotnik (Tringa ochropus). Zarówno nazwa
rodzajowa, jak i nazwa gatunkowa pochodzą ze starożytnej greki. Mały brodzący ptak nazywany
trungas przykuł kiedyś uwagę Arystotelesa, a słowo ochropus można rozbić na dwa
starogreckie słowa okhros i pous, oznaczające ochrę i stopę.
Jedynym żyjącym gatunkiem blisko spokrewnionym z samotnikiem jest brodziec ciemnorzytny.
Zasięg występowania samotników jest bardzo duży i obejmuje prawie wszystkie kontynenty.
Samotniki pochodzą z Azji, a zimą migrują do cieplejszych krajów. Żyją i żerują w różnych
środowiskach wodnych. W stawach, rzekach i podmokłych lasach samotniki znajdują
pożywienie, jakim są owady, pająki, drobne skorupiaki, ryby i rośliny.
Samotniki mają szeroką pierś i krótką szyję. Ich dzioby są długie i smukłe. Z bliska na
zielonobrązowych skrzydłach samotników widoczne są małe jasne kropki. Kolor upierzenia jest
odwrotnością koloru jaj, które są płowożółte z brązowymi plamkami. W typowym lęgu znajduje
się od dwóch do czterech jaj, z których po trzech tygodniach wylęgają się pisklęta. Samotniki
wysiadują jaja w porzuconych gniazdach innych ptaków, a nawet w gniazdach wiewiórek.
Wiele gatunków z okładek książek wydawnictwa OʼReilly jest zagrożonych. Wszystkie one są
ważne dla świata.
Ilustracja z okładki została opracowana przez Karen Montgomery na podstawie czarno-białej
ryciny z książki General Zoology Shawa.
Spis treści
Opinie na temat książki Kubernetes. Tworzenie natywnych aplikacji działających w chmurze
Przedmowa
Dla kogo przeznaczona jest ta książka?
Po co napisaliśmy tę książkę?
Ekosystem
Technologie, które powinieneś znać
Konwencje stosowane w tej książce
Korzystanie z przykładowego kodu
Podziękowania
Rozdział 1. Wprowadzenie
Czym jest programowanie dla Kubernetesa?
Przykład wprowadzający
Wzorce rozszerzania
Kontrolery i operatory
Pętla sterowania
Zdarzenia
Wyzwalacze sterowane zmianami i sterowane poziomem
Modyfikowanie świata zewnętrznego lub obiektów w klastrze
Współbieżność optymistyczna
Operatory
Podsumowanie
Rozdział 2. Podstawy API Kubernetesa
Serwer API
Interfejs HTTP serwera API
Terminologia związana z API
Wersjonowanie API w Kubernetesie
Deklaratywne zarządzanie stanem
Używanie API w wierszu poleceń
W jaki sposób serwer API przetwarza żądania?
Podsumowanie
Rozdział 3. Podstawy klienta client-go
Repozytoria
Biblioteka klienta
Typy w API Kubernetesa
Repozytorium API Machinery
Tworzenie i używanie klientów
Wersjonowanie i kompatybilność
Wersje API i gwarancje kompatybilności
Obiekty Kubernetesa w Go
TypeMeta
ObjectMeta
Sekcje spec i status
Zbiory klientów
Podzasoby status — UpdateStatus
Wyświetlanie i usuwanie obiektów
Czujki
Rozszerzanie klientów
Opcje klientów
Informatory i buforowanie
Kolejka zadań
Repozytorium API Machinery — szczegóły
Rodzaje
Zasoby
Odwzorowania REST
Schemat
Vendoring
glide
dep
Moduły języka Go
Podsumowanie
Rozdział 4. Używanie niestandardowych zasobów
Wykrywanie informacji
Definicje typów
Zaawansowane mechanizmy niestandardowych zasobów
Sprawdzanie poprawności niestandardowych zasobów
Kategorie i krótkie nazwy
Wyświetlane kolumny
Podzasoby
Podzasób status
Podzasób scale
Niestandardowe zasoby z perspektywy programisty
Klient dynamiczny
Klienty typizowane
Budowa typu
Struktura pakietów języka Go
Klienty typizowane tworzone za pomocą generatora client-gen
Klient controller-runtime z narzędzi Operator SDK i Kubebuilder
Podsumowanie
Rozdział 5. Automatyzowanie generowania kodu
Po co stosować generatory kodu?
Wywoływanie generatorów
Kontrolowanie generatorów za pomocą znaczników
Znaczniki globalne
Znaczniki lokalne
Znaczniki dla generatora deepcopy-gen
runtime.Object i DeepCopyObject
Znaczniki dla generatora client-gen
Generatory informer-gen i lister-gen
Podsumowanie
Rozdział 6. Narzędzia służące do tworzenia operatorów
Czynności wstępne
Wzorowanie się na projekcie sample-controller
Przygotowania
Logika biznesowa
Kubebuilder
Przygotowania
Logika biznesowa
Operator SDK
Przygotowania
Logika biznesowa
Inne podejścia
Wnioski i przyszłe kierunki rozwoju
Podsumowanie
Rozdział 7. Udostępnianie kontrolerów i operatorów
Zarządzanie cyklem życia i pakowanie
Pakowanie — trudności
Helm
Kustomize
Inne techniki pakowania kodu
Najlepsze praktyki z obszaru pakowania kodu
Zarządzanie cyklem życia
Instalacje gotowe do użytku w środowisku produkcyjnym
Odpowiednie uprawnienia
Zautomatyzowany proces budowania i testowania
Niestandardowe kontrolery i obserwowalność
Rejestrowanie zdarzeń
Monitorowanie, instrumentacja i audyty
Podsumowanie
Rozdział 8. Niestandardowe serwery API
Scenariusze stosowania niestandardowych serwerów API
Przykład — pizzeria
Architektura — agregowanie
Usługi API
Wewnętrzna struktura niestandardowego serwera API
Delegowane uwierzytelnianie i obsługa zaufania
Delegowana autoryzacja
Pisanie niestandardowych serwerów API
Wzorzec opcji i konfiguracji oraz szablonowy kod potrzebny do uruchomienia serwera
Pierwsze uruchomienie
Typy wewnętrzne i konwersja
Pisanie typów API
Konwersje
Ustawianie wartości domyślnych
Testowanie konwersji powrotnych
Sprawdzanie poprawności
Rejestr i strategia
Rejestr generyczny
Strategia
Podłączanie strategii do rejestru generycznego
Instalowanie API
Kontrola dostępu
Implementacja
Rejestrowanie
Podłączanie zasobów
Instalowanie niestandardowych serwerów API
Manifesty instalacji
Konfigurowanie systemu RBAC
Uruchamianie niestandardowego serwera API bez zabezpieczeń
Certyfikaty i zaufanie
Współdzielenie systemu etcd
Podsumowanie
Rozdział 9. Zaawansowane zasoby niestandardowe
Wersjonowanie niestandardowych zasobów
Poprawianie kodu do obsługi pizzerii
Architektura webhooków konwersji
Implementacja webhooka konwersji
Przygotowywanie serwera HTTPS
Instalowanie webhooka konwersji
Konwersja w praktyce
Webhooki kontroli dostępu
Wymogi związane z kontrolą dostępu w przykładzie
Architektura webhooków kontroli dostępu
Rejestrowanie webhooków kontroli dostępu
Implementowanie webhooka kontroli dostępu
Webhook kontroli dostępu w praktyce
Schematy strukturalne i przyszłość definicji CRD
Schematy strukturalne
Okrajanie a zachowywanie nieznanych pól
Sterowanie okrajaniem
IntOrString i RawExtension
Wartości domyślne
Podsumowanie
Dodatek A. Materiały
Ogólne
Książki
Samouczki i przykłady
Artykuły
Repozytoria
O autorach
Kolofon