Trzyetapowe scalanie w werf: wdrażanie w Kubernetes z Helm «na sterydach»

Nastało to, na co długo czekaliśmy (i nie tylko my): werf, nasze narzędzie Open Source do tworzenia aplikacji i ich dostarczania w Kubernetes, teraz wspiera wprowadzanie zmian za pomocą patchy 3-way-merge! Dodatkowo pojawiła się możliwość adoptowania istniejących zasobów K8s do wydań Helm bez konieczności ich ponownego tworzenia.

Trzyetapowe scalanie w werf: wdrażanie w Kubernetes z Helm «na sterydach»

Mówiąc krótko, ustawiamy WERF_THREE_WAY_MERGE=enabled — otrzymujemy wdrożenie „jak w kubectl apply”, kompatybilne z istniejącymi instalacjami na Helm 2, a nawet trochę więcej.

Ale zacznijmy od teorii: czym w ogóle są patchy 3-way-merge, jak ludzie doszli do podejścia z ich generowaniem i dlaczego są ważne w procesach CI/CD opartych na infrastrukturze Kubernetes? A potem przyjrzymy się, czym jest 3-way-merge w werf, jakie tryby są domyślnie używane i jak nimi zarządzać.

Czym jest patch 3-way-merge?

Zaczynamy od zadania wdrożenia zasobów opisanych w manifestach YAML w Kubernetes.

Do pracy z zasobami API Kubernetes oferuje takie podstawowe operacje jak: create, patch, replace i delete. Zakłada się, że za ich pomocą należy skonstruować wygodne, ciągłe wdrażanie zasobów w klastrze. Jak?

Impertywne komendy kubectl

Pierwszym podejściem do zarządzania obiektami w Kubernetes jest użycie impertywnych komend kubectl do tworzenia, modyfikowania i usuwania tych obiektów. Mówiąc prościej:

  • poleceniem kubectl run możemy uruchomić Deployment lub Job:
    kubectl run --generator=deployment/apps.v1 NAZWA_DEPOLAMENTU --image=IMAGE
  • poleceniem kubectl scale — zmienić liczbę replik:
    kubectl scale --replicas=3 deployment/mysql
  • itd.

To podejście może wydawać się wygodne na pierwszy rzut oka. Jednak są problemy:

  1. Jak odzwierciedlić konfigurację automatyzować.
  2. Jak w Git? Jak dokonywać przeglądu zmian, które zachodzą w klastrze? Jak zapewnić
  3. konfigurację po restarcie? odtwarzalność Jasne jest, że takie podejście źle komponuje się z przechowywaniem razem z kodem aplikacji i infrastrukturą jako kodem (IaC; a nawet

jako bardziej nowoczesna wersja, zdobywająca popularność w ekosystemie Kubernetes). Dlatego dalszy rozwój tych komend w kubectl nie znalazł uznania. GitOps Operacje create, get, replace i delete

Z podstawowym

tworzeniem wszystko jest proste: wysyłamy manifest do operacji w kube api i zasób jest stworzony. Reprezentacja YAML manifestu może być przechowywana w Git, a do utworzenia — używamy komendy create kubectl create -f manifest.yaml usunięcie.

Z też jest proste: podstawiamy ten sam manifest.yaml z Git do komendy kubectl delete -f manifest.yaml kubectl delete -f manifest.yaml.

Operacja replace umożliwia całkowitą wymianę konfiguracji zasobu na nową, bez tworzenia zasobu od nowa. Oznacza to, że przed dokonaniem zmiany w zasobie warto zapytać o obecną wersję w operacji get, zmienić ją i zaktualizować operacją replace. W kube apiserver wbudowane jest optymistyczne blokowanie , i jeśli po operacji get obiekt się zmieni, to operacja replace nie powiedzie się.

Aby przechowywać konfigurację w Git i aktualizować za pomocą replace, należy wykonać operację get, połączyć konfigurację z Gita z tym, co otrzymaliśmy, i wykonać replace. Standardowo kubectl pozwala jedynie korzystać z komendy kubectl replace -f manifest.yaml, gdzie z Git do komendy — już całkowicie przygotowany (w naszym przypadku — połączony) manifest, który należy zainstalować. Wynika z tego, że użytkownik musi wdrożyć łączenie manifestów, a to nie jest proste zadanie…

Warto również zauważyć, że chociaż z Git do komendy jest przechowywana w Gicie, nie możemy z góry wiedzieć, czy należy tworzyć obiekt, czy go aktualizować — to musi zrobić oprogramowanie użytkownika.

Podsumowując: czy możemy zbudować ciągłe wdrożenie tylko za pomocą create, replace i delete, zapewniając przechowywanie konfiguracji infrastruktury w Gicie razem z kodem i wygodnym CI/CD?

W zasadzie, możemy… Aby to osiągnąć, należy zrealizować operację łączenia manifestów oraz jakąś obudowę, która:

  • sprawdza, czy obiekt istnieje w klastrze,
  • wykonuje początkowe utworzenie zasobu,
  • aktualizuje lub usuwa go.

Podczas aktualizacji należy wziąć pod uwagę, że zasób mógł się zmienić od ostatniego get i automatycznie obsługiwać przypadek optymistycznego blokowania — wykonywać ponowne próby aktualizacji.

Jednak dlaczego wynajdować koło na nowo, skoro kube-apiserver oferuje inny sposób aktualizacji zasobów: operację patch, która uwalnia użytkownika od części opisanych problemów?

Patch

Z dotarliśmy do poprawek.

Poprawki to podstawowy sposób wprowadzania zmian w istniejących obiektach w Kubernetes. Operacja patch działa w ten sposób, że:

  • użytkownik kube-apiserver musi wysłać poprawkę w formacie JSON i wskazać obiekt,
  • a apiserver sam rozwiąże aktualny stan obiektu i przekształci go do wymaganego kształtu.

Optymistyczne blokowanie w tym przypadku nie jest wymagane. Ta operacja jest bardziej deklaratywna w porównaniu z replace, chociaż początkowo może się wydawać odwrotnie.

Tak więc:

  • za pomocą operacji create tworzymy obiekt na podstawie manifestu z Gita,
  • z pomocą usuń — usuwamy, jeśli obiekt nie jest już potrzebny,
  • z pomocą patch — zmieniamy obiekt, przekształcając go do formy opisanej w Gicie.

Aby to zrobić, należy stworzyć odpowiednią łatkę!

Jak działają łatki w Helm 2: 2-way-merge

Podczas pierwszej instalacji wersji Helm wykonuje operację create dla zasobów wykresu.

Podczas aktualizacji wersji Helm dla każdego zasobu:

  • oblicza łatkę między wersją zasobu z poprzedniego wykresu a aktualną wersją wykresu,
  • zastosowuje tę łatkę.

Taką łatkę będziemy nazywać 2-way-merge patch,ponieważ w jej tworzeniu biorą udział 2 manifesty:

  • manifest zasobu z poprzedniej wersji,
  • manifest zasobu z aktualnej wersji.

Podczas usuwania operacja usuń w kube apiserver jest wywoływana dla zasobów, które zostały zadeklarowane w poprzedniej wersji, ale nie są zadeklarowane w aktualnej.

Podejście z 2-way-merge patch ma problem: prowadzi to do niesynchronizacji rzeczywistego stanu zasobu w klastrze i manifestu w Git.

Ilustracja problemu na przykładzie

  • W Git, w wykresie jest zapisany manifest, w którym pole image w Deployment ma wartość ubuntu:18.04.
  • Użytkownik przez kubectl edit zmienił wartość tego pola na ubuntu:19.04.
  • Podczas ponownego wdrażania wykresu Helm nie generuje łatki,ponieważ pole image w poprzedniej wersji wydania i w aktualnym wykresie są identyczne.
  • Po ponownym wdrożeniu image pozostaje ubuntu:19.04, chociaż w wykresie napisano ubuntu:18.04.

Uzyskaliśmy niesynchronizację i straciliśmy deklaratywność.

Co to jest zsynchronizowany zasób?

Ogólnie rzecz biorąc, Pełne zgodność manifestu zasobu w działającym klastrze z manifestem z Git jest niemożliwe do osiągnięcia. Ponieważ w rzeczywistym manifeście mogą być adnotacje/etykiety, dodatkowe kontenery i inne dane, które są dynamicznie dodawane i usuwane przez jakieś kontrolery. Te dane chcemy trzymać poza Git. Jednak chcemy, aby przy wdrażaniu te pola, które wyraźnie wskazaliśmy w Git, przyjmowały odpowiednie wartości.

Powstaje ogólne zasada zsynchronizowanego zasobu: przy wdrażaniu zasobu można zmieniać lub usuwać tylko te pola, które są wyraźnie określone w manifeście z Git (lub były określone w poprzedniej wersji, a teraz zostały usunięte).

3-way-merge patch

Podstawowa idea 3-way-merge patch: Generuje łatkę między ostatnią zastosowaną wersją manifestu z Git a docelową wersją manifestu z Git, uwzględniając aktualną wersję manifestu z działającego klastra. Ostateczna łatka musi odpowiadać zasadzie zsynchronizowanego zasobu:

  • Nowe pola, dodane do wersji docelowej, są dodawane za pomocą łatki;
  • Pola, które wcześniej istniały w ostatniej zastosowanej wersji i nie istnieją w docelowej, są zerowane za pomocą łaty;
  • Pola w bieżącej wersji obiektu, różniące się od docelowej wersji manifestu, są aktualizowane za pomocą łaty.

To jest zasada, według której generowane są łatki. kubectl apply:

  • Ostatnia zastosowana wersja manifestu jest zapisywana w adnotacji samego obiektu,
  • docelowa — jest pobierana z określonego pliku YAML,
  • bieżąca — z działającego klastra.

Teraz, kiedy wyjaśniliśmy teorię, pora opowiedzieć, co zrobiliśmy w werf.

Zastosowanie zmian w werf

Wcześniej werf, podobnie jak Helm 2, używał łat 2-way-merge.

Repair patch

Aby przejść na nowy typ łatek — 3-way-merge — pierwszym krokiem wprowadziliśmy tzw. łatki repair.

Podczas wdrażania używana jest standardowa łatka 2-way-merge, ale werf dodatkowo generuje taką łatkę, która synchronizuje rzeczywisty stan zasobu z tym, co jest napisane w Git (ta łatka jest tworzona z użyciem tej samej zasady synchronizowanego zasobu, opisanej powyżej).

W przypadku wystąpienia niesynchronizacji, na końcu wdrożenia użytkownik otrzymuje OSTRZEŻENIE z odpowiednim komunikatem oraz łatką, którą należy zastosować, aby przywrócić zasób do stanu synchronizowanego. Ta łatka jest również zapisywana w specjalnej adnotacji werf.io/repair-patch. Zakłada się, że użytkownik ręcznie sam zastosuje tę łatkę: werf jej nie zastosuje zasadniczo.

Generowanie łat repair jest tymczasowym środkiem, który umożliwia przetestowanie w praktyce tworzenie łatek według zasady 3-way-merge, ale nie ich automatyczne stosowanie. Obecnie ten tryb pracy jest włączony domyślnie.

Łatka 3-way-merge tylko dla nowych wersji

Od 1 grudnia 2019 r. wersje beta i alfa werf zaczynają domyślnie używać pełnych łat 3-way-merge do stosowania zmian tylko dla nowych wydań Helm, wdrażanych przez werf. Już istniejące wydania będą nadal korzystać z podejścia 2-way-merge + łatki repair.

Ten tryb pracy można włączyć jawnie przez ustawienie WERF_THREE_WAY_MERGE_MODE=onlyNewReleases już teraz.

Uwaga: funkcja ta była wprowadzana w werf przez kilka wydań: w kanale alfa została ukończona od wersji v1.0.5-alpha.19, a w kanale beta — od v1.0.4-beta.20.

łatka 3-way-merge dla wszystkich wydań

Od 15 grudnia 2019 r. wersje beta i alfa werf automatycznie używają pełnych łatek 3-way-merge do wprowadzania zmian w wszystkich wydaniach.

Ten tryb pracy można włączyć jawnie przez ustawienie WERF_THREE_WAY_MERGE_MODE=enabled już teraz.

Jak poradzić sobie z automatycznym skalowaniem zasobów?

W Kubernetesie istnieją dwa typy automatycznego skalowania: HPA (horyzontalne) i VPA (wertykalne).

Horyzontalne automatycznie wybiera liczbę replik, wertykalne — liczbę zasobów. Zarówno liczba replik, jak i wymagania dotyczące zasobów są określane w manifeście zasobu (patrz: spec.replicas lub spec.containers[].resources.limits.cpu, spec.containers[].resources.limits.memory i inne).

Problem: jeśli użytkownik skonfiguruje zasób w wykresie tak, że będą w nim określone wartości dla zasobów lub replik, a dla danego zasobu włączone będą auto-skaler, to przy każdym wdrożeniu werf zresetuje te wartości do tych, które są zapisane w manifeście wykresu.

Rozwiązania dla problemu są dwa. Na początek najlepiej jest zrezygnować z wyraźnego określenia automatycznie skalowanych wartości w manifeście wykresu. Jeśli jednak ten wariant z jakichś powodów nie jest odpowiedni (na przykład, ponieważ w wykresie wygodnie jest określić początkowe ograniczenia zasobów i liczbę replik), werf oferuje następujące adnotacje:

  • werf.io/set-replicas-only-on-creation=true
  • werf.io/set-resources-only-on-creation=true

W przypadku posiadania takiej adnotacji werf nie zresetuje odpowiednich wartości przy każdym wdrożeniu, a jedynie ustawi je przy początkowym utworzeniu zasobu.

Szczegóły — patrz w dokumentacji projektu na HPA i VPA.

Zabroń użycia 3-way-merge patch

Użytkownik może na razie zabronić używania nowych łatek w werf poprzez zmienną środowiskową WERF_THREE_WAY_MERGE_MODE=disabled. Jednak od 1 marca 2020 roku ten zakaz przestanie działać i możliwe będzie jedynie użycie łatek 3-way-merge.

Adopcja zasobów w werf

Opanowanie metody wprowadzania zmian za pomocą łatek 3-way-merge pozwoliło nam od razu wdrożyć taką funkcję jak adopcja istniejących w klastrze zasobów w wydaniu Helm.

Helm 2 ma problem: nie można dodać do manifestów wykresu zasobu, który już istnieje w klastrze, bez jego przeregenerowania od podstaw (patrz: #6031, #3275). Nauczyliśmy werf przyjmować istniejące zasoby w wydaniu. W tym celu należy na bieżącą wersję zasobu z działającego klastra ustawić adnotację (na przykład za pomocą kubectl edit):

"werf.io/allow-adoption-by-release": RELEASE_NAME

Teraz zasób musi być opisany w wykresie, a przy następnym wdrażaniu za pomocą werf, o odpowiedniej nazwie, istniejący zasób zostanie przyjęty do tego wydania i pozostanie pod jego zarządem. Co więcej, podczas przyjmowania zasobu do wydania werf przekształci obecny stan zasobu z działającego klastra do stanu opisanego w wykresie, używając tych samych łatek 3-way-merge i zasady synchronizowanego zasobu.

Uwaga: konfiguracja WERF_THREE_WAY_MERGE_MODE nie wpływa na adopcję zasobów — w przypadku adopcji zawsze używana jest łatka 3-way-merge.

Szczegóły — w dokumentacji.

Wnioski i dalsze plany

Mam nadzieję, że po tym artykule stało się jasne, czym są łatki 3-way-merge i dlaczego do nich doszliśmy. Z praktycznego punktu widzenia rozwoju projektu werf ich wdrożenie stało się jeszcze jednym krokiem w kierunku ulepszania wdrożeń podobnych do Helm. Teraz można zapomnieć o problemach z synchronizacją konfiguracji, które często występowały przy używaniu Helm 2. Jednocześnie dodano nową użyteczną funkcję adopcji już pobranych zasobów Kubernetes w wydaniu Helm.

W wdrożeniach podobnych do Helm wciąż pozostają pewne problemy i trudności, takie jak użycie szablonów Go, i będziemy je dalej rozwiązywać.

Informacje o metodach aktualizacji zasobów i adopcji można również znaleźć na tej stronie dokumentacji.

Helm 3

Osobnej uwagi wymaga niedawno wydana dosłownie w ciągu ostatnich dni nowa główna wersja Helm — v3, — która również wykorzystuje łatki 3-way-merge i eliminuje Tiller. Nowa wersja Helm wymaga migracji już istniejących instalacji, aby przekonwertować je na nowy format przechowywania wydań.

Werf ze swojej strony już teraz pozbył się użycia Tiller, przeszedł na 3-way-merge i dodał wiele innych, pozostając przy tym kompatybilnym z już istniejącymi instalacjami na Helm 2 (nie ma potrzeby wykonywania żadnych skryptów migracyjnych). Dlatego, dopóki werf nie został przełączony na Helm 3, użytkownicy werf nie tracą podstawowych zalet Helm 3 przed Helm 2 (też są obecne w werf).

Niemniej jednak, przełączenie werf na bazę kodu Helm 3 jest nieuniknione i nastąpi w najbliższej przyszłości. Przypuszczalnie będzie to werf 1.1 lub werf 1.2 (aktualnie główna wersja werf to 1.0; więcej o systemie wersjonowania werf można znaleźć tutaj). W tym czasie Helm 3 zdąży się ustabilizować.

P.S.

Przeczytaj także na naszym blogu:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster