L’intégration continue est une pratique de développement qui nécessite des développeurs qu’ils poussent du code dans un dépôt partagé plusieurs fois par jour. Chaque commit est ensuite vérifié par un build automatisé, ce qui permet aux équipes de détecter les problèmes à un stade précoce. En intégrant régulièrement, il est possible de détecter rapidement les erreurs et de les localiser plus facilement.

Je vous ai déjà présenté le serveur Jenkins dans d’autres articles : comment l’installer sur AWS et comment l’utiliser pour détecter des vulnérabilités. Il existe par ailleurs une multitude d’exemples en ligne pour mettre en place une CI pour des projets en Java ou Javascript.

Malheureusement les ressources sont plus rares concernant les projets implémentés en Go. Je vous propose donc de regarder dans cet article comment mettre en place un pipeline sous Jenkins pour compiler et tester une lambda implémentée en Go.

Golang, une brève introduction

Go, également connu sous le nom de Golang, est un langage de programmation compilé et statiquement typé. Le projet est porté par Google, qui l’a open-sourcé en 2009.

Go présente des similarités avec le C. Cependant, il améliore le C en ajoutant, entre autres, un mécanisme de garbage collection, le typage structurel et la sécurité de la mémoire.

Le langage de programmation Go a bénéficié d’un soutien croissant de la part des développeurs du monde entier, et il figure désormais en bonne place dans le classement “Most Loved, Dreaded, and Wanted” de StackOverflow :

Extrait de l’étude de StackOverflow

Extrait de l’étude de StackOverflow

Si vous souhaitez en savoir plus sur ce langage, j’ai écris un article en début d’année : “Apprendre le langage Go en 2020

Préparation du serveur Jenkins

Afin de pouvoir compiler notre lambda nous allons avoir besoin d’une installation de Go. Plusieurs options s’offrent à nous : installer Go sur le serveur Jenkins, créer une nouvelle instance EC2 avec Go et la rattacher en tant que “slave” au serveur Jenkins, installer Docker sur le serveur Jenkins et utiliser une image Go pour notre pipeline. Je préfère partir sur cette dernière option.

Installer manuellement Go sur le serveur n’est pas forcément compliqué. En revanche avec la multiplication des projets à compiler on risque à la longue de se retrouver avec trop d’outils à installer et à maintenir.

L’utilisation de slaves est un bonne alternative pour éviter cela, en déployant un slave par type de projet. L’inconvénient est que d’une part cela complexifie le déploiement et d’autre part cela augmente la facture AWS à la fin du mois.

La dernière solution consiste à installer Docker sur le serveur Jenkins et ensuite à déployer un conteneur comme slave pour la chaque construction du projet. Cela permet de limiter le nombre d’outils installés sur le serveur tout en permettant de construire des projets de types variés. De plus il devient plus facile de construire un même projet sur différents runtimes cibles : Golang 1.13 et 1.14-rc par exemple.

L’installation de docker se fait à l’aide de ces commandes :

yum install docker -y   
usermod -aG docker jenkins  
service docker start  
sudo systemctl enable docker  
service jenkins restart

La première ligne installe Docker depuis le gestionnaire de packets yum. 
La seconde rajoute le groupe “docker” à l’utilisateur “jenkins”, afin que cet utilisateur puisse lancer des commandes docker. 
La troisième ligne démarre le service Docker sur la machine.
La quatrième ligne indique au système de démarrer le service Docker à chaque redémarrage de la machine.
Enfin la dernière ligne redémarre le service Jenkins afin de prendre en compte les modifications de groupes/utilisateurs.

Si vous avez déjà un serveur Jenkins de déployé vous pouvez vous connecter à votre instance EC2 via SSH et taper ces commandes dans la console.
Si vous n’avez pas de serveur de déployé vous pouvez rajouter ces instructions dans le script d’initialisation que j’ai présenté dans cet article.

Première étape : la compilation

Nous allons donc créer un nouveau pipeline sur le serveur Jenkins. Cliquez sur “Nouveau Item”, saisissez le nom “budgetcategorizer”, sélectionnez “Pipeline” puis cliquez sur “OK”. Faites défiler le nouvel écran qui s’affiche puis en bas de la page renseignez le script suivant :

#!/usr/bin/groovy
pipeline {
    agent {
        docker { image 'golang:1.13' }
    }
    environment {
        XDG_CACHE_HOME='/tmp/.cache'
        GOOS='linux'
        GOARCH='amd64'
    }
    stages {
        stage('Build') {
            steps {
                // Get the code from GitHub repository
                git 'https://github.com/jbleduigou/budgetcategorizer.git'

                // Code is checked out in a separate folder, create symlink to GOPATH
                sh 'mkdir -p $GOPATH/src/github/jbleduigou'
                sh 'ln -s $WORKSPACE $GOPATH/src/github/jbleduigou/budgetcategorizer'

                // Build the go project
                sh 'cd $GOPATH/src/github/jbleduigou/budgetcategorizer && go build -o budgetcategorizer ./cmd/budgetcategorizer'
            }
            post {
                success {
                    archiveArtifacts artifacts: 'budgetcategorizer', fingerprint: true
                }
            }
        }
    }
}

Cliquez sur “Sauver” et lancez le pipeline. Vous devriez obtenir un résultat qui ressemble à ceci :

Regardons les instructions du pipeline d’une peu plus près si vous le voulez bien.

Tout d’abord il y a l’instruction qui déclare l’agent sur lequel va s’exécuter la construction du projet :

agent {  
  docker { image 'golang:1.13' }  
}

Ici on déclare que l’on souhaite utiliser un container Docker qui se basera sur l’image “golang:1.13”. On comprend qu’il est possible de basculer vers une autre image en changeant cette ligne, par exemple en remplaçant “1.13” par “1.14-rc”.

Ensuite il y a trois variables d’environnement :

environment {  
  XDG_CACHE_HOME='/tmp/.cache'  
  GOOS='linux'  
  GOARCH='amd64'  
}

XDG_CACHE_HOME permet de spécifier le répertoire où les données mises en cache vont être écrites. Ce répertoire est utilisé lors de la résolution des modules Go. Si on ne le spécifie pas le user du container n’a pas les droit d’écriture et cela perturbe le build.

GOOS permet de spécifier le système d’exploitation cible pour la compilation. Ici nous choisissons “linux” car c’est la valeur nécessaire pour AWS Lambda.

GOARCH permet de spécifier l’architecture cible (en gros le type de processeur) pour la compilation. De même nous choisissons la valeur nécessaire pour AWS Lambda.

Enfin le reste du code permet de récupérer le projet à partir de GitHub, créer un lien symbolique dans le répertoire GOPATH et pour finir de compiler le projet.

Deuxième étape : les tests unitaires

L’étape suivante de notre pipeline sera de lancer les tests unitaires et d’afficher un rapport des résultats obtenus. En effet pour l’instant la seule chose dont on est sûr c’est que notre code compile, cela ne veut dire qu’il fait ce qu’il est sensé faire !

Nous allons donc rajouter une étape dans notre Jenkinsfile :


stage('Test') {
    steps {
        // Retrieve tool for converting output to junit format
        sh 'go get -u github.com/jstemmer/go-junit-report'
        
        // Run unit tests and redirect output to go-junit-report
        sh 'cd $GOPATH/src/github/jbleduigou/budgetcategorizer && go test -v ./... 2>&1 | go-junit-report > report.xml'
    }
    post {
        always {
            // Publish test results
            step([$class: 'JUnitResultArchiver', testResults: 'report.xml'])
        }
    }
}

Une fois la configuration du pipeline mise à jour et après une nouvelle construction de ce dernier vous devriez voir ceci :

Concernant les instructions de cette étape, elles consistent à installer un outil qui s’appele “go-junit-report” dans un premier temps. Cet outil permet de formater les résultats des tests Go en utilisant le format Junit, ce qui nous permet par la suite d’utiliser le plugin Jenkins associé pour afficher le rapport de tests.

Ensuite on lance les tests Go en redirigeant la sortie vers l’outil “go-junit-report” qui à son tour écrira le résultat dans le fichier “report.xml”.

Enfin on a une étape qui permet d’afficher le rapport de tests après l’exécution de l’étape en utilisant le plugin “JUnitResultArchiver”.

Il peut-être intéressant de vérifier le bon fonctionnement de ces instructions dans le cas où un test échoue. Pour cela il est possible de modifier un test à la volée pour le faire échouer sciemment. Nous utiliserons alors la commande “sed” qui est un éditeur de stream, en remplaçant “???” par “N/A” :

sh "sed -i -e 's|???|N/A|g' categorizer/categorizer.go"

Si nous relançons le pipeline nous constatons que le test échoue et que le rapport permet de constater cet échec et d’en afficher les détails :

Affichage de l’historique du pipeline

Affichage de l’historique du pipeline

Détails du test en échec

Détails du test en échec

Troisième étape : le linting

Qu’est-ce que le linting ? Le linting est la vérification automatisée de votre code source pour détecter des erreurs de programmation et de style. Le linting est important pour améliorer la qualité globale de votre code. A long terme cela peut vous aider à développer plus vite et à réduire les coûts en trouvant les erreurs plus tôt.

L’écosystème Go propose deux outils de ce type : GoLint et GoVet. Le premier s’attarde principalement sur le style du code source tandis que le second signale les constructions suspectes, telles qu’un Printf dont les arguments ne correspondent pas à la string de formattage.

Rajoutons un étape supplémentaire à notre pipeline :

stage('Violations') {
    steps {
        // Retrieve golint tool
        sh 'go get -u golang.org/x/lint/golint'

        // Run golint
        sh 'cd $GOPATH/src/github/jbleduigou/budgetcategorizer && golint ./...'

        // Run go vet 
        sh 'cd $GOPATH/src/github/jbleduigou/budgetcategorizer && go vet ./... || true'
    }
    post {
        always {
        recordIssues enabledForFailure: true, tool: goLint(), qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]]
        }
    }
}

Il vous faudra également installer le plugin Warnings Next Generation, en allant dans le menu “Administrer Jenkins”, puis dans “Gestion des Plugins”.

Une fois le plugin installé, la configuration du pipeline mise à jour et après une nouvelle construction de ce dernier vous devriez voir ceci :

Comme pour les tests unitaires il est possible de modifier les fichiers à la volée afin de s’assurer que les instructions de cette étape font le job :

sh "sed -i -e 's|NewBroker will|this will|g' messaging/message.go"

sh "sed -i -e 's|objectKey, bucket|objectKey, 1337|g' config/config.go"

Si nous relançons le pipeline nous constatons que des avertissements sont détectés et que le rapport permet d’en afficher les détails :

Affichage de l’historique du pipeline

Affichage de l’historique du pipeline

Affichage du rapport global de linting

Affichage du rapport global de linting

Détail d’un avertissement remonté par GoVet

Détail d’un avertissement remonté par GoVet

Détail d’un avertissement remonté par GoLint

Détail d’un avertissement remonté par GoLint

Améliorations possibles

Le pipeline fonctionne parfaitement en l’état, il y a toutefois quelques détails qui me chagrinent.

Tout d’abord il y a la répétition des commandes :

cd $GOPATH/src/github/jbleduigou/budgetcategorizer &&

Ces commandes alourdissent le code à mon avis. Cela rend plus difficile la compréhension du pipeline à la première lecture. J’ai essayé de rajouter une variable d’environnement GOPATH afin de pouvoir utiliser la syntaxe :

dir("GOPATH") {  
 [...]  
}

Malheureusement je n’ai pas réussi à ce que cela fonctionne donc pour l’instant je reste avec la syntaxe de changement de répertoire.

Autre chose, j’ai voulu zipper le binaire obtenu en sortie de compilation. Malheureusement la commande zip n’est pas présente dans l’image Docker officielle de Golang. Je pourrais construire ma proche image Docker pour cela mais je trouve que c’est beaucoup de travail pour un détail pas si important.


J’espère que cet article vous a plu ! 
Le code de la lambda utilisée pour cet article est sur GitHub : https://github.com/jbleduigou/budgetcategorizer
Le pipeline Jenkins complet se trouve dans le répertoire “ci” : https://github.com/jbleduigou/budgetcategorizer/blob/master/ci/Jenkinsfile.groovy
N’hésitez pas à me faire part de vos commentaires ou questions en bas de cet article ou en m’envoyant un message sur LinkedIn :
http://www.linkedin.com/in/jbleduigou/en.
Photo de couverture par Alexander Schimmeck.
Cet article a initialement été publié sur Medium.