Jarosław Łukow
CodiLime
Jarosław ŁukowCI Engineer @ CodiLime

Wiele środowisk, jedna konfiguracja

Zobacz, jak odpowiednio współdzielić konfigurację między środowiskami CI i deweloperskimi.
20.05.20196 min
Wiele środowisk, jedna konfiguracja

Ten post jest kontynuacją naszego wystąpienia z konferencji OpenStack Summit w Berlinie, w którym omawialiśmy utrzymywanie zunifikowanych pipeline’ów CI i kompilowania przy wykorzystaniu Zuulu - open source’owego systemu CI. Nagranie wystąpienia jest dostępne tutaj, a na OpenStack Superuser można przeczytać wywiad, w którym rozmawiam o szczegółach.

Doszedłem do wniosku, że dla niektórych może być użyteczne pogłębienie tematu unifikacji środowisk i ukierunkowanego tworzenia oprogramowania. Wyjaśnię również, dlaczego może to być pomocne także w zapewnieniu bezproblemowego przebiegu projektu.  

Najczęstsze przeszkody w usprawnianiu procesów tworzenia oprogramowania

Osoby obeznane z podejściem i praktykami DevOpsowymi zawsze poszukują sposobów na optymalizację i zabezpieczenie swoich procesów deweloperskich. Przez optymalizację rozumiem usprawnienie pracy programistów, a przez zabezpieczenie - upewnienie się, że systemy zabezpieczające repozytoria kodu źródłowego przed zaakceptowaniem nieprawidłowych zmian, działają wystarczająco dobrze, aby nie dopuścić do awarii środowisk produkcyjnych.

Aby to wszystko ułatwić, dobrze jest wykonać poniższe kroki:

  1. Opracować sposób szybkiego konfigurowania środowiska pracy (ang. sandbox) przez deweloperów, testowania wprowadzonych przez nich zmian i wykonywania automatycznych testów;
  2. Jeśli chodzi o optymalizację, lista koniecznych do wykonania kroków powinna być krótka. W ten sposób można szybko zacząć pracować, nawet jeśli trzeba będzie tworzyć środowisko zupełnie od nowa.
  3. Co nie mniej ważne, lepiej jest przechowywać instrukcje w postaci standardowych skryptów, a nie w postaci tekstowej do kopiowania i wklejania. Dzięki temu można używać ich do testowania procesu budowania i korzystać wielokrotnie bez konieczności przepisywania.


Być może zastanawiacie się, dlaczego warto ich ponownie używać. System CI jest czasami traktowany jako osobne miejsce do przechowywania skryptów. Niezależnie od tego, czy jest to Groovy w Jenkins, YAML w Circle CI lub Travis CI, polecenia kompilacji są często kopiowane do definicji zadań i zaczynają żyć własnym życiem.

Co gorsza, źródłami tych poleceń mogą być rozmowy dwóch deweloperów z różnych działów firmy lub jakiś magiczny wycinek kodu przesłany za pośrednictwem czatu. Komunikacja i współpraca są ważnymi częściami kultury DevOps, ale czasami trzeba je sformalizować.

Ponadto podjęte decyzje powinny być udokumentowane dla innych członków zespołu. Może to prowadzić do dwóch niepożądanych sytuacji. Po pierwsze, gdy zmiana w kompilacji wymaga modyfikacji w wielu miejscach, aby zapewnić synchronizację.  Po drugie, gdy jedynym sposobem dla użytkownika lub dewelopera, aby się dowiedzieć, jak oprogramowanie jest zbudowane lub wdrożone, jest inżynieria wsteczna systemu CI. Ma to szczególne znaczenie, gdy nie można uruchomić zadania spoza systemu.

Pierwsza sytuacja będzie powodować duże problemy z utrzymaniem, a druga może łatwo przeszkodzić w odtwarzaniu i debugowaniu problemów, które wystąpiły w systemie CI. Dlatego najlepiej rozwiązać te problemy na jak najwcześniejszym etapie projektu, określić jasno zdefiniowane miejsca lokalizacji dla skryptów i zależności oraz przypisać im odpowiedni zakres odpowiedzialności. Może to być szczególnie użyteczne w przypadku wybrania podejścia „infrastruktura jako kod” (ang. infrastructure-as-code). Poniżej pokażę, jak poradziliśmy sobie z tymi problemami.

Droga do dostarczania oprogramowania wysokiej jakości

W naszym projekcie zastosowaliśmy dwojakie podejście do zachowania spójności środowisk: CI, kompilacji i deweloperskiego. Pierwszym krokiem było ujednolicenie procesów CI i kompilacji. Zuul, system CI zastosowany w projekcie Tungsten Fabric, dzięki naciskowi położonemu na współdzielenie konfiguracji, ułatwił ponowne użycie pipeline’ów i powiązanego z nimi kodu Ansible. Użyliśmy takiej samej konfiguracji systemu Zuul jak w przypadku CI i platformy wydawania oprogramowania. Nasze pipeline’y systemu CI i kompilacji korzystają z dokładnie tych samych zadań i ich definicji, dzięki czemu kroki kompilacji są przechowywane tylko w jednej lokalizacji (ponieważ jest to w ramach systemu CI, w przypadku zmian, testowanie wykonywane jest automatycznie).

Następnym zadaniem było stworzenie środowiska deweloperskiego, możliwie dokładnie odtwarzającego warunki CI. Ponieważ dość trudno jest uruchomić Zuulowe joby poza systemem, musieliśmy znaleźć inny sposób na dostarczenie deweloperom pipeline’u do budowania. (Aby zobaczyć inny dobry przykład tego, jak deweloperzy mogą uruchomić zadania CI bezpośrednio z poziomu swoich maszyn, przeczytajcie artykuł (po angielsku) Circle's awesome local CLI).

Postanowiliśmy zminimalizować ilość kodu związanego z CI w  Zuulowych playbookach i pozostawienie jednolinijkowych wywołań kodu przechowywanych w repozytoriach razem z kodem źródłowym. Interfejs do kompilowania i testowania jest więc teraz wygodnie przechowywany w Makefile'ach, Dockerfile'ach, plikach RPM spec lub podobnych. Zajmują się one konfiguracją środowiska uruchomieniowego, aby umożliwić bezproblemowe uruchamianie bez konieczności ręcznej konfiguracji. Dzięki tej konfiguracji można łatwo umieścić skrypty w obrazie kontenera i opublikować go jako platformę bazową do kompilacji projektu. Powierzchnia kontaktu między kontenerem a kodami źródłowymi jest naprawdę mała, co umożliwia zminimalizowanie kopiowania poleceń, a także eliminuje potrzebę duplikowania na stałe zakodowanych zależności (z wyjątkiem niektórych podstawowych narzędzi, takich jak system kontroli wersji używany do pobierania kolejnych projektów).

Użyty w tym przykładzie Docker i ogólnie rzecz biorąc konteneryzacja, nie są uniwersalnymi rozwiązaniami wszystkich problemów i mają również swoje wady. Jednakże mają jedną podstawową zaletę: umożliwiają tworzenie izolowanych i odtwarzalnych środowisk, co stanowi dużą zaletę przy projektowaniu deweloperskiego przepływu pracy.

Niezależnie od tego, czy zdecydujemy się na traktowanie obrazów kontenera jako ostatecznych artefaktów, korzystanie z czegoś w stylu wzorca kontenera budującego (ang. build container pattern) lub przygotowanie interaktywnego obrazu wraz ze wszystkimi narzędziami deweloperskimi potrzebnymi do pracy w projekcie, kontenery pomogą w utrzymaniu porządku w zależnościach. Zapewnią one również wsparcie dla użytkowników pracujących na różnych platformach, nawet jeśli przygotowane zostanie tylko jedno „oficjalne” środowisko. Osoby używające Linuksa, Windowsa i MacOS mogą uruchamiać te same kontenery i korzystać z automatyzacji ich konfiguracji. Oznacza to mniej pracy dla dewelopera i daje większą wygodę użytkownikom.

Należy tylko pamiętać o tym, aby nie przyjmować żadnych założeń na temat systemu hosta i zachować wszystkie ważne działania wewnątrz kontenerów. Użytkownicy na pewno nie będą szczęśliwi, gdy przed uruchomieniem przepływu pracy kontenera będą musieli wykonać polecenia właściwe dla systemu Ubuntu. Takie rozwiązanie będzie bardziej problematyczne dla użytkowników niekorzystających z tego określonego systemu.

O czym pamiętać, aby stworzyć szybsze przepływy pracy do tworzenia, kompilowania, testowania i wdrażania

Należy zacząć od podstawowych działań DevOpsów. Trzeba zastanowić się, ile instrukcji kompilacji i testowania jest przechowywanych w lokalizacjach poza skryptami, takich jak dokumentacja, wiki i instrukcje „read me”. Instrukcje takie mogą się szybko zdezaktualizować, ponieważ nie można ich automatycznie zweryfikować. Mogą również nie zadziałać po wprowadzeniu zmian w kodach źródłowych. Należy pamiętać o nieuniknionym losie po macoszemu traktowanych kodów źródłowych: nieużywany kod się zepsuje, a nieprzetestowany kod nie będzie działał. Lepiej jest przechowywać wszystkie polecenia w skrypcie z kodem źródłowym i uruchamiać je podczas testowania.

W ten sposób każda zmiana będzie musiała być zgodna z procedurą zdefiniowaną w skrypcie lub będzie modyfikować skrypt, aby mógł on dalej działać po akceptacji modyfikacji. Co nie mniej istotne, skrypty mogą być używane w wielu miejscach, takich jak system CI, środowisko do tworzenia i kompilowania, dzięki czemu nie trzeba będzie myśleć o ich synchronizacji.

Podsumujmy więc nasze rozważania w postaci zestawu użytecznych wskazówek:  

  1. Należy upewnić się, że wszystkie informacje o zależnościach i instrukcje przygotowania środowiska są przechowywane razem z kodem źródłowym.
  2. Należy upewnić się, że zostały one napisane w postaci skryptu i można je przetestować w systemie CI, aby zachować ich aktualność (lepiej używać skryptów w systemach CI i kompilowania, aby mogły działać przez cały czas).
  3. Należy wybrać platformę (dystrybucję/wersję), która będzie wspierana w środowisku deweloperskim. Dodatkowa wskazówka: narzędzia takie jak Docker i Vagrant mogą ułatwić rozszerzenie wsparcia na inne systemy operacyjne.
  4. Należy od czasu do czasu spróbować postawić się w sytuacji dewelopera i przygotować zmianę w utworzonym środowisko deweloperskim. Wszelkie braki lub wady w projektowaniu wyjdą wtedy na jaw, co umożliwi dalsze ulepszenie narzędzia.
<p>Loading...</p>