Qu’est ce donc ?
Concourse CI est une plateforme d’intégration et de déploiement continu (CI/CD) open source et développée en GO, supportée par Pivotal.
Ce qui différencie Concourse des autres plateformes de CI est l’exécution des tâches du pipeline dans des conteneurs. Cela permet de rendre la définition du pipeline et des ressources qui le compose indépendantes de l’environnement d’exécution.
Nous verrons les détails de tout ça plus loin 😉
Dans le reste de l’article, les termes liés aux concepts amenés par Concourse seront volontairement laissés en anglais afin de faciliter le lien avec l’utilisation du produit.
Voici un aperçu de l’interface de Concourse :
Concepts & Architecture
En premier lieu, intéressons nous à l’architecture et à la modélisation des concepts utilisés.
Voici un petit schéma pour situer les concepts que nous allons aborder :
Pipeline
Comme évoqué plus haut, à quoi correspond un Pipeline dans Concourse ?
C’est la définition globale de notre chaîne de construction et de déploiement. Il peut être matérialisé par un ou plusieurs fichiers au format Yaml.
On y retrouvera la définition des Jobs et des Resources.
Un Pipeline a son cycle de vie propre. Cela lui permet de lancer des Builds pour les Jobs définis lorsque les Resources associées ont de nouvelles données d’entrée.
Voici un exemple de Pipeline qui affiche le traditionnel « Hello world » dans la console :
jobs:
# On définit le nom du Job
- name: job-hello-world
public: true
# On définit le Build Plan
plan:
# On définit le nom de la Task qui sera exécutée lors du Build
- task: hello-world
# On configure l'image du conteneur d'exécution dans lequel notre Build va être exécuté
config:
platform: linux
image_resource:
type: docker-image
source: {repository: busybox}
# On définit la commande qui sera lancée pour cette Task
run:
path: echo
args: [hello world]
Jobs
Les Jobs permettent de déterminer les actions d’un Pipeline. Ils vont définir la façon dont les Resources qui leurs sont propres vont évoluer durant les Builds.
Un Job possède un Build Plan qui permet de définir le séquencement des Steps qui seront exécutées durant les Builds.
Un Build est donc par essence l’exécution d’un Build Plan.
Steps
Les principaux Steps utilisés par les Build Plan sont les suivants :
- Le Task Step : qui permet l’exécution d’une Command/Task
- Le Get Step : qui permet de récupérer une Resource
- Le Put Step : qui permet de mettre à jour une Resource
Resources
Les Resources représentent les entrées et les sorties externes d’un Job. Elles font office de connecteur.
La liste des Resources disponibles se situe sur : https://resource-types.concourse-ci.org/
On peut y retrouver, par exemple, une Resource GIT qui va permettre à un Job de se sourcer sur un repository GIT et d’être à l’écoute des modifications sur ce dernier.
Si on reprend notre Pipeline « Hello World » et qu’on y ajoute, pour l’exemple, une Resource GIT :
# On déclare un bloc dans lequel on va définir les Resources utilisées par le Pipeline
resources:
# On définit un nom à notre Resource pour pouvoir y faire référence dans les Jobs
- name: git-demo
type: git
source:
# On définit les paramètres propres à notre Resource
# Ici passés sous forme de variables externes
uri: ((git-uri))
private_key: ((git-key))
branch: ((git-branch))
jobs:
- name: job-hello-world
public: true
plan:
# On ajoute à notre Build Plan une Get Task vers la Resource de notre repository GIT
- get: git-demo
# On définit un trigger pour scruter les changements de façon automatique
trigger: true
- task: hello-world
config:
platform: linux
image_resource:
type: docker-image
source: {repository: busybox}
run:
path: echo
args: [hello world]
Task
Les Tasks représentent les actions effectuées par le Job. Elles sont exécutées dans un conteneur et sont par essence sans état (stateless).
Elles peuvent utiliser les Resources définies dans le Job sous forme d’input ou d’output.
Une Task étant exécutée dans un conteneur, les données (fichiers, archives, console output…) qu’elle va générer seront éphémères. Elles seront perdues après la destruction du conteneur.
Pour remédier à ce problème, la Task peut écrire dans un output défini. Généralement un répertoire qui sera accessible par les autres Steps du Build Plan. Par exemple, cet output peut être utilisé par une Resource dans un Put Step.
Exemple de Pipeline
Voici un exemple d’un Pipeline qui prend un Repository Git en Resource d’entrée et un Nexus en Sortie. La Task exécutée est une compilation Maven.
# On utilise un type de Resource d'une image docker Maven
resource_types:
- name: maven
type: docker-image
source:
repository: nulldriver/maven-resource
tag: latest
resources:
# La Resource vers le Repository Git
- name: git-kanoma
type: git
source:
uri: ((git-uri))
private_key: ((git-key))
branch: ((git-branch))
# La Resource vers le Repository Nexus Maven
- name: nexus-mvn
# On utilise notre type de Resource défini plus haut
type: maven
source:
url: http://localhost:8081/repository/maven-releases/
snapshot_url: http://localhost:8081/repository/maven-snapshots/
artifact: fr.kanoma:demo-concourse:jar
username: admin
password: admin123
skip_cert_check: true
jobs:
- name: mvn-build-demo
serial: true
plan:
# Le Get Step se base sur la Resource Git
- get: git-kanoma
trigger: true
# La Task effectue un build Maven
- task: build-demo-concourse
config:
# L'image docker utilisée est une image Maven
platform: linux
image_resource:
type: docker-image
source:
repository: maven
# En Input de notre Task on retrouve notre Resource Git
inputs:
- name: git-kanoma
# En Output de notre Task, on défini le repertoire de sortie
outputs:
- name: demo-concourse-build-output
# On définit un cache qui sera utilisé pour les différentes construction du Build
# Ce qui évite de télécharger de nouveau les dépendances Maven
caches:
- path: git-kanoma/demo-concourse/m2
# On définit l'action de notre Task, ici un "Maven install"
run:
path: sh
args:
- -exc
- |
cd git-kanoma/demo-concourse
rm -rf ~/.m2
ln -fs $(pwd)/m2 ~/.m2
mvn install
# On recopie notre archive Jar générée dans l'output de la Task
cp target/*.jar ../../demo-concourse-build-output/demo-concourse.jar
# On définit notre Put Step qui va écrire dans le Repository Nexus
- put: nexus-mvn
params:
# On utilise l'output de la Task de notre Build Plan
file: demo-concourse-build-output/demo-concourse.jar
pom_file: git-kanoma/demo-concourse/pom.xml
Bien entendu ici, le Pipeline est d’un seul tenant dans un seul fichier. Rien n’empêche de fractionner notre Pipeline en plusieurs fichiers.
Fragmentation du Pipeline
Focalisons nous sur le Job. Nous allons extraire notre tâche dans un fichier distinct.
# ... import des Resources inchangé ...
jobs:
- name: mvn-build-demo
serial: true
plan:
- get: git-kanoma
trigger: true
# On fait désormais référence à un fichier Yaml présent sur dans notre Repository Git
- task: build-demo-concourse
file: git-kanoma/pipelines/tasks/build-demo-concourse.yml
- put: nexus-mvn
params:
file: demo-concourse-build-output/demo-concourse.jar
pom_file: git-kanoma/demo-concourse/pom.xml
De cette manière, la définition de notre Task est portée par notre projet et sa description est commitée avec le reste du code. La Task de notre Pipeline fait alors référence à un fichier de description Yaml présent dans la Resource Git définie.
Néanmoins, cette division peut entraîner une compréhension plus difficile du Pipeline. En effet, sa définition est fragmentée et maintenue à plusieurs endroits distincts.
Le fichier de définition de notre Task est le suivant (build-demo-concourse.yml) :
# On configure l'image à utiliser
platform: linux
image_resource:
type: docker-image
source:
repository: maven
inputs:
- name: git-kanoma
outputs:
- name: demo-concourse-build-output
caches:
- path: git-kanoma/demo-concourse/m2
# On définit l'action de notre Task dans un fichier séparé
run:
path: /bin/bash
args: [ git-kanoma/pipelines/tasks/build-demo-concourse.sh ]
Le script représentant l’action de la Task (build-demo-concourse.sh) :
#!/bin/bash
cd git-kanoma/demo-concourse
rm -rf ~/.m2
ln -fs $(pwd)/m2 ~/.m2
mvn install
cp target/*.jar ../../demo-concourse-build-output/demo-concourse.jar
FLY CLI
Une fois que l’on a rédigé notre Pipeline au format Yaml, que doit-on en faire ?
L’outil de lignes de commandes FLY CLI est là pour ça.
En effet, il va nous permettre d’exécuter les principales actions suivantes sur une instance Concourse :
- se connecter / déconnecter
- envoyer et vérifier des définitions de Pipelines (Yaml)
- récupérer la définition d’un Pipeline
- supprimer un Pipeline
- lancer / mettre en pause des Pipelines
Exemple :
# Se connecter à l'instance Concourse
fly -t handson login -c http://localhost:8080 -u test -p test
# Créer le pipeline
fly set-pipeline -t handson -p demo -c pipeline.yml -l credentials.yml
# Lancer le job
fly up -t handson -p demo
Interface Concourse
Une fois notre Pipeline envoyé à l’instance Concourse voici le résultat :
On retrouve notre Resource Git en entrée, notre Task de construction Maven et notre Resource Nexus en sortie.
Lançons un Build et vérifions ce qui se passe, en cliquant sur notre Build on obtient l’écran de détails suivant :
On retrouve notre Get Step de récupération des sources via la Resource Git. Puis notre Task de construction Maven. Et enfin, notre Put Step pour pousser le résultat via notre Resource Nexus.
Lorsque l’on retourne sur l’écran principal, on constate que notre Build est correctement passé et s’affiche désormais en vert 😉
Conclusion
Concourse CI est une plateforme simple à prendre en main et ses concepts sont rapidement assimilables. Le fait de travailler avec des conteneurs et d’avoir une définition des Pipelines complètement dissociée de l’environnement d’exécution reste un plus indéniable.
Néanmoins, il faut rester vigilant sur l’écriture des descripteurs de Pipelines car ils peuvent rapidement devenir assez imposants et difficile à lire. Pour palier à ce problème, la segmentation en plusieurs fichiers peut permettre de réduire la complexité mais parfois au détriment de la lisibilité.
Documentation
- Documentation officielle : concourse-ci.org
- Tutoriels officiels : concoursetutorial.com
- Demo officielle : https://ci.concourse-ci.org/teams/main/pipelines/concourse
Technical Lead chez KANOMA