Problem z "inteligentnym" czyszczeniem obrazów kontenerów oraz jego rozwiązanie w werf

Problem z "inteligentnym" czyszczeniem obrazów kontenerów oraz jego rozwiązanie w werf

Artykuł omawia problematykę usuwania obrazów, które gromadzą się w rejestrach kontenerów (Docker Registry i jego odpowiednikach) w kontekście nowoczesnych pipeline'ów CI/CD dla aplikacji cloud native dostarczanych do Kubernetes. Przedstawiono główne kryteria aktualności obrazów oraz związane z nimi trudności przy automatyzacji czyszczenia, oszczędzania miejsca i zaspokajania potrzeb zespołów. Wreszcie, na przykładzie konkretnego projektu Open Source przedstawimy, jak można przezwyciężyć te trudności.

Wprowadzenie

Liczba obrazów w rejestrze kontenerów może szybko rosnąć, zajmując coraz więcej miejsca w magazynie, a tym samym znacznie zwiększając jego koszt. Aby kontrolować, ograniczać lub utrzymywać akceptowalny wzrost zajmowanego miejsca w rejestrze, przyjęto:

  1. używać stałej liczby tagów dla obrazów;
  2. w jakiś sposób czyścić obrazy.


Pierwsze ograniczenie jest czasami dopuszczalne dla małych zespołów. Jeśli programiści wystarczająco często korzystają z stałych tagów (latest, main, test, boris itp.), rejestr nie będzie się powiększał w rozmiarze i przez długi czas można w ogóle nie myśleć o czyszczeniu. Wszystkie przestarzałe obrazy są nadpisywane, a do czyszczenia po prostu nie zostaje żadna praca (wszystko robi standardowy zbieracz śmieci).

Jednakże takie podejście znacznie ogranicza rozwój i rzadko znajduje zastosowanie w CI/CD nowoczesnych projektów. Niezbędnym elementem rozwoju stała się automatyzacja, która pozwala znacznie szybciej testować, wdrażać i dostarczać nowe funkcje użytkownikom. Na przykład, w naszych wszystkich projektach przy każdym commitcie automatycznie tworzony jest pipeline CI. W nim zbierany jest obraz, testowany, wdrażany do różnych konturów Kubernetes do debugowania i pozostałych kontroli, a jeśli wszystko dobrze — zmiany trafiają do końcowego użytkownika. I to już dawno przestało być rocket science, a stało się codziennością dla wielu — prawdopodobnie i dla Ciebie, skoro czytasz ten artykuł.

Ponieważ eliminacja błędów i rozwój nowych funkcji odbywa się równolegle, a wydania mogą mieć miejsce kilka razy dziennie, oczywiste jest, że proces rozwoju towarzyszy znaczna liczba commitów, a więc — duża liczba obrazów w rejestrze. W rezultacie, pilnie rodzi się pytanie o organizację efektywnego czyszczenia rejestru, tzn. usuwania przestarzałych obrazów.

Jak w ogóle określić, czy obraz jest aktualny?

Kryteria aktualności obrazu

W przeważającej większości przypadków główne kryteria będą następujące:

1. Pierwsze (najsłynniejsze i najbardziej krytyczne z wszystkich) — to obrazy, które są obecnie używane w Kubernetes. Usunięcie tych obrazów może prowadzić do poważnych kosztów związanych z przestojami produkcji (na przykład obrazy mogą być potrzebne podczas replikacji) lub unieważnić wysiłki zespołu zajmującego się debugowaniem w jednym z obszarów. (Z tego powodu stworzyliśmy nawet specjalny eksporter Prometheus, który monitoruje brak takich obrazów w dowolnym klastrze Kubernetes.)

2. Drugie (mniej oczywiste, ale również bardzo ważne i ponownie związane z eksploatacją) — to obrazy, które są potrzebne do rollbacku w przypadku wykrycia poważnych problemów w aktualnej wersji. Na przykład w przypadku Helm to obrazy, które są używane w zapisanych wersjach release'u. (Swoją drogą, domyślnie w Helm jest limit 256 rewizji, ale czy ktokolwiek naprawdę potrzebuje zachować tak dużą liczbę wersji?..) W końcu przechowujemy wersje, aby można je było później wykorzystać, to znaczy 'wracać' do nich w razie potrzeby.

3. Trzecie — potrzeby deweloperów: wszystkie obrazy, które są związane z ich aktualnymi pracami. Na przykład jeśli rozważamy PR, warto pozostawić obraz, który odpowiada ostatniemu commitowi i powiedzmy poprzedniemu commitowi: dzięki temu deweloper będzie mógł szybko powrócić do dowolnego zadania i pracować z najnowszymi zmianami.

4. Czwarte — obrazy, które odpowiadają wersjom naszej aplikacji, to znaczy są końcowym produktem: v1.0.0, 20.04.01, sierra itd.

NB: Kryteria określone tutaj zostały sformułowane na podstawie doświadczeń z pracy z dziesiątkami zespołów deweloperskich z różnych firm. Jednak oczywiście w zależności od charakterystyki procesów deweloperskich i używanej infrastruktury (na przykład, jeśli Kubernetes nie jest używany), kryteria te mogą się różnić.

Zgodność z kryteriami i istniejące rozwiązania

Popularne usługi z rejestrem kontenerów zazwyczaj oferują własne zasady usuwania obrazów: można w nich określać warunki, na jakich tag jest usuwany z rejestru. Możliwości tych warunków ograniczają się jednak do takich parametrów, jak nazwy, czas utworzenia i liczba tagów*.

* Zależy od konkretnych implementacji rejestru kontenerów. Analizowaliśmy możliwości następujących rozwiązań: Azure CR, Docker Hub, ECR, GCR, GitHub Packages, GitLab Container Registry, Harbor Registry, JFrog Artifactory, Quay.io — na stan z września 2020 roku.

Taki zestaw parametrów jest w zupełności wystarczający, aby spełnić czwarty kryterium — czyli do selekcji obrazów odpowiadających wersjom. Jednak dla wszystkich pozostałych kryteriów trzeba wybierać jakieś kompromisowe rozwiązanie (bardziej rygorystyczną lub, przeciwnie, łagodniejszą politykę) — w zależności od oczekiwań i możliwości finansowych.

Na przykład trzecie kryterium — związane z potrzebami deweloperów — można rozwiązać poprzez organizację procesów wewnątrz zespołów: specyficzne nazewnictwo obrazów, prowadzenie specjalnych list dozwolonych oraz wewnętrzne ustalenia. Ale ostatecznie to i tak trzeba zautomatyzować. A jeśli możliwości gotowych rozwiązań są niewystarczające, trzeba stworzyć coś własnego.

Podobna sytuacja dotyczy dwóch pierwszych kryteriów: nie można ich zaspokoić bez uzyskania danych z zewnętrznego systemu — tego samego, w którym odbywa się wdrażanie aplikacji (w naszym przypadku jest to Kubernetes).

Ilustracja workflow w Git

Zakładając, że pracujesz mniej więcej według takiego schematu w Git:

Problem z "inteligentnym" czyszczeniem obrazów kontenerów oraz jego rozwiązanie w werf

Ikona z głową na schemacie oznacza obrazy kontenerów, które w danym momencie są wdrożone w Kubernetes dla jakichkolwiek użytkowników (użytkowników końcowych, testerów, menedżerów itd.) lub używane przez deweloperów do debugowania i podobnych celów.

Co się stanie, jeśli zasady usuwania pozwalają na pozostawienie (nieusunięcie) obrazów tylko po określonych nazwach tagów?

Problem z "inteligentnym" czyszczeniem obrazów kontenerów oraz jego rozwiązanie w werf

Oczywiście, taki scenariusz nikogo nie ucieszy.

Co się zmieni, jeśli zasady pozwalają na nieusuwanie obrazów po określonym przedziale czasowym / liczbie ostatnich commitów?

Problem z "inteligentnym" czyszczeniem obrazów kontenerów oraz jego rozwiązanie w werf

Wynik stał się znacznie lepszy, jednak wciąż jest daleki od ideału. Przecież mamy wciąż deweloperów, którzy potrzebują obrazów w rejestrze (lub nawet wdrożonych w K8s) do debugowania błędów…

Podsumowując sytuację na rynku: dostępne w rejestrach kontenerów funkcje nie oferują wystarczającej elastyczności przy czyszczeniu, a głównym powodem jest brak możliwości interakcji ze światem zewnętrznym. Okazuje się, że zespoły, które potrzebują takiej elastyczności, są zmuszone do samodzielnego realizowania usuwania obrazów „na zewnątrz”, korzystając z Docker Registry API (lub natywnego API odpowiedniej implementacji).

Szukaliśmy jednak uniwersalnego rozwiązania, które automatyzowałoby czyszczenie obrazów dla różnych zespołów korzystających z różnych rejestrów…

Nasza droga do uniwersalnego czyszczenia obrazów

Skąd taka potrzeba? Sprawa polega na tym, że nie jesteśmy oddzielną grupą programistów, ale zespołem, który obsługuje wiele takich grup, pomagając kompleksowo rozwiązywać problemy CI/CD. A głównym narzędziem technicznym w tym celu jest Open Source-owa aplikacja werf. Jej cechą charakterystyczną jest to, że nie wykonuje jednej funkcji, ale towarzyszy procesom ciągłej dostawy na wszystkich etapach: od budowy po wdrożenie.

Publikacja w rejestrze* obrazów (zaraz po ich zbudowaniu) — oczywista funkcja takiej aplikacji. A ponieważ obrazy są tam przechowywane, to — jeżeli twoje miejsce przechowywania nie jest nieograniczone — musisz również odpowiadać za ich późniejsze czyszczenie. O tym, jak osiągnęliśmy sukces w tej kwestii, spełniając wszystkie zadane kryteria, będzie mowa dalej.

* Chociaż same rejestry mogą być różne (Docker Registry, GitLab Container Registry, Harbor itp.), ich użytkownicy napotykają te same problemy. Uniwersalne rozwiązanie w naszym przypadku nie zależy od implementacji rejestru, ponieważ wykonuje się poza samymi rejestrami i oferuje jednolite zachowanie dla wszystkich.

Mimo że używamy werf jako przykład implementacji, mamy nadzieję, że zastosowane podejścia będą przydatne również innym zespołom, które napotkały podobne trudności.

Zajęliśmy się zewnętrzną implementacją mechanizmu do czyszczenia obrazów — zamiast tych możliwości, które są już wbudowane w rejestry dla kontenerów. Pierwszym krokiem było użycie Docker Registry API do stworzenia tych samych prostych polityk dotyczących liczby tagów i czasu ich stworzenia (wspomnianych wcześniej). Do nich dodano listę dozwoloną na podstawie obrazów używanych w wdrożonej infrastrukturze, czyli Kubernetes. Dla tego ostatniego wystarczyło, aby przeszukać wszystkie wdrożone zasoby za pomocą Kubernetes API i uzyskać listę wartości. image.

To trywialne rozwiązanie zamknęło najważniejszy problem (kryterium nr 1), ale było tylko początkiem naszej drogi do poprawy mechanizmu oczyszczania. Kolejnym — i znacznie ciekawszym — krokiem było rozwiązanie połączenia publikowanych obrazów z historią Git..

Schematy tagowania

Na początek wybraliśmy podejście, w którym końcowy obraz musi przechowywać niezbędne informacje do oczyszczania, i opracowaliśmy proces oparty na schematach tagowania. Podczas publikacji obrazu użytkownik wybierał określoną opcję tagowania (git-branch, git-commit lub git-tag) i używał odpowiedniej wartości. W systemach CI te wartości były ustawiane automatycznie na podstawie zmiennych środowiskowych. W istocie końcowy obraz był powiązany z określonym Gitem-prymitywem, przechowując niezbędne dane do oczyszczania w etykietach.

W ramach tego podejścia powstał zbiór polityk, które pozwalały używać Git jako jedynego źródła prawdy:

  • Przy usuwaniu gałęzi/tagu w Git automatycznie usuwane były również powiązane obrazy w rejestrze.
  • Liczbę obrazów powiązanych z tagami i commitami Git można było regulować liczbą tagów użytych w wybranym schemacie i czasem utworzenia powiązanego commita.

Ogólnie rzecz biorąc, zrealizowane rozwiązanie spełniało nasze potrzeby, ale wkrótce czekało nas nowe wyzwanie. Otóż w czasie korzystania ze schematów tagowania według prymitywów Git napotkaliśmy szereg wad. (Ponieważ ich opis wykracza poza temat tego artykułu, wszyscy chętni mogą zapoznać się ze szczegółami tutaj.) Dlatego, decydując się na przejście do bardziej efektywnego podejścia do tagowania (tagowanie oparte na treści), musieliśmy przeanalizować również wdrożenie oczyszczania obrazów.

Nowy algorytm

Dlaczego? Przy tagowaniu na podstawie treści każdy tag może odpowiadać wielu commitom w Git. Przy oczyszczaniu obrazów nie można już wyjść tylko z commita, na którym nowy tag został dodany do rejestru.

Dla nowego algorytmu oczyszczania zdecydowano się odejść od schematów tagowania i zbudować proces na meta-obrazach, z których każdy przechowuje powiązanie z:

  • komita, na którym przeprowadzono publikację (przy czym nie ma znaczenia, czy dodano, zmieniono, czy pozostawiono dotychczasowy obraz w rejestrze kontenerów);
  • i naszego wewnętrznego identyfikatora, odpowiadającego zbudowanemu obrazowi.

Innymi słowy, zapewniono powiązanie publikowanych tagów z komitami w Git.

Finalna konfiguracja i ogólny algorytm

Użytkownikom przy konfiguracji czyszczenia udostępnione zostały polityki, według których wybierane są aktualne obrazy. Każda taka polityka definiowana jest przez:

  • wiele referencji, tj. Git-tagów lub gałęzi Git, które są używane podczas skanowania;
  • oraz limitem wyszukiwanych obrazów dla każdej referencji z tego zestawu.

Dla ilustracji — oto jak wyglądała konfiguracja polityk domyślnych:

cleanup:
  keepPolicies:
  - references:
      tag: \/.*\/ 
      limit:
        last: 10
  - references:
      branch: \/.*\/ 
      limit:
        last: 10
        in: 168h
        operator: And
    imagesPerReference:
      last: 2
      in: 168h
      operator: And
  - references:  
      branch: \/^(main|staging|production)$\/ 
    imagesPerReference:
      last: 10

Taka konfiguracja zawiera trzy polityki, które odpowiadają następującym zasadom:

  1. Zachowywać obraz dla 10 ostatnich Git-tagów (według daty utworzenia tagu).
  2. Zachowywać nie więcej niż 2 obrazy opublikowane w ciągu ostatniego tygodnia, dla nie więcej niż 10 gałęzi z aktywnością w ciągu ostatniego tygodnia.
  3. Zachowywać po 10 obrazów dla gałęzi main, staging i production.

Ostateczny algorytm sprowadza się do następujących kroków:

  • Pobranie manifeście z rejestru kontenerów.
  • Wykluczenie obrazów używanych w Kubernetes, ponieważ zostały już uprzednio przefiltrowane, pytając K8s API.
  • Skanowanie historii Git i wykluczenie obrazów według zadanych polityk.
  • Usunięcie pozostałych obrazów.

Wracając do naszej ilustracji, oto co otrzymujemy z werf:

Problem z "inteligentnym" czyszczeniem obrazów kontenerów oraz jego rozwiązanie w werf

Jednakże, nawet jeśli nie używasz werf, podobne podejście do zaawansowanego czyszczenia obrazów — w takiej lub innej formie (zgodnie z preferowanym podejściem do tagowania obrazów) — może być stosowane także w innych systemach/narzędziach. Wystarczy pamiętać o problemach, które się pojawiają, i znaleźć te możliwości w twoim stosie, które pozwalają na jak najszybsze wbudowanie ich rozwiązania. Mamy nadzieję, że droga, którą przeszliśmy, pomoże spojrzeć na twój przypadek z nowymi szczegółami i pomysłami.

Podsumowanie

  • Prędzej czy później z problemem przepełnienia rejestru boryka się większość zespołów.
  • Podczas poszukiwania rozwiązań najpierw należy określić kryteria aktualności obrazu.
  • Narzędzia oferowane przez popularne serwisy rejestru kontenerów umożliwiają bardzo prostą procedurę czyszczenia, która nie uwzględnia „zewnętrznego świata”: obrazy używane w Kubernetes oraz cechy procesów roboczych w zespole.
  • Elastyczny i efektywny algorytm musi mieć pojęcie o procesach CI/CD i operować nie tylko na danych obrazów Docker.

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