I. 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 WEBWorld Wide Web, yeah baby 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 termes 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 finalement 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ées 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.
II. 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 capot. 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'œil 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émâche 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.
II-A. 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
II-B. 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.
II-C. 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).
À 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é ?
II-D. 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 packagings JARJava ARchive ou WARWeb ARchive, 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épendance (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é CIContinuous Integration c'est Jenkins qui mettra mvn dans le PATH. Tiens, Jenkins, encore une plateforme qui vient avec son propre packaging pour ses plugins.
Jenkins propose beaucoup de paquets natifs, y compris des RPM.
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 pris 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 approche. 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.
II-E. 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és, 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épendant 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 avertit 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.
III. Script de build▲
À 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 ».
III-A. 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).
III-B. 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.
III-C. 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}"
IV. 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 soi. 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 VMVirtual Machine 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.
V. Remerciements▲
Cet article a été publié avec l'aimable autorisation de Dridi Boukelmoune. L'article original (Intégrer RPM avec Maven et Jenkins 2/2) peut être vu sur le blog/site de Zenika.
Nous tenons à remercier zoom61 pour sa relecture orthographique attentive de cet article puis Mickael Baron et mlny84 pour la mise au gabarit.