In progetti di sviluppo del software con una certa complessità, può risultare molto utile basare l‘applicazione su una architettura modulare, incaricando diversi gruppi di sviluppo di realizzare separatamente i diversi moduli che compongono l‘architettura. CruiseControl è un framework che può aiutarci a integrare le varie parti e a mantenere la sincronizzazione fra i diversi gruppi in maniera lineare, ottimizzando il processo di sviluppo.
Integrazione continua o task unico di integrazione?
Quando affrontiamo la pianificazione dello sviluppo di un progetto software complesso, se abbiamo a che fare con una architettura applicativa modulare, una strategia comune è quella di indirizzare lo sviluppo di ciascuno modulo a gruppi di sviluppo distinti. L’immediato beneficio di questa strategia, dove gruppi separati sviluppano in parallelo, è la riduzione del tempo totale di realizzazione.
Chiaramente ci dobbiamo far carico di alcuni costi: quelli economici, derivanti dal maggior numero di risorse umane e strumentali necessarie allo sviluppo, e i costi organizzativi, perche’ ci vediamo costretti a introdurre una raffinata e rigida organizzazione. Occorre poi definire le interfacce dei moduli comuni e implementarne i corrispondenti mock object. Infine c’è il costo dell’integrazione di tutti i componenti realizzati.
Generalmente le attività di analisi, sviluppo e integrazione di un progetto software vengono pianificate in sequenza temporale. Avviare il processo di integrazione al termine dello sviluppo però potrebbe tradursi in costo eccessivo.
Per quanto si possa disporre di un’analisi dettagliata e per quanto possa essere diffusa la comunicazione tra i gruppi in tema di variazioni delle interfacce o modifiche dei componenti comuni, la comunicazione di queste variazioni viene tralasciata, soprattutto in corrispondenza delle date di rilascio quando ogni gruppo concentra le proprie energie alla conclusione dello sviluppo: i problemi che ne conseguono e che si riflettono sulla fase di integrazione sono noti a tutti coloro che abbiano affrontato un qualche progetto del genere. La sessione di integrazione tra i vari gruppi potrebbe diventare lunga, snervante e complessa e, cosa ben più importante, in definitiva potrebbe essere vanificato il vantaggio derivante dalla riduzione del tempo di realizzazione di questo scenario di sviluppo parallelo.
In questo contesto introdurre uno strumento che a intervalli regolari, o a sessioni stabilite, compili automaticamente tutti i componenti può mitigare gli effetti di un’integrazione fatta alla fine dello sviluppo. I gruppi di lavoro dovranno dedicare parte delle risorse a loro disposizione in sessioni di integrazione più brevi e ravvicinate. In questo modo il numero di errori di integrazione si riducono e sono anche più semplici da correggere. Se il software è compilato e supera il test di integrazione in ogni intervallo di tempo stabilito, allora gli errori rilevati sono al più da attribuire solo all’ultima compilazione, e quindi alla quota di sorgenti codificata in questo intervallo di tempo.
Viene da se’ che i gruppi del team di sviluppo sono più produttivi in quanto impegnati quasi esclusivamente alla realizzazione delle funzioni applicative a loro assegnate, minimizzando le energie da indirizzare all’integrazione.
In questa maniera sono sempre disponibili le ultime versioni dei componenti compilati, aspetto tanto più importante quanto più sono numerosi i gruppi di sviluppo e le componenti applicative comuni; si tratta inoltre di un aspetto da tenere anche in considerazione quando siamo in presenza di gruppi di sviluppo localizzati a distanza.
In termini di costo totale, la quota di quello indirizzabile all’integrazione viene di fatto scomposto in fasi di integrazione più brevi e produttive. L’aspetto a mio parere importante in uno scenario di sviluppo costituito da numerosi gruppi di sviluppo è la più precisa e tempestiva diffusione delle informazioni circa le nuove versioni dei componenti realizzati e delle modifiche delle interfacce: aspetto da non sottovalutare nell’economia di gestione dello sviluppo. La modifica di un’interfaccia di un componente comune è subito palesata dal risultato della compilazione, e dagli errori di compilazione dei corrispondenti consumer, qualora questi non si siano adeguati a questa nuova interfaccia per negligenza o difetto di comunicazione.
L’effetto di tutto ciò è una “naturale” introduzione di un maggior livello di disciplina tra gli sviluppatori che sono portati a sincronizzarsi con maggiore frequenza e a fissare con tempestività gli errori di compilazione: non sarà più accettato l’alibi “ma sulla mia macchina funzionava”. Questo li obbligherebbe a ad avere sempre un ambiente di sviluppo certificato. L’esperienza ci insegna che frequentemente si va in deroga agli standard definiti, per esempio modificando sui workspace i riferimenti alle librerie comuni, o utilizzandone versioni obsolete.
Architettura dell’applicazione
Nel semplice grafico di figura 1 viene illustrata una possibile organizzazione del nostro progetto. Supponiamo di riuscire a individuare per l’applicazione che vogliamo realizzare alcuni componenti che risolvono le funzionalità principali e uno o più componenti di infrastruttura il cui compito è quello di essere provider di servizi (accesso a dati o sistemi legacy, motori di workflow, etc.). Ciascun modulo sarà affidato quindi per la sua realizzazione a un gruppo di lavoro distinto.
Affinche’ i vari gruppi di sviluppo possano avviare il loro lavoro, non è necessario attendere il rilascio delle eventuali componenti trasversali. La dichiarazione delle interfacce permetterà ai gruppi di sviluppo di realizzare delle classi mock, necessarie allo sviluppo delle funzionalità, ai test unitari e alla compilazione. Stesso discorso nel caso in cui esistano delle relazioni tra le funzioni applicative. I primi rilasci consistenti di questi componenti rappresenteranno i primi momenti di integrazione, il momento in cui gli oggetti mock verranno sostituiti dalle classi di effettiva implementazione.
Scenario operativo
Ciascun gruppo, una volta ultimato lo sviluppo ed eseguito con successo il test unitario provvederà ad aggiornare l’area di competenza assegnatagli all’interno del repository SCM dei sorgenti. Da questo ambiente verrà aggiornata la copia locale dei sorgenti su cui Cruise Control avvierà il processo di build automatica. In una ipotetica sequenza di compilazione dapprima verranno compilate le librerie comuni e i componenti trasversali necessari alla build dei componenti applicativi. A compilazione corretta sarà possibile eseguire un task che verifichi la non regressione del software e un task di aderenza del codice alle metriche stabilite, superati i quali si potrà introdurre uno step di distribuzione verso gli ambienti target.
Figura 2 – Scenario operativo di esempio (ciclo di vita).
Chiaramente questo processo impone una certa disciplina anche nel rilascio dei sorgenti. L’aggiornamento del repository dovrà avvenire solo dopo un test unitario accurato, magari controllato dal responsabile del gruppo. Circa l’integrazione tra componenti è necessario che questi siano sufficientemente stabili, non soggetti quindi a continue modifiche delle interfacce, in altre parole meglio rivedere l’analisi e interrompere il processo di build piuttosto che fallire sistematicamente le compilazioni.
Circa l’articolazione del repository, in questa sede assumiamo che ciascun gruppo di lavoro aggiornerà i sorgenti in una propria area di lavoro.
Cruise Control
Cruise Control è un framework java open source per processi di compilazione automatica e schedulata. Realizzato dagli sviluppatori di ThoughtWorks, può essere reperito al sito SourceForge [3] mentre la documentazione ufficiale è disponibile all’indirizzo indicato in [4] nei riferimenti.
Si tratta di un ambiente le cui funzionalità sono estendibile a numerosi plug-in e componenti di terze parti [5]. Può utilizzare quale motore di compilazione altri framework come Ant o Maven, oltre a integrare strumenti di gestione della configurazione (CVS, PVCS, Subversion, ClearCase) e alcuni notification schema. CruiseControl dispone inoltre di una applicazione web attraverso la quale è possibile controllare il processo di compilazione.
Tra gli strumenti integrabili sono interessanti i progetti ConfigurationGUI e CCScrape. Il primo è una applicazione Java utile alla creazione dei file di configurazione e al monitoraggio dello stato delle compilazioni, mentre CCScrape è una utility che aiuta il team di sviluppo a prestare attenzione allo stato del progetto e alle metriche che si ritiene opportuno siano rispettate.
Figura 3 – Moduli di CruiseControl.
I moduli principali di CruiseControl sono il Build Loop e l’applicazione web che, come già detto, pubblica i risultati delle compilazioni, fornendo anche indicazione circa gli errori di compilazione e i risultati di eventuali test; attraverso questa applicazione è anche possibile accedere agli artefatti Java.
Al Build Loop è demandato l’avvio dei cicli di compilazione e la notifica dell’esecuzione degli stessi attraverso varie tecniche di pubblicazione. L’avvio delle operazioni di build può essere o schedulato oppure avviato in risposta a modifiche sul sottostante ambiente di gestione della configurazione. La definizione delle operazioni eseguite dal Build Loop è riportata nel file di configurazione config.xml. La Dashboard è un modulo introdotto a partire dalla versione 2.7 come strumento di visualizzazione dello stato di compilazione dei progetti.
Dal punto di vista operativo le compilazioni ottenute con CruiseControl sono sempre quelle dell’ultima versione del software rilasciato, dove ogni file è ricompilato a partire da zero. Poiche’ tutte le compilazioni vengono storicizzate, è possibile accedere ad ogni versione del software compilato.
In estrema sintesi si possono sintetizzare le caratteristiche di CruiseControl nei punti che riportiamo di seguito
Modularità
I moduli di questo strumento possono essere distribuiti anche su ambienti diversi. È per esempio possibile distribuire l’applicazione di reportistica su una macchina distinta da quella di esecuzione della build loop.
Integrabilità
- con framework di compilazione (p.e.: Ant o Maven);
- integrazione di Plug-in di terze parti attraverso il protocollo JMX;
- con JUnit, attraverso il quale è possibile inserire esecuzione di test automatici di non regressione da cui dipende l’esito complessivo del task di compilazione;
- con framework di verifica di metriche di qualità del software, quale ulteriore step di validazione del processo di compilazione.
Copertura del ciclo di vita del software
Il ciclo di vita del software viene coperto fino al deploy per esempio introducendo nella configurazione specifici task di Ant (deploy di EAR su application server locali o remoti).
Log4J
Cruise Control è integrato nativamente con Log4J, con gli evidenti vantaggi che ciò comporta.
Installazione e configurazione del progetto
Dopo aver installato Cruise Control, operazione ampiamente descritta nella documentazione ufficiale, per avviare un’istanza di CruiseControl, è necessario lanciare dalla cartella di installazione l’eseguibile cruisecontrol.bat . Contestualmente CruiseControl lancia una istanza di Jetty in un thread separato. È questo il web container dove viene eseguita la web application di controllo dello stato delle compilazioni (http://localhost:8080/cruisecontrol) e quella che pubblica la copia locale della documentazione (http://localhost:8080/documentation).
Riportiamo il contenuto del file cruisecontrol.bat così come viene rilasciato nel bundle di distribuzione della versione 2.7 scaricabile dall’indirizzo [9]
REM Set this if you're using SSH-based CVS REM set CVS_RSH= REM Uncomment the following line if you have OutOfMemoryError errors REM set CC_OPTS=-Xms128m -Xmx256m REM The root of the CruiseControl directory. The key requirement is that this is the parent REM directory of CruiseControl's lib and dist directories. REM By default assume they are using the batch file from the local directory. REM Acknowledgments to Ant Project for this batch file incantation REM %~dp0 is name of current script under NT set CCDIR=%~dp0 :checkJava if not defined JAVA_HOME goto noJavaHome set JAVA_PATH="%JAVA_HOME% injava" set CRUISE_PATH=%JAVA_HOME%lib ools.jar goto setCruise :noJavaHome echo WARNING: You have not set the JAVA_HOME environment variable. Any tasks relying on the tools.jar file (such as ) will not work properly. set JAVA_PATH=java :setCruise set LIBDIR=%CCDIR%lib set LAUNCHER=%LIBDIR%cruisecontrol-launcher.jar set EXEC=%JAVA_PATH% %CC_OPTS% -Djavax.management.builder.initial =mx4j.server.MX4JMBeanServerBuilder -jar "%LAUNCHER%" %* -jmxport 8000 -webport 8080 -rmiport 1099 echo %EXEC% %EXEC%
Tra le variabili, attraverso CC_OPTS è possibile definire l’allocazione minima (-Xms) e massima (-Xmx ) dello heap della JVM, in relazione alla quantità di memoria del calcolatore che ospita il processo Cruise Control. Ciascun parametro è ampiamente descritto nella documentazione ufficiale che li organizza in tre sezioni relative alle opzioni standard, alle opzioni delle web application e opzioni del layer JMX. Di seguito presentiamo uno specchio riassuntivo.
Di seguito diamo una breve descrizione dei file e delle cartelle che si trovano nel percorso di installazione di CuiseControl.
Ed ecco una illustrazione con i moduli del progetto:
Figura 4 – Moduli Java EE del progetto
La documentazione ufficiale [2] illustra tutti gli script Ant necessari alla realizzazione di uno scenario di build standard definito dal file config.xml. Non riportiamo esempi per non annoiare il lettore vista la vasta diffusione su Internet di script relativi ai task di aggiornamento dell’SCM, compilazione dei sorgenti ed export J2EE. Diamo invece un interessante esempio di integrazione con un plug-in, di un task di non regressione e di un task che distribuisce i moduli Java EE su un application server remoto.
Integrazione con Apache Ivy
Nello scenario che abbiamo descritto, la realizzazione delle librerie comuni è assegnata a uno specifico gruppo di lavoro. In teoria, solo al rilascio di queste librerie, o per lo meno delle loro interfacce, inizia l’attività dei gruppi che sviluppano le componenti applicative. Questa modalità organizzativa, però, viene spesso disattesa: nell’ambito del progetto potranno quindi coesistere versioni multiple degli stessi componenti comuni utilizzabili anche in fasi diverse del progetto.
Un approccio per la risoluzione delle dipendenze potrebbe essere quello di fare continue baseline di queste librerie sull’SCM di progetto; queste, marcate da opportuni meta informazioni, individuano il set delle librerie comuni compatibili con il modulo software che vogliamo compilare. Si tratta però di un processo piuttosto oneroso difficilmente governabile in presenza di complesse matrici di dipendenze. Vediamo come attraverso l’integrazione di Apache Ivy nelle direttive Ant è possibile risolvere questo problema in modo più elegante e robusto.
Diamo adesso un flash operativo; innanzi tutto è necessario modificare il file di avvio di CruiseControl per includere le librerie di Ivy:
set CRUISE_PATH=ivy.jar; ivycruise.jar;%ANT_HOME%libxercesImpl.jar;
Nel file ivy.xml dichiariamo quali sono le dipendenze del nostro componente.
Dichiariamo il namespace di Ivy e includiamo un target che referenzia il tag . Questa rappresenta la direttiva che risolve le dipendenze, cercate nel repository definito per Ivy, copiando le librerie individuate sul percorso dir.lib, prima che venga invocata la direttiva di compilazione.
name="moduloConDipendenzeRisolteDaIvy" default="build">
Il percorso di risoluzione delle librerie da cui dipende il nostro componente viene dichiarato nel file ivyconf.xml.
/[module]/[revision] /[artifact]-[revision].[ext]"/> [module]/[revision]/[artifact]-[revision].[ext]"/> [module]/[revision]/ivy-[revision].xml"/>
Abbiamo quindi definito una catena con due resolver. Il primo cerca tulle le versioni delle librerie nella directory ${ivy.conf.dir}/compilati. Il secondo le cerca su un percorso web dove potrebbero essere state pubblicate.
Per specificare il puntamento a questo file lo script di build deve definire una proprietà ivy.conf.dir.
A questo punto, dipendente dal target jar, non ci resta che mettere a disposizione il modulo che abbiamo compilato attraverso la direttiva .
resolver="libraryResolver" pubrevision="${version}" status="release"/> ${ant.project.name}; Versione: ${version}"/>
Notiamo nel fragment che il componente compilato è caratterizzato da una versione univoca ottenuta attraverso il task di Ant che incrementa automaticamente la versione, referenziata nel nostro esempio dalla variabile $build.number.
Task di non regressione
Il task di non regressione prevede l’esecuzione sul database di riferimento delle DDL che preparano i dati in relazione ai test JUnit che dobbiamo lanciare. Banalmente possiamo inserire un task di Ant che elimina i dati presenti nelle tabelle seguito da uno che le carica. In questa maniera al test JUnit che segue diamo la consistenza voluta. Riportiamo di seguito alcuni esempi.
Allineamento database
.jar"/> userid="userName" password="password" url="jdbc:oracle:thin:@:1521:ORA001" delimiter=";" classpathref="lib_reference_build"> userid="userName" password="password" url="jdbc:oracle:thin:@:1521:ORA001" delimiter=";" classpathref="lib_reference_build">
Legenda delle variabili
${lib.oracle.dir}
è il path del driver JDBC di Oracle;
${sql.oracle.script.dir}
è il path delle DDL di aggiornamento del database.
Lancio dei JUnit
<!—PROPRIETA' PER LA LIBRERIA CON I JUNIT DA ESEGUIRE <!— PATH DELLA LIBRERIA CON I JUNIT DA ESEGUIRE <!— TASK ANT DI ESECUZIONE DEI JUNIT
Task di distribuzione su un server remoto
Di seguito riportiamo il task Ant di distribuzione dei moduli Java EE su un application server remoto WebSphere.
Nel nostro caso il prerequisito alla sua esecuzione è avere a disposizione sulla workstation un profilo WebSphere con una versione compatibile con quella dell’application server remoto. Per il nostro esempio utilizziamo un profilo WebSphere 6.1.
Propedeutico all’avvio del task di Ant è l’aggiornamento del keystore locale di WebSphere. Questo viene fatto lanciando il comando wsadmin dal percorso di installazione del profilo di WebSphere () indicando i puntamenti e le credenziali del server remoto.
untimes ase_v61profilesAppSrv01 in>wsadmin.bat -conntype SOAP -host -port DN soggetto: CN=, O=IBM, C=US DN emittente: CN=, O=IBM, C=US Numero di serie: 1234567890 Scadenza: Wed Apr 28 11:45:59 CEST 2010 Digest SHA-1: 7F:C1:98:C9:97:55:2A:21:DF:10:21:DB:47:87:44:89:CB:04:88:90 Digest MD5: B6:68:D7:91:B6:DF:9C:20:42:91:22:ED:DE:F8:FE:0C [exec] Aggiungere ora il firmatario all'archivio trust? (s/n)
A questo punto il keystore locale al percorso
untimes ase_v61profilesAppSrv01etc rust.p12
è aggiornato e da questo momento sarà possibile lanciare da questa workstation i comandi di amministrazione remota del server WebSphere “trusted”; solo adesso sarà possibile integrare questi script nella configurazione di Cruise Control che saranno eseguiti a valle dell’esportazione dei moduli Java EE.
Riportiamo di seguito alcuni script di esempio.
deployWasCfg.xml
DeployApp.jacl
$AdminApp uninstall $AdminApp install /.ear { -appname -MapWebModToVH { { "" .war,WEB-INF/web.xml default_host } } -MapModulesToServers { { "" .jar,META-INF/ejb-jar.xml WebSphere_cell=HostNameCell, node=HostNameNodeStdAlone, server=server1 } { "" .war,WEB-INF/web.xml WebSphere_cell=HostNameCell, node=HostNameNodeStdAlone, server=server1 } } } $AdminConfig save
Legenda delle variabili
è il percorso locale della workstation dove si trovano gli Ear da distribuire;
è il nome del modulo web definito nel descrittore di deployment web.xml .war;
è il nome del modulo ejb definito nel descrittore di deployment ejb-jar.xml .jar;
HostNameCell
è la cella di deploy di WAS;
HostNameNodeStdAlone
è il nodo di WAS definito nella cella;
Server
è il server di WebSphere definito nel nodo.
Deploy.properties
websphere.host.name= websphere.soap.port= project.name= websphere.user= websphere.user.pwd= deploy.ear.jacl=/DeployStartApp.jacl
Conclusioni
Mossi dall’esigenza di integrare con efficienza i nostri componenti applicativi, abbiamo esteso il campo d’applicazione di CruiseControl a importanti fasi di progetto quali il test di non regressione, e la distribuzione dei moduli Java EE. Il risultato è un tentativo di descrivere un esempio di orchestrazione di framework a supporto delle fasi del ciclo di vita del software scelto per il nostro progetto.
Questo approccio però non può considerarsi un vestito buono per tutte le stagioni. Prima di introdurre un sistema simile è necessario fare una attenta valutazione sulla sua convenienza globale. Chiaramente tanto più è complesso il nostro progetto software in ordine ai componenti software, alla loro dipendenza e al grado di parallelismo scelto, tanto maggiori saranno i benefici derivanti dall’aver industrializzato le fasi che ne costituiscono il ciclo di vita.
Saremo in questa maniera in grado di rientrare dall’investimento in termini di risorse umane e strumentali necessarie alla messa in esercizio e alla gestione di questo sistema.
Riferimenti
[1] Continuous Integration
http://www.martinfowler.com/articles/continuousIntegration.html
[2] Ant
http://ant.apache.org/manual/tasksoverview.html
[3] Cruise Control
http://cruisecontrol.sourceforge.net/
[4] Documentazione Cruise Control
http://confluence.public.thoughtworks.org/display/CC/Home
[5] Estensioni verso terze parti
http://confluence.public.thoughtworks.org/display/CC/3rdPartyCCStuff
[6] Apache Ivy
http://ant.apache.org/ivy/index.html
http://studios.thoughtworks.com/2007/9/24/keep-your-dependencies-to-yourself
http://asantoso.wordpress.com/2008/02/27/continuous-integration-with-cruisecontrol-271-clearcase/
[7] CheckStyle
http://checkstyle.sourceforge.net/
[8] JUnit
[9] Eseguibile versione 2.7