Cu toți am auzit de "legacy systems" și poate majoritatea dintre noi am și lucrat cu un astfel de sistem. Responsabilitatea oricărui Build Manager sau DevOps nu este doar să asigure continua funcționare a acestor sisteme ci să implementeze și schimbări. De multe ori, aceste schimbări sunt întâmpinate cu entuziasm de către dezvoltatori și manageri dar sunt și cazuri în care schimbarea nu este acceptată din diverse motive: timp, dificultate, lipsă de cunoștințe, etc. .
În domeniul IT există un proces de "digital transformation" în care se vrea trecerea de la "infrastructure on premise" la cea de "cloud infrastructure". Prin această transformare, multe firme scad costurile aferente menținerii unei infrastructuri globale și timpul de execuție a diferitor taskuri, având totodată posibilitatea de scalare a resurselor aproape instantaneu.
Transformarea sistemelor de build și de testare automată este la rândul său atât o migrare de infrastructură dacă se dorește, dar și o migrare de mentalitate. Dacă acum cincisprezece ani, SVN și scripturile de build bazate pe ant, bash sau batch erau la modă, în ziua de azi avem o multitudine de opțiuni din care putem alege. Acestea încep de la platforme online gen GitHub sau Bitbuchet care oferă flexibilitate sporită pentru colaborare echipelor, până la Amazon AWS care le oferă infrastructură la cerere.
În acest articol aș dori să prezint rapid "journey-ul" pe care l-am străbătut împreună cu colegii mei în transformarea unui sistem legacy vechi și demodat într-un sistem scalabil și relativ modern.
Poate că vă întrebați de ce am ales termenul relativ? Deoarece trebuie luat în considerare și ce fel de produs folosește acest sistem. De exemplu, produsul pe care lucrez are installer de windows, linux cât și support pentru docker și kubernetes. Acest lucru introduce anumite limitări de tip tehnic, deoarece ne obligă să avem mașini de compilare pe windows. La rândul său, testarea produsului are loc pe diferite sisteme de operare și diferite tipuri de baze de date.
Orice transformare pornește de la identificarea problemelor și iterarea peste acestea în pași mici.
Dacă sunteți în situația mea și ați lucrat zi de zi cu sistemul, timp de x ani, ar trebui să aveți deja majoritatea informațiilor despre produs și o idee despre ce trebuie făcut când doriți să schimbați ceva.
Dacă nu aveți destulă experiență cu produsul începeți prin a pune întrebări. Nu trebuie să vă fie frică de faptul că veți fi judecați pentru lipsa de cunoștințe, mai important este să începeți să gândiți înafara cutiei ai cărei pereți sunt procesele și taskurile zilnice repetate la nesfârșit.
Pe parcurs, am identificat câteva aspecte care au fost de un ajutor enorm în înțelegerea și transformarea produsului nostru:
Felul în care este structurat produsul este important. Deoarece softul evoluează constant, se descoperă arhitecturi noi în fiecare an și prin urmare arhitecturile "curente" devin învechite. Am pornit de la arhitecturi monolitice și am ajuns în ziua de astăzi la arhitecturi bazate pe module independente din punct de vedere al metodei de build.
Produsul nostru a avut un număr de 15 foldere pe SVN și se builduia ca un monolit într-o oră. În trecut s-au încercat metode de a face buildul mai rapid prin scripturi de groovy și joburi specializate, dar au avut doar un succes minim și unele au introdus probleme noi la nivel de build.
În momentul în care am preluat ștafeta, am început să identific problemele și să caut soluții. Am aplicat o metodologie învățată din experiența acumulată și anume:
Identificarea problemei,
Implementarea unui prototip,
Colectarea de feedback,
Principala schimbarea ce trebuia făcută era să împărțim codul în mai multe module, lucru care a fost posibil după o analiză a arhitecturii. În momentul de față, avem nouă module într-o ierarhie pe mai multe nivele, unde propagarea versiunilor de la un modul la altul se face pe baza de release independent, proprietăți și versiuni.
Astfel dintr-o arhitectură monolitică de build, am ajuns să avem module mai mici builduite rapid, deoarece nu builduim modulele pe care nu le-am atins.
Faptul că produsul nostru este scris în java și că folosim maven ca framework de build a constituit un avantaj, dar această abordare tehnică se poate folosi cu succes și pentru alte limbaje de programare atâta timp cât nu ne este frică de schimbare.
Pe parcursul vieții unui produs, partea de testare automată ar trebui să fie flexibilă, ușor de implementat sau de schimbat. Într-un sistem vechi, aceasta nu este ușor de întreținut, mai ales dacă produsul este compatibil cu o listă mare de sisteme de operare și de baze de date. Aceleași teste automate, trebuie să ruleze pe o multitudine de combinații OS/DB pentru o acoperire corectă. Aceasta înseamnă un stres asupra sistemului de build, deoarece necesită un număr impresionant de resurse hardware pentru a rula eficient testarea automată.
Revenind la exemplul produsului nostru, la fel ca și arhitectura de build și testarea automată trebuie să fie concepută după principiile scalabilității și a eficienței.
Momentan avem două frameworkuri interne scrise în java:
Frameworkul de instalare care se ocupă de tot ce înseamnă instalarea produsului cu alocarea resurselor dintr-un pool comun.
Rezultatele execuției testelor automate sunt afișate într-un framework de reporting numit Allure. Acesta este open-source și disponibil aici: http://allure.qatools.ru/
Urmând principiile menționate mai sus, am trecut de la o testare automată ce rula între 3 și 5 ore, pe 24 de combinații OS/DB pe mașini specializate, fără rapoarte clare despre ce teste au rulat pe care mașini, la o testare automată ce rulează 20 de minute pe combinații aleatorii la fiecare build de produs sau PR (Pull Request), cu raportare a infrastructurii aferente fiecărei suite de teste. Trebuie menționat că din cele 20 de minute, 5 minute durează instalarea produsului care la rândul ei este automatizată. Resursele hardware nu mai sunt specializate, ci există într-un pool de resurse comune. Accesul la ele se face prin două load balancere (unul pentru OS și celălalt pentru DB) create de noi pentru a monitoriza și scala testele automate. Totul este propagat într-un singur raport, de unde stakeholderii pot să își facă o idee clară asupra stării produsului.
La fel ca în cazul arhitecturii sistemului de build, am decis că vom avea un ROI (return on investment) mai mare dacă rescriem de la 0. Acest lucru a funcționat, deoarece vechiul sistem devenise așa anevoios de folosit și întreținut încât efortul investit să îl reparăm și să întreținem testele existente, nu lăsa destul timp pentru crearea de teste noi.
La ora actuală există peste 50 de astfel de sisteme care ajută un DevOps să își facă munca. La locația de mai jos puteți găsi cele mai folosite 50 de astfel de sisteme: https://stackify.com/top-continuous-integration-tools/
După cum puteți vedea, decizia este una grea, mai ales că majoritatea acestor sisteme au aceleași funcționalități majore.
În cazul nostru, am optat pentru cel mai folosit și apreciat sistem de build open-source și anume Jenkins. Este un sistem de build ce ne oferă flexibilitate suficientă pentru toate activitățile dorite: compilare, rulare automation, back-up, security scanning, etc. .
La ora actuală, am transferat majoritatea joburilor importante de development în modul pipeline , folosind funcționalitatea Blue Ocean: https://jenkins.io/projects/blueocean/ .
Am decis în favoarea opțiunii de pipeline-uri scriptate. Aceasta permite ca Jenkins să execute clase și metode scrise în groovy. Avantajul este posibilitatea de a avea joburi generice a căror logică se află pe un repository de git. Testarea acestei logici și a eventualelor modificări, se poate face foarte ușor, fără să aibă impact asupra producției.
Sistemul de build și de testare automată veche foloseau aproximativ 250 de slave-uri de Jenkins și 10 servere de DB. După transformarea noastră, am redus acest număr la aproximativ 40 de slave-uri în Jenkins, 60 de mașini de instalare și 25 de servere de DB, acestea din urmă fiind situate în poolul comun de resurse al frameworkului nostru de instalare.
Acest lucru a fost posibil, deoarece deja cunoșteam nevoile prodului nostru atât din punct de vedere al buildului cât și al testării automate.
În cazul unei echipe de 50+ de dezvoltatori pe un produs legacy/nou este recomandat să se folosească un sistem centralizat de management al artifactelor. Acesta ajută enorm la timpul de development/testare/release al produsului, deoarece este un centralizator al tuturor dependințelor proprii și 3rd party de care are nevoie produsul. La fel ca și sistemele de build, există un număr impresionant de astfel de sisteme. Aici găsiți o listă cu cele mai folosite: https://xebialabs.com/the-ultimate-devops-tool-chest/repository-management/.
În cazul nostru, am mers din nou pe varianta open-source și anume Sonatype Nexus. Ca și Jenkins, nu oferă un UI modern, dar oferă ceea ce DevOps-ul își dorește cel mai mult, și anume stabilitate și integrarea cu alte sisteme de management a artifactelor cum ar fi: maven, docker, nodejs, yum, etc. .
Aceste sisteme trebuie doar întreținute și de făcut din când în când curățenie pe ele, mai ales dacă sunt folosite pentru toate artefactele din ciclul de development.
Acest sistem ne ajută să menținem codul sursă la zi și ne oferă un loc centralizat de unde putem să luăm ultimele modificări făcute de alți membri ai echipei. Ne oferă posibilitatea să privim înapoi în timp, de la concepția produsului și să vedem cine/când/cum a făcut o modificare în produs.
Developerul și DevOps-ul de azi folosește GIT.
Există o multitudine de implementări de servere de git atât open-source cât și contra cost. Mai jos vă prezint o listă cu cele mai folosite servere de git open source
De cele mai multe ori transformarea unui produs din punct de vedere al codului este migrarea către alt sistem sau schimbarea structurii produsului.
În cazul nostru le-am făcut pe amândouă. Am migrat tot codul de pe SVN pe GIT, iar în momentul în care am început migrarea, am rupt acel monolit și l-am împărțit în nouă repository-uri mai mici.
Faptul că am împărțit produsul în blocuri funcționale mai mici, ne ajută să avem o viziune de ansamblu mai bună asupra procesului de build/testare/release cât și un CI\CD mai ușor de menținut și adaptat. Am continuat pe linia optimizării produsului la nivel de build. Înțelegând din arhitectura produsului ce trebuie să se întâmple la fiecare pas din lifecycle-ul de build, am reușit să reducem substanțial timpii de compilare ai fiecărui modul, înțelegând în același timp interdependența dintre module. Astfel, în momentul în care am adoptat sistemul de build pe bază de pipeline, ne-a fost ușor să construim ecosistemul de build pentru produs.
Ca exemplu, avem configurat un pipeline ce ne permite să builduim tot produsul de la primul la ultimul modul și să rulăm toate suitele de teste automate pe artefactele rezultate. Un developer are acces la rezultatul compilării/testării în mai puțin de 45 de minute, direct în Jenkins, alături de un link către raportul de test, folosind Allure.
Sistemul de build suportă ca aceste tipuri de pipeline să funcționeze în paralel, fără ca rezultatele să fie dependente între ele. Atâta timp cât există resurse libere controlate de Jenkins și resurse în poolul de automation, pipeline-uri noi pot fi inițiate.
Totul a fost posibil datorită modularizării produsului la nivel de build și a frameworkurilor scrise de noi. Felul în care putem lega diferitele module în funcție de interdependența lor în pipeline și să rulăm testele automate este asemenea construirii unei mașini din lego.
În momentul de față, suportăm pe același sistem de CI/CD 3 produse separate care folosesc aceleași resurse hardware, fără să fi fost nevoie să creăm resurse noi, deoarece totul se auto-balansează iar mentenanța mașinilor se face automat de către un job în Jenkins.
Avem implementate sisteme de pipeline pentru PR pentru fiecare modul existent și avem "manual PR checkers" pipeline care ajută developerul să compileze produsul și să ruleze testele automate direct pe codul din branchul creat de el. Astfel, dacă se lucrează la o funcționalitate nouă pe mai multe module ale produsului, se poate testa în timp real, dacă sunt probleme cu codul. Într-un sistem legacy, de cele mai multe ori nu îți poți da seama dacă ai stricat ceva decât în momentul în care instalezi produsul, după ce ai făcut commit și rulezi testele manuale sau automate.
Transformarea pornită cu echipa noastră a început cu un an și jumătate în urmă și abia acum putem spune că avem un produs legacy ce folosește un sistem modern de build și de automation. Schimbările s-au făcut incremental, ținând cont de calendarul de release-uri al produsului și de feedbackul primit de la echipă.
Această muncă a fost începută de doi oameni. Pe parcurs, mai mulți colegi s-au alăturat efortului de a testa prototipurile implementate, de a sugera noi împărțiri ale repository-urilor, de a refactoriza dependințe și de a formula cerințele pentru dezvoltarea frameworkului de testare.
Dacă ne întrebați cum am transformat sistemul legacy, răspunsul este că nu am terminat. Acest efort se întinde pe toată perioada de viață a unui produs. Oricât de mult se optimizează și schimbă sistemul și se reduce durata de testare automată, există noi tooluri sau idei care se pot implementa ca să ajute echipa și produsul. Important este să colaborezi cu colegii și să existe la nivel de echipă o viziune comună când este vorba de transformare, mai ales în partea de DevOps, unde partea de build merge mână în mână cu partea de testare automată.