Skip to content
← Wszystkie wpisy
5 min czytania Dawid Skłodowski

Pipeline'y CI/CD, które nie flakują

Pipeline CI jest użyteczny tylko, jeśli mu ufasz — a flakujący jest gorszy niż żaden, bo uczy zespół ignorować czerwone. Przewodnik po budowie pipeline'ów, które są szybkie, deterministyczne i godne zaufania, aż do deployu.

Pipeline CI/CD ma jedno zadanie: powiedzieć ci, niezawodnie, czy twój kod jest bezpieczny do wysłania. Słowem kluczowym jest niezawodnie. Pipeline, który jest wolny albo — znacznie gorzej — flakujący, zawodzi w tym zadaniu w szczególnie korozyjny sposób: uczy twój zespół mu nie ufać. Gdy „build znów jest czerwony, po prostu odpal ponownie” staje się odruchem, czerwony build nic nie znaczy, a prawdziwa porażka przemyka, bo nikt nie wierzy sygnałowi. Flakujący pipeline jest naprawdę gorszy niż żaden, bo produkuje fałszywą pewność. Oto jak zbudować taki, któremu ludzie faktycznie ufają.

Wróg: flakujące testy

Największym zagrożeniem dla wiarygodności pipeline’u jest flakujący test — taki, który przechodzi i zawodzi na tym samym kodzie, niedeterministycznie. Każdy flakujący test to mała dawka trucizny: uczy wszystkich, że czerwone niekoniecznie znaczy zepsute, a ta lekcja, raz wyuczona, stosuje się też do prawdziwych porażek. Traktuj flakujące testy jako incydenty, nie uciążliwości. Częste przyczyny i ich poprawki:

  • Zależność od kolejności. Test, który przechodzi tylko, bo wcześniejszy zostawił stan, zawodzi, gdy kolejność się zmienia. Uruchamiaj zestaw w losowej kolejności (--order random), by je ujawnić, i niech każdy test sam buduje i sprząta swój świat. (To ta sama dyscyplina, którą wymusza testowanie równoległe.)
  • Czas i daty. Testy zależne od bieżącego czasu albo zakładające, że „teraz” nie przekroczy granicy dnia w trakcie, zawodzą sporadycznie. Zamrażaj czas w testach (timecop/travel_to), zamiast używać prawdziwego zegara.
  • Async i wyścigi. Testy feature asercjujące przed ukończeniem akcji async flakują nieustannie. Czekaj na warunek (matchery Capybary czekają), nigdy nie sleepuj dowolnej ilości i nie miej nadziei.
  • Współdzielony stan zewnętrzny. Dwa testy albo dwaj pracownicy równolegli dotykający tego samego pliku, portu, bazy Redis albo kasety VCR. Nazwij przestrzeniami i izoluj je.

Reguła, której się trzymamy: gdy test flakuje, naprawiasz go albo usuwasz — nie odpalasz ponownie i idziesz dalej. Naprawiony albo poddany kwarantannie flakujący test trzyma zestaw uczciwym; tolerowany go gnije.

Czyń buildy deterministycznymi

Flakowanie to po prostu niedeterminizm przeciekający do twojego buildu, więc szerszą zasadą jest uczynienie całego pipeline’u deterministycznym — te same wejścia zawsze produkują ten sam wynik:

  • Przypnij wszystko. Zablokuj wersje gemów (Gemfile.lock), wersje Node, bazowy obraz Docker, wersje pakietów systemowych. Build, który pobiera „latest” czegokolwiek, to build, który może pęknąć bez zmiany kodu — najbardziej doprowadzający do szału rodzaj czerwonego.
  • Buduj w czystym, odtwarzalnym środowisku. Tu opłaca się praca z Dockerem: uruchamianie CI w tym samym kontenerze, w którym rozwijasz, eliminuje „działa na mojej maszynie, zawodzi w CI” spowodowane dryfem środowiska. Samo środowisko buildu jest przypięte i odtwarzalne.
  • Brak zależności od usług zewnętrznych w testach jednostkowych. Test uderzający w prawdziwe API strony trzeciej flakuje, ilekroć to API jest wolne albo leży. Stubuj wywołania zewnętrzne; testuj integrację osobno i świadomie.

Deterministyczny build oznacza, że czerwony build zawsze jest prawdziwym sygnałem — co jest całym sensem.

Czyń go szybkim, bo wolny też eroduje zaufanie

Poprawny pipeline trwający 40 minut to też porażka, w subtelniejszy sposób: ludzie przestają na niego czekać, mergują przed jego zakończeniem i przełączają kontekst, więc pętla sprzężenia, dla której CI istnieje, jest zepsuta. Szybkość to funkcja:

  • Zrównoleglaj zestaw testów przez rdzenie i węzły CI, balansowany czasem (omówione we wpisie o testowaniu równoległym) — to zwykle największa pojedyncza wygrana.
  • Cache’uj agresywnie. Cache’uj zainstalowane gemy, node_modules i skompilowane assety między przebiegami, by każdy build nie płacił pełnego kosztu konfiguracji. Cache’uj warstwy Docker.
  • Zawodź szybko. Uruchamiaj szybkie, tanie sprawdzenia najpierw (linting, szybkie testy jednostkowe), by oczywista porażka raportowała się w sekundy, a nie po pełnym wolnym zestawie. Uruchamiaj kosztowne testy przeglądarki ostatnie.
  • Buduj tylko to, co się zmieniło, gdzie możesz, w większej bazie kodu.

Celem jest pipeline dość szybki, by ludzie na niego czekali — wtedy CI faktycznie kształtuje zachowanie.

Od CI do CD: bezpieczne wdrażanie

Ciągła integracja mówi ci, że kod jest dobry; ciągłe dostarczanie/wdrażanie doprowadza go na produkcję bezpiecznie i automatycznie. Pipeline rozszerza się poza testy w deploy, a dyscypliny trzymające deploye bezpiecznymi budują na wcześniejszych wpisach:

  • Każdy commit do main jest potencjalnie wysyłalny — co czynią możliwym trunk-based development i feature flagi (omówione wcześniej): niekompletna praca chowa się za flagami, więc main zostaje gotowy do wydania.
  • Automatyzuj deploy, by był powtarzalny i nudny — te same kroki za każdym razem, żaden ręczny rytuał do pomylenia, z właściwościami atomowego release’u i łatwego rollbacku, które opisał wpis o Capistrano.
  • Wdrażaj stopniowo i obserwuj. Wdróż na staging, potem produkcję za flagą albo na ułamek ruchu (canary) i obserwuj metryki, które mają znaczenie (złote sygnały z wpisu o monitoringu). Jeśli wskaźnik błędów albo opóźnienie skoczy, wycofaj — szybko i automatycznie, gdzie możesz.
  • Uczyń rollback trywialnym. Najważniejszą właściwością bezpieczeństwa pipeline’u deployu jest to, że możesz cofnąć zły release w sekundy. Jeśli rollback jest trudny, każdy deploy jest straszny; jeśli to jedna komenda (albo automatyczny przy zawiedzionym health checku), deploye stają się rutyną.

Werdykt

Pipeline CI/CD jest wart dokładnie tyle, ile zaufania zespołu do niego, a zaufanie jest niszczone przez flakowanie i wolność. Więc priorytety, po kolei: poluj na flakujące testy jak na incydenty (napraw albo usuń, nigdy odpal-ponownie-i-wzrusz-ramionami), uczyń cały build deterministycznym (przypnij wszystko, buduj w czystym kontenerze, stubuj usługi zewnętrzne), by czerwone zawsze znaczyło zepsute, i uczyń go szybkim (zrównoleglaj, cache’uj, zawodź szybko), by ludzie faktycznie na niego czekali. Potem rozszerz ten godny zaufania fundament w deploy — zautomatyzowany, stopniowy, obserwowany i natychmiast odwracalny. Zrób to dobrze, a pipeline staje się tym, czym powinien być: szybkim, uczciwym sygnałem, który pozwala ci wysyłać małe zmiany z pewnością wiele razy dziennie. Zrób to źle, a zbudowałeś kosztowny generator liczb losowych, który wszyscy nauczyli się ignorować.