I. À la découverte de RPM▲
Cet article suppose une connaissance préalable de Maven, Jenkins sert surtout à illustrer la partie intégration continue. Avant de mettre les mains dans le code, je vous propose un tour d'horizon de RPM.
Pour commencer, RPM signifie RPM Package Manager (très geek le nom récursif) et fait partie de la LSBLinux Standard Base en tant que gestionnaire de paquets officiel. Les puristes préfèreront le packaging Deb, mais pour des environnements cibles RHELRed Hat Entreprise Linux, le choix est vite fait. Le packaging natif apporte plus qu'une simple archive (zip, tar.gz, jar, war…) c'est un mélange de fichiers, de métadonnées et de scripts, une archive autonome et autosuffisante (aux dépendances près). Dans le cas de RPM, on dispose de plusieurs outils en ligne de commande (principalement la commande rpm) pour exploiter ces paquets. Outre les classiques installations et désinstallations, il est possible d'extraire beaucoup d'informations à partir d'un paquet.
Côté exploitation donc, on utilisera rarement la commande rpm directement, on lui préfèrera généralement des surcouches. La plus connue est probablement YUMYellowdog Updater Modified qui propose en ligne de commande un jeu d'instructions plus humain et de plus haut niveau (eg. yum install <nom> à la place de rpm -i <fichier>) et permet surtout de récupérer automatiquement les dépendances. Il n'est pas forcément nécessaire de passer par de la ligne de commande, il existe des surcouches Web, et certains outils comme Chef sont capables de travailler avec des RPM. À noter qu'il est recommandé de signer ses RPM, en particulier pour un usage sur environnement cible.
I-A. Socle d'installation▲
Un fichier RPM, ce n'est pas juste des fichiers, des métadonnées et des scripts comme dit précédemment. C'est aussi des workflows d'exécution prédéfinis, signe d'une certaine maturité de l'outil. L'installation d'un paquet par exemple, passera par une série prédéfinie d'étapes qui correspondent chacune à l'exécution d'une scriptlet. On peut y faire à peu près ce qu'on veut, comme créer un utilisateur dédié à l'application, enregistrer un service au démarrage du système.
Worflow simplifié d'installation :
- exécution de la scriptlet %pre;
- copie des fichiers ;
- exécution de la scriptlet %post.
Worflow simplifié de désinstallation :
- exécution de la scriptlet %preun;
- suppression des fichiers ;
- exécution de la scriptlet %postun.
Les scriptlets ne sont exécutées que si elles existent dans le RPM, et pour en savoir plus sur la construction de RPM, nous allons nous intéresser à la commande rpmbuild.
II. Construire des RPM▲
La construction de RPM n'est pas un exercice difficile en soi, il nécessite cependant quelques connaissances système. La difficulté principale, se situera dans les paquets à créer, au cas par cas, en fonction de ce qu'on cherche à réaliser. Par exemple, comment réaliser une mise à jour (eg. yum upgrade ou rpm -U) d'une application en cours d'exécution ? C'est à mon sens un des points-clés du DevOps : certaines questions de packaging pourront être adressées par les développeurs, d'autres en revanche nécessiteront une collaboration avec d'autres acteurs (administrateurs système, DBA, etc.).
La commande rpmbuild donc, s'appuie sur des fichiers spec qui décrivent au moins un paquet. Le fichier spec est l'équivalent de notre pom.xml, et va contenir les éléments suivants :
- une (des) fiche(s) d'identité ;
- les relations de dépendances (dans les deux sens) ;
- des scripts ;
- un changelog.
On distinguera plusieurs types de scripts, ceux exécutés au build-time, et ceux embarqués dans le RPM, les scriptlets.
Le cycle de vie de construction passe par les sections suivantes, l'équivalent des phases de Maven :
- %prep -> à peu près process-sources ;
- %build -> compile ;
- %check -> test ;
- %install -> à peu près prepare-package (en adoptant cette correspondance entre sections rpmbuild et phase Maven on peut faire une croix sur les tests d'intégration) ;
- %files -> particulier, on peut l'assimiler de loin à verify ;
- %clean -> n'est pas un cycle à part entière comme pour Maven.
Je ne vais pas détailler l'ensemble aujourd'hui, parce qu'il y a beaucoup à dire, et qu'aujourd'hui nous allons utiliser le rpm-maven-plugin qui nous masque l'invocation de rpmbuild et la création de la spec RPM.
On notera cependant qu'il existe une étape de construction dans rpmbuild qui s'utilise traditionnellement avec les outils make, automake et autoconf. J'ai donc identifié deux intégrations possibles entre Maven et RPM :
- naturelle du point de vue RPM : invocation de mvn dans la section %build;
- naturelle du point de vue Maven : extension du packaging à l'aide d'un plugin dédié.
Chaque approche essaye de résoudre le conflit sur le point d'entrée du build, que les deux outils revendiquent de par leurs conceptions respectives. On notera cependant que la construction du fichier RPM sera toujours postérieure au build Maven.
III. Sirkuttaa▲
Afin d'illustrer la construction de RPM, il est nécessaire d'avoir plus qu'un simple livrable Java à construire. J'ai donc décidé de créer un client twitter en ligne de commande. Pour offrir une expérience utilisateur convenable, la ligne de commande ne doit pas commencer par java -jar ou nécessiter de faire passer toute la configuration à grand renfort de -Dsystem.properties. En termes de configuration, il doit être possible de choisir le nombre de tweets maximum à récupérer et de définir un timeout global.
Le paquet contiendra donc :
- un script dans /usr/bin;
- des jar dans /usr/lib;
- de la configuration dans /etc.
C'est ainsi qu'est né Sirkuttaa, fruit d'un intense brainstorming avec Google Traduction. Après être parti à la découverte des APIApplication Programming Interface Twitter (oui sirkuttaa est mon hello world Twitter :), en l'espace de quelques classes j'obtenais mon client en ligne de commande. Il ne restait donc plus qu'à attaquer (enfin !) la création du RPM. C'est donc armé d'un moteur de recherche, que je suis allé voir ce que Maven a à m'offrir.
III-A. Plugins Maven▲
N'ayant jamais construit de RPM à l'aide de Maven, un petit travail de recherche était nécessaire. J'ai rapidement trouvé le maven-rpm-plugin (il existe un second maven-rpm-plugin que je n'ai pas testé) et le rpm-maven-plugin. Aucun de ces plugins n'est activement maintenu, la mise à jour la plus récente remonte à 2010, une version alpha !
Après quelques tests, je découvre que ces plugins sont en fait des wrappers à la commande rpmbuild, qui fonctionnent un peu comme le maven-assembly-plugin. La documentation est aussi assez claire là-dessus : l'utilisation de ces plugins suppose une connaissance préalable dans la construction de RPM. Cela dit, cette connaissance est à relativiser. Pour des paquets simples, je pense que ce n'est pas critique. Par contre c'est indispensable lorsqu'on commence à utiliser des mécanismes plus avancés comme les scriptlets. Le rpm-maven-plugin semble cependant se démarquer : dernière mise à jour plus récente, et c'est celui évoqué dans toutes les présentations que j'ai vues sur le sujet. J'ai en toute logique choisi de l'utiliser.
À l'utilisation, le plugin s'avère un peu bancal. Le vocabulaire adopté n'est pas toujours celui de RPM (par exemple license devient copyright). Ensuite, pas sa conception, le plugin mélange maladroitement les sections %install et %files.
<plugin>
<groupId>
org.codehaus.mojo</groupId>
<artifactId>
rpm-maven-plugin</artifactId>
<version>
2.0.1</version>
<configuration>
<name>
sirkuttaa</name>
<group>
Zenika/Blog</group>
<packager>
Zenika</packager>
<needarch>
noarch</needarch>
<copyright>
GPLv2+</copyright>
<defaultFilemode>
644</defaultFilemode>
<defaultUsername>
root</defaultUsername>
<defaultGroupname>
root</defaultGroupname>
<defaultDirmode>
755</defaultDirmode>
<requires>
<require>
java</require>
</requires>
<mappings>
<mapping>
<directory>
/usr/lib/sirkuttaa</directory>
<dependency/>
<sources>
<source>
<location>
${project.build.directory}/${project.build.finalName}.jar</location>
</source>
</sources>
</mapping>
<mapping>
<directory>
/usr/bin</directory>
<directoryIncluded>
false</directoryIncluded>
<filemode>
755</filemode>
<sources>
<source>
<location>
src/main/scripts</location>
</source>
</sources>
</mapping>
<mapping>
<directory>
/etc</directory>
<directoryIncluded>
false</directoryIncluded>
<configuration>
noreplace</configuration>
<sources>
<source>
<location>
src/main/config</location>
</source>
</sources>
</mapping>
</mappings>
</configuration>
</plugin>
Et surtout, le plugin invoque explicitement la commande rpmbuild, ce qui faire perdre sa portabilité au build. Selon moi, je dois pouvoir arriver sur un projet Maven et exécuter mvn install, si ce n'est pas le fonctionnement par défaut (la convention), c'est que le build est à revoir (idéalement j'invoque juste mvn et une phase par défaut a été prévue). Le développeur doit pouvoir reproduire le build, et sous Windows ce n'est possible qu'avec Cygwin et son paquet rpm-build. Heureusement, Maven propose via ses mécanismes de profils de ne générer le RPM que si la commande rpmbuild est disponible, et en plus ce n'est pas verbeux :
<!-- un "simple" if file /usr/bin/rpmbuild exists then attach rpm -->
<profiles>
<profile>
<id>
rpmbuild</id>
<activation>
<file>
<exists>
/usr/bin/rpmbuild</exists>
</file>
</activation>
<build>
<plugins>
<plugin>
<groupId>
org.codehaus.mojo</groupId>
<artifactId>
rpm-maven-plugin</artifactId>
<executions>
<execution>
<phase>
package</phase>
<goals>
<goal>
attached-rpm</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Finalement, j'ai tout de même eu l'impression de me battre avec l'outil et de devoir faire sans arrêt des compromis, un peu comme avec JPA. Mais j'obtiens tout de même un RPM, dont l'installation se déroule correctement, et je suis capable d'utiliser Sirkuttaa. Je peux même exécuter quelques commandes pour par exemple connaître la liste des fichiers, voire la liste des fichiers de configuration :
$ rpm -ql sirkuttaa
/etc/sirkuttaa
/etc/sysconfig/sirkuttaa
/usr/bin/sirkuttaa
/usr/lib/sirkuttaa
/usr/lib/sirkuttaa/commons-io-2.3.jar
/usr/lib/sirkuttaa/jackson-core-asl-1.9.7.jar
/usr/lib/sirkuttaa/jackson-mapper-asl-1.9.7.jar
/usr/lib/sirkuttaa/sirkuttaa-1.jar
$ rpm -qc sirkuttaa
/etc/sirkuttaa
/etc/sysconfig/sirkuttaa
IV. Continuous Delivery▲
La construction du RPM en elle-même ne présente finalement pas tant d'intérêt que ça, si ce n'est que l'équipe de développement est impliquée très tôt dans la construction des livrables pour les environnements cibles. Si on ajoute une petite dose d'intégration continue à notre cocktail Maven-RPM, on obtient à tout moment un livrable vérifié, stable, prêt à être installé. On peut aller plus loin en signant automatiquement tout RPM construit, voire en le déployant automatiquement : rappelez-vous, un paquet natif est autonome (attention aux régressions dans la spec RPM, il faut de toute façon tester son RPM (installation, mise à jour…)).
La seule chose à prévoir du côté de Jenkins, c'est évidemment d'installer rpmpbuild. Le rpm-maven-plugin attache automatiquement le RPM généré au Reactor de Maven, il sera donc automatiquement archivé par Jenkins. Il sera également déployé (au sens Maven cette fois) automatiquement. On obtient donc clé en main de quoi faire de la livraison continue, sans avoir à fournir d'autre effort que créer un job de type Maven.
V. Conclusion▲
Arrivé ici, vous devriez normalement vous dire que finalement vous n'avez pas appris grand-chose sur RPM, c'est normal. Pour ma part j'ai découvert avec vous le rpm-maven-plugin et je dois avouer que je suis plutôt surpris. J'avais un a priori très négatif sur ce plugin, et je pense aujourd'hui que c'est probablement la solution la plus efficace pour construire des RPM avec un projet Maven. On obtient à moindre coût une infrastructure de livraison continue, sans effort important de configuration ! Ça ne dispense par contre pas de connaître rpmbuild et la construction classique de RPM, ce que je vous propose de découvrir dans un prochain article, avec à nouveau Sirkuttaa en guise d'exemple.
En attendant, vous pouvez toujours jouer avec Sirkuttaa :
$ mvn clean package
$ sudo rpm -i target/rpm/sirkuttaa/RPMS/noarch/sirkuttaa-1-1.noarch.rpm
$ sirkuttaa ZenikaIT
> Retour sur le #MongoDB Day Paris: http://t.co/M7uwPdgg, merci @10gen pour cet évènement!
> Bilan, photos et interviews vidéos des équipes de la #zNight, tout est ici : http://t.co/3XifivM9 #hackathon #GoogleTV #Android
> RT @alecharp: Prochain @HckrgartenParis chez @ZenikaIT. Une nouvelle édition pleine de bonne humeur et de commiter! http://t.co/sSw2ZRHu
> La 1re #zNight est finie, bravo à tous pour vos super idées #Android #GoogleTV, un grand, grand merci à #Google et #Sony pour leur soutien
> Plus que quelques minutes à la #zNight avant les démos des applis réalisées ! #Android #GoogleTV
> RT @rolios: Hey @googletvdev we have about 20 developers spending their night coding on #googleTV in France! Hope to get nice apps! #zni ...
> RT @queinnec: #GoogleTV by #Sony http://t.co/lb1yHLUq
> #Sony #GoogleTV at Zenika, admiring the remote featuring 2 sides, keyboard + classic remote… very nice looking! #Android #hackathon
> Zenika organise une conférence sur l'architecture de #Varnish jeudi; il reste quelques places, inscrivez-vous vite sur http://t.co/oHhUl5WU
> Y'a plein de boissons énergisantes :) RT @n0tnull: Ce soir hackathon sur une Google TV, ça va être funky vu le peu de sommeil... *_*
Les sources de l'article sont récupérables sur github.
VI. Remerciements▲
Cet article a été publié avec l'aimable autorisation de Dridi Boukelmoune. L'article original (Intégrer RPM avec Maven et Jenkins 1/2) peut être vu sur le blog/site de Zenika.
Nous tenons à remercier Claude Leloup pour sa relecture orthographique attentive de cet article puis Mickael Baron et mlny84 pour la mise au gabarit.