Rozgryźmy Cache Task w Azure Pipelines, czyli mechanizm, dzięki któremu oszczędzisz setki godzin.
Procesy Ciągłej Integracji i Ciągłego Dostarczania, wraz z rozwojem oprogramowania, trwają coraz dłużej. Gdy nieodpowiednio urosną, tracimy nie tylko czas potrzebny na ich wykonanie, ale również odczuwamy to w innych obszarach. Wcześniej przenieśmy się do biura.
W biurze
Piątek, 17:15. Jestem sam. Towarzystwa dotrzymuje mi ekran wraz z mrugającą świetlówką. Mruga, rozprasza i wydaje ten charakterystyczny brzdęk, rozbijając jednostajną symfonię szumu komputerów i szmerów chłodnego powietrza wdmuchiwanego z klimatyzatorów.
Ten chłód. Ta bryza o niepodrabianym, stęchliwym, lekko grzybiczym, zapachu, wdzierająca się przez nozdrza przy każdym wdechu. Ignorowana przez mózg. On już się przyzwyczaił, a może po prostu ignoruje. Jest skupiony na czymś innym. Na tym, aby poprawić już tego buga i pójść spokojnie do domu.
Ostanie stuknięcia w klawiaturę git commit -m "fix bux"
, potem git push
. Chwila magii na czarnej konsoli, a mózg, przełącza się na inny tryb. Wyłapuje bodźce z otoczenia, bo czeka. Czeka. I czeka, bo…
Najwolniejsza część Pipeline
… serwer CI, nie wiadomo dlaczego, po każdym commicie pobiera półtora gigabajta pakietów z internetu. Gdyby nie powolny npm install
to skończyłbym pracę już dawno temu. Jednak takie mamy czasy. Czekam. Na zakończenie następującego pipeline:
pool:
vmImage: 'windows-2019'
steps:
- task: Npm@1
inputs:
command: 'install'
workingDir: 'src/frontend'
Może to koszt szybkiego dewelopmentu, gotowych rozwiązań, automatyzacji? Najgorzej, jeżeli będę musiał puścić build jeszcze raz. Za każdym razem pobiorę wszystkie pakiety znów, a to trwa….
i trwa, i trwa, i jest całkowicie bez sensu, bo przecież te pliki już tam były 5 minut temu. Odnoszę wrażenie, że ten zgrzybiały zapach wydobywa się jednak z serwera CI, a nie z klimy.
A gdyby to naprawić, odświeżyć, przyspieszyć?
Cache Task w Azure Pipelines
Jest na to sposób. Serwery CI radzą sobie z tym na różne sposoby. W rozwiązaniu od Microsoftu istnieje Cache Task w Azure Pipelines. Umożliwia ponowne użycie plików pomiędzy kolejnymi wywołaniami Pipeline. Dzięki czemu możemy uniknąć niepotrzebnego pobierania gigabajtów z internetu i oszczędzić czas na to potrzebny.
Oto jego najprostsza konfiguracja:
pool:
vmImage: 'windows-2019'
steps:
- task: Cache@2
inputs:
key: 'src/frontend/package.json'
path: 'src/frontend/node_modules/'
- task: Npm@1
inputs:
command: 'install'
workingDir: 'src/frontend'
w której:
key: 'src/frontend/package.json'
– jest definicją klucza, na podstawie którego zostanie wyliczony hash. Ten hash będzie użyty jako klucz do zachowania i odtworzenia plików określonych wpath
.
W tym przykładzie jest to zawartość plikupackage.json
. Co za tym idzie, jeżeli zmieni się zawartość tego pliku, cache nie zostanie pobrany.path: 'src/frontend/node_modules/'
– jest to ścieżka do plików, które chcemy użyć ponownie w kolejnych wywołaniach. Zostaną one zapisane na koniec konkretnego pipeline w specjalnym zadaniuPost-job: Cache
, a następnie przywrócone, jeżeli hash wyliczony zkey
będzie taki sam.
Przy takiej konfiguracji pierwsze uruchomienie pipeline, będzie dłuższe, ponieważ musimy pobrać paczki z internetu oraz dodać je do cache. Co widać na screenie.
Dodatkowo pierwsze wywołania również są wolniejsze, ale z czasem powinno się to ustabilizować. Tak to wygląda na moim przykładzie:
package.json w Cache Task to za mało
Prawdopodobnie zwróciłeś uwagę, że użyłem w key
package.json. Nie jest to dobry pomysł, ponieważ może się okazać, że:
- Zmienią się wersje używanych pakietów, a pakietów nie zmieniając
package.json
. - Zmienisz
package.json
,nie ruszając definicji pakietów, np. dodając coś do sekcjiscripts
Lepiej oprzeć się na pliku package-lock.json
, ponieważ na jego podstawie jesteśmy w stanie określić konkretne wersje pakietów do zainstalowania.
Czyli nasza konfiguracja, będzie wyglądać następująco:
pool:
vmImage: 'windows-2019'
steps:
- task: Cache@2
inputs:
key: 'src/frontend/package-lock.json'
path: 'src/frontend/node_modules'
- task: Npm@1
inputs:
command: 'install'
workingDir: 'src/frontend'
Już zyskujesz. Co dalej?
Używając powyższej konfiguracji, oszczędzasz. Czas wykonania spadł z 3:30-5:40 do 2:08-2:42, czyli o jedną/dwie minuty. Wydaje się to niewiele, ale jeżeli jest to uruchamiane setki razy dziennie, to daje olbrzymie liczby.
Procesy CI/CD powinny być rozwijane przyrostowo, wraz z rozwojem oprogramowania. Dzięki czemu ten proces wciąż będzie działał sprawnie i wydajnie.
Pamiętaj, że jest to początkowa konfiguracja i możesz ją znacznie ulepszyć, aby oszczędzać jeszcze więcej czasu i stabilności. Przeczytasz o tym kolejnej części
Zaawansowany Cache Task W Azure Pipelines,
Czyli Ekstremalne Przyspieszenie
Czy już dodałeś Cache Task do Azure Pipelines? A jeżeli nie to dlaczego?
Daj mi o tym znać w komentarzu.
8 Komentarzy
Bartek · 21 sierpnia, 2020 o 17:51
Czy wiesz może czy istnieje odpowiednich Cache Task dla Azure DevOps 2019? Sprawdzałem w Marketplace i jest tylko Hash and Cache.
Jerzy Wickowski · 22 sierpnia, 2020 o 14:22
Cześć @Bartek,
Na stronie https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/cache?view=azure-devops jest napisane, że powinien być już dostępny w Azure DevOps Server 2019. Przy czym on jest wprowadzony dość niedawno, więc może go jeszcze nie być.
Ireneusz Patalas · 21 sierpnia, 2020 o 14:51
Cześć 🙂
Temat jak wiesz u mnie dość na czasie, także dzięki.. wypróbuję. Ale widzę tutaj jeszcze jedno miejsce do poprawy. Od dłuższego czasu do instalacji npm’ów w CI powinno się używać komendy `npm ci`, a nie `npm install`.
Ta pierwsza omija (całkiem sporą) część logiki zwykłego installa, ale jak masz package-lock.json to i tak ściągamy konkretne wersje bibliotek. Polecam, u nas czas instalacji spadł prawie o połowę (!).
Instalacja npm w CI trwa ~30 sekund, więc już nie wiele możemy zyskać na cachu.
Za to dużym problemem jest NuGet restore. Trwa to u nas 2-3 minuty. Niestety z tego co się dowiedziałem domyślne cachowanie nie działa na Hosted agentach, a takimi dysponujemy.
Tutaj już Cache task mógłby pomóc, ale… jaki ustawić klucz?
Mamy projekt .NET Core 2.2 i tam informacje o zależnościach są w plikach .csproj od każdego projektu. Nie ma już packages.config jak kiedyś. Masz pomysł jak to ugryźć? 🙂
Jerzy Wickowski · 22 sierpnia, 2020 o 14:17
Cześć @Ireneusz,
Używając
npm ci
nie zalecane jest keszowanie node_modules. Możesz to przeczytać w dokumentacji od MS, ale jeszcze przyjrzę się tematowi i dam znać.Odnośnie NuGeta to możesz ustawiść klucz z każdym csprojem, czyli np.
src/backend/project1.csproj | src/backend/project2.csproj
, ale możesz to zrobić w bardziej cywilizowany sposób, czylisrc/backend/**/*.csproj
. Oczywiście pozostaje jeszcze opcja, czy to wszystko, czy może coś będzie trzeba dodać.Więcej info na stronie MS: https://docs.microsoft.com/en-us/azure/devops/pipelines/release/caching?view=azure-devops
Piotr · 21 sierpnia, 2020 o 08:18
Tutaj może link do dokumentacji Microsoftu
https://docs.microsoft.com/en-us/azure/devops/pipelines/release/caching?view=azure-devops
Piotr · 21 sierpnia, 2020 o 08:13
Dzięki za dzielenie się doświadczeniem, Jerzy.
Rozumiem, że to też może odnieść się do paczek NuGet. Zgadza się? Czy spotkałeś się z jakimiś problemami w związku z cachowaniem?
Jerzy Wickowski · 22 sierpnia, 2020 o 14:23
Cześć @Piotr,
Tak. Zastosujesz to również dla NuGeta, ale musisz to dostosować.
Problem? Owszem, ale wynikał z mojego błędu. Źle skonfigurowałem odświeżanie cache i pobierało się nie to, co powinno. Opiszę to w kolejnym artykule.
Jak NIE zarządzać konfiguracją. Chyba że lubisz kłopoty! · 30 września, 2020 o 12:11
[…] A jak już jesteśmy przy marnowaniu czasu. Sprawdź jak go oszczędzić wprowadzając Cache Task do Azure Pipelines. […]
Możliwość dodawania komentarzy nie jest dostępna.