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_modulesi 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ć.