Les pipelines Jenkins sont de plus en plus utilisés, et à juste titre : ils remplacent les jobs de type « freestyle », qui ne permettaient pas beaucoup de liberté et qui étaient constitués d’un enchaînement de « pre-build steps » et de « post-build steps » avec bien souvent de nombreuses lignes Bash entre les deux. De plus, dans de nombreuses intégrations continues on trouvait souvent plusieurs jobs chaînés pour construire un seul projet (dans le but d’avoir quelque chose de plus flexible).
Les Pipelines offrent une multitude d’avantages par rapport à un job freestyle : ils sont lisibles, versionnés, flexibles, réutilisables (via les librairies partagées, ou « shared libraries ») et se trouvent (en général) avec le code source de votre projet.
Cependant, un des défauts des Pipelines vient de leur processus de développement : ils sont difficilement testables et peuvent vite être chronophage si votre environnement de développement n’est pas approprié.
Dans cet article nous allons voir comment développer localement et sereinement un Pipeline Jenkins.
Pourquoi développer localement ?
Lorsque l’on débute le développement de Pipelines Jenkins, bien souvent, on se sert du Jenkins déjà à notre disposition. C’est à dire un Jenkins opérationnel et utilisé en production.
Tous les développeurs n’ont pas forcément les connaissances ou les ressources nécessaires pour disposer d’une machine avec un Jenkins dédié à notre utilisation.
Le workflow de développement pourrait se présenter comme suit :
- Dupliquer un projet actuel (les Pipelines étant versionnés avec le projet nous n’allons pas modifier l’historique d’un projet uniquement à des fins de test) ;
- Créer un nouveau repository pour ce projet ;
- Créer un nouveau job sur le Jenkins de production ;
- Développer une version basique du Pipeline ;
- Pousser un nouveau commit contenant le Pipeline ;
- Exécuter le job ;
- Constater que ça ne fonctionne pas ;
- Corriger le ou les problème(s) ;
- Pousser un nouveau commit contenant les modifications ;
- Exécuter le job ;
- Constater que ça ne fonctionne pas ;
- Corriger le ou les problème(s) ;
- …
Vous vous rendez vite compte de la pénibilité de la chose.
Pour accélérer le cycle de développement il est possible d’utiliser la fonction « replay » qui permet de modifier directement le code du Pipeline dans Jenkins. Néanmoins cette fonctionnalité n’est disponible que pour les jobs de Pipelines simples (pas de multi-branches Pipelines donc) et n’est pas toujours utilisable (si une grosse erreur s’est produite vous ne pourrez pas rejouer le Pipeline).
Le replay offre un gain de temps appréciable mais il reste toujours les questions de l’utilisation d’un Jenkins de production (avec des files d’attentes, l’impossibilité d’ajouter des plugins en fonction de ses droits, etc.) et le temps passé avec les commits.
Configuration de base
Un Jenkins local
La façon la plus simple et rapide d’avoir un Jenkins local est d’utiliser Docker. Le compte officiel Jenkins sur Docker Hub met à disposition de nombreuses images dont un Jenkins complètement opérationnel.
Nous allons tout de suite passer par Docker Compose pour utiliser cette image afin d’avoir une configuration plus souple et versionnable :
version: '3' services: jenkins: image: jenkins/jenkins:lts ports: - 8080:8080 volumes: - .docker/jenkins_data:/var/jenkins_home - ${PROJECT_VOLUME:-.}:/home/project
Deux remarques par rapport à cette configuration :
- Le premier volume sert à persister les données de Jenkins, exposées dans l’image sur
/var/jenkins_home
(le répertoire.docker/jenkins_data
est ignoré par Git) ; - L’utilisation d’une variable d’environnement qui pointe vers le projet cible. Si la variable d’environnement n’est pas présente, la valeur par défaut sera le répertoire courant (cf. https://docs.docker.com/compose/compose-file/#variable-substitution). Cela permet d’avoir un Jenkins générique et non un Jenkins propre à un projet.
La commande pour exécuter Docker Compose se présente comme suit :
$ PROJECT_VOLUME=/path/to/my/project docker-compose up
Jenkins est alors accessible localement sur le port 8080.
Un repository Git local
Maintenant que nous avons un Jenkins tournant localement, occupons nous du problème du dépôt Git. Sans passer par une plate-forme en ligne telle que Github ou Gitlab il est tout à fait possible d’avoir un repository Git local. Cela accélère les modifications de nos pipelines car nous éliminons les latences réseau et cela nous évite également l’étape de création d’un nouveau projet (et aussi la possibilité que nous oublions de supprimer ce projet test une fois le développement terminé).
Rien de compliqué : dans le projet dont nous construisons l’intégration continue, supprimer le répertoire .git actuel puis :
$ git init
Dans la page de configuration du job Jenkins, à la place d’une URL Git nous y renseignons un chemin, en l’occurrence /home/project
(cf. configuration du docker-compose.yaml un peu plus haut). Pour plus d’informations sur les URLs possibles de git clone
la documentation officielle est claire et compréhensible : https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a.
Après ce commit initial votre workflow Git pourrait très bien ressembler au suivant :
$ git add Jenkinsfile $ git commit -v --no-edit --amend
Si vous utilisez OhMyZsh! et le plugin Git c’est encore plus rapide :
$ ga Jenkinsfile $ gcn!
Vous remarquerez qu’on ne pousse rien vers un repository distant, tout reste en local.
Pour des intégrations continues basiques ces quelques étapes pourraient suffire, mais c’est rarement le cas. Il est souvent nécessaire d’installer des outils de build propres au projet tels que Node ou PHP.
Une intégration continue avec Docker
Il y a deux philosophies pour installer les dépendances requises :
- Les installer directement sur la machine du Jenkins ;
- Utiliser Docker (Compose) pour construire le projet.
La première approche est la plus simple et la plus rapide mais a plusieurs inconvénients : il faut maintenir les paquets à jour (et mettre à jour certains paquets pourrait casser le build des projets existants), en fonction des technologies utilisées il est parfois compliqué de disposer de plusieurs versions en parallèle (PHP par exemple), la gestion des droits avec Jenkins est par moment difficile…
L’utilisation de Docker quant à elle nous évite ces inconvénients, le Jenkins est agnostique et permet de construire n’importe quel type de projet, il suffit « juste » d’installer et de configurer Docker. Elle n’est pas non plus dépourvue de désavantages puisque vous devrez passer du temps à écrire un fichier docker-compose.yaml et très probablement un Dockerfile.
Si le mot « juste » se retrouve entre guillemets ce n’est pas un hasard. Exécuter Docker sur un système complet est facile mais pour rappel notre Jenkins local tourne déjà dans Docker. Exécuter Docker dans Docker n’est pas évident la première fois.
Le but de cet article n’est pas de rentrer dans les détails explicatifs du « pourquoi c’est compliqué » et potentiellement « pourquoi c’est mal » (c’est pour du développement local donc pas très grave). Si vous voulez en savoir plus je vous conseille cet article : Using Docker-in-Docker for your CI or testing environment? Think twice.
Bien que l’article préconise le partage du socket Docker entre votre machine hôte et le conteneur Docker ce n’est pas ce que nous suivrons. La problématique des chemins des volumes s’avère pénible et nécessite de faire des adaptations spécifiques aux volumes déclarés (le socket étant celui de votre hôte tous les volumes montés par le conteneur Jenkins doivent être relatifs à votre machine et non pas au conteneur), chose que nous ne voulons pas. L’idée est de développer localement, puis de porter notre développement sur un système complet sans devoir à nouveau apporter des modifications.
Nous allons utiliser une image officielle, nommée « Dind » pour Docker in Docker :
version: '3' services: dind: image: docker:dind privileged: true expose: - 2375 - 2376 volumes: - .docker/jenkins_data:/var/jenkins_home jenkins: image: jenkins/jenkins:lts environment: DOCKER_HOST: tcp://dind:2375 ports: - 8080:8080 links: - dind volumes: - .docker/jenkins_data:/var/jenkins_home - ${PROJECT_VOLUME:-.}:/home/project
Deux détails qui ont leur importance :
- L’image exécutant Docker doit gagner en privilèges (pour plus d’informations : Exécuter Karma avec headless Chrome dans un conteneur Docker) ;
- La variable d’environnement
DOCKER_HOST
qui indique au client Docker où se trouve le démon.
Il faut également installer Docker Compose sur le conteneur de Jenkins :
FROM jenkins/jenkins:lts LABEL maintainer="Antoine Descamps <antoine.descamps@ineat-conseil.fr>" USER root RUN echo "deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main" >> /etc/apt/sources.list && apt-key adv --keyserver https://keyserver.ubuntu.com:443 --recv-keys 93C4A3FD7BB9C367 RUN apt-get update RUN curl -fsSL https://get.docker.com | bash - \ && curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose \ && chmod +x /usr/local/bin/docker-compose RUN usermod -aG docker jenkins USER jenkins
Nous avons maintenant une installation locale complète permettant de construire n’importe quel type de projet du moment que celui-ci dispose d’une configuration Docker.
Les noeuds Jenkins
Si le Jenkins cible (de production donc) est configuré en mode « master + agent » vous ne pourrez pas porter tel quel votre Pipeline puisqu’il faudra sélectionner le ou les nœud(s) sur le(s)quel(s) exécuter le script.
Un agent n’est rien d’autre qu’une nouvelle instance de Jenkins : ce qui, dans notre cas, se traduit par un nouveau service Docker Compose.
version: '3' services: dind: image: docker:dind privileged: true expose: - 2375 - 2376 volumes: - .docker/jenkins_data:/var/jenkins_home jenkins: build: .docker environment: DOCKER_HOST: tcp://dind:2375 ports: - 8080:8080 - 50000:50000 links: - dind volumes: - .docker/jenkins_data:/var/jenkins_home - ${PROJECT_VOLUME:-.}:/home/project node_php: image: jenkins/jnlp-slave environment: JENKINS_URL: http://jenkins:8080 JENKINS_SECRET: 283b1438bde93ac95f6c8f6ae8b2e94342a1c00bafcddbb8c721412d1b435036 JENKINS_AGENT_NAME: Node_PHP JENKINS_AGENT_WORKDIR: /home/jenkins restart: on-failure links: - jenkins depends_on: - jenkins
Peu de configuration supplémentaire grâce à la présence sur Docker hub d’une image slave de Jenkins, mais encore une fois deux points d’attention :
- Le ou les agents doivent démarrer après Jenkins ;
- Même en gérant les dépendances entre les services l’agent démarre plus vite que le master, d’où le paramètre
restart: on-failure
.
Pour information la valeur de JENKINS_SECRET
est obtenue une fois le nœud déclaré dans l’interface de Jenkins, ne le copiez pas à partir de cet article.
Conclusion
Dans cet article nous avons vu, une fois les subtilités de configuration passées, qu’il était simple et rapide de monter un environnement local Jenkins correspondant à l’état de votre production. Grâce à Docker et Docker Compose il nous aura fallu moins d’une centaine de lignes pour profiter d’un gain de temps et d’une facilité de développement accrus.