
La RIT 2019, colegul nostru Alexandr Korotkov a discutat despre automatizarea dezvoltării la CIAN: pentru a simplifica viața și munca, folosim propria noastră platformă Integro. Aceasta urmărește ciclul de viață al sarcinilor, îndepărtează sarcinile repetitive de la dezvoltatori și reduce semnificativ numărul de bug-uri în producție. În această postare, vom completa prezentarea lui Alexandr și vom împărtăși cum am evoluat de la scripturi simple la integrarea produselor open source prin propria noastră platformă și cu ce se ocupă o echipă separată de automatizare.
Nivelul zero
„Nu există nivel zero, nu știu așa ceva”
Mister Shifu din m/f „Kung Fu Panda”
Automatizarea la CIAN a început la 14 ani după înființarea companiei. Atunci, echipa de dezvoltare număra 35 de persoane. E greu de crezut, nu? Desigur, într-o formă oarecare, automatizarea exista, dar un domeniu separat pentru integrarea continuă și livrarea codului a început să se contureze exact în anul 2015.
La acel moment, aveam un monolit uriaș din Python, C# și PHP, desfășurat pe servere Linux/Windows. Pentru a desfășura acest monstru, aveam un set de scripturi pe care le rulam manual. Mai exista și construcția monolitului, care provoca dureri și suferințe din cauza conflictelor la unirea ramurilor, corectării defectelor și reconstrucării „cu un alt set de sarcini în build”. Simplificat, procesul arăta astfel:

Nu eram mulțumiți de asta și voiam să construim un proces de construcție și desfășurare repetabil, automatizat și gestionat. Pentru aceasta, aveam nevoie de un sistem CI/CD, și am ales între versiunea gratuită Teamcity și Jenkins gratuit, deoarece lucrasem cu ambele și ne satisfăceau prin setul de funcții. Am ales Teamcity ca produs mai nou. Atunci nu foloseam arhitectură de microservicii și nu ne așteptam la un număr mare de sarcini și proiecte.
Ajungem la ideea despre propriul sistem
Implementarea Teamcity a redus doar o parte din munca manuală: a rămas încă necesar să se creeze Pull Request-uri, să se promoveze sarcinile prin statusuri în Jira, să se aleagă sarcinile pentru lansare. Cu acest lucru, sistemul Teamcity nu mai reușea. Trebuia să alegem calea unei automatizări suplimentare. Am luat în considerare opțiuni de lucru cu scripturi în Teamcity sau tranziția la sisteme externe de automatizare. Dar, în cele din urmă, am decis că avem nevoie de cea mai mare flexibilitate, pe care o oferă doar o soluție proprie. Așa a apărut prima versiune a sistemului nostru intern de automatizare numit Integro.
Teamcity se ocupă cu automatizarea la nivelul lansării proceselor de construire și implementare, în timp ce Integro s-a concentrat pe automatizarea la un nivel superior a proceselor de dezvoltare. A fost necesar să combinăm lucrul cu sarcinile din Jira cu procesarea codului sursă asociat în Bitbucket. În acest stadiu, în interiorul Integro au început să apară fluxuri de lucru proprii pentru gestionarea sarcinilor de diferite tipuri.
Datorită creșterii automatizării în procesele de business, numărul de proiecte și run-uri în Teamcity a crescut. Așa a apărut o nouă problemă: un singur instanț gratuit de Teamcity nu mai era suficient (3 agenți și 100 de proiecte), am adăugat încă un instanț (încă 3 agenți și 100 de proiecte), apoi încă unul. În final, am obținut un sistem format din mai multe clustere, pe care era dificil să îl gestionăm:

Când a apărut întrebarea despre al patrulea instanț, am realizat că nu mai putem continua astfel, deoarece costurile totale pentru susținerea a patru instanțe depășeau orice limită. A apărut întrebarea achiziționării unui Teamcity plătit sau alegerea unei soluții gratuite Jenkins. Am realizat calculele pentru instanțe și planurile de automatizare și am decis că vom continua pe Jenkins. După câteva săptămâni, am trecut pe Jenkins și ne-am eliberat de o parte din durerea de cap legată de gestionarea mai multor instanțe Teamcity. Așadar, am putut să ne concentrăm pe dezvoltarea Integro și adaptarea Jenkins pentru nevoile noastre.
Odată cu creșterea automatizării de bază (sub forma creării automate de Pull Request-uri, colectării și publicării acoperirii codului și altor verificări) a apărut o dorință persistentă de a renunța la lansările manuale și de a lăsa această muncă roboților. Pe lângă asta, în interiorul companiei a început o migrare către microservicii, care necesitau lansări frecvente, separate unele de altele. Astfel, am ajuns treptat la lansări automate ale microserviciilor noastre (monolitul este încă lansat manual din cauza complexității procesului). Dar, așa cum se întâmplă de obicei, a apărut o nouă dificultate.
Automatizăm testarea

Datorită automatizării lansărilor, procesele de dezvoltare s-au accelerat, parțial și prin omisiunea unor etape de testare. Iar aceasta a dus la o pierdere temporară a calității. Sună banal, dar împreună cu accelerarea lansărilor, a fost necesar să schimbăm și metodologia de dezvoltare a produsului. A fost nevoie să ne gândim la automatizarea testării, la promovarea responsabilității personale (aici se vorbește despre "asumarea ideii în minte", nu despre amenzi financiare) a dezvoltatorului pentru codul livrat și erorile din acesta, precum și la o decizie privind livrarea/nerularea sarcinii prin intermediul unui deploy automatizat.
Eliminând problemele legate de calitate, am ajuns la două soluții importante: am început să desfășurăm teste canarie și am implementat monitorizarea automată a erorilor cu reacție automată la depășirea acestora. Prima soluție a permis identificarea erorilor evidente înainte de a ajunge codul în producție, iar a doua a redus timpul de reacție la problemele din producție. Erorile, desigur, există, dar cheltuim mai mult timp și efort nu pe corectare, ci pe minimizare.
Echipa de automatizare
În prezent, avem un personal de 130 de dezvoltatori și continuăm . Echipa de integrare și livrare continuă a codului (în continuare – echipa Deploy and Integration sau DI) este formată din 7 persoane și lucrează în 2 direcții: dezvoltarea platformei de automatizare Integro și DevOps.
DevOps este responsabil pentru mediile Dev/Beta ale site-ului CIAN, pentru mediu Integro, ajutând dezvoltatorii să rezolve probleme și dezvoltând noi abordări pentru scalarea mediilor. Direcția de dezvoltare Integro se ocupă atât de Integro în sine, cât și de servicii conexe, cum ar fi pluginuri pentru Jenkins, Jira, Confluence, precum și dezvoltarea de utilitare și aplicații auxiliare pentru echipele de dezvoltare.
Echipa DI colaborează cu echipa Platformei, care se ocupă de dezvoltarea arhitecturii, bibliotecilor și abordărilor de dezvoltare în cadrul companiei. În plus, orice dezvoltator din cadrul CIAN poate contribui la automatizare, de exemplu, realizând micro-automatizări pentru nevoile echipei sau împărtășind idei interesante pentru a îmbunătăți automatizarea.
Pachetul stratificat de automatizare la CIAN

Toate sistemele implicate în automatizare pot fi împărțite în mai multe straturi:
- Sisteme externe (Jira, Bitbucket etc.). Acestea sunt utilizate de echipele de dezvoltare.
- Platforma Integro. Cel mai adesea dezvoltatorii nu interacționează direct cu aceasta, dar ea susține funcționarea întregii automatizări.
- Servicii de livrare, orchestrare și detectare (cum ar fi Jenkins, Consul, Nomad). Cu ajutorul lor desfășurăm codul pe servere și asigurăm comunicarea serviciilor între ele.
- Nivelul fizic (servere, SO, software conex). Pe acest nivel rulează codul nostru. Poate fi atât un server fizic, cât și unul virtual (LXC, KVM, Docker).
Pe baza acestei concepții, împărțim zonele de responsabilitate în cadrul echipei DI. Primele două niveluri se află în responsabilitatea direcției de dezvoltare Integro, iar ultimele două în responsabilitatea DevOps. Această împărțire permite concentrarea asupra sarcinilor și nu împiedică interacțiunea, deoarece suntem aproape unul de celălalt și schimbăm constant cunoștințe și experiențe.
Integro
Să ne concentrăm asupra Integro și să începem cu tehnologia utilizată:
- CentOs 7
- Docker + Nomad + Consul + Vault
- Java 11 (monolitul vechi Integro va rămâne pe Java 8)
- Spring Boot 2.X + Spring Cloud Config
- PostgreSql 11
- RabbitMQ
- Apache Ignite
- Camunda (încorporat)
- Grafana + Graphite + Prometheus + Jaeger + ELK
- Interfață Web: React (CSR) + MobX
- SSO: Keycloak
Respectăm principiul dezvoltării microserviciilor, deși avem și un legacy sub formă de monolit din versiunea timpurie a Integro. Fiecare microserviciu rulează în propriul său container Docker, iar serviciile comunică între ele prin solicitări HTTP și mesaje RabbitMQ. Microserviciile se găsesc reciproc prin Consul și îi fac solicitări, trecând prin autentificare prin SSO (Keycloak, OAuth 2/OpenID Connect).

Ca exemplu concret, să luăm interacțiunea cu Jenkins, care constă din următorii pași:
- Microserviciul de gestionare a fluxului de lucru (denumit în continuare microserviciul Flow) vrea să inițieze o construcție în Jenkins. Pentru aceasta, găsește prin Consul IP:PORT-ul microserviciului de integrare cu Jenkins (denumit în continuare microserviciul Jenkins) și îi trimite o solicitare asincronă pentru a lansa construcția în Jenkins.
- Microserviciul Jenkins, după primirea solicitării, formează și trimite înapoi un ID de job, prin care va putea fi identificat ulterior rezultatul execuției. Împreună cu aceasta, inițiază construcția în Jenkins prin apelul API REST.
- Jenkins execută construcția și, la finalizarea acesteia, trimite un webhook cu rezultatele execuției către microserviciul Jenkins.
- Microserviciul Jenkins, primind webhook-ul, formează un mesaj de finalizare a procesării solicitării și atașează rezultatele execuției. Mesajul format este trimis în coada RabbitMQ.
- Prin RabbitMQ, mesajul publicat ajunge la microserviciul Flow, care află despre rezultatul procesării sarcinii sale, corelând ID-ul job-ului din solicitare cu mesajul primit.
Acum avem aproximativ 30 de microservicii, care pot fi împărțite în mai multe grupuri:
- Gestionarea configurațiilor.
- Informarea și interacțiunea cu utilizatorii (messenger, email).
- Lucrul cu codul sursă.
- Integrarea cu instrumentele de deploy (Jenkins, Nomad, Consul etc.).
- Monitorizare (lansări, erori etc.).
- Utilitare web (UI pentru gestionarea mediilor de testare, colectarea statisticilor etc.).
- Integrarea cu sisteme de urmărire a sarcinilor și similare.
- Gestionarea fluxului de lucru pentru diferite sarcini.
Fluxul de lucru al sarcinii
Integro automatizează acțiunile legate de ciclul de viață al unei sarcini. Simplificat, vom înțelege prin ciclul de viață al unei sarcini fluxul de lucru al sarcinii în Jira. În procesele noastre de dezvoltare există câteva variații de flux de lucru în funcție de proiect, tipul sarcinii și opțiunile alese în cadrul sarcinii specifice.
Să analizăm fluxul de lucru pe care îl folosim cel mai des:

În schemă, rotița indică faptul că transition este apelat automat de Integro, în timp ce figurina omului înseamnă că transition este inițiat manual de o persoană. Să luăm în considerare câteva căi prin care sarcina poate trece în acest workflow.
Testare complet manuală pe DEV+BETA fără teste canar (de obicei, așa lansăm monolitul):

Pot exista și alte combinații de transition. Uneori, calea pe care o va urma sarcina poate fi aleasă prin opțiuni în Jira.
Mișcarea sarcinii
Să luăm în considerare pașii principali care sunt realizați în timpul mișcării sarcinii prin workflow-ul „Testare pe DEV + teste canar”:
1. Dezvoltatorul sau PM-ul creează o sarcină.
2. Dezvoltatorul preia sarcina. După finalizare, o trimite în statusul IN REVIEW.
3. Jira trimite un Webhook către microserviciul Jira (responsabil cu integrarea cu Jira).
4. Microserviciul Jira trimite o cerere către serviciul Flow (responsabil cu workflow-urile interne, în care se desfășoară activitatea) pentru a lansa workflow-ul.
5. În interiorul serviciului Flow:
- Se desemnează revizorii pentru sarcină (microserviciul Users, care știe tot despre utilizatori, + microserviciul Jira).
- Prin microserviciul Source (cunoaște despre repo-uri și ramuri, dar nu lucrează cu codul în sine) se caută repo-uri în care există o ramură a sarcinii noastre (pentru a simplifica căutarea, numele ramurii coincide cu numărul sarcinii din Jira). Cel mai adesea, sarcina are doar o ramură într-un singur repo, ceea ce simplifică gestionarea cozii pentru deploy și reduce legătura dintre repo-uri.
- Pentru fiecare ramură găsită se execută o astfel de secvență de acțiuni:
i) Încorporarea ramurii master (microserviciul Git pentru a lucra cu codul).
ii) Ramura este blocată de modificări de către dezvoltator (microserviciul Bitbucket).
iii) Se creează un Pull Request pentru această ramură (microserviciul Bitbucket).
iv) Se trimite un mesaj despre noul Pull Request în chat-urile dezvoltatorilor (microserviciul Notify pentru a lucra cu notificările).
v) Se lansează compilarea, testarea și deploy-ul sarcinii pe DEV (microserviciul Jenkins pentru a lucra cu Jenkins).
vi) Dacă toate punctele anterioare s-au încheiat cu succes, Integro își pune aprovarea în Pull Request (microserviciul Bitbucket). - Integro așteaptă aprobatul în Pull Request de la revizorii desemnați.
- Odată ce au fost primite toate aprobările necesare (inclusiv testele automatizate trecute pozitiv), Integro trimite sarcina în statusul Test on Dev (microserviciul Jira).
6. Testeri efectuează testarea sarcinii. Dacă nu sunt probleme, atunci transferă sarcina în statutul Ready For Build.
7. Integro „vede” că sarcina este gata de lansare și inițiază desfășurarea acesteia în modul canary (microserviciul Jenkins). Pregătirea pentru lansare este determinată de un set de reguli. De exemplu, sarcina trebuie să fie în statutul corect, fără blocaje pe alte sarcini, iar în prezent nu trebuie să existe desfășurări active ale acestui microserviciu etc.
8. Sarcina este transferată în statutul Canary (microserviciul Jira).
9. Jenkins inițiază desfășurarea sarcinii prin Nomad în modul canary (de obicei 1-3 instanțe) și notifică serviciul de monitorizare a desfășurărilor (microserviciul DeployWatch).
10. Microserviciul DeployWatch colectează fondul de erori și reacționează la acesta, dacă este necesar. Dacă fondul de erori depășește norma (care este calculată automat), dezvoltatorii sunt notificați prin microserviciul Notify. Dacă după 5 minute dezvoltatorul nu a reacționat (a apăsat Revert sau Stay), atunci inițiază o revenire automată a instanțelor canary. Dacă fondul nu este depășit, dezvoltatorul trebuie să inițieze manual desfășurarea sarcinii în Production (apăsând un buton în UI). Dacă în termen de 60 de minute dezvoltatorul nu a inițiat desfășurarea în Production, instanțele canary vor fi, de asemenea, retrase din motive de securitate.
11. După inițierea desfășurării în Production:
- Sarcina este transferată în statutul Production (microserviciul Jira).
- Microserviciul Jenkins inițiază procesul de desfășurare și notifică despre desfășurare microserviciul DeployWatch.
- Microserviciul DeployWatch verifică că toate containerele din Production au fost actualizate (au existat cazuri în care nu toate au fost actualizate).
- Prin microserviciul Notify se trimite o notificare referitoare la rezultatele desfășurării în Production.
12. Dezvoltatorii vor avea 30 de minute pentru a iniția revenirea sarcinii din Production în cazul în care se descoperă un comportament incorect al microserviciului. La expirarea acestui timp, sarcina va fi integrată automat în master (microserviciul Git).
13. După un merge reușit în master, statutul sarcinii va fi modificat în Closed (microserviciul Jira).
Schema nu pretinde a fi complet detaliată (de fapt, sunt mai multe etape), dar permite evaluarea gradului de integrare în procese. Nu considerăm că această schemă este ideală și îmbunătățim procesele de asistență automată pentru desfășurări și desfășurări.
Ce urmează
Avem planuri mari pentru dezvoltarea automatizării, cum ar fi renunțarea la operațiunile manuale la lansările monolitului, îmbunătățirea monitorizării în timpul desfășurării automate, îmbunătățirea colaborării cu dezvoltatorii.
Dar ne oprim aici pentru acum. Am atins multe subiecte în revizuirea automatizării la o scară superficială, iar unele nu le-am abordat deloc, așa că suntem bucuroși să răspundem la întrebări. Așteptăm sugestii despre ce să detaliem, scrieți în comentarii.
Sursa: habr.com
