Intégrer RPM avec Maven et Jenkins 2/2
Après avoir découvert avec vous le rpm-maven-plugin, je vous propose de continuer l’immersion dans le packaging natif avec le même cocktail Maven-RPM, toujours servi par Jenkins. Cette fois, je vais par contre présenter une approche plus en phase avec ce que j’ai pu expérimenter sur le terrain. C’est donc parti pour un petit retour d’expérience DevOps.
Contexte du projet
Si vous êtes normalement constitué, la première partie devrait vous avoir laissé un goût de c’est-super-j’ai-fait-une-assembly-mais-pas-vraiment-et-avec-un-peu-de-magie-noire-j’ai-eu-un-RPM (enfin j’exagère peut-être une peu). On va donc se construire un RPM (toujours Sirkuttaa ) mais cette fois en prenant le problème sous un autre angle.
Mon expérience personnelle porte sur le packaging d’une application WEB Java déployée sur un serveur Tomcat, avec un Varnish en frontal. Il a donc fallu packager Tomcat et Varnish, en plus de l’application. Les besoins en terme d’exploitation ont émergé progressivement, ce qui m’a permis d’absorber facilement les demandes sachant que je découvrais rpmbuild. Au final le RPM Tomcat n’est qu’une coquille vide contenant binaires et scripts, le RPM Varnish a été forké à partir du RPM pour RHEL et le RPM applicatif contient au final l’essentiel de l’intelligence (non exhaustif) :
des scripts variés (init.d, contrôle de l’instance …)
divers fichiers (binaires, configuration, documentation …)
la glue avec l’installation de Tomcat (CATALINA_BASE
)
des scriptlets pour gérer
- les montés de version
- l’enregistrement des services au démarrage du système
Une des contraintes était de ne pas utiliser de plugin Maven et d’invoquer explicitement rpmbuild, en partant d’un template de spec RPM (chose amusante, les RPM partis en production n’ont plus rien de commun avec le template de départ). La première difficulté était l’intégration de deux outils de build.
Plusieurs questions se posent :
- quel point d’entrée pour le build ?
- comment gérer les montés de version ?
- intégrer Maven à la spec RPM ou les invoquer séparément ?
- …
Le contexte de Sirkuttaa étant différent de mon expérience en projet (eg. jar vs war), j’ai décidé de m’attaquer au problème en partant de zéro. Mais avant d’y répondre, allons explorer la spec RPM et les différentes sections qu’on y trouve.
Ce que le rpm-maven-plugin nous cache
Je l’ai dit précédemment, une des choses que je n’apprécie pas avec l’utilisation d’un plugin Maven, c’est l’effet boîte noire qui nous masque ce qui se passe sous le capôt. On voit des logs rpmbuild défiler dans la console, encore faut-il être attentif. Tout est masqué donc, par un genre d’assembly… vraiment ?
Et si on jetait un coup d’oeil au fichier ${project.build.directory}/rpm/${project.artifactId}/SPECS/${project.artifactId}.spec
? Après tout, si je suis parti d’un template, pourquoi pas vous ?
Name: sirkuttaa Version: 1 Release: 1 Summary: sirkuttaa License: GPLv2+ Group: Zenika/Blog Packager: Zenika Requires: java autoprov: yes autoreq: yes BuildRoot: /path/to/repo/target/rpm/sirkuttaa/buildroot %description %files %defattr(644,root,root,755) /usr/lib/sirkuttaa %attr(755,root,root) /usr/bin/sirkuttaa %config(noreplace) /etc/sirkuttaa/default.properties %config(noreplace) /etc/sysconfig/sirkuttaa
Hum, c’est vide… Pourquoi ? Parce que le plugin s’occupe de tout et prémache une spec minimaliste à rpmbuild. On trouve la fiche d’identité du paquet, une description vide (parce que je n’en ai pas défini dans le pom.xml) et la liste des fichiers et des droits.
La section %files
On trouve d’abord les attributs par défauts :
- droits d’accès des fichiers
- utilisateur propriétaire
- groupe propriétaire
- droits d’accès des dossiers
Vient ensuite la liste des fichiers et dossier à embarquer, qui peuvent être modifiés au cas par cas : on ajoute par exemple des droits d’exécution sur le script de lancement de Sirkuttaa. Par défaut, tous les fichiers déclarés doivent être présents dans la buildroot, et réciproquement, tout fichier dans la buildroot doit être déclaré ici.
Ici le plugin prend le parti d’embarquer le dossier contenant les JAR plutôt que de les déclarer un par un. Le risque est d’embarquer dans le RPM des fichiers qui n’étaient pas prévus. Dans le cas d’un mvn clean package ce n’est pas grave, mais maintenant que nous allons construire le RPM à la main on va s’assurer de déclarer chaque fichier pour éviter les mauvaises surprises (mvn dependency:tree
…), et puis si on oublie de mettre à jour la liste Jenkins ne manquera pas de nous le rappeler, c’est aussi son rôle.
%files %defattr(644,root,root,755) /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-1.jar %attr(755,root,root) /usr/bin/sirkuttaa %config(noreplace) /etc/sirkuttaa/default.properties %config(noreplace) /etc/sysconfig/sirkuttaa
La fiche d’identité
En dehors des fichiers, j’en profite pour compléter la liste des tags en début de spec, pour ajouter l’architecture cible, et une petite description. J’en profite aussi pour retirer certains tags qui, d’expérience, ne me serviront pas pour Sirkuttaa.
Name: sirkuttaa Version: 1 Release: 1 BuildArch: noarch Summary: The famous CLI Twitter client License: GPLv2+ Group: Zenika/Blog Packager: Zenika Requires: java %description The ultimate command line experience for Twitter written in Java.
Maintenant, nous allons pouvoir renseigner les différentes sections de notre spec et créer notre RPM, mais avant ça, un petit mot sur les macros.
Le cauchemar des macros
RPM propose un système de macros puissant, mais déroutant. Dans la première partie j’ai présenté quelques sections et leur syntaxe. On trouve par exemple %prep
. Dans la section %files
, on trouve aussi des directives dont la syntaxe est identique comme %dir
ou paramétrée comme %attr(750,root,root)
. En plus des sections on peut déclarer des scriptlets, encore avec la même syntaxe (eg. %post
).
A cela nous pouvons ajouter les macros, toujours avec des syntaxes similaires :
%une_macro
%{une_macro}
%(du shell à exécuter)
On a beau utiliser systématiquement un %
, on s’y retrouve très rapidement entre sections, scriptlets et macros ;). Avec un bon éditeur de texte (emacs, vim voire gedit) la coloration syntaxique facilite encore plus la lecture d’une spec.
Chaque installation de rpmbuild vient avec son lot de macros prédéfinies, pour faciliter certaines opérations et les rendre portables. On trouvera traditionnellement les macros %setup
et %configure
invoquées dans la section %prep
, la macro %__make
invoquée dans la section %build
, parfois une exécution de %__make check
dans la section %check
et la macro %makeinstall
dans la section %install
. Sauf qu’ici on utilise Maven, donc, pourquoi ne pas créer nos propres macros pour garder ce niveau de simplicité ?
Intégration du build Maven
Un des inconvénients du packaging natif, c’est qu’il s’intègre moins bien avec les plateformes de plus haut niveau. On trouve par exemple en java les packaging < acronym title=”Java ARchive”>JAR ou WAR, Maven va chercher lui-même ses dépendances, Ruby propose un système de gems… Je devrais donc en théorie ajouter une dépendence (BuildRequires
NDLR) vers Maven, mais non. Trop de complications, je préfère partir de l’hypothèse qu’un développeur installera lui-même Maven, et que côté CI c’est Jenkins qui mettra mvn
dans le PATH
. Tiens, Jenkins, encore une plateforme qui vient avec son propre packaging pour ses plugins[1] ;).
Cette petite digression terminée, on commence une intégration de Maven à la make. On va donc invoquer mvn
dans les sections %prep
, %build
et %install
. Il faudra aussi transmettre à Maven le numéro de version, et sans version, pas de build : premier réflexe, utiliser les mécanismes de properties. Pour émuler la macro %makeinstall
, je n’ai rien trouvé de mieux qu’une assembly de type répertoire pour aller copier les fichiers directement dans la build root. Pour que le build Maven fonctionne en dehors de l’exécution de rpmbuild, j’ajoute des propriétés par défaut :
<properties> <project.rpm.version>dev</project.rpm.version> <project.rpm.installDirectory>${project.build.directory}/${project.artifactId}</project.rpm.installDirectory> <project.rpm.appendAssemblyId>true</project.rpm.appendAssemblyId> </properties>
Côté rpmbuild, on transmet les propriétés en ligne de commande, et pour une intégration à la make je me crée quelques macros avec la macro %define
:
%define mvn_opts -Dproject.rpm.version=%{version}-%{build_id} \\\ -Dproject.rpm.installDirectory=%{buildroot} \\\ -Dproject.rpm.appendAssemblyId=false %define mvn mvn %{mvn_opts}
Il ne reste plus qu’à invoquer la macro %mvn
, et j’ai opté pour le mapping section-phase suivant :
%prep %{mvn} clean %build %{mvn} package %install %{mvn} verify
La configuration du maven-assembly-plugin avec les propriétés définies ci-dessus :
<plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.3</version> <configuration> <attach>false</attach> <finalName>/</finalName> <appendAssemblyId>${project.rpm.appendAssemblyId}</appendAssemblyId> <outputDirectory>${project.rpm.installDirectory}</outputDirectory> <descriptors> <descriptor>src/main/assembly/rpm.xml</descriptor> </descriptors> </configuration> <executions> <execution> <phase>verify</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
Et enfin l’assembly correspondante (allégée grâce à la section %files
de la spec) :
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd"> <id>rpm</id> <formats> <format>dir</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <fileSets> <fileSet> <directory>src/main/scripts</directory> <outputDirectory>/usr/bin</outputDirectory> </fileSet> <fileSet> <directory>src/main/config</directory> <outputDirectory>/etc</outputDirectory> </fileSet> </fileSets> <dependencySets> <dependencySet> <outputDirectory>/usr/lib/${project.artifactId}</outputDirectory> </dependencySet> </dependencySets> </assembly>
J’ai prise le parti d’invoquer %mvn verify
dans la section %install
car aucune section ne lui correspond réellement. En attendant la dernière phase pour générer l’assembly, c’est du temps gagné côté développement lorsqu’on veut juste construire son JAR/WAR ou exécuter les tests d’intégration. Les phases install
et deploy
perdent aussi de l’intérêt avec cette aproche. C’est plutôt le RPM que je chercherai à archiver. Côté intégration continue d’ailleurs, le principe de snapshot Maven a disparu, et n’existe plus que du côté développeur.
Gérer les versions
La gestion des versions n’est pas vraiment abordée dans cet article, et dans le cas de Sirkuttaa, c’est en dur dans la spec. Attention cependant, le numéro de version complet se compose du duo %{version}-%{release}
. Pour la partie release on peut utiliser la variable d’environnement BUILD_NUMBER
de Jenkins.
Pour plus de fiabilité, je vais utiliser la variable BUILD_ID
. On pourrait récupérer cette valeur facilement avec la macro %(echo $BUILD_ID)
, mais je préfère créer un script de build indép
endant qui puisse être exécuté en dehors de Jenkins. Au passage, l’utilisation du plugin ZenTimestamp devient indispensable car par défaut le BUILD_ID
contient des tirets.
Le seul réel point noir dans la gestion des versions, c’est que Maven ne fonctionne pas sans. De mon point de vue, le périmètre de Maven est beaucoup trop large et certains aspects (même lorsqu’ils sont issus de plugins) débordent du simple build (au hasard les releases ou la création de site). Du coup Maven nous averti qu’il n’apprécie pas du tout notre hack et qu’à terme il ne fonctionnerait plus :
[INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for com.zenika.blog.rpm:sirkuttaa:jar:1-SNAPSHOT [WARNING] 'version' contains an expression but should be a constant. @ com.zenika.blog.rpm:sirkuttaa:${project.rpm.version}, pom.xml, line 10, column 11 [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed projects. [WARNING]
En attendant certains outils de build comme Gradle s’en sortent très bien avec un numéro de version transmis en ligne de commande, voire même sans version ;).
Script de build
A la racine d’un projet, j’apprécie la présence de quelques scripts utilitaires. Par exemple pour générer la configuration de l’IDE quand celui-ci ne sait pas importer directement un projet Maven. Dans le cas du build, le script a deux utilités : reproduire un build local et simplifier la configuration du job de construction sur le serveur d’intégration continue. Toujours dans une optique DevOps, le développeur doit pouvoir construire facilement son paquet natif s’il y apporte des modifications. Au lieu d’exécuter mvn
, on exécutera ./build.sh
pour reconstruire un RPM “snapshot”.
Intégration avec Maven
Une chose qu’il faut savoir avec rpmbuild, c’est que le répertoire de travail (par exemple ~/rpmbuild
) est partagé par défaut par les différentes specs, il correspond à la macro %_topdir
. Dans l’arborescence, certains dossiers seront partagés par les différentes specs alors que d’autres seront spécifiques à une version spécifique d’une spec. Une des choses que j’apprécie avec Maven, c’est que tout se passe par défaut dans un dossier target
et qu’on peut supprimer (mvn clean
) ce dossier en toute sécurité. On va donc faire pareil pour notre construction de RPM (ce que fait le rpm-maven-plugin au passage) mais de façon moins radicale : on ne redéfinit que les dossiers utilisés (macros %_rpmdir
et %_builddir
).
Intégration avec Jenkins
L’intégration avec Jenkins consiste en fait à réutiliser l’environnement fourni par Jenkins. La variable BUILD_ID
a déjà été évoquée, j’utilise également la variable WORKSPACE
pour atteindre le dossier $WORKSPACE/target
de Maven. Par défaut rpmbuild va créer le RPM dans un sous-dossier noarch
, qui correspond à l’architecture cible. Une dernière chose, je configure manuellement l’archivage des RPM générés avec le pattern target/noarch/*.rpm
.
build.sh
#!/bin/sh set -e if [ -z "$WORKSPACE" ] ; then case "$0" in /*) WORKSPACE="`dirname $0`" ;; *) WORKSPACE="`pwd`/`dirname $0`" ;; esac fi rpmbuild -bb $WORKSPACE/sirkuttaa.spec \ --define "_builddir $WORKSPACE" \ --define "_rpmdir $WORKSPACE/target" \ --define "build_id ${BUILD_ID:-SNAPSHOT}"
Conclusion
Et voilà, nous avons gratté la surface du packaging natif avec RPM. J’apprécie beaucoup la simplicité d’une spec RPM (une fois la syntaxe des sections/scriptlets/macros assimilée) en particulier après avoir eu affaire à des pom.xml
sur des projets de moyenne ou grande envergures. Le packaging natif est puissant, mais n’est pas une fin en soit. Certaines limitations peuvent être un frein à la distribution au grand public, mais dans un environnement maîtrisé, ce n’est pas un problème. C’est aussi un socle solide, mais trop bas niveau et limité à la machine cible. Pour une vraie infrastructure de déploiement automatisable, une surcouche ajoutant des possibilités “réseau” sera nécessaire. J’ai cité YUM qui ajoute la notion de repository, mais d’autres outils radicalement différents comme Chef permettent de gérer des RPM.
Pour ce qui est de l’intégration avec Maven, je pense que le rpm-maven-plugin est la solution la plus adaptée. Le build n’est cependant plus portable, et je trouve ça dommage lorsqu’on fait du java. Avoir un build qui ne soit pas portable n’est pas gênant en soit dans un projet d’entreprise, mais toujours dans une optique DevOps, ça le devient lorsque les développeurs travaillent sous Windows. Il y a de toute façon des choses à redire dans les deux approches que j’ai présentées.
Jenkins de son côté ne sert pas à grand chose. De toute façon, Jenkins se résume (j’exagère un peu) à un orchestrateur de jobs, des notifications et un système de plugins… On pourrait par contre pousser l’intégration beaucoup plus loin en testant l’installation, la mise à jour à partir de la version en production et exécuter une scriptlet %verifyscript
. Le tout dans une VM créée à la volée.
Enfin, comment parler d’une intégration Maven/RPM sans évoquer dpkg ? Pour construire des paquets Deb, on trouve plusieurs plugins. Le maven-deb-plugin qui fonctionne comme le rpm-maven-plugin, c’est un wrapper de l’outil dpkg. Une alternative intéressante existe, le plugin jdeb qui propose une implémentation 100% en Java qui règle les problèmes de portabilité.
Pour le mot de la fin, l’intégration avec Maven est plutôt bancale, mais représente peu d’effort pour résultat très satisfaisant. RPM quant à lui est vraiment un outil puissant qui permet de faire bien plus que le peu que j’ai présenté.
Pour démarrer avec RPM, le site rpm.org est un bon début.
Les sources de l’article sont récupérables sur github.
Deuxième partie toute aussi excellente que la première.
Pour finir la boucle ne manque plus qu’un article sur la construction d’un repository Yum avec les packages crées pour leur utilisation dans des environnements de Dev, QA ou production via yum/zypper.
Et il y aura donc une chaine Continuous Integration to Continuous Deployment complète 🙂
En attendant de faire du déploiement continu, j’ai ajouté un lien vers les sources de la seconde partie 🙂