
Na RIT 2019 nasz kolega Aleksandr Korotkow wygłosił wystąpienie na temat automatyzacji rozwoju w CIAN: aby uprościć życie i pracę, używamy naszej własnej platformy Integro. Śledzi ona cykl życia zadań, odciąża programistów od rutynowych operacji i znacznie redukuje liczbę błędów w produkcji. W tym wpisie uzupełnimy wystąpienie Aleksandra i opowiemy, jak przeszliśmy od prostych skryptów do integracji produktów open source przez naszą własną platformę oraz czym zajmuje się nasz zespół ds. automatyzacji.
Poziom zerowy
„Poziomu zerowego nie ma, nie znam czegoś takiego”
Mistrz Shifu z filmu „Kung Fu Panda”
Automatyzacja w CIAN zaczęła się 14 lat po założeniu firmy. Wtedy w zespole programistycznym było 35 osób. Trudno w to uwierzyć, prawda? Oczywiście, w jakiejś formie automatyzacja faktycznie istniała, ale osobny kierunek dotyczący ciągłej integracji i dostarczania kodu zaczął się kształtować dopiero w 2015 roku.
W tamtym czasie mieliśmy ogromny monolit z Pythonem, C# i PHP, uruchomiony na serwerach Linux/Windows. Do wdrożenia tego potwora mieliśmy zestaw skryptów, które uruchamialiśmy ręcznie. Było też budowanie monolitu, które przynosiło ból i cierpienie z powodu konfliktów przy łączeniu gałęzi, poprawkach błędów i ponownym budowaniu „z innym zestawem zadań w budowaniu”. Uproszczony proces wyglądał tak:

Nie satysfakcjonowało nas to, chcieliśmy zbudować powtarzalny, zautomatyzowany i zarządzany proces budowy i wdrożenia. Do tego potrzebny był nam system CI/CD, a my wybieraliśmy między bezpłatną wersją Teamcity a bezpłatnym Jenkins, ponieważ pracowaliśmy z nimi i obie odpowiadały nam funkcjonalnościami. Wybór padł na Teamcity jako na nowszy produkt. Wtedy jeszcze nie używaliśmy architektury mikroserwisów i nie przewidywaliśmy dużej liczby zadań i projektów.
Przechodzimy do pomysłu stworzenia własnego systemu
Wdrożenie Teamcity zredukowało tylko część pracy ręcznej: pozostało jeszcze tworzenie Pull Requestów, przesuwanie zadań według statusów w Jira i wybór zadań do wydania. Z tym system Teamcity już sobie nie radził. Należało wybrać kierunek dalszej automatyzacji. Rozważaliśmy opcje pracy ze skryptami w Teamcity lub przejście na zewnętrzne systemy automatyzacji. Ostatecznie zdecydowaliśmy, że potrzebna jest maksymalna elastyczność, którą daje tylko własne rozwiązanie. Tak powstała pierwsza wersja wewnętrznego systemu automatyzacji o nazwie Integro.
Teamcity zajmuje się automatyzacją na poziomie uruchamiania procesów budowy i wdrożenia, podczas gdy Integro skupiło się na wyższej automatyzacji procesów rozwoju. Należało połączyć pracę z zadaniami w Jira z przetwarzaniem powiązanego kodu źródłowego w Bitbucket. Na tym etapie w Integro zaczęły pojawiać się własne workflow do pracy z różnymi typami zadań.
W wyniku zwiększonej automatyzacji w procesach biznesowych wzrosła liczba projektów i runów w Teamcity. Tak pojawił się nowy problem: jeden darmowy instans Teamcity przestał wystarczać (3 agenty i 100 projektów), dodaliśmy kolejny instans (kolejne 3 agenty i 100 projektów), potem jeszcze jeden. W rezultacie otrzymaliśmy system składający się z kilku klastrów, który był trudny w zarządzaniu:

Kiedy stanął przed nami problem czwartego instansu, zrozumieliśmy, że nie możemy dalej tak funkcjonować, ponieważ skumulowane koszty wsparcia czterech instansów wymknęły się spod kontroli. Pojawiła się kwestia zakupu płatnej wersji Teamcity lub wyboru darmowego Jenkins. Przeprowadziliśmy obliczenia dotyczące instansów i planów automatyzacji oraz zdecydowaliśmy, że będziemy korzystać z Jenkins. Po kilku tygodniach przeszliśmy na Jenkins i pozbyliśmy się części bólu głowy związanego z utrzymywaniem kilku instansów Teamcity. Dzięki temu mogliśmy skupić się na rozwijaniu Integro i dostosowywaniu Jenkins do własnych potrzeb.
Wraz z rozwojem podstawowej automatyzacji (w postaci automatycznego tworzenia Pull Requestów, zbierania i publikowania pokrycia kodu oraz innych kontrol) pojawiła się silna chęć maksymalnego ograniczenia ręcznych wydań i oddania tej pracy robotom. Ponadto w firmie rozpoczęła się migracja na mikroserwisy, które wymagały częstych wydań, przy czym każde z nich oddzielnie. Stopniowo przeszliśmy do automatycznych wydań naszych mikroserwisów (monolit na razie wydajemy ręcznie z powodu złożoności procesu). Ale, jak to zwykle bywa, pojawił się nowy problem.
Automatyzujemy testowanie

Dzięki automatyzacji wydań procesy programowania przyspieszyły, częściowo kosztem pomijania niektórych etapów testowania. To z kolei doprowadziło do czasowej utraty jakości. Brzmi banalnie, ale wraz z przyspieszeniem wydania należało zmienić także metodologię rozwoju produktu. Trzeba było pomyśleć o automatyzacji testowania, wdrożeniu osobistej odpowiedzialności (tu mówimy o „przyjęciu idei w głowie”, a nie o karach finansowych) programisty za wydany kod i błędy w nim, a także o decyzji dotyczącej wydania / niewydania zadania poprzez automatyczny deployment.
Eliminując problemy z jakością, doszliśmy do dwóch ważnych rozwiązań: zaczęliśmy przeprowadzać testy kaskadowe i wdrożyliśmy automatyczne monitorowanie błędów w tle z automatyczną reakcją na jego przekroczenie. Pierwsze rozwiązanie pozwoliło znaleźć oczywiste błędy zanim kod trafił do produkcji, drugie zmniejszyło czas reakcji na problemy w produkcji. Błędy się zdarzają, ale większą część czasu i wysiłków spędzamy nie na ich naprawie, ale na minimalizacji.
Zespół automatyzacji
Aktualnie mamy zespół składający się z 130 programistów, i wciąż . Zespół zajmujący się ciągłą integracją i dostarczaniem kodu (dalej — zespół Deploy and Integration lub DI) składa się z 7 osób i pracuje w 2 obszarach: rozwój platformy automatyzacji Integro oraz DevOps.
Zespół DevOps odpowiada za środowiska Dev/Beta strony CIAN, środowiska Integro, wspiera programistów w rozwiązywaniu problemów oraz opracowuje nowe podejścia do skalowania środowisk. Kierunek rozwoju Integro zajmuje się zarówno samą Integro, jak i usługami pokrewnymi, na przykład wtyczkami do Jenkins, Jira, Confluence, a także opracowuje pomocnicze narzędzia i aplikacje dla zespołów deweloperskich.
Zespół DI współpracuje z zespołem Platformy, który zajmuje się projektowaniem architektury, bibliotek oraz podejściem do rozwoju wewnątrz firmy. Dodatkowo każdy programista w CIAN może przyczynić się do automatyzacji, na przykład wprowadzić mikroautomatyzację pod potrzeby zespołu lub podzielić się świetnym pomysłem, jak jeszcze poprawić automatyzację.
Warstwowe podejście do automatyzacji w CIAN

Wszystkie systemy zaangażowane w automatyzację można podzielić na kilka warstw:
- Systemy zewnętrzne (Jira, Bitbucket itp.). Z nimi pracują zespoły deweloperskie.
- Platforma Integro. Najczęściej programiści nie pracują z nią bezpośrednio, ale to ona wspiera działanie całej automatyzacji.
- Usługi dostarczania, orkiestracji i odkrywania (na przykład Jenkins, Consul, Nomad). Dzięki nim wdrażamy kod na serwerach i zapewniamy współpracę usług.
- Poziom fizyczny (serwery, systemy operacyjne, oprogramowanie pokrewne). Na tym poziomie działa nasz kod. Może to być zarówno serwer fizyczny, jak i wirtualny (LXC, KVM, Docker).
Na podstawie tej koncepcji dzielimy obszary odpowiedzialności wewnątrz zespołu DI. Dwa pierwsze poziomy leżą w zakresie odpowiedzialności kierunku rozwoju Integro, a dwa ostatnie poziomy już w zakresie odpowiedzialności DevOps. Takie podział pozwala skupić się na zadaniach i nie przeszkadza w interakcji, ponieważ znajdujemy się blisko siebie i nieustannie wymieniamy się wiedzą i doświadczeniem.
Integro
Skupimy się na Integro i zaczniemy od stosu technologicznego:
- CentOS 7
- Docker + Nomad + Consul + Vault
- Java 11 (stary monolit Integro pozostanie na Java 8)
- Spring Boot 2.X + Spring Cloud Config
- PostgreSQL 11
- RabbitMQ
- Apache Ignite
- Camunda (embedded)
- Grafana + Graphite + Prometheus + Jaeger + ELK
- Interfejs webowy: React (CSR) + MobX
- SSO: Keycloak
Trzymamy się zasady rozwijania mikrousług, choć mamy też spuściznę w postaci monolitu wczesnej wersji Integro. Każda mikrousługa działa w swoim własnym kontenerze docker, a usługi komunikują się ze sobą za pomocą zapytań HTTP i wiadomości RabbitMQ. Mikrousługi odnajdują się nawzajem przez Consul i wykonują do niego zapytania, przechodząc autoryzację przez SSO (Keycloak, OAuth 2/OpenID Connect).

Jako realistyczny przykład rozważmy interakcję z Jenkins, która składa się z następujących kroków:
- Mikrousługa zarządzania workflow (dalej mikrousługa Flow) chce uruchomić budowę w Jenkins. W tym celu za pośrednictwem Consul odnajduje IP:PORT mikrousługi integracji z Jenkins (dalej mikrousługa Jenkins) i wysyła do niej asynchroniczne zapytanie o uruchomienie budowy w Jenkins.
- Mikrousługa Jenkins po otrzymaniu zapytania formułuje i zwraca w odpowiedzi identyfikator zadania (Job ID), dzięki któremu będzie można później zidentyfikować wynik pracy. Jednocześnie uruchamia budowę w Jenkins poprzez wywołanie REST API.
- Jenkins przeprowadza budowę, a po jej zakończeniu wysyła webhook z wynikami wykonania do mikrousługi Jenkins.
- Mikrousługa Jenkins, po otrzymaniu webhooka, formułuje komunikat o zakończeniu przetwarzania zapytania i dołącza do niego wyniki wykonania. Sformułowany komunikat jest wysyłany do kolejki RabbitMQ.
- Poprzez RabbitMQ opublikowany komunikat trafia do mikrousługi Flow, która dowiaduje się o wyniku przetwarzania swojego zadania, porównując identyfikator zadania (Job ID) z zapytania i otrzymanego komunikatu.
Obecnie mamy około 30 mikrousług, które można podzielić na kilka grup:
- Zarządzanie konfiguracjami.
- Informowanie i interakcja z użytkownikami (messengery, e-mail).
- Praca z kodem źródłowym.
- Integracja z narzędziami do wdrażania (jenkins, nomad, consul itp.).
- Monitorowanie (wydania, błędy itp.).
- Webowe narzędzia (interfejs do zarządzania środowiskami testowymi, zbierania statystyk itp.).
- Integracja z trackerami zadań i podobnymi systemami.
- Zarządzanie workflow dla różnych zadań.
Workflow zadania
Integro automatyzuje działania związane z cyklem życia zadania. W uproszczeniu przez cykl życia zadania rozumiemy workflow zadania w Jira. W naszych procesach rozwoju istnieje kilka wariacji workflow w zależności od projektu, typu zadania i opcji wybranych w konkretnej sprawie.
Rozważmy workflow, z którego korzystamy najczęściej:

Na schemacie kółko zębate wskazuje, że przejście jest wywoływane automatycznie przez Integro, podczas gdy postać człowieka oznacza, że przejście jest wywoływane ręcznie przez człowieka. Przyjrzyjmy się kilku ścieżkom, którymi zadanie może przejść w tym workflow.
Całkowicie ręczne testowanie na DEV+BETA bez testów kanaryjskich (zazwyczaj w ten sposób wydajemy monolit):

Mogą występować także inne kombinacje przejść. Czasami ścieżkę, którą podąży zadanie, można wybrać za pomocą opcji w Jira.
Ruch zadania
Przyjrzyjmy się podstawowym krokom, które są podejmowane podczas ruchu zadania w workflow „Testowanie na DEV + testy kanaryjskie”:
1. Programista lub PM tworzy zadanie.
2. Programista podejmuje się zadania. Po zakończeniu przenosi je w status IN REVIEW.
3. Jira wysyła webhook do mikroserwisu Jira (odpowiada za integrację z Jira).
4. Mikroserwis Jira wysyła zapytanie do serwisu Flow (odpowiada za wewnętrzne workflow, w których wykonywana jest praca) w celu uruchomienia workflow.
5. Wewnątrz serwisu Flow:
- Przypisywani są recenzenci do zadania (mikroserwis Users, który zna wszystkich użytkowników + mikroserwis Jira).
- Za pośrednictwem mikroserwisu Source (znającego repozytoria i gałęzie, ale nie pracującego z samym kodem) przeprowadzany jest przeszukiwanie repozytoriów, w których znajduje się gałąź naszego zadania (dla uproszczenia wyszukiwania nazwa gałęzi pokrywa się z numerem zadania w Jira). Najczęściej zadanie ma tylko jedną gałąź w jednym repozytorium, co upraszcza zarządzanie kolejką na wdrożenie i zmniejsza powiązania między repozytoriami.
- Dla każdej znalezionej gałęzi wykonywana jest następująca sekwencja działań:
i) Podlewanie gałęzi master (mikroserwis Git do pracy z kodem).
ii) Gałąź jest blokowana przed zmianami przez programistę (mikroserwis Bitbucket).
iii) Tworzony jest Pull Request dla tej gałęzi (mikroserwis Bitbucket).
iv) Wysyłana jest wiadomość o nowym Pull Requeście do czatów programistów (mikroserwis Notify do pracy z powiadomieniami).
v) Rozpoczynają się budowanie, testowanie i wdrożenie zadania na DEV (mikroserwis Jenkins do pracy z Jenkins).
vi) Jeśli wszystkie poprzednie punkty zakończyły się sukcesem, to Integro wydaje swoje zatwierdzenie w Pull Requeście (mikroserwis Bitbucket). - Integro oczekuje na zatwierdzenie w Pull Requeście od przypisanych recenzentów.
- Gdy otrzymane zostaną wszystkie niezbędne zatwierdzenia (w tym pozytywne wyniki testów automatycznych), Integro przenosi zadanie w status Test on Dev (mikroserwis Jira).
6. Testerzy przeprowadzają testy zadania. Jeśli nie ma problemów, zmieniają status zadania na Gotowe do Budowy.
7. Integro „widzi”, że zadanie jest gotowe do wydania, i uruchamia jego wdrożenie w trybie kanaryjnym (mikroserwis Jenkins). Gotowość do wydania określona jest przez zestaw reguł. Na przykład, zadanie ma właściwy status, nie ma blokad na inne zadania, nie ma aktywnych wdrożeń tego mikroserwisu itd.
8. Zadanie zostaje przeniesione do statusu Kanaryjnego (mikroserwis Jira).
9. Jenkins uruchamia wdrożenie zadania w trybie kanaryjnym przez Nomad (zazwyczaj 1-3 instancje) i powiadamia o wdrożeniu mikroserwis monitorujący wydania (mikroserwis DeployWatch).
10. Mikroserwis DeployWatch zbiera tło błędów i reaguje na nie, jeżeli to konieczne. W przypadku przekroczenia tła błędów (norma tła obliczana jest automatycznie) następuje powiadomienie programistów przez mikroserwis Notify. Jeśli programista nie zareagował w ciągu 5 minut (nie kliknął Revert ani Stay), automatycznie uruchamiany jest rollback instancji kanaryjnych. Jeśli tło błędów nie zostało przekroczone, programista musi ręcznie uruchomić wdrożenie zadania na Production (klikając przycisk w interfejsie). Jeśli w ciągu 60 minut programista nie uruchomił wdrożenia na Production, instancje kanaryjne również dla bezpieczeństwa zostaną wycofane.
11. Po uruchomieniu wdrożenia na Production:
- Zadanie zostaje przeniesione do statusu Produkcyjnego (mikroserwis Jira).
- Mikroserwis Jenkins uruchamia proces wdrożenia i powiadamia o wdrożeniu mikroserwis DeployWatch.
- Mikroserwis DeployWatch sprawdza, czy wszystkie kontenery na Production zostały zaktualizowane (były przypadki, gdy nie wszystkie były aktualizowane).
- Przez mikroserwis Notify wysyłane jest powiadomienie o wynikach wdrożenia na Production.
12. Programiści będą mieli 30 minut na uruchomienie rollbacku zadania z Production w przypadku stwierdzenia nieprawidłowego działania mikroserwisu. Po upływie tego czasu zadanie zostanie automatycznie połączone z masterem (mikroserwis Git).
13. Po pomyślnym połączeniu z masterem status zadania zostanie zmieniony na Zamknięty (mikroserwis Jira).
Schemat nie rości sobie pretensji do pełnej szczegółowości (w rzeczywistości kroków jest jeszcze więcej), ale pozwala ocenić stopień integracji w procesy. Nie uważamy tej schematu za idealny i stale udoskonalamy procesy automatycznego wsparcia wydań i wdrożeń.
Co dalej
Mamy wielkie plany dotyczące rozwoju automatyzacji, na przykład rezygnacja z ręcznych operacji podczas wydania monolitu, poprawa monitorowania podczas automatycznego wdrażania, a także lepsza współpraca z programistami.
Na tym etapie zatrzymamy się. Wiele tematów poruszonych w przeglądzie automatyzacji omówiliśmy powierzchownie, niektóre w ogóle nie były poruszone, dlatego chętnie odpowiemy na pytania. Czekamy na propozycje, co szczegółowo omówić, piszcie w komentarzach.
Źródło: habr.com
