werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

27 maja w głównym hallu konferencji DevOpsConf 2019, która odbywa się w ramach festiwalu RIT++ 2019, w ramach sekcji „Ciągłe dostarczanie”, wygłoszono referat „werf – nasze narzędzie do CI/CD w Kubernetes”. W nim mówimy o tych problemach i wyzwaniach, przed którymi staje każdy przy wdrażaniu w Kubernetes, a także o niuansach, które mogą nie być od razu zauważalne. Rozważając możliwe drogi rozwiązania, pokazujemy, jak to jest zrealizowane w narzędziu Open Source werf.

Od momentu wystąpienia nasza aplikacja (wcześniej znana jako dapp) przekroczyła historyczną granicę 1000 gwiazdek na GitHubie — mamy nadzieję, że rosnąca społeczność jej użytkowników ułatwi życie wielu inżynierom DevOps.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

I tak, przedstawiamy wideo z wykładem (~47 minut, znacznie bardziej informacyjne niż artykuł) i główną esencję z niego w formie tekstowej. Zaczynajmy!

Dostarczanie kodu w Kubernetes

W referacie mowa będzie już nie o werf, a o CI/CD w Kubernetes, przyjmując, że nasze oprogramowanie jest zapakowane w kontenery Docker (o tym opowiadałem w referacie z 2016 roku), a K8s będzie używane do uruchamiania go w produkcji (o tym — w 2017 roku).

Jak wygląda dostarczanie w Kubernetes?

  • Jest repozytorium Git z kodem i instrukcjami do jego budowy. Aplikacja jest kompilowana do obrazu Docker i publikowana w Docker Registry.
  • W tym samym repozytorium znajdują się instrukcje dotyczące tego, jak aplikację wdrożyć i uruchomić. Na etapie wdrożenia te instrukcje są wysyłane do Kubernetes, który pobiera potrzebny obraz z registry i go uruchamia.
  • Ponadto zwykle są testy. Niektóre z nich można wykonać przy publikacji obrazu. Można również (wg tych samych instrukcji) wdrożyć kopię aplikacji (w oddzielnej przestrzeni nazw K8s lub oddzielnym klastrze) i uruchamiać tam testy.
  • W końcu potrzebny jest system CI, który otrzymuje zdarzenia z Git'a (lub naciśnięcia przycisków) i wywołuje wszystkie określone etapy: budowanie, publikowanie, wdrażanie, testowanie.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Tutaj jest kilka ważnych uwag:

  1. Ponieważ mamy infrastrukturę niezmienną (immutable infrastructure), obraz aplikacji, który jest używany na wszystkich etapach (staging, production itd.), musi być jeden. Więcej o tym i z przykładami mówiłem tutaj.
  2. Ponieważ stosujemy podejście infrastruktura jako kod (IaC), kod aplikacji, instrukcje do jego budowy i uruchomienia muszą leżeć właśnie w jednym repozytorium. Więcej o tym — patrz w tym samym referacie.
  3. Łańcuch dostarczania (delivery) zwykle widzimy tak: aplikację zbudowano, przetestowano, wydano (etap release) i już — dostawa się odbyła. W rzeczywistości użytkownik otrzymuje to, co wprowadziliście na rynek, nie kiedy dostarczyliście to do produkcji, a kiedy mógł to zobaczyć i ta produkcja działała. Dlatego uważam, że łańcuch dostaw kończy się tylko na etapie eksploatacji (uruchom), a mówiąc dokładniej, nawet w momencie, gdy kod został usunięty z produkcji (zastąpiony nowym).

Wróćmy do zaznaczonego wcześniej schematu dostawy w Kubernetes: wynaleźli go nie tylko my, ale dosłownie każdy, kto zajmował się tym problemem. W istocie ten wzorzec nazywa się teraz GitOps (więcej o terminie i ideach, które za nim stoją, można przeczytać tutaj). Przyjrzyjmy się etapom schematu.

Etap budowy (build)

Wydaje się, że można opowiedzieć w 2019 roku o budowie obrazów Docker, kiedy wszyscy potrafią pisać pliki Dockerfile i je uruchamiać. docker build?.. Вот нюансы, на которые хотелось бы обратить внимание:

  1. Waga obrazu ma znaczenie, dlatego używajcie multi-stage, aby pozostawić w obrazie tylko to, co rzeczywiście potrzebne do działania aplikacji.
  2. Liczba warstw powinna być minimalizowana, łącząc łańcuchy z RUN-komend według ich znaczenia.
  3. Jednak to dodaje problemów w debugowaniu, ponieważ przy awarii budowy trzeba znaleźć tę odpowiednią komendę z łańcucha, która spowodowała problem.
  4. Szybkość budowy jest ważna, ponieważ chcemy szybko wprowadzać zmiany i obserwować rezultaty. Na przykład, nie chcemy przebudowywać zależności w bibliotekach języka przy każdej budowie aplikacji.
  5. Często z jednego repozytorium Git potrzebne są wiele obrazów, co można rozwiązać zestawem Dockerfile’ów (lub nazwanymi etapami w jednym pliku) oraz skryptem Bash do ich kolejnej budowy.

To była tylko wierzchołek góry lodowej, z którym wszyscy się mierzą. Ale są i inne problemy, a w szczególności:

  1. Często na etapie budowy potrzebujemy coś zamontować (na przykład, zbuforować wynik komendy typu apt w zewnętrznym katalogu).
  2. Chcemy Ansible zamiast pisać w shellu.
  3. Chcemy zbierać bez Docker (po co nam dodatkowa wirtualna maszyna, w której trzeba wszystko konfigurować, skoro już jest klaster Kubernetes, na którym można uruchamiać kontenery?).
  4. Równoległa budowa, którą można rozumieć na różne sposoby: różne polecenia z Dockerfile (jeśli używa się multi-stage), kilka commitów jednego repozytorium, kilka Dockerfile’ów.
  5. Rozproszona budowa: chcemy zbierać coś w podach, które są «efemeryczne», ponieważ mają one przestarzały cache, a więc trzeba go przechowywać gdzie indziej.
  6. W końcu nazwałem szczyt marzeń automagią: idealnie byłoby wejść do repozytorium, wpisać jakąś komendę i otrzymać gotowy obraz, zbudowany z zrozumieniem jak i co właściwie zrobić. Jednak osobiście nie jestem pewien, czy wszystkie niuanse można przewidzieć.

I oto są projekty:

  • moby/buildkit — budowniczy od firmy Docker Inc (już zintegrowany w aktualnych wersjach Docker), który stara się rozwiązać wszystkie te problemy;
  • kaniko — budowniczy od Google, pozwalający budować bez Docker;
  • Buildpacks.io — próba CNCF stworzenia automagii i, w szczególności, ciekawe rozwiązanie z rebase dla warstw;
  • i jeszcze wiele innych narzędzi, takich jak buildah, genuinetools/img

… i zobaczcie, ile mają gwiazdek na GitHub. Z jednej strony, docker build są i mogą coś zrobić, ale w rzeczywistości problem nie został do końca rozwiązany — dowodem na to jest równoległy rozwój alternatywnych budowniczych, z których każdy rozwiązuje jakąś część problemów.

Budowanie w werf

Tak dotarliśmy do werf (wcześniej znanej jak dapp) — Open Source-narzędzie firmy «Flant», które tworzymy już od wielu lat. Wszystko zaczęło się około 5 lat temu od skryptów Bash, optymalizujących budowanie Dockerfile’ów, a w ciągu ostatnich 3 lat prowadzono pełnoprawny rozwój w ramach jednego projektu z własnym repozytorium Git (najpierw w Ruby, a potem przepisaliśmy na Go, a przy okazji zmieniliśmy nazwę). Jakie problemy związane z budowaniem zostały rozwiązane w werf?

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Problemy zaznaczone na niebiesko są już zrealizowane, równoległe budowanie zostało zrealizowane w ramach jednego hosta, a wydzielone pytania zaznaczone na żółto planujemy ukończyć do końca lata.

Etap publikacji w registry (publish)

Wypełniliśmy docker push… — co może być trudnego w tym, aby załadować obraz do registry? I tutaj pojawia się pytanie: «Jaki tag nadać obrazowi?» Pojawia się ono z tego powodu, że mamy Gitflow (lub inną strategię Gita) i Kubernetes, a przemysł dąży do tego, aby to, co dzieje się w Kubernetes, podążało za tym, co dzieje się w Gicie. W końcu Git jest naszym jedynym źródłem prawdy.

Co w tym trudnego? Gwarantowanie reprodukowalności: od commita w Git, który z natury jest niezmienny (immutable), do obrazu Docker, który powinien pozostać taki sam.

Dla nas również ważne jest określać pochodzenie, ponieważ chcemy rozumieć, z którego commita zbudowano aplikację uruchomioną w Kubernetes (wtedy będziemy mogli robić różnice i podobne rzeczy).

Strategie tagowania

Pierwsza to prosty git tag. Mamy registry z obrazem oznaczonym jako 1.0. W Kubernetes są stage i production, do których ten obraz został wysłany. W Git robimy commity i w pewnym momencie stawiamy tag 2.0. Budujemy go według instrukcji z repozytorium i umieszczamy w registry z tagiem 2.0. Wdrażamy na stage, a jeśli wszystko dobrze, potem na production.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Problem z takim podejściem polega na tym, że najpierw postawiliśmy tag, a dopiero potem przetestowaliśmy i wdrożyliśmy. Dlaczego? Po pierwsze, to po prostu nielogiczne: wydajemy wersję oprogramowania, które jeszcze nie zostało sprawdzone (nie możemy zrobić inaczej, ponieważ do przeprowadzenia testów trzeba postawić tag). Po drugie, taki sposób nie pasuje do Gitflow.

Druga opcja to git commit + tag. W gałęzi master jest tag 1.0; dla niego w registry — obraz wdrożony na production. Ponadto w klastrze Kubernetes są kontury preview i staging. Następnie postępujemy zgodnie z Gitflow: w głównej gałęzi do rozwoju (develop) wprowadzamy nowe funkcje, co prowadzi do powstania commita o identyfikatorze #c1. Budujemy go i publikujemy w registry, używając tego identyfikatora (#c1). Z tym samym identyfikatorem wdrażamy na preview. Podobnie postępujemy z commitami #c2 i #c3.

Kiedy zrozumiemy, że funkcji jest wystarczająco, zaczynamy wszystko stabilizować. W Git tworzymy gałąź release_1.1 (opartego na #c3 z develop). Budowanie tej wersji nie będzie potrzebne, ponieważ zostało to zrobione na poprzednim etapie. Dlatego możemy po prostu wdrożyć ją na staging. Naprawiamy błędy w #c4 i podobnie wdrażamy na staging. Równocześnie trwa rozwój w develop, do którego okresowo wprowadzane są zmiany z release_1.1. W pewnym momencie otrzymujemy zbudowany i wdrożony na staging commit, którym jesteśmy zadowoleni (#c25).

Wtedy wykonujemy merge (z fast-forwardem) gałęzi release (release_1.1) w master. Stawiamy na ten commit tag z nową wersją (1.1). Ale ten obraz już został zbudowany w registry, więc aby nie budować go jeszcze raz, po prostu dodajemy drugi tag do istniejącego obrazu (teraz ma w registry tagi #c25 i 1.1). Po tym wdrażamy go na production.

Jest niedogodność, że na staging został wdrożony jeden obraz (#c25), a na production — niejako inny (1.1), ale wiemy, że „fizycznie” to ten sam obraz z registry.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Prawdziwym minusem jest to, że brak jest wsparcia dla merge commitów, trzeba robić fast-forward.

Możemy pójść dalej i wykonać sztuczkę… Rozważmy przykład prostego Dockerfile:

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

Zbudujmy z tego plik według takiej zasady, aby wziąć:

  • SHA256 od identyfikatorów używanych obrazów (ruby:2.3 i nginx:alpine), które są sumami kontrolnymi ich zawartości;
  • wszystkie komendy (RUN, POLECENIE itp.);
  • SHA256 od plików, które były dodawane.

… i weźmiemy sumę kontrolną (ponownie SHA256) z takiego pliku. To sygnatura wszystkiego, co definiuje zawartość obrazu Docker.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Wróćmy do schematu i zamiast commitów będziemy używać takich sygnatur, tzn. tagować obrazy sygnaturami.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Teraz, gdy będzie konieczne np. ‘scalanie’ zmian z wydania do master, możemy zrobić prawdziwy commit scalania: będzie miał inny identyfikator, ale tę samą sygnaturę. Z takim samym identyfikatorem wprowadzimy obraz na produkcję.

Minusem jest to, że teraz nie będzie można określić, jaki commit został wprowadzony na produkcję — sumy kontrolne działają tylko w jedną stronę. Problem ten rozwiązuje dodatkowa warstwa z metadanymi — opowiem o tym więcej później.

Tagowanie w werf

W werf poszliśmy jeszcze dalej i przygotowujemy się do zbudowania rozproszonego z cachem, który nie jest przechowywany na jednej maszynie… Tak więc, mamy Docker obrazy dwóch typów, nazywamy je stage i image.

W repozytorium Git werf przechowywane są specyficzne instrukcje do budowy, opisujące różne etapy budowy (beforeInstall, install, beforeSetup, setup). Pierwszy obraz etapu budujemy z sygnaturą określoną jako suma kontrolna pierwszych kroków. Następnie dodajemy kod źródłowy, dla nowego obrazu etapu obliczamy jego sumę kontrolną… Te operacje powtarzają się dla wszystkich etapów, w rezultacie czego otrzymujemy zestaw obrazów etapu. Następnie tworzymy końcowy obraz, który zawiera także metadane o jego pochodzeniu. I już ten obraz tagujemy na różne sposoby (szczegóły później).

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Niech po tym pojawi się nowy commit, w którym zmieniono tylko kod aplikacji. Co się stanie? Dla zmian kodu zostanie utworzona łatka, przygotowany nowy obraz stage. Jego sygnatura zostanie określona jako suma kontrolna starego obrazu stage i nowej łatki. Na podstawie tego obrazu zostanie utworzony nowy końcowy obraz image. Podobne zachowanie będzie miało miejsce przy zmianach na innych etapach.

W ten sposób obrazy stage są pamięcią podręczną, którą można przechowywać rozproszono, a już stworzone z niej obrazy image są ładowane do Docker Registry.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Czyszczenie registry

Nie będzie mowy o usuwaniu warstw, które pozostały wiszące po usuniętych tagach — to standardowa funkcjonalność samego Docker Registry. Mowa o sytuacji, gdy nagromadzi się wiele tagów Docker i zdajemy sobie sprawę, że ich część nie jest nam już potrzebna, a zajmują miejsce (i/lub płacimy za nie).

Jakie są strategie czyszczenia?

  1. Można po prostu nic nie czyścić. Czasami rzeczywiście prościej jest trochę zapłacić za dodatkową przestrzeń niż rozplątywać ogromny kłębek tagów. Ale to działa tylko do pewnego momentu.
  2. Pełne zresetowanie. Jeśli usuniemy wszystkie obrazy i zbudujemy tylko aktualne w systemie CI, może wystąpić problem. Jeśli na produkcji zostanie ponownie uruchomiony kontener, dla niego załadowany zostanie nowy obraz — taki, który jeszcze nie był testowany. Zabiwa to ideę niezmiennej infrastruktury.
  3. Blue-green. Gdy jeden registry zaczyna być zapełniony — ładujemy obrazy do innego. Ta sama kwestia co w poprzedniej metodzie: w którym momencie można oczyścić ten registry, który zaczął się przepełniać?
  4. Na czas. Usuwać wszystkie obrazy starsze niż 1 miesiąc? Ale na pewno znajdzie się usługa, która nie była aktualizowana przez cały miesiąc...
  5. Ręcznie określać, co już można usunąć.

Naprawdę wykonalne opcje to dwie: nie czyścić lub kombinacja blue-green + ręcznie. W tym drugim przypadku mowa o następującym: gdy zrozumiesz, że nadszedł czas na czyszczenie registry, tworzysz nowy i przez na przykład miesiąc dodajesz do niego wszystkie nowe obrazy. A po miesiącu sprawdzasz, które pod'y w Kubernetes wciąż korzystają ze starego registry i przenosisz je również do nowego registry.

Do czego doszliśmy w werf? Мы собираем:

  1. Git head: wszystkie tagi, wszystkie gałęzie — zakładając, że wszystko, co jest otagowane w Gicie, jest nam potrzebne również w obrazach (a jeśli nie, to trzeba usunąć w samym Gicie);
  2. wszystkie pod’y, które są teraz wyciągane w Kubernetes;
  3. stare ReplicaSet’y (to, co niedawno zostało wyciągnięte), oraz zamierzamy skanować Helm-releasy i wybierać najnowsze obrazy tam.

… i tworzymy z tego zestawu whitelistę — listę obrazów, które nie będziemy usuwać. Wszystko inne usuwamy, po czym znajdujemy osierocone obrazy stage i także je usuwamy.

Etap wdrożenia (deploy)

Niezawodna deklaratywność

Pierwsza kwestia, na którą chciałbym zwrócić uwagę w wdrożeniu, to wprowadzenie zaktualizowanej konfiguracji zasobów, ogłoszonej deklaratywnie. Oryginalny dokument YAML z opisem zasobów Kubernetes zawsze znacząco różni się od wyniku, który rzeczywiście działa w klastrze. Ponieważ Kubernetes dodaje do konfiguracji:

  1. identyfikatory;
  2. informacje służbowe;
  3. wiele wartości domyślnych;
  4. sekcję z bieżącym statusem;
  5. zmiany dokonane w ramach pracy webhooka admission;
  6. wyniki działania różnych kontrolerów (i planisty).

Dlatego, gdy pojawia się nowa konfiguracja zasobu (nowy), nie możemy po prostu zaktualizować jej bieżącej, „żywej” konfiguracji (live). Musimy porównać nowy z wcześniej zastosowaną konfiguracją (last-applied) i nałożyć na live otrzymany patch.

Takie podejście nazywa się 2-way merge. Jest stosowane, na przykład, w Helm.

Jest jeszcze 3-way merge, który różni się tym, że:

  • porównując last-applied i nowy, sprawdzamy, co zostało usunięte;
  • porównując nowy i live, sprawdzamy, co zostało dodane lub zmienione;
  • łącznie nakładamy patch na live.

Wdrożamy ponad 1000 aplikacji z Helm, dlatego faktycznie żyjemy z 2-way merge. Jednak ma on szereg problemów, które rozwiązaliśmy naszymi patchami, pomagającymi Helmowi działać prawidłowo.

Rzeczywisty status wdrożenia

Po tym, jak nasz system CI wygenerował nową konfigurację dla Kubernetes w kolejnej akcji, przekazuje ją do zastosowania (apply) w klastrze — za pomocą Helm lub kubectl apply. Następnie odbywa się opisany już N-way merge, na co API Kubernetes pozytywnie odpowiada systemowi CI, a ten — swojemu użytkownikowi.

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Jednak istnieje ogromny problem: ponieważ pomyślne zastosowanie nie oznacza pomyślnego wdrożenia. Jeśli Kubernetes zrozumiał, jakie zmiany należy zastosować, aplikuje je — ale jeszcze nie wiemy, jaki będzie wynik. Na przykład, aktualizacja i restart pod’ów w frontendzie mogą przebiec pomyślnie, a w backendzie — nie i otrzymamy różne wersje uruchomionych obrazów aplikacji.

Aby wszystko robić poprawnie, w tej schemacie potrzebny jest dodatkowy element — specjalny tracker, który będzie otrzymywał informacje o statusie z API Kubernetes i przekazywał je do dalszej analizy rzeczywistej sytuacji. Stworzyliśmy bibliotekę open source w Go — kubedog (zob. jej zapowiedź tutaj), — która rozwiązuje ten problem i jest zintegrowana z werf.

Zachowanie tego trackera na poziomie werf jest konfigurowane za pomocą adnotacji, które są umieszczane na Deploymentach lub StatefulSets. Główna adnotacja — fail-mode — rozumie następujące wartości:

  • IgnoreAndContinueDeployProcess — ignorujemy problemy związane z wdrażaniem tego komponentu i kontynuujemy deploy;
  • FailWholeDeployProcessImmediately — błąd w tym komponencie zatrzymuje proces wdrażania;
  • HopeUntilEndOfDeployProcess — mamy nadzieję, że ten komponent zadziała do końca wdrożenia.

Na przykład, taka kombinacja zasobów i wartości adnotacji fail-mode:

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Kiedy wdrażamy po raz pierwszy, baza danych (MongoDB) może jeszcze nie być gotowa — Deploymenty się nie powiodą. Ale można poczekać, aż się uruchomi, i wdrożenie dalej przejdzie.

Istnieją jeszcze dwie adnotacje dla kubedog w werf:

  • failures-allowed-per-replica — liczba dozwolonych upadków na każdą replikę;
  • show-logs-until — reguluje moment, do którego werf pokazuje (w stdout) logi ze wszystkich wdrażanych podów. Domyślnie to PodIsReady (aby ignorować komunikaty, które prawdopodobnie nie są nam potrzebne, gdy pod zaczyna otrzymywać ruch), jednak dopuszczalne są również wartości ControllerIsReady i EndOfDeploy.

Czego jeszcze oczekujemy od wdrożenia?

Oprócz już opisanych dwóch punktów chcielibyśmy:

  • widzieć logi — i to tylko te potrzebne, a nie wszystkie;
  • śledzić postęp, ponieważ jeśli zadanie „milczy” przez kilka minut, ważne jest, aby rozumieć, co się dzieje;
  • mieć automatyczny rollback na wypadek, gdyby coś poszło nie tak (a zatem krytycznie wiedzieć, jaki jest rzeczywisty status wdrożenia). Wdrożenie powinno być atomowe: albo przechodzi do końca, albo wszystko wraca do poprzedniego stanu.

Podsumowanie

Jako firma, aby zrealizować wszystkie opisane drobiazgi na różnych etapach dostawy (budowa, publikacja, wdrożenie), wystarcza nam system CI oraz narzędzie werf.

Na zakończenie:

werf — nasze narzędzie do CI/CD w Kubernetes (przegląd i wideo wykładu)

Dzięki werf zrobiliśmy znaczne postępy w rozwiązaniu wielu problemów inżynierów DevOps i z przyjemnością, jeśli szersze społeczność spróbuje tego narzędzia w akcji. Osiągnięcie dobrego wyniku razem będzie łatwiejsze.

Wideo i slajdy

Wideo z wystąpienia (~47 minut):

Odtwarzaj wideo

Prezentacja wystąpienia:

P.S.

Inne artykuły o Kubernetes 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