août
2012
Intégrer RPM avec Maven et Jenkins 2/2
Tags : devops, jenkins, linux, maven, packaging natif, rpm
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
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é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 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.


Commentaires
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 :)