
Durante il RIT 2019, il nostro collega Alexander Korotkov ha parlato dell'automazione nello sviluppo in CIAN: per semplificare la vita e il lavoro, utilizziamo la nostra piattaforma Integro. Essa monitora il ciclo di vita delle attività, solleva gli sviluppatori dalle operazioni routinarie e riduce significativamente il numero di bug in produzione. In questo post, completeremo la presentazione di Alexander e racconteremo come siamo passati da semplici script all'integrazione di prodotti open source tramite la nostra piattaforma e di cosa si occupa un team separato dedicato all'automazione.
Livello zero
«Non esiste un livello zero, non lo so»
Master Shifu dal film «Kung Fu Panda»
L'automazione in CIAN è iniziata 14 anni dopo la fondazione dell'azienda. All'epoca, il team di sviluppo era composto da 35 persone. Fa fatica a crederci, vero? Certamente, in qualche forma l'automazione esisteva già, ma un segmento dedicato all'integrazione continua e alla consegna del codice ha iniziato a prendere forma nel 2015.
A quel tempo avevamo un enorme monolite scritto in Python, C# e PHP, distribuito su server Linux/Windows. Per il deploy di questo mostro avevamo un insieme di script che eseguivamo manualmente. C'era anche la compilazione del monolite, che portava dolore e sofferenza a causa dei conflitti durante la fusione dei rami, delle correzioni dei difetti e della ricompilazione «con un altro insieme di attività nella build». Il processo semplificato appariva così:

Questo non ci soddisfaceva, e volevamo costruire un processo di compilazione e deploy ripetibile, automatizzato e gestito. Per questo avevamo bisogno di un sistema CI/CD, e dovevamo scegliere tra la versione gratuita di Teamcity e Jenkins gratuito, dato che avevamo già esperienza con entrambi e soddisfacevano entrambe le nostre esigenze funzionali. Abbiamo scelto Teamcity come prodotto più recente. A quel tempo, non utilizzavamo ancora un'architettura a microservizi e non prevedevamo un numero elevato di attività e progetti.
Siamo arrivati all'idea di un nostro sistema
L'implementazione di Teamcity ha ridotto solo una parte del lavoro manuale: è rimasta ancora la creazione di Pull Request, la gestione delle attività negli stati in Jira e la selezione delle attività per il rilascio. Su questo aspetto, il sistema Teamcity non era in grado di gestire. Era necessario scegliere una strada per ulteriori automazioni. Abbiamo considerato opzioni di scripting in Teamcity o la transizione a sistemi di automazione esterni. Ma alla fine abbiamo deciso che avevamo bisogno della massima flessibilità, che solo una soluzione interna poteva offrire. Così è nata la prima versione del sistema di automazione interno chiamato Integro.
Teamcity si occupa dell'automazione a livello di avvio dei processi di build e deployment, mentre Integro si è focalizzata sull'automazione di alto livello dei processi di sviluppo. Era necessario integrare il lavoro con le attività in Jira con la gestione del codice sorgente correlato in Bitbucket. In questa fase, in Integro sono iniziati a emergere flussi di lavoro propri per gestire attività di diversi tipi.
Con l'aumento dell'automazione nei processi aziendali, è cresciuto il numero di progetti e run in Teamcity. Così è emerso un nuovo problema: un'istanza gratuita di Teamcity non era più sufficiente (3 agenti e 100 progetti), abbiamo aggiunto un'altra istanza (altri 3 agenti e 100 progetti), poi un'altra ancora. Alla fine, abbiamo ottenuto un sistema composto da più cluster, difficile da gestire:

Quando è emersa la necessità di una quarta istanza, ci siamo resi conto che non era più possibile continuare in questo modo, poiché i costi complessivi per supportare 4 istanze stavano già oltrepassando ogni limite. Si è presentata la questione dell'acquisto di Teamcity a pagamento o della scelta di Jenkins gratuito. Abbiamo effettuato calcoli sulle istanze e sui piani di automazione e abbiamo deciso di optare per Jenkins. Dopo qualche settimana, siamo passati a Jenkins e abbiamo alleviato parte del mal di testa associato al supporto di più istanze di Teamcity. Così siamo riusciti a concentrarci sullo sviluppo di Integro e sull'ottimizzazione di Jenkins per le nostre esigenze.
Con l'aumento dell'automazione di base (attraverso la creazione automatica di Pull Request, la raccolta e pubblicazione della copertura del codice e altri controlli) è nata la forte volontà di abbandonare il più possibile i rilascio manuali e delegare questo lavoro ai robot. Inoltre, all'interno dell'azienda è iniziato il passaggio ai microservizi, che richiedevano rilasci frequenti, separati l'uno dall'altro. Così siamo arrivati progressivamente ai rilasci automatici dei nostri microservizi (mentre il monolite viene ancora rilasciato manualmente a causa della complessità del processo). Ma, come accade spesso, è emersa una nuova difficoltà.
Automatizziamo i test

Grazie all'automazione dei rilasci, i processi di sviluppo si sono accelerati, parzialmente a causa della saltazione di alcune fasi di testing. Questo ha comportato una temporanea perdita di qualità. Sembra banale, ma insieme all'accelerazione dei rilasci era necessario cambiare anche la metodologia di sviluppo del prodotto. È stato necessario riflettere sull'automazione dei test, sull'assegnazione della responsabilità personale (qui si parla di 'accettazione dell'idea nella mente', e non di multe monetarie) per il codice rilasciato e i relativi bug, così come sulla decisione di rilasciare o non rilasciare una task attraverso un deploy automatico.
Affrontando i problemi di qualità, siamo giunti a due importanti decisioni: abbiamo iniziato a effettuare test canarini e abbiamo implementato il monitoraggio automatico degli errori in background con una risposta automatica al superamento di una soglia. La prima decisione ha permesso di identificare errori evidenti prima che il codice arrivasse in produzione, la seconda ha ridotto il tempo di risposta ai problemi in produzione. Gli errori, ovviamente, ci sono, ma dedichiamo la maggior parte del nostro tempo e delle nostre energie non a correggerli, bensì a minimizzarli.
Il team di automazione
Attualmente abbiamo un organico di 130 sviluppatori e continuiamo . Il team di integrazione e consegna continua del codice (di seguito — team Deploy and Integration o DI) è composto da 7 persone e lavora in 2 direzioni: sviluppo della piattaforma di automazione Integro e DevOps.
Il team DevOps è responsabile degli ambienti Dev/Beta del sito CIAN, dell'ambiente Integro, aiuta gli sviluppatori nella risoluzione dei problemi e sviluppa nuove strategie per scalare gli ambienti. La direzione dello sviluppo di Integro si occupa sia di Integro stessa, sia di servizi correlati, come plugin per Jenkins, Jira, Confluence, e sviluppa anche strumenti e applicazioni di supporto per i team di sviluppatori.
Il team DI lavora a stretto contatto con il team della Piattaforma, che si occupa dello sviluppo dell'architettura, delle librerie e delle metodologie di sviluppo all'interno dell'azienda. Inoltre, qualsiasi sviluppatore all'interno di CIAN può contribuire all'automazione, ad esempio, realizzando microautomazioni per le esigenze del team o condividendo idee brillanti su come migliorare ulteriormente l'automazione.
La torta stratificata dell'automazione in CIAN

Tutti i sistemi coinvolti nell'automazione possono essere suddivisi in più livelli:
- Sistemi esterni (Jira, Bitbucket, ecc.). Questi sono utilizzati dai team di sviluppo.
- Piattaforma Integro. Di solito, gli sviluppatori non lavorano direttamente con essa, ma è fondamentale per il funzionamento di tutta l'automazione.
- Servizi di distribuzione, orchestrazione e discovery (ad esempio, Jenkins, Consul, Nomad). Ci aiutano a distribuire il codice sui server e a garantire il corretto funzionamento dei servizi tra di loro.
- Livello fisico (server, OS, software correlato). A questo livello opera il nostro codice. Può trattarsi di un server fisico o virtuale (LXC, KVM, Docker).
In base a questo concetto, suddividiamo le aree di responsabilità all'interno del team DI. I primi due livelli sono sotto la responsabilità della direzione dello sviluppo di Integro, mentre gli ultimi due livelli sono sotto la responsabilità del DevOps. Questa divisione consente di concentrarsi sui compiti senza ostacolare l'interazione, poiché siamo fisicamente vicini e ci scambiamo continuamente conoscenze ed esperienze.
Integro
Concentriamoci su Integro e iniziamo con lo stack tecnologico:
- CentOs 7
- Docker + Nomad + Consul + Vault
- Java 11 (il vecchio monolite di Integro rimarrà su Java 8)
- Spring Boot 2.X + Spring Cloud Config
- PostgreSql 11
- RabbitMQ
- Apache Ignite
- Camunda (embedded)
- Grafana + Graphite + Prometheus + Jaeger + ELK
- Interfaccia Web: React (CSR) + MobX
- SSO: Keycloak
Seguiamo il principio dello sviluppo a microservizi, anche se abbiamo una legacy rappresentata dal monolite della vecchia versione di Integro. Ogni microservizio gira nel proprio contenitore Docker e i servizi comunicano tra loro tramite richieste HTTP e messaggi RabbitMQ. I microservizi si trovano l'uno con l'altro tramite Consul e effettuano una richiesta, passando l'autenticazione tramite SSO (Keycloak, OAuth 2/OpenID Connect).

Come esempio concreto, consideriamo l'interazione con Jenkins, che consiste nei seguenti passaggi:
- Il microservizio di gestione del workflow (d'ora in poi microservizio Flow) vuole avviare una build in Jenkins. A tal fine, trova tramite Consul l'IP:PORT del microservizio di integrazione con Jenkins (d'ora in poi microservizio Jenkins) e invia a questo una richiesta asincrona per avviare la build in Jenkins.
- Il microservizio Jenkins, dopo aver ricevuto la richiesta, genera e restituisce un Job ID, che poi sarà possibile utilizzare per identificare il risultato del lavoro. Contestualmente, avvia la build in Jenkins tramite una chiamata all'API REST.
- Jenkins esegue la build e, al termine, invia un webhook con i risultati dell'esecuzione al microservizio Jenkins.
- Il microservizio Jenkins, ricevuto il webhook, genera un messaggio di conclusione dell'elaborazione della richiesta e allega i risultati dell'esecuzione. Il messaggio generato viene inviato in coda a RabbitMQ.
- Attraverso RabbitMQ, il messaggio pubblicato arriva al microservizio Flow, che scopre il risultato dell'elaborazione della sua attività confrontando il Job ID della richiesta con quello del messaggio ricevuto.
Attualmente abbiamo circa 30 microservizi, che possono essere suddivisi in diversi gruppi:
- Gestione delle configurazioni.
- Informazione e interazione con gli utenti (messaggeri, email).
- Gestione del codice sorgente.
- Integrazione con gli strumenti di deployment (Jenkins, Nomad, Consul, ecc.).
- Monitoraggio (rilascio, errori, ecc.).
- Web utility (interfaccia utente per la gestione degli ambienti di test, raccolta statistiche, ecc.).
- Integrazione con i tracker di attività e sistemi simili.
- Gestione del workflow per compiti differenti.
Workflow del compito
Integro automatizza le azioni legate al ciclo di vita di un compito. In modo semplificato, per ciclo di vita di un compito intendiamo il workflow di un'attività in Jira. Nei nostri processi di sviluppo, ci sono diverse varianti di workflow a seconda del progetto, del tipo di compito e delle opzioni selezionate per il compito specifico.
Consideriamo il workflow che utilizziamo più frequentemente:

Nello schema, l'ingranaggio indica che il transition viene attivato automaticamente da Integro, mentre la figura umana indica che il transition è attivato manualmente da una persona. Esaminiamo alcuni percorsi che un'attività può seguire in questo workflow.
Testing manuale completo su DEV+BETA senza test canary (di solito così rilasciamo il monolite):

Possono esserci anche altre combinazioni di transition. A volte il percorso che seguirà l'attività può essere scelto tramite le opzioni in Jira.
Movimento dell'attività
Esaminiamo i passaggi principali che vengono eseguiti durante il movimento dell'attività nel workflow "Testing su DEV + test canary":
1. Uno sviluppatore o un PM crea l'attività.
2. Lo sviluppatore prende in carico l'attività. Una volta completata, la sposta nello stato IN REVIEW.
3. Jira invia un Webhook al microservizio Jira (responsabile dell'integrazione con Jira).
4. Il microservizio Jira invia una richiesta al servizio Flow (responsabile dei workflow interni, nei quali viene eseguito il lavoro) per avviare il workflow.
5. All'interno del servizio Flow:
- Vengono assegnati i revisori per l'attività (microservizio Users, che conosce tutto sugli utenti + microservizio Jira).
- Attraverso il microservizio Source (che conosce i repository e i branch, ma non lavora con il codice stesso) viene eseguita la ricerca dei repository che contengono il branch della nostra attività (per semplificare la ricerca, il nome del branch corrisponde al numero dell'attività in Jira). Di solito, l'attività ha solo un branch in un unico repository, il che semplifica la gestione della coda per il deployment e riduce la dipendenza tra i repository.
- Per ogni branch trovato viene eseguita la seguente sequenza di azioni:
i) Unione del branch master (microservizio Git per lavorare con il codice).
ii) Il branch viene bloccato da modifiche da parte dello sviluppatore (microservizio Bitbucket).
iii) Viene creato un Pull Request per questo branch (microservizio Bitbucket).
iv) Viene inviata una notifica riguardo al nuovo Pull Request nelle chat degli sviluppatori (microservizio Notify per gestire le notifiche).
v) Viene avviata la compilazione, il test e il deployment dell'attività su DEV (microservizio Jenkins per lavorare con Jenkins).
vi) Se tutti i punti precedenti sono stati completati con successo, Integro approva il Pull Request (microservizio Bitbucket). - Integro attende un'approvazione nel Pull Request dai revisori assegnati.
- Non appena vengono ricevute tutte le approvazioni necessarie (compresi i test automatizzati superati positivamente), Integro sposta l'attività nello stato Test on Dev (microservizio Jira).
6. I collauder effettuano il test del compito. Se non ci sono problemi, spostano il compito nello stato Pronto per la Build.
7. Integro 'vede' che il compito è pronto per il rilascio e avvia il suo deploy in modalità canary (microservizio Jenkins). La prontezza al rilascio è determinata da un insieme di regole. Ad esempio, il compito si trova nello stato corretto, non ci sono blocchi su altri compiti, attualmente non ci sono distribuzioni attive di questo microservizio, ecc.
8. Il compito viene spostato nello stato Canary (microservizio Jira).
9. Jenkins avvia tramite Nomad il deploy del compito in modalità canary (solitamente 1-3 istanze) e notifica la distribuzione al servizio di monitoraggio delle release (microservizio DeployWatch).
10. Il microservizio DeployWatch raccoglie il background degli errori e vi reagisce, se necessario. Se il background degli errori supera il limite (il limite è calcolato automaticamente), viene inviata una notifica agli sviluppatori tramite il microservizio Notify. Se dopo 5 minuti lo sviluppatore non ha reagito (premendo Revert o Stay), viene avviato il rollback automatico delle istanze canary. Se il limite non viene superato, lo sviluppatore deve avviare manualmente il deploy del compito in Produzione (premendo un bottone nell'interfaccia). Se entro 60 minuti lo sviluppatore non ha avviato il deploy in Produzione, le istanze canary saranno anch'esse ritirate per motivi di sicurezza.
11. Dopo l'avvio del deploy in Produzione:
- Il compito viene spostato nello stato Produzione (microservizio Jira).
- Il microservizio Jenkins avvia il processo di deploy e notifica la distribuzione al microservizio DeployWatch.
- Il microservizio DeployWatch controlla che tutti i contenitori siano stati aggiornati in Produzione (si sono verificate situazioni in cui non tutti venivano aggiornati).
- Viene inviata una notifica sui risultati del deploy in Produzione tramite il microservizio Notify.
12. Gli sviluppatori avranno 30 minuti per avviare il rollback del compito in Produzione in caso di comportamento anomalo del microservizio. Dopo questo periodo, il compito sarà automaticamente fuso nel master (microservizio Git).
13. Dopo un merge riuscito nel master, lo stato del compito verrà modificato in Chiuso (microservizio Jira).
Lo schema non pretende di essere completamente dettagliato (nella realtà i passaggi sono ancora di più), ma consente di valutare il livello di integrazione nei processi. Non consideriamo questo schema ideale e stiamo migliorando i processi di supporto automatico per le release e i deploy.
E ora?
Abbiamo grandi progetti per sviluppare l'automazione, ad esempio, eliminare le operazioni manuali durante il rilascio del monolite, migliorare il monitoraggio durante il deployment automatico e ottimizzare l'interazione con gli sviluppatori.
Ma ci fermeremo qui per ora. Abbiamo trattato molte tematiche nell'overview sull'automazione in modo superficiale e alcune non le abbiamo affrontate affatto, quindi siamo felici di rispondere alle vostre domande. Aspettiamo suggerimenti su cosa approfondire, scriveteci nei commenti.
Fonte: habr.com
