Comment nous avons migré plus de 350 jobs Maven en Pipeline as code avec Jenkins 2 et Docker !
Part1: Créer ses propres images Docker pour Jenkins
Il y a un an, chez eXo, nous avons pris la décision de construire tous nos projets dans des conteneurs Docker.
Dans cette série d’articles, nous vous expliquerons pourquoi et comment nous avons migré plus de 350 jobs Maven “standards” de Jenkins vers du Pipeline as code sur nos serveurs d’Intégration Continue avec Jenkins 2 et Docker.
C’est l’occasion de revenir sur les problèmes que nous avons rencontrés ainsi que les solutions apportées, et sur certaines bonnes pratiques autour des builds Maven / Gradle / Android dans des conteneurs Docker, le tout géré par du Pipeline as Code de Jenkins.
Comme toute migration technique importante, nous l’avons réalisée étape par étape. Les 3 grandes étapes ont été les suivantes:
- Créer nos propres images CI Docker
- Utiliser les plugins Jenkins Pipeline & Pipeline Docker
- Générer tous les jobs Pipeline avec le Job DSL plugin
Le diagramme suivant représente le flux de travail lorsque toutes ces étapes sont effectuées:
Dans cet article, nous détaillerons le contexte (pourquoi nous l’avons fait) et la première étape de cette migration sur la création de vos propres images CI Docker.
Qu’avions-nous à construire ?
eXo Platform est bâtie sur l’open-source et les standards ouverts. La plateforme est compatible avec la pile Java EE et s’appuie sur de nombreuses bibliothèques et composants open-source.
Pour différentes raisons que nous allons vous expliquer, nous avons à gérer un grand nombre de builds sur nos serveurs d’Intégration Continue .
Composants d’eXo Platform et Add-ons
La première raison est que nous avons des dépôts git pour chacun des composants requis dans les livrables des distributions d’eXo Platform:
- 25+ composants à construire
- projets pour la plateforme (cf. diagramme ci-dessous)
- projets Juzu…
- De nombreux Add-ons
- 15+ Add-ons supportés
- 100+ Add-ons communautaires
- Projets natifs pour le mobile
- application Android
- application iOS
Git Workflow
Nous utilisons un workflow git basé sur un modèle de branches que tous les projets eXo doivent suivre:
- develop : branche qui contient les derniers développements validés
- feature/xxx : branches dédiées aux nouvelles fonctionnalités majeures (de nombreux commits), “xxx” correspond au nom de la fonctionnalité.
- stable/xxx : branches utilisées pour faire des releases et pour corriger des problèmes sur des versions stables. “xxx” correspond au numéro de version stabilisée (e.g 1.0.x).
- fix/xxx : branches dédiées aux correctifs qui doivent être ensuite intégrés dans la branche develop. Dans certains cas, ces corrections peuvent être aussi appliquées sur la branche stable.
- integration/xxx : branches dédiées aux processus automatiques (exemple: processus de traductions…).
- poc/xxx : branches dédiées à la réalisation de Proof of Concept (PoC)
Clients et versions d’eXo Platform
Depuis près de 15 ans maintenant, eXo a publié de nombreuses versions d’eXo Platform. Avec eXo Platform 5 en cours de développement et les versions que nous devons maintenir pour nos clients (eXo Platform 4.x: 4.0, 4.1, 4.2, 4.3, 4.4), nous avons ainsi plusieurs versions de nos environnements de build à gérer.
Actuellement, nous devons être en mesure de construire des projets avec les versions JDK6, JDK7 ou JDK8 et Maven 3.0, Maven 3.2 ou Maven 3.3. L’application Android native est construite par Gradle.
Vous vous souvenez comment créer un job Maven dans l’interface Jenkins ?
Même si Jenkins 2 a fait de nombreux efforts pour l’améliorer, il est toujours contraignant de créer de nouveaux jobs Maven via l’interface utilisateur, comme vous pouvez le voir ci-dessous:
“Jenkins DSL et jobs Pipeline dans Docker à la rescousse !”
Pour ces différentes raisons, nous devions gérer beaucoup de jobs Maven à partir de l’interface utilisateur de Jenkins et maintenir nombre d’outils dans plusieurs versions (Maven, JDK …) sur les agents Jenkins ainsi que sur les postes de développeurs. Ce travail n’étant ni intéressant ni efficace, nous avons décidé de trouver une solution pour l’automatiser et le gérer différemment.
La technologie basée sur les conteneurs (Docker), combinée à l’émergence d’outils d’automatisation dans Jenkins avec Pipeline et l’efficacité du DSL Job plugin, nous a alors semblé être la meilleure solution dans le contexte eXo.
1ere étape: Créer ses propres images Docker
Il est important de respecter les bonnes pratiques générales lorsque l’on créé nos propres images Docker, mais également quelques spécificités propres aux environnements de build.
Choisir une image de base pour la distribution
Tous nos serveurs qui font office d’agents Jenkins sont gérés à partir de Puppet et sont basés sur la distribution Ubuntu. Nous avons donc décidé d’utiliser une image Docker plus légère basée sur cette distribution nommée baseimage-docker.
C’était un bon compromis entre une image très légère comme Alpine qui avait des problèmes liés à l’installation de JDK et l’image Docker officielle d’Ubuntu trop volumineuse.
FROM phusion/baseimage:0.9.21
LABEL MAINTAINER “eXo Platform <docker@exoplatform.com>”
Définir la locale
Qui n’a jamais eu de tests en échec en raison de problèmes d’encodage ? Les tests passent sur mon poste Linux, mais ils échouent sur le portable de mon collègue sous Windows !
Il est très fortement recommandé de définir une locale pour s’assurer que tous les développeurs auront le même comportement quel que soit leur environnement et que ce comportement sera conforme à la configuration du serveur d’Intégration Continue.
# Set the locale RUN locale-gen en_US.UTF-8 ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8
Créer un utilisateur CI dédié
“…take care of running your processes inside the containers as non-privileged users (i.e., non-root).” est l’une des recommandations les plus importantes du guide de sécurité Docker.
Dans le contexte de Jenkins, il est important de créer un utilisateur de CI dédié qui correspond à l’uid:gid de l’utilisateur utilisé par Jenkins sur vos agents.
# eXo CI User ARG EXO_CI_USER_UID=13000 ENV EXO_CI_USER ciagent ENV EXO_GROUP ciagent ... # Create user and group with specific ids RUN useradd --create-home --user-group -u ${EXO_CI_USER_UID} --shell /bin/bash ${EXO_CI_USER}
En effet, lorsque Jenkins exécute un job Pipeline avec Docker, il partage l’espace de travail du job ainsi que d’autres dossiers requis dans le conteneur Docker.
Par exemple, Jenkins exécute la commande suivante sur l’agent:
$ docker run -t -d -u 13000:13000 -v m2-cache-ecms-develop-ci:/home/ciagent/.m2/repository -v /srv/ciagent/workspace/ecms-develop-ci:/srv/ciagent/workspace/ecms-develop-ci:rw -v /srv/ciagent/workspace/ecms-develop-ci@tmp:/srv/ciagent/workspace/ecms-develop-ci@tmp:rw ... -e ******** --entrypoint cat exoplatform/ci:jdk8-maven33
La création d’un utilisateur CI dédié vous évite ainsi les problèmes d’autorisations avec les volumes Docker.
Vous pouvez constater que nous définissons la variable EXO_CI_USER_UID avec l’instruction Docker ARG, elle aura son importance pour aider le développeur dans son environnement de de développement local et nous expliquerons pourquoi plus loin dans cette série d’articles.
ENTRYPOINT et CMD
Dans un Dockerfile, l’instruction ENTRYPOINT est une définition facultative pour la première partie de la commande à exécuter. Ainsi, les instructions ENTRYPOINT ou CMD, spécifiées dans votre Dockerfile, identifient l’exécutable par défaut pour l’image Docker. Mais la meilleure solution est de combiner ces deux instructions en utilisant CMD pour fournir des arguments par défaut pour le ENTRYPOINT.
Dans les versions antérieures du plugin Jenkins Docker Pipeline, Jenkins n’utilisait pas l’option –entrypoint lors du démarrage des conteneurs, la commande exécutée était alors:
$ docker run ... -e ******** exoplatform/ci:jdk8-maven33 cat
Dans ce cas, si votre image Docker utilisait l’instruction ENTRYPOINT pour déclarer une commande à exécuter, Jenkins ne pouvait pas utiliser votre image.
C’est pourquoi nous avons ajouté un script personnalisé dans nos images Docker, comme solution de contournement, afin de pouvoir exécuter Maven comme commande au démarrage du conteneur, mais également la commande cat et d’autres commandes à partir du moment où elles sont appelées via un chemin absolu:
Dockerfile
COPY docker-entrypoint.sh /usr/bin/docker-entrypoint # Workaround to be able to execute others command than "mvn" as entrypoint ENTRYPOINT ["/usr/bin/docker-entrypoint"] CMD ["mvn", "--help"]
docker-entrypoint.sh
# Hack for Jenkins Pipeline: authorize cat without absolute path if [[ "$1" == "/"* ]] || [[ "$1" == "cat" ]]; then exec "$@" fi exec mvn "$@"
Depuis plusieurs versions du plugin Jenkins Docker Pipeline, ce problème a été corrigé pour utiliser l’option –entrypoint afin que l’instruction ENTRYPOINT soit toujours surchargée:
$ docker run ... -e ******** --entrypoint cat exoplatform/ci:jdk8-maven33
Utiliser l’héritage pour éviter la duplication de code
Nous avons créé et continuons à créer des images Docker pour couvrir toutes nos environnements de build, aujourd’hui cette liste contient notamment :
- exoplatform/ci:jdk6-maven30
- exoplatform/ci:jdk7-maven30
- exoplatform/ci:jdk7-maven32
- exoplatform/ci:jdk8-maven32
- exoplatform/ci:jdk8-maven33
- exoplatform/ci:jdk8-gradle2
- …
Comme vous pouvez l’imaginer, il n’y a pas beaucoup de différences entre toutes ces images. Nous avons donc créé des images de base CI à plusieurs niveaux afin d’éviter autant que possible la duplication du code.
Le schéma ci-dessous montre comment ces images sont organisées :
Ci-dessous, il s’agit d’un extrait du Dockerfile pour l’image exoplatform/ci:jdk8-maven33 et vous pouvez voir qu’il n’y a que des instructions Docker liées à l’installation Maven car toutes les autres configurations ont été effectuées dans les images héritées (définir les paramètres régionaux, créer un utilisateur CI …).
FROM exoplatform/ci:jdk8 # CI Tools version ENV MAVEN_VERSION 3.3.9 # Install Maven RUN mkdir -p /usr/share/maven \ && curl -fsSL http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz \ | tar xzf - -C /usr/share/maven --strip-components=1 \ && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn … # Custom configuration for Maven ENV M2_HOME=/usr/share/maven ENV MAVEN_OPTS="-Dmaven.repo.local=${HOME}/.m2/repository -XX:+UseConcMarkSweepGC -Xms1G -Xmx2G -XX:MaxMetaspaceSize=1G -Dcom.sun.media.jai.disableMediaLib=true -Djava.io.tmpdir=${EXO_CI_TMP_DIR} -Dmaven.artifact.threads=10 -Djava.awt.headless=true" ENV PATH=$JAVA_HOME/bin:$M2_HOME/bin:$PATH ... # Workaround to be able to execute others command than "mvn" as entrypoint ENTRYPOINT ["/usr/bin/docker-entrypoint"] CMD ["mvn", "--help"]
Comme expliqué précédemment dans cet article, nous avons combiné les instructions Docker ENTRYPOINT et CMD pour pouvoir exécuter toutes les commandes Maven dans ce conteneur facilement:
docker run ... -e ******** exoplatform/ci:jdk8-maven33 clean package
Et nous ajoutons également un script personnalisé, pour pouvoir exécuter la commande cat et d’autres commandes que Maven en donnant le chemin d’accès absolu à la commande:
docker run exoplatform/ci:jdk8-maven33 /bin/echo hello
Enfin, vous avez peut-être remarqué que nous avons déclaré les variables d’environnement M2_REPO et MAVEN_OPTS sous forme d’instructions Docker ENV avec des valeurs par défaut pour tous les paramètres importants. Elles peuvent être surchargées via l’option -e dans la commande de démarrage de docker.
Tester ses Images Docker
Comme pour tout autre type de code source, il est possible de créer des suites de tests pour ses images Docker et Dockerfiles. Pour les images Docker eXo CI, nous utilisons Goss à travers son wrapper dgoss.
- Goss est un outil, basé sur YAML, pour valider la configuration d’un serveur
- dgoss est une enveloppe autour de Goss qui vise à apporter la simplicité de Goss aux conteneurs Docker.
La première étape consiste à créer un fichier YAML pour décrire ce que vous souhaitez tester dans votre conteneur Docker. Il existe des exemples en ligne qui peuvent aider à créer ce fichier de configuration, mais cela peut également être généré par la ligne de commande de goss.
Par exemple, nous voulons vérifier que certains fichiers de configuration Maven existent dans le conteneur. Nous voulons également être sûr que la commande mvn –version est conforme aux versions Maven et JDK installées dans le container.
goss.yaml
file: /home/ciagent/.m2/repository: title: Validating the presence Maven repository folder exists: true /home/ciagent/.m2/settings.xml: title: Validating the absence of eXo USER settings file exists: false /usr/share/maven/conf/settings.xml: title: Validating the presence of eXo GLOBAL settings file exists: true package: git: installed: true title: Check that git is installed command: mvn --version: exit-status: 0 stdout: - 3.3.9 - 1.8.0 - "Default locale: en_US, platform encoding: UTF-8" stderr: [] timeout: 0
Ensuite, pour exécuter ces tests, il suffit simplement d’exécuter une ligne de commande via dgoss:
dgoss run -it exoplatform/ci:jdk8-maven33 cat
Conclusion
Si vous êtes intéressés pour tester ou utiliser ces images Docker:
- tous les Dockerfiles sont disponibles dans le projet GitHub dédié exo-docker/exo-ci
- les images Docker peuvent être téléchargées à partir de l’organisation eXo sur le DockerHub
N’hésitez pas à nous faire part de vos commentaires ou à proposer vos idées.
Prochaine étape
Maintenant que nous avons créé nos propres Images Docker pour toutes nos environnements de build de l’intégration continue, nous vous expliquerons dans le prochain article (Partie 2), comment utiliser ces images dans un environnement de développement local quel que soit votre système d’exploitation.
Découvrez comment eXo Platform peut vous aider à transformer votre entreprise!