Jak GitOps pomaga w deploymencie aplikacji

5 marca, 2021 Krzysztof Kąkol

Deployment workloadów na klastrze Kubernetesowym nie jest prostym zadaniem. Istnieje na szczęście całe mnóstwo metod, technologii i narzędzi pozwalających na kontrolę tego, co jest uruchomione na naszych serwerach. Wiele z tych podejść jest związanych z tradycyjnymi sposobami deploymentu. Tradycyjnymi, tzn. w pewnej mierze imperatywnymi. Oczywiście sam Kubernetes wykorzystuje deklaratywne manifesty do opisu tego, co jest na nim uruchomione, ale jakieś narzędzie musi zostać użyte do tego, by te manifesty na klastrze się znalazły. A co, jeśli chcielibyśmy, żeby cały proces (lub niemal cały) był deklaratywny? Żeby było jedno źródło prawdy?

 

Mikroserwisy na Kubernetesie

 

Wyobraźmy sobie aplikację opartą o mikroserwisy, uruchamianą na klastrze kubernetesowym. Nie byłoby w tym nic specjalnego, gdyby nie kilka założeń:

  • Każdy mikroserwis ma osobne repozytorium, jest kompletnie niezależny i niezależnie deployowany.
  • Istnieje definicja wersji aplikacji, jako całości. Wersja ta jest określona przez wersje pojedynczych mikroserwisów.
  • Aplikacja musi być utrzymywana w kilku wersjach jednocześnie – wydawana jest bowiem w cyklu release’ów comiesięcznych (lub nieco częstszych).
  • Powyższe założenie implikuje konieczność utrzymywania jednocześnie kilku wersji aplikacji (pamiętajmy, że każda wersja to zestaw mikroserwisów w konkretnych wersjach). Przecież testerzy muszą mieć możliwość weryfikowania zarówno nowych funkcjonalności jak i np. Hotfixów. Co więcej, jednocześnie w procesie developmentu są funkcjonalności, planowane do uruchomienia podczas najbliższego release’u, jak i te, które na swoją kolej muszą jeszcze poczekać.
  • Klient oczekuje, że jego aplikacja będzie mogła być odpalona jako multi-tenant na jego infrastrukturze, ale musi istnieć przynajmniej teoretyczna możliwość uruchomienia jej na innym klastrze, nawet na infrastrukturze klienta!

 

Jeśli powyższy przykład nie jest dostatecznie złożony, to dodajmy jeszcze konieczność uruchamiania testów integracyjnych w pipelinie dowolnego mikroserwisu, a testy te obejmują zarówno testy API, jak i testy frontendu.

 

Zobacz ->>>> Kim jest DevOps i co musi umieć

 

Deployment mikroserwisów

 

Powyższy scenariusz nie jest fikcyjny. Tak funkcjonuje jeden z projektów, w których mam okazję pracować. Większość z tych założeń wynika oczywiście z potrzeb biznesowych klienta, na których omówienie nie ma tutaj miejsca. Ważne jest jednak to, że musimy jakoś poradzić sobie z tym skomplikowanym scenariuszem i uczynić go realnym.

 

Typowy pipeline aplikacji jest zawsze w pewnej części imperatywny, nieważne jak bardzo byśmy udawali, że jest inaczej. Na przykład proces deploymentu pojedynczego kontenera wymaga z reguły wykonania poniższego minimalnego zestawu kroków:

  • Zbudowanie obrazu.
  • Wypchnięcie zbudowanego obrazu do repozytorium obrazów.
  • Zmiana definicji deploymentu na kubernetesie z uwzględnieniem nowego obrazu.

 

Wygląda to mniej więcej tak, jeśli mamy do czynienia z wieloma klastrami:

Proces deploymentu pojedynczego kontenera

 

 

W zasadzie wszystkie powyższe kroki są imperatywne. Dopiero to, co zrobi kubernetes, jest deklaratywne – uwzględniając manifest, zaktualizuje stan klastra. Deployment jest bowiem deklaratywnym “przepisem” na to, co ma być uruchomione na klastrze. To oczywiście spore uproszczenie, bo deploymenty nie są w żadnym razie jedynym przepisem, który kubernetes musi wziąć pod uwagę, ale na potrzeby niniejszego artykułu w zupełności wystarczającym.

 

Konsekwencje podejścia imperatywnego

 

Powyższy scenariusz nie ma w sobie oczywiście niczego złego. Po prostu od pewnego stopnia skomplikowania struktury samej aplikacji, jak i liczby środowisk, którymi musimy zarządzać, imperatywne podejście ma coraz więcej wad:

  • Aplikacja, służąca do obsługi pipeline’ów CI/CD, musi wiedzieć, gdzie są klastry, na które musi deployować aplikację.
  • Ta sama aplikacja musi mieć również dostęp do tych klastrów – konieczne jest np. zarządzanie dostępem do klastrów.
  • Tworzenie pipeline’ów staje się coraz trudniejsze, wraz ze wzrostem stopnia skomplikowania układu środowisk, w którym pracujemy. Można próbować oczywiście rozdzielać proces budowania obrazów i samego deploymentu, ale mimo wszystko musimy być na każdym etapie świadomi co i po co budujemy.
  • Nie ma już jednego źródła prawdy (source of truth) – wszystko “musi” wiedzieć aplikacja CI/CD i trudno jest sprawdzić jaki jest stan poszczególnych klastrów. Innymi słowy, nie wiemy tak naprawdę, jaka wersja aplikacji jest uruchomiona na konkretnym klastrze.

 

Takich konsekwencji można wyliczać wiele i przy wielu typach aplikacji nie mają one tak dużego znaczenia. W przypadku aplikacji, o której opowiedziałem we wstępie, te konsekwencje są absolutnie kluczowe i zmuszają nas do zastosowania podejścia (niemal) w pełni deklaratywnego – czyli GitOps.

 

Co to jest GitOps?

 

GitOps to podejście lub – może lepiej – zbiór dobrych praktyk, które pozwalają nam wykorzystywać repozytorium Git w procesach związanych z zarządzaniem workloadem. To oznacza, że to Git jest jedynym źródłem prawdy o stanie systemu, a także to, że wszystkie zmiany tego stanu przez to repozytorium muszą “przejść”.

 

Czego wymaga GitOps?

 

Aby podejście takie mogło zostać zastosowane, musimy być przygotowani na spełnienie kilku podstawowych wymagań:

  • Stan systemu musi być opisany deklaratywnie. Najlepszym sposobem przechowywania tego stanu jest oczywiście repozytorium Git. Dzięki temu, zyskujemy pewność, że każda zmiana stanu systemu jest odzwierciedlona jako commit. Do tego wiemy zawsze, kto tej zmiany dokonał.
  • Zmiany stanu systemu muszą być automatycznie aplikowane do środowiska, którym zarządzamy. Mamy dzięki temu jedno źródło prawdy i możemy zawsze zobaczyć, jaki jest stan systemu po prostu weryfikując repozytorium. Ponadto, odwrócenie ról – stan systemu w repozytorium – powoduje, że aplikacje odpowiedzialne za deployment nie muszą znać credentiali do klastrów. Mało tego, nie muszą w ogóle wiedzieć, gdzie te klastry są i ile ich jest.
  • Klaster musi mieć do dyspozycji agenty, które z jednej strony pozwolą na zarządzanie stanem klastra (wykonywanie deploymentu) jak i będą w stanie poinformować nas o tym, kiedy stan klastra nie jest zgodny z naszymi oczekiwaniami. Proces self-healingu jest tu rozumiany nieco szerzej niż zwykle. Nie chodzi wyłącznie o podnoszenie node’ów czy odtwarzanie podów, ale też “uzdrawianie” aplikacji po błędzie ludzkim.

 

 

GitOps w praktyce

 

Jak to wygląda w praktyce? Przede wszystkim potrzebujemy jakiegoś narzędzia – np. FluxCD czy ArgoCD. Poniżej postaram się w skrócie opisać, jak to wygląda w przypadku naszego projektu:

  1. Na klastrze zainstalowany jest FluxCD, którego rolą jest obserwowanie repozytorium obrazów, pod kątem pojawiania się nowych wersji obrazu mikroserwisu uruchomionego na klastrze. Drugim zadaniem FluxaCD jest obserwowanie repozytorium konfiguracji klastra, żeby automatycznie wdrożyć zmiany tam zadeklarowane – np. dodanie nowego serwisu. Z trzeciej strony, gdy FluxCD wykryje zmianę obrazu (a deklaratywny opis stanu klastra “nakazuje” mu automatyczną aktualizację na klastrze), zmienia on też konfigurację w repozytorium konfiguracji. Dzięki temu repozytorium konfiguracji klastra zawiera zawsze aktualny stan tego, co jest na klastrze uruchomione. Proces wygląda z grubsza tak, jak na poniższym rysunku: Na klastrze zainstalowany jest FluxCD, którego rolą jest obserwowanie repozytorium obrazów, pod kątem pojawiania się nowych wersji obrazu mikroserwisu uruchomionego na klastrze.
  2. Każdy klaster opisany jest przez zestaw tzw. HelmRelease’ów – czyli customowych resourców kubernetesowych, używanych przez FluxCD. HelmRelease zawiera tak naprawdę to, co aktualizowane jest przez FluxCD, czyli między innymi lokalizacje obrazów, deployowanych na konkretnym środowisku. Jeśli więc chcemy stworzyć nowe środowisko, to (pomijając zarządzalne usługi uruchamiane dla tego środowiska na chmurze) tworzymy nowy opis HelmRelease, definiujemy jakie wersje mikroserwisów mają się na nim odpalić, komitujemy do repozytorium i… resztą zajmuje się FluxCD, który, przypomnijmy, obserwuje to repozytorium. Stworzenie więc nowego środowiska w nowej wersji zajmuje kilka-kilkanaście minut.
  3. Proces deploymentu wygląda dla większości środowisk tak, że Jenkins buduje obrazy dockerowe i wypycha je do repozytorium obrazów (pomijam tu dla uproszczenia kwestię analizy statycznej kodu, unit testów, testów integracyjnych itp.). Na tym koniec, bo FluxCD obserwuje stan obrazów i zaktualizuje konfigurację klastra automatycznie. Można to zrobić na wiele sposobów, np. poprzez zadeklarowanie, że FluxCD ma obserwować obrazy z tagiem związanym z konkretną wersją semantyczną (typu ~1.0) lub po prostu najnowsze.

 

 

Korzyści z podejścia GitOps

Wróćmy więc do naszych początkowych wyzwań i spróbujmy opisać, co zyskaliśmy dzięki podejściu GitOps:

  • Możemy uruchamiać dowolną liczbę klastrów i w dowolny sposób konfigurować uruchamianie wersji na klastrze (np. poprzez uruchamianie równoległe kilku wersji aplikacji na jednym klastrze, wykorzystując mechanizm namespace’ów kubernetesowych). Jedynym wymogiem jest to, żeby klaster miał dostęp do repozytorium obrazów i repozytorium konfiguracji klastra (w naszym projekcie repozytorium obrazów to ECR uruchomiony na jednym koncie AWSowym, a klastry odpowiedzialne za grupy środowisk uruchamiane są na innych kontach).
  • Jenkins nie musi nic wiedzieć o klastrach, nie musi znać do nich uprawnień, nie musi mieć dostępu do żadnych secretów środowiskowych. Jedyne, co Jenkins musi, to zbudować obraz i wypchnąć do repozytorium (no i oczywiście wykonać testy i inne niezbędne czynności, kompletnie jednak niezależne od środowiska uruchomieniowego).
  • Tworzenie nowych środowisk jest uproszczone do maksimum, jeśli uruchamiane są na istniejących klastrach. Typowym przypadkiem jest konieczność utworzenia środowiska z wersją X.Y jako setup testowy dla klienta Z. Jedyne co należy zrobić, to utworzyć HelmRelease z opisem wersji i wkomitować do repozytorium konfiguracji klastra.
  • Dzięki podejściu GitOps developerzy zawsze wiedzą, gdzie można sprawdzić stan danej wersji aplikacji, bo istnieje jedno źródło prawdy. Jeśli więc coś nie działa tak, jak powinno, nie ma nic prostszego niż sprawdzić, jakie wersje obrazów są tam uruchomione.
  • Ze względu na odseparowanie procesu budowania obrazów i deploymentu, jesteśmy bezpieczniejsi. Jenkins już nie jest słabym punktem naszej infrastruktury. A nawet jeśli nie mamy do tego zaufania, to zmniejszyliśmy znacznie powierzchnię ataku (attack surface).

 

Podsumowanie

 

GitOps to specyficzne podejście do procesu deploymentu aplikacji. Nie w każdym przypadku będzie ono właściwe. Nie jest też zupełnie bez wad i wszystko zależy od narzędzi, z których korzystamy. Czasem miałem do czynienia ze zmniejszoną świadomością tego, co się na klastrze wydarzyło – np. FluxCD padł i nie było wiadomo, dlaczego aplikacja się nie aktualizuje. Oczywiście z czasem narzędzia stają się coraz lepsze. Flux v2 jest następcą FluxCD, napisanym w zasadzie od nowa i poprawiającym większość błędów młodości. Uważam też, że sama koncepcja całkowicie deklaratywnego procesu release’u jest warta tego, by wybaczyć narzędziom pewne niedociągnięcia. A elastyczność i łatwość zarządzania klastrami wynagradza wszelkie niedogodności.

 

Kontakt z autorem: Krzysztof Kąkol

 

Praca DevOps w PGS Software

 

 

 

 

Najnowsze wpisy