Quand on travaille avec des langages compilés, construire ou téléverser des artefacts sont des tâches qui peuvent devenir répétitives. On peut automatiser ces tâches avec des outils qui ont fait leurs preuves comme make. Mais il existe un outil qui permet d’automatiser un grand nombre de ces tâches en très peu de temps quand on fait du Golang : GoReleaser. Dans cet article, nous allons expérimenter de manière incrémentale ses fonctionnalités.
Lorsque j’ai débuté mon apprentissage du Golang il y a quelques mois, j’ai rapidement voulu automatiser un certain nombre d’actions répétitives, notamment la compilation. Au cours de mon apprentissage d’autres langages compilés, j’avais déjà utilisé make et des fichiers Makefile pour réaliser ce genre d’actions.
Cette solution, totalement fonctionnelle, n’est pas nécessairement la plus efficace dans le monde Golang. On peut faire la même chose (et même plus) beaucoup plus rapidement à l’aide d’un outil qui s’appelle GoReleaser.
1. Présentation de GoReleaser
GoReleaser est un outil qui automatise le processus de construction et de diffusion des binaires Go. Il peut être utilisé pour construire des binaires pour différentes plateformes, les regrouper dans des artefacts de diffusion, et les téléverser sur divers canaux de distribution.
C’est un projet open source (disponible sur GitHub [1]) avec une version « Pro » payante qui propose des fonctionnalités complémentaires.
Dans cet article, nous ne parlerons que de la version open source, qui est probablement suffisante pour la plupart des cas d’usage. Nous partirons dans un premier temps d’un exemple très simple et nous ajouterons progressivement des fonctionnalités à notre processus d’automatisation.
2. Prérequis
Avant de commencer, vous devez disposer sur votre machine des prérequis suivants :
- Go ;
- Git ;
- un projet en Go à automatiser.
Pour installer GoReleaser, il existe plusieurs méthodes, décrites sur le site officiel [2].
Il existe des paquets disponibles sur la plupart des gestionnaires de paquets/ports. On peut aussi utiliser un script qui ne fait que récupérer le binaire, utiliser une image Docker… À vous de voir.
Ici, nous allons installer le paquet DEB pour une machine compatible Debian :
3. Initialisation
Plutôt que de partir d’un helloworld, nous allons utiliser l’exemple de godraft [3], une très simple application web écrite en Golang.
La première chose que nous indique la doc officielle est de créer le fichier de configuration pour GoReleaser avec la sous-commande init :
Mais le fichier généré par défaut est assez verbeux et contient beaucoup de choses, pas forcément utiles pour une première étape.
Pour simplifier la compréhension, nous allons partir d’une version plus simple du fichier, qui sera complété au fur et à mesure.
4. Première étape : compilation
Repartons du Makefile donné en tout début d’article et remplaçons les étapes prepare et build.
Ici, build va juste compiler le programme pour GNU/Linux sur une machine x86_64 (amd64). L’option -o a été ajoutée pour indiquer où le binaire de sortie sera généré, et -ldflags "-X main.Version=$$VERSION" pour ajouter en dur la version dans le programme à partir d’une variable d’environnement.
On génère donc le binaire via make avec la commande suivante :
Essayons de faire la même chose avec GoReleaser. À la racine de notre projet, on va créer le fichier de configuration .goreleaser.yaml contenant le code YAML (Yet Another Markup Language) suivant :
À titre personnel, je trouve ça déjà plus clair et plus lisible, même si c’est au prix d’une indentation stricte (en vrai, c’est déjà le cas avec make) et d’un nombre de lignes plus important.
La section before/hooks va nous permettre d’exécuter la commande go mod tidy avant de compiler le code, au même titre qu’on pouvait le faire avec le Makefile via la dépendance build: prepare.
On notera également la présence de {{.Version}}, une variable automatique qui remplace la variable d’environnement manuelle $$VERSION de notre Makefile d’exemple. Nous y reviendrons.
5. Deuxième étape : Git tag
GoReleaser va s’appuyer sur les tags Git pour tout ce qui est gestion d’artefacts. Si l’on veut faire une nouvelle version, on va donc devoir taguer notre code dans Git.
Une fois que notre dépôt Git dispose d’un tag tout neuf, on lance GoReleaser avec la sous-commande build dans un premier temps, pour construire des artefacts sans les téléverser, et l’option --clean qu’on ajoutera à chaque fois pour écraser les artefacts produits précédemment :
Pour plus de concision et de clarté, j’ai omis (via « [...] ») une partie des messages affichés sur la sortie standard pour ne garder que les messages importants pour la compréhension de l’article.
Si l’on essaye d’exécuter GoReleaser sur un commit qui ne correspond à aucun tag, on va recevoir l’erreur suivante :
Cependant, si vous voulez générer des artefacts sans pour autant avoir à créer un tag, il est possible de forcer GoReleaser à générer des artefacts (toujours sans les publier) avec la combinaison build et --snapshot :
Cette option --snapshot (seule, sans la sous-commande build) sera aussi utile pour les projets qui mettent en place des versions « snapshots » ou « nightly » quotidiennes, par exemple.
Si l’on regarde un peu plus en détail le contenu affiché à l’écran dans cette précédente capture, on peut remarquer plusieurs choses.
D’abord, que notre binaire a été généré avec succès et qu’il est disponible ici au chemin dist/godraft_linux_amd64_v1/bin/godraft.
Ensuite que la variable {{.Version}} dont on parlait plus tôt a été générée automatiquement. Ici, le commit n’était rattaché à aucun tag, elle est déterminée à partir du dernier tag et de l’ID du commit actuel.
Si l’on vérifie que le programme fonctionne, on remarquera que cette variable a bien été intégrée dans notre binaire, comme on le faisait précédemment avec le Makefile, mais sans devoir indiquer la version à la main via une variable d’environnement :
6. Troisième étape : crossbuild
Vous avez peut-être remarqué dans le tout premier fichier .goreleaser.yaml que nous spécifions explicitement la GOARCH (architecture processeur cible) et GOOS (système d’exploitation cible).
Dans le cas de l’application godraft, il n’y a pas de raison particulière à compiler pour un autre système d’exploitation que GNU/Linux. En revanche, il pourrait être intéressant d’avoir une version pour les architectures arm64 en plus de x86_64, de manière à héberger l’application sur des Raspberry Pi, par exemple.
La production aisée de binaires multiplate-formes est un des arguments de Golang. Il est donc probable, si vous développez des outils en Golang, que vous ayez envie de mettre à disposition des binaires sur un certain nombre d’architectures.
Si vous omettez complètement goos et goarch dans .goreleaser.yaml, vous allez vous retrouver par défaut avec une combinaison de plusieurs plateformes courantes, sans rien faire de plus :
Pour revenir à l’exemple, ajoutons simplement arm64 dans la liste goarch des architectures souhaitées :
À partir de maintenant, GoReleaser livrera les artefacts à la fois pour amd64 et pour arm64, sans qu’on ait besoin de faire une boucle ou d’ajouter du code.
7. Quatrième étape : téléverser une release sur GitHub/GitLab/Gitea
Une « release » est une version spécifique d’un logiciel ou d’un projet qui est publiée pour permettre aux utilisateurs de l’utiliser ou de la tester.
En plus de compiler notre code, GoReleaser va aussi se charger de téléverser des releases directement sur le site qui héberge notre code.
GoReleaser est capable d’interagir avec GitHub, GitLab et Gitea. Cela vaut pour les instances publiques (github.com ou gitlab.com), mais aussi si vous disposez d’une instance privée (GitLab Entreprise, par exemple [4]).
En partant du principe que votre code est hébergé sur GitHub (comme pour godraft), il va nous falloir créer un « personnal access token » pour permettre à GoReleaser de s’authentifier et de faire des actions pour nous.
Connectez-vous sur github.com, naviguez dans Settings puis Developer settings et enfin dans Tokens ou allez directement à ce menu via l’URL https://github.com/settings/tokens/new [5].
Les droits nécessaires pour que GoReleaser fonctionne sont write:packages. Il est également conseillé pour des raisons de sécurité de laisser une durée d’expiration du token (au cas où il serait volé).
On peut enfin fournir ce token à GoReleaser, soit par le biais d’une variable d’environnement (GITHUB_TOKEN), soit dans un fichier (~/.config/goreleaser/github_token par défaut) :
Pour notre projet, nous allons également ajouter des archives au format tar.gz à la liste des artefacts qui seront générés et envoyés à github.com, ainsi qu’une liste de checksums pour qu’il soit possible pour nos utilisateurs de s’assurer que ces artefacts n’aient pas été altérés lors de leur distribution.
Si l’on teste la génération d’une release, voilà ce qui devrait se passer :
On remarque que nos binaires ont bien été compilés pour amd64 et arm64, qu’un fichier de checksums.txt a été créé et que tout ceci a été envoyé vers l’URL https://github.com/zwindler/godraft/releases/tag/2.1.2 [6].
8. Cinquième étape : construire une image Docker et l’envoyer sur une registry
Une «registry» quand on parle dans un contexte de containérisation est un service permettant de téléverser et de télécharger des images versionnées de containers prêtes à l’emploi. La plus connue est https://hub.docker.com (Dockerhub).
On commence à avoir un processus de livraisons industrialisé qui va beaucoup plus loin que ce qui était fait avec le Makefile du début d’article, et avec relativement peu de lignes de YAML.
Cependant, il reste encore cette partie qu’on a pour le moment ignorée :
Ceci est appelé avec la commande suivante :
Vous vous en doutez, GoReleaser est aussi capable de créer des images Docker et de les pousser sur une registry (privée ou publique).
Et l’on va même faire mieux, puisqu’on va s’économiser l’étape de compilation qui est incluse dans le fichier Dockerfile initial :
J’utilise ici ce qu’on appelle un « multistage build », ce qui veut dire que le container est créé en plusieurs étapes.
La première (qui commence au premier FROM) part d’un container contenant tout ce qu’il faut pour compiler le code. La seconde (qui commence au second FROM) est un nouveau container qui ne contient rien d’autre que le binaire.
On peut ainsi limiter la taille de l’image Docker et surtout la surface d’attaque puisqu’un éventuel pirate informatique qui compromettrait le code n’aurait aucun outil (environnement de compilation Go, shell…) à sa disposition pour faciliter un « rebond » ailleurs sur notre infrastructure.
On va commencer par retirer tout ce qui est en rapport avec la phase de compilation de notre binaire, puisque c’est GoReleaser qui s’en charge maintenant. Le Dockerfile devrait ressembler à ça :
Une fois que c’est fait, on va insérer la section YAML dockers: dans notre fichier .goreleaser.yaml :
Dans la section dockers, on va réaliser la construction et l’envoi de notre image Docker (l’équivalent des commandes docker build et docker push). Voici à quoi servent les différentes options :
- image_templates permet de spécifier les noms des images (l’équivalent de l’option -t de docker build) ;
- build_flag_templates permet de spécifier la plateforme (ici, linux/amd64) et la valeur du « build arg » VERSION (utilisé par notre application pour afficher la version de l’application dans le log) ;
- extra_files permet d’indiquer à GoReleaser quels fichiers doivent être accessibles. Par défaut, seuls les artefacts créés (le binaire) sont accessibles pour l’instruction COPY de notre Dockerfile. Or, dans l’exemple de godraft, il est nécessaire d’ajouter un fichier de CSS et des templates HTML à l’image Docker.
Si l’on relance une nouvelle release, on obtiendra les messages suivants en plus, par rapport à nos essais précédents :
On peut vérifier sur le site https://hub.docker.com [7], l’image est bien présente.
Une fois de plus, on remarque que la gestion des versions et des tags se fait automatiquement grâce à l’utilisation de la variable {{.Version}}.
Il existe bien d’autres variables que nous n’aborderons pas dans cet article, la liste complète est disponible sur le site officiel de GoReleaser [8].
Conclusion
Dans cet article, j’espère vous avoir prouvé qu’on pouvait automatiser les étapes de la construction de nos artefacts Go avec GoReleaser et seulement quelques lignes de YAML.
Nous avons supprimé la gestion manuelle de la version, simplifié l’étape docker build (puisqu’on a supprimé l’étape de compilation de notre Dockerfile « multistage build »). On peut compiler notre binaire et nos images Docker pour plusieurs plateformes avec peu d’efforts, et mettre le tout à disposition via une seule commande.
Toutes ces étapes auraient bien entendu pu être réalisées avec make, mais auraient nécessité d’écrire du code ou des scripts, là où GoReleaser s’en charge pour nous.
Pour terminer, les options présentées ici sont les plus communes de GoReleaser, mais il est évidemment possible d’aller beaucoup plus loin dans notre processus de livraison.
On peut créer des artefacts pour divers gestionnaires de paquets des distributions, des snaps… On peut aussi intégrer GoReleaser dans d’autres outils d’automatisation comme GitHub Action, interagir avec des outils comme Slack… Il existe enfin d’autres options pour la construction d’images Docker, notamment le support des docker manifests.
À vous maintenant d’explorer la documentation et de laisser libre cours à votre imagination !
Références
[1] Site officiel de GoReleaser : https://github.com/goreleaser/goreleaser
[2] Documentation d’installation de GoReleaser : https://goreleaser.com/install
[3] Projet personnel godraft sur GitHub : https://github.com/zwindler/godraft
[4] Configuration de GoReleaser pour téléverser les artefacts sur une instance Gitlab privée : https://goreleaser.com/scm/gitlab/#gitlab-enterprise-or-private-hosted
[5] Créer un token pour authentifier GoReleaser sur GitHub : https://github.com/settings/tokens/new
[6] Ensembles d’artefacts téléversés par GoReleaser sur GitHub pour la release 2.1.2 : https://github.com/zwindler/godraft/releases/tag/2.1.2
[7] Images Docker amd64 et arm64 de godraft en version 2.1.3, téléversées avec GoReleaser : https://hub.docker.com/r/zwindler/godraft/tags?page=1&name=2.1.3
[8] Variables automatiques utilisables avec GoReleaser : https://goreleaser.com/customization/templates/