
8 kwietnia na konferencji , w ramach sekcji „DevOps i eksploatacja”, odbyła się prezentacja „Rozszerzamy i uzupełniamy Kubernetes”, w której uczestniczyło trzech pracowników firmy „Flant”. W niej omawiamy wiele sytuacji, w których chcieliśmy rozszerzyć i uzupełnić możliwości Kubernetes, ale nie znajdowaliśmy gotowego i prostego rozwiązania. Potrzebne rozwiązania pojawiły się w postaci projektów Open Source, którym również poświęcone jest to wystąpienie.
Zgodnie z tradycją, z radością przedstawiamy (50 minut, znacznie bardziej informatywne niż artykuł) oraz główne założenia w formie tekstowej. Zaczynajmy!
Jądro i dodatki w K8s
Kubernetes zmienia branżę oraz podejścia do administracji, które były ustalone od dawna:
- Dzięki jego abstrakcjom, operujemy już nie tylko takimi pojęciami jak konfiguracja lub uruchamianie poleceń (Chef, Ansible…), lecz korzystamy z grupowania kontenerów, usług itp.
- Możemy tworzyć aplikacje, nie martwiąc się o szczegóły dotyczące tej konkretnej platformy, na której będą uruchamiane: bare metal, chmura jednego z dostawców itp.
- K8s stał się bardziej dostępny niż kiedykolwiek najlepsze praktyki organizacji infrastruktury: techniki skalowania, samonaprawy, odporności na awarie itp.
Jednakże, rzecz jasna, nie wszystko jest tak proste: z Kubernetes pojawiły się również nowe wyzwania.
Kubernetes nie jest narzędziem, które rozwiązuje wszystkie problemy wszystkich użytkowników. Rdzeń Kubernetes odpowiada tylko za zestaw minimalnych funkcji, które są obecne w każdym klastrze:

W jądrze Kubernetes zdefiniowany jest podstawowy zestaw prymitywów — do grupowania kontenerów, zarządzania ruchem i tak dalej. Więcej na ten temat opowiadaliśmy w .
Z drugiej strony, K8s oferuje wspaniałe możliwości rozszerzenia dostępnych funkcji, które pomagają zaspokoić inne — specyficzne — potrzeby użytkowników. Za dodatki w Kubernetes odpowiadają administratorzy klastrów, którzy muszą zainstalować i skonfigurować wszystko, co jest potrzebne, aby ich klaster „przyjął odpowiednią formę” [dla rozwiązania ich specyficznych zadań]. Jakie to są dodatki? Przyjrzyjmy się kilku przykładom.
Przykłady dodatków
Po zainstalowaniu Kubernetes możemy się zdziwić, że sieć, która jest tak niezbędna do interakcji podów zarówno w ramach węzła, jak i między węzłami, nie działa sama w sobie. Jądro Kubernetes nie gwarantuje potrzebnych połączeń — zamiast tego definiuje sieciowy interfejs () dla zewnętrznych dodatków. Musimy zainstalować jeden z takich dodatków, który będzie odpowiedzialny za konfigurację sieci.

Przykład bliski tematu — rozwiązania do przechowywania danych (dysk lokalny, sieciowe urządzenie blokowe, Ceph…). Początkowo znajdowały się w jądrze, ale wraz z pojawieniem się sytuacja zmienia się na podobną do opisanej wcześniej: w Kubernetes interfejs, a jego realizacja — w zewnętrznych modułach.
Wśród innych przykładów:
- Ingress-kontrolery (przegląd ich znajdziesz w ).
- :

- — to cały zestaw rozszerzeń (w tym cert-manager), które definiują prymitywy i kontrolery. Ich logika działania ograniczona jest jedynie naszą wyobraźnią, co pozwala na przekształcanie gotowych komponentów infrastruktury (np. CDB) w prymitywy, które są znacznie prostsze w obsłudze (niż zestaw kontenerów i ich konfiguracji). Istnieje mnóstwo operatorów — choć wiele z nich nie jest jeszcze gotowych do produkcji, to tylko kwestia czasu:

- Metryki — kolejna ilustracja tego, jak w Kubernetes oddzielono interfejs (Metrics API) od implementacji (zewnętrzne rozszerzenia, takie jak Prometheus adapter, Datadog cluster agent…).
- Dla monitoring i statystyki, gdzie w praktyce potrzebne są nie tylko , ale także kube-state-metrics, node-exporter itd.
I to nadal nie jest pełna lista rozszerzeń… Na przykład w firmie „Flant” na każdy klaster Kubernetes instalujemy aktualnie 29 rozszerzeń (wszystkie razem tworzą 249 obiektów Kubernetes). Innymi słowy, nie wyobrażamy sobie życia klastra bez rozszerzeń.
Automatyzacja
Operatory zostały stworzone w celu automatyzacji rutynowych operacji, z którymi na co dzień się spotykamy. Oto przykłady z życia, w których świetnym rozwiązaniem byłoby stworzenie operatora:
- Istnieje prywatny (tzn. wymagający logowania) rejestr z obrazami aplikacji. Zakłada się, że do każdego poda przypisany jest specjalny sekret, który umożliwia autoryzację w rejestrze. Naszym celem jest zapewnienie, że ten sekret znajduje się w przestrzeni nazw, aby pod’y mogły pobierać obrazy. Aplikacji (z których każda potrzebuje sekrety) może być bardzo wiele, a same sekrety warto regularnie aktualizować, więc opcja ręcznego rozmieszczania sekretów odpada. W tym momencie z pomocą przychodzi operator: tworzymy kontroler, który będzie czekał na pojawienie się przestrzeni nazw i w związku z tym zdarzeniem doda sekret do przestrzeni nazw.
- Domyślnie dostęp z podów do internetu jest zablokowany. Jednak czasami może być potrzebny: logiczne, aby mechanizm przyznawania dostępu działał prosto, nie wymagając specyficznych umiejętności, — na przykład w oparciu o obecność określonej etykiety w namespace. Jak w tym pomoże operator? Tworzymy kontroler, który oczekuje na pojawienie się etykiety w namespace i dodaje odpowiednią politykę dostępu do internetu.
- Podobna sytuacja: załóżmy, że potrzebujemy dodać do węzła określony , jeśli ma on podobną etykietę (z jakimś prefiksem). Działania z operatorem są oczywiste…
W każdej klastra musimy rozwiązywać rutynowe zadania, a właściwie robić to — przy pomocy operatorów.
Podsumowując wszystkie opisane historie, doszliśmy do wniosku, że dla komfortowej pracy w Kubernetesie potrzebne jest: a) instalowanie dodatków, b) opracowywanie operatorów (do rozwiązywania codziennych zadań administracyjnych).
Jak napisać operatora dla Kubernetes?
Ogólnie schemat jest prosty:

… ale tutaj okazuje się, że:
- API Kubernetes jest dość złożoną rzeczą, która wymaga sporo czasu na opanowanie;
- programowanie nie jest dla każdego (język Go wybrany jako preferowany, ponieważ istnieje dla niego specjalny framework — );
- sytuacja z frameworkiem jest podobna.
Podsumowanie: aby napisać kontroler (operator), należy poświęcić znaczne zasoby na naukę materiału teoretycznego. Byłoby to uzasadnione dla "dużych" operatorów — powiedzmy, dla systemów DB MySQL. Ale jeśli przypomnimy sobie opisane wcześniej przykłady (rozmieszczanie sekretów, dostęp podów do internetu…), które również chcemy wykonać poprawnie, zrozumiemy, że włożony wysiłek przeważa nad potrzebnym w danym momencie wynikiem:

Ogólnie pojawia się dylemat: poświęcić wiele zasobów i zdobyć odpowiednie narzędzie do pisania operatorów czy działać „starym sposobem” (ale szybko). Aby znaleźć kompromis między tymi skrajnościami, stworzyliśmy nasz projekt: (patrz także jego na habrze).
Shell-operator
Jak to działa? W klastrze znajduje się pod, w którym znajduje się binarka Go z shell-operator. Obok niego przechowywany jest zestaw hooków (więcej o nich — zob. poniżej). Sam shell-operator subskrybuje określone wydarzenia w API Kubernetes, w wyniku wystąpienia których uruchamia odpowiednie haki.
Jak operator powłoki odróżnia, które haki wywołać przy jakich zdarzeniach? Te informacje przekazują operatorowi powłoki same haki, a robią to w bardzo prosty sposób.
Hak to skrypt w Bashu lub inny plik wykonywalny, który obsługuje jeden argument --config i w odpowiedzi zwraca JSON. Ten ostatni definiuje, które obiekty go interesują i na jakie zdarzenia (dla tych obiektów) powinien reagować:

Zilustruję implementację operatora powłoki na jednym z naszych przykładów — rozkładaniu sekretów do dostępu do prywatnego rejestru z obrazami aplikacji. Składa się ona z dwóch etapów.
Praktyka: 1. Piszemy hak
Najpierw w haku obsłużymy --config, wskazując, że interesują nas namespace'y, a konkretnie — moment ich tworzenia:
[[ $1 == "--config" ]] ; then
cat << EOF
{
"onKubernetesEvent": [
{
"kind": "namespace",
"event": ["add"]
}
]
}
EOF
…Jak będzie wyglądać logika? Również całkiem prosto:
…
else
createdNamespace=$(jq -r '.[0].resourceName' $BINDING_CONTEXT_PATH)
kubectl create -n ${createdNamespace} -f - << EOF
Kind: Secret
...
EOF
fi Pierwszym krokiem dowiadujemy się, który namespace został stworzony, a drugim — tworzymy przez kubectl sekret dla tej przestrzeni nazw.
Praktyka: 2. Tworzymy obraz
Pozostaje przekazać utworzony hook do shell-operatora — jak to zrobić? Sam shell-operator dostarczany jest jako obraz Docker, więc naszym zadaniem jest dodać hook do specjalnego katalogu w tym obrazie:
FROM flant/shell-operator:v1.0.0-beta.1
ADD my-handler.sh /hooksBędzie trzeba go zbudować i wypchnąć:
$ docker build -t registry.example.com/my-operator:v1 .
$ docker push registry.example.com/my-operator:v1Ostatni krok — wdrożyć obraz do klastra. W tym celu napiszemy Wdrożenie:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: my-operator
spec:
template:
spec:
containers:
- name: my-operator
image: registry.example.com/my-operator:v1 # 1
serviceAccountName: my-operator # 2Należy zwrócić uwagę na dwa aspekty:
- określenie właśnie utworzonego obrazu;
- to komponent systemowy, który (co najmniej) potrzebuje uprawnień do subskrybowania zdarzeń w Kubernetes oraz do rozkładania sekretów po namespaces, dlatego tworzymy dla hooka ServiceAccount (i zestaw reguł).
Efekt — rozwiązaliśmy nasz problem native sposobem na Kubernetes, tworząc operatora do rozkładania sekretów.
Inne możliwości shell-operatora
Aby ograniczyć obiekty wybranego przez Ciebie typu, z którymi będzie pracować hook, można je filtrować, wybierając według określonych etykiet (lub przy pomocy matchExpressions):
"onKubernetesEvent": [
{
"selector": {
"matchLabels": {
"foo": "bar",
},
"matchExpressions": [
{
"key": "allow",
"operation": "In",
"values": ["wan", "warehouse"],
},
],
}
…
}
]Zaprojektowano mechanizm deduplikacji, który — za pomocą filtru jq — umożliwia przekształcanie dużych JSONów obiektów w małe, w których pozostają tylko te parametry, które chcemy śledzić.
Podczas wywoływania hooka shell-operator przekazuje mu dane o obiekcie, które mogą być wykorzystywane do różnych celów.
Wydarzenia, przy których są wywoływane hooki, nie ograniczają się do wydarzeń Kubernetes: shell-operator obsługuje także wywoływanie hooków w określonym czasie (analogicznie do crontab w tradycyjnych harmonogramach), a także specjalne wydarzenie onStartup. Wszystkie te wydarzenia mogą być łączone i przypisane do tego samego hooka.
I jeszcze dwie cechy shell-operatora:
- Działa asynchronicznie. Od momentu wystąpienia zdarzenia w Kubernetes (na przykład, utworzenia obiektu) mogły zdarzyć się również inne wydarzenia w klastrze (na przykład, usunięcie tego samego obiektu), co należy uwzględnić w hakach. Jeśli hak zakończy się błędem, domyślnie zostanie wywołany ponownie do pomyślnego zakończenia (to zachowanie można zmienić).
- Eksportuje metryki dla Prometheusa, dzięki którym można zrozumieć, czy shell-operator działa, poznać liczbę błędów dla każdego haka oraz bieżący rozmiar kolejki.
Podsumowując tę część raportu:

Instalacja wtyczek
Aby komfortowo pracować z Kubernetes, wspomniano również o potrzebie instalacji wtyczek. Opowiem o tym na przykładzie drogi naszej firmy do tego, jak robimy to obecnie.
Rozpoczęliśmy pracę z Kubernetes od kilku klastrów, w których jedynym dodatkiem był Ingress. W każdym klastrze trzeba było go instalować na różne sposoby, dlatego przygotowaliśmy kilka konfiguracji YAML dla różnych środowisk: bare metal, AWS…
Klastrów przybywało — w związku z tym przybywało również konfiguracji. Co więcej, poprawialiśmy same te konfiguracje, co doprowadziło do ich znacznej niejednorodności:

Aby wszystko uporządkować, zaczęliśmy od skryptu (install-ingress.sh), który przyjmował jako argument typ klastra, do którego będziemy wdrażać, generował potrzebną konfigurację YAML i wdrażał ją w Kubernetes.
Krótko mówiąc, nasza dalsza droga oraz towarzyszące temu rozważania wyglądały następująco:
- do pracy z konfiguracjami YAML potrzebny jest silnik szablonów (na wstępnym etapie użyliśmy prostego sed);
- w miarę wzrostu liczby klastrów pojawiła się potrzeba automatycznego aktualizowania (najwcześniejsze rozwiązanie to umieszczenie skryptu w Gicie, aktualizowanie go przez cron i uruchamianie);
- podobny skrypt był potrzebny dla Prometheus (
install-prometheus.sh), jednak wyróżnia się tym, że wymaga znacznie większej ilości danych wejściowych oraz ich przechowywania (najlepiej — w sposób scentralizowany w klastrze), przy czym niektóre dane (hasła) można było generować automatycznie:
- ryzyko wdrożenia czegoś niewłaściwego na rosnącą liczbę klastrów nieustannie wzrastało, dlatego zrozumieliśmy, że instalatorzy (tj. dwa skrypty: dla Ingress i Prometheus) potrzebowali etapu testowego (kilka gałęzi w Gicie, kilka cronów do ich aktualizacji w odpowiednich: stabilnych lub testowych — klastrach);
- z
kubectl applystało się trudno pracować, ponieważ nie jest on deklaratywny i potrafi tylko tworzyć obiekty, ale nie podejmować decyzji dotyczących ich statusu/usuwania; - brakowało niektórych funkcji, które w tamtym czasie nie zostały przez nas w ogóle zrealizowane:
- pełnej kontroli nad wynikiem aktualizacji klastrów,
- automatycznego określania niektórych parametrów (wprowadzających dla skryptów instalacyjnych) na podstawie danych, które można uzyskać z klastra (odkrycie),
- jego logicznego rozwoju w postaci ciągłego odkrywania.
Całe to zgromadzone doświadczenie zrealizowaliśmy w ramach innego projektu — .
Addon-operator
W jego podstawie leży już wspomniany shell-operator. Cały system wygląda następująco:
Do haków shell-operatora dodawane są:
- magazyn wartości,
- Helm-chart,
- komponent, który nadzoruje magazyn wartości i — w przypadku jakichkolwiek zmian — prosi Helm o ponowne wdrożenie chartu.

W ten sposób możemy zareagować na zdarzenie w Kubernetes, uruchomić hak, a z tego haka wprowadzić zmiany w magazynie, po czym chart zostanie ponownie wdrożony. W uzyskanej schemacie wyróżniamy zestaw haków i chart w jeden komponent, który nazywamy modułem:

Mogą istnieć liczne moduły, a my dodajemy do nich globalne haki, globalne przechowywanie wartości oraz komponent, który monitoruje to globalne przechowywanie.
Teraz, gdy w Kubernetes coś się dzieje, możemy zareagować na to za pomocą globalnego haka i wprowadzić zmiany w globalnym przechowywaniu. Ta zmiana zostanie zauważona i spowoduje aktualizację wszystkich modułów w klastrze:

Ten schemat spełnia wszystkie wymagania dotyczące instalacji dodatków, które zostały wcześniej przedstawione:
- Za szablonowanie i deklaratywność odpowiada Helm.
- Problem automatycznego aktualizowania rozwiązano za pomocą globalnego haka, który według harmonogramu sprawdza rejestr i, jeśli widzi nowy obraz systemu, aktualizuje go (tj. 'samo siebie').
- Przechowywanie ustawień w klastrze jest realizowane za pomocą ConfigMap, w którym zapisane są dane początkowe dla składowisk (w momencie uruchamiania są one ładowane do składowisk).
- Problemy z generowaniem haseł, odkrywaniem i ciągłym odkrywaniem zostały rozwiązane za pomocą haków.
- Stage'owanie osiągnięto dzięki znakom, które Docker obsługuje standardowo.
- Kontrola wyników odbywa się za pomocą metryk, na podstawie których możemy ocenić status.
Cały ten system został zrealizowany jako jeden binarny plik w Go, który otrzymał nazwę addon-operator. Dzięki temu schemat wygląda prościej:

Głównym składnikiem w tym schemacie jest zestaw modułów (podświetlone na szaro na dole). Teraz możemy z niewielkim wysiłkiem napisać moduł dla potrzebnego dodatku i być pewnym, że zostanie zainstalowany w każdym klastrze, będzie aktualizowany i będzie reagował na potrzebne mu zdarzenia w klastrze.
„Flant” używa na ponad 70 klastrach Kubernetes. Aktualny status to wersja alfa. Obecnie przygotowujemy dokumentację, aby wydać wersję beta, a tymczasem w repozytorium , na podstawie których można stworzyć własny addon.
Skąd wziąć moduły dla addon-operator? Publikacja własnej biblioteki to nasz następny krok, planujemy to zrobić latem.
Wideo i slajdy
Wideo z wystąpienia (~50 minut):

Prezentacja wykładu:
P.S.
Inne wykłady w naszym blogu:
- «»; (Dmitrij Stoliarow; 8 listopada 2018 na HighLoad++);
- «»; (Dmitrij Stoliarow; 28 maja 2018 na RootConf);
- «»; (Dmitrij Stoliarow; 7 listopada 2017 na HighLoad++);
- «»; (Dmitrij Stolyarov; 6 czerwca 2017 na RootConf).
Może Cię także zainteresować następujące publikacje:
- «».
Źródło: habr.com



