Gérer et partager des templates de pipelines GitLab CI pour Terraform
Comment gérer et partager des templates de pipelines GitLab CI pour Terraform au sein de vos équipes ?
Vous voulez partager des templates de pipeline GitLab CI pour Terraform au sein de vos équipes et vous vous demandez peut-être comment vous y prendre ? Dans cet article, je parlerai de quelques fonctionnalités offertes par GitLab pour atteindre vos objectifs. Nous verrons comment avoir un pipeline complet de déploiement Terraform avec une dizaine de lignes YAML.
Je ferai le focus sur deux fonctionnalités de GitLab :
- Pouvoir créer des templates de pipelines et
les inclure dans n’importe quel projet. Ces pipelines templates embarqueront par défaut :
- Quelques vérifications :
terraform validate|fmt
et tflint. - La notion d’environnements où déployer Terraform via des tags associés aux jobs et aux runners.
- L’impossibilité de lancer simultanément certains jobs via des
resource_group
pour éviter les erreurs de lock Terraform. - …
- Quelques vérifications :
- Pouvoir définir un groupe de projets comme projets templates pour créer facilement de nouvelles stacks Terraform. Nécessite GitLab Premium !
Devoir gérer des dizaines de pipelines Terraform au sein d’une entreprise sans une certaine harmonisation des pratiques CI/CD via des templates partageables facilement (par exemple) peut devenir un cauchemar à maintenir sur le long terme. Non seulement les templates nous font gagner du temps, mais permettent d’avoir également une certaine consistence entre les projets.
Cet article est basé sur des pratiques que nous avons mises en place chez un client dans un contexte AWS où Terraform est utilisé pour faire de l’Infrastructure as Code. Il s’adresse surtout aux personnes ayant quelques bases en Terraform et GitLab CI.
Table des matières
Templates de pipelines
GitLab CI permet d’inclure des pipelines qui proviennent d’autres projets, du même projet ou à partir de fichiers accessibles publiquement. L’objectif est de réutiliser un pipeline complet ou juste étendre des jobs cachés. Les jobs cachés servent de points d’extension pour nos templates ou pour réutiliser un ensemble de paramètres (voir plus bas pour de détails).
Le mot clé include
permet donc d’inclure des pipelines dans un projet GitLab. Voici quelques exemples
d’utilisation tirés de la documentation officielle :
include:
- 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml' # Depuis une URL publique
- '/templates/.after-script-template.yml' # Depuis un fichier local au projet
- template: Auto-DevOps.gitlab-ci.yml # Depuis les templates fournis par GitLab : https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates
- project: 'my-group/my-project' # Depuis un autre projet auquel j'ai accès
ref: main # Référence Git du projet à utiliser. Peut être une branche, un tag, un commit, HEAD (valeur par défaut)...
file:
- '/templates/.gitlab-ci-template.yml'
- ...
# La suite fichier .gitlab-ci.yml...
La position de l’include
n’a pas d’importance dans le fichier .gitlab-ci.yml
mais la bonne pratique est de le mettre
dans les premières lignes du fichier. Dans les faits, à partir de ce mot clé, GitLab va récupérer le contenu du
fichier cible pour l’injecter dans le projet durant l’exécution du pipeline.
Un template de pipeline est un pipeline qui a été pensé pour être utilisé dans différents contextes de la manière la plus générique possible. En ce sens, il n’est pas différent d’un pipeline normal vis-à-vis de GitLab.
Organisation
Nous avons créé un projet GitLab spécifique contenant tous nos templates de pipelines. Chaque développeur a accès à ce projet et peut faire des contributions via des merge requests qui seront revues par les différents responsables des templates.
Nous utilisons du semantic versioning et chaque projet utilisant les templates pointe vers une version spécifique ou une branche de manière temporaire.
Parmi ces templates, nous pouvons peut citer des pipelines pour construire des projets Java (Maven ou Gradle), Angular, dumper/restaurer des bases de données Mongo ou pour faire des tests de compatibilité de schémas Avro pour Kafka.
Le projet est structuré de la manière suivante (vue simplifiée) :
📦ci-project-templates
┣ 📜.gitlab-ci.yml # Pipeline pour ce projet pour la génération de la doc et la vérfication des templates (voir plus bas)
┣ 📂examples/ # Répertoire avec quelques exemples d'utilisation des templates
┃ ┗ ...
┣ 📂functions/ # Répertoire contenant des jobs cachés utilisés dans les pipelines
┃ ┣ ...
┃ ┗ 📜terraform.yml # Fichier des jobs cachés génériques Terraform
┣ 📂pipelines/ # Répertoire des templates de pipelines utilisables directement dans les projets
┃ ┣ 📜...
┃ ┣ 📜terraform-simple.yml
┃ ┗ 📜terraform-single-branch.yml
┣ 📂workflows/ # Répertoire des workflows: https://docs.gitlab.com/ee/ci/yaml/workflow.html
┃ ┗ 📜branch-pipelines.yml
┣ 📜CHANGELOG.md
┣ 📜ci-lint.py # Script Python faisant un lint via l'API GitLab https://docs.gitlab.com/ee/api/lint.html#validate-a-ci-yaml-configuration-with-a-namespace
┗ 📜README.md
Deux répertoires principaux sont à considérer : le répertoire functions
et le répertoire pipelines
.
Jobs cachés dans le repertoire functions
Le répertoire functions
contient essentiellement des jobs cachés (jobs dont le nom commence par un point “.
”). Ces
jobs sont ignorés par GitLab et ont juste pour vocation d’être étendus avec extends
. Les inclure directement dans un
projet n’aura donc aucun effet.
Ce découpage permet de réutiliser facilement un ensemble de paramètres dans différents jobs et pipelines: ce sont des jobs templates. Ils sont utilisés notamment par nos pipelines Terraform qui ont tous des jobs d’initialisation-validation, de plan et apply sur nos trois environnements principaux : développement ( dev), staging (stg) et production (prd). Ces functions n’ont pas la notion d’étapes (stages) GitLab : cette notion est propre à un pipeline dans notre découpage.
Parmi ces functions, le fichier functions/terraform.yml peut être divisé en plusieurs parties :
- Docker Image et variables : nous utilisons notre propre image Terraform avec des outils préinstallés comme tflint
et la CLI AWS. Les variables d’environnement
TF_*
définissent certains comportements de Terraform. - Workflow standard Terraform: cette partie définie le workflow standard de Terraform à savoir: initiation,
planification, application et destruction.
.init-validate
: Ce job configure la clé SSH permettant de récupérer nos modules Terraform privés, initialise la stack Terraform, fait une validation et un formatage de la configuration. En sortie, elle met en artéfacts plusieurs répertoires dont le plus important est le répertoire${TF_DIR}/.terraform
qui contient les providers ainsi que les modules que “terraform init” a téléchargé. En le mettant en artéfacts, on n’initialise ainsi qu’une seule fois Terraform pour l’ensemble du pipeline. Les autres jobs récupéreront ce répertoire avec le mot cléneeds
que nous verrons par la suite.
Ce job utilise les tagsdev
etcheck
associés à nos runners privés GitLab :dev
pour environnement de développement etcheck
pour désigner un runner dédié aux vérifications (instances EC2 spot notamment)..plan
: Job de lancement de tflint1 et du plan terraform. Ce job produit en artéfacts le plan Terraform sous deux formats : le premier est à destination de “terraform apply” (le fichier${TF_DIR}/${TF_WORKSPACE}.tfplan
) et le second à destination de GitLab (${TF_DIR}/${TF_WORKSPACE}.json
). Ce dernier, appelé “reports”, permet d’avoir un récapitulatif des changements à effectuer par Terraform dans une merge request :.apply
: Job de lancement de “terraform apply”. Le job concret associé à ce job aura besoin des artéfacts des jobs qui étendent.init-validate
et.plan
..destroy
: Job de lancement de “terraform destroy”. Comme pour le job.apply
, il aura besoin de.init-validate
et.plan
.
- Configuration des environnements : Nous utilisons des workspaces Terraform pour gérer nos différents
environnements applicatifs. Les jobs de cette partie définissent cette notion d’environnement avec la variable
TF_WORKSPACE
permettant de choisir automatiquement le bon workspace Terraform.
On retrouve également le tagdev
vu plus haut ainsi qu’un nouveau tagstable
qui permet de choisir des runners stables (instances EC2 non-spot notamment). Les tagsstg
etprd
définissent respectivement les runners de staging et de production.
Ces jobs définissent également des groupes de ressources GitLab CI. Deux jobs partageant le mêmeresource_group
ne peuvent s’exécuter simultanément : un des jobs sera en attente de la fin de l’autre. Le groupe de ressource agit comme un lock à l’image du lock Terraform mais à l’avantage de ne pas faire échouer un job comme le ferait un lock Terraform si on lançait deux plan ou apply en même temps sur le même environnement. Le groupe de ressource ne remplace pas le lock Terraform mais propose une fonctionnalité supplémentaire dans la CI pour éviter des échecs de jobs à cause de lock Terraform. - Configuration du workflow Terraform avec les environnements : Ces jobs sont des extensions des jobs précédents.
Chaque job combine la notion d’environnement à une étape du workflow standard de Terraform :
.plan-dev
,.apply-dev
,.destroy-dev
,.plan-stg
etc.
Ce sont ces jobs apply (.apply-dev
,.apply-stg
et.apply-prd
) qui définissent la notion d’environnement GitLab CI. Cette notion permet d’avoir un historique des déploiements effectués dans chaque environnement et également de protéger certains environnements en définissant qui a le droit ou non de déployer sur cet environnement :
Exemples de templates dans le repertoire pipelines
Une fois le cadre des pipelines défini, la concrétisation des fonctions s’effectue dans le répertoire pipelines/
.
Chaque fichier dans ce répertoire correspond à un pipeline complet utilisable directement dans un projet : ce sont
des pipelines templates.
Prenons l’exemple du template
pipelines/terraform-single-branch.yml. Ce pipeline implémente un workflow Terraform avec une seule
branche : la branche par défaut du projet représentée par la variable d’environnement CI_DEFAULT_BRANCH
.
Le second exemple, pipelines/terraform-simple.yml, se base sur la branche develop déployer en dev uniquement. La branche par défaut quant à elle permet de déployer sur staging et production.
Ces deux templates implémentent un workflow basé sur les branches : workflows/branch-pipelines.yml. GitLab CI permet également de lancer des pipelines de merge request différents des pipelines de branches. Dans un prochain article, je parlerai en détail de quelques workflows de déploiement.
Les dépendances entre les jobs sont matérialisées par needs
2:
- Tous les jobs dépendent du job
init-validate
pour récupérer les providers et les modules téléchargés. - Les jobs de déploiement (apply) dépendent en plus des jobs plan pour récupérer les opérations Terraform à effectuer.
Deux groupes de variables supplémentaires sont définies :
DISABLE_*
: Permet de désactiver le déploiement dans un environnement. Nous l’utilisons sur certains projets qui ne sont pas forcément déployés dans tous les environnements.AUTO_APPLY_*
: Permet de faire du déploiement continue en appliquant les modifications automatiquement après le plan.
Exemples d’utilisation
Exemple de .gitlab-ci.yml
:
include:
- project: chemin/vers/le/projet/ci-project-templates
ref: "1.0.0"
file: pipelines/terraform-single-branch.yml
variables:
TF_IMAGE_VERSION: tf1.0.10 # Version Terraform à utiliser. C'est la seule variable obligatoire
Pour les projets ne faisant que du Terraform, ces lignes suffisent dans la plupart des cas. Certains projets
peuvent néanmoins avoir besoin de fournir certaines variables à des jobs spécifiques. Dans ces cas, nous
pouvons rajouter ces jobs avec les mêmes noms que ceux dans les templates pour les surcharger.
Exemple : On surcharge les jobs plan-dev
et plan-stg
pour fournir des variables.
GitLab va merger ces paramètres3 avec ceux définis au niveau des jobs :
include:
- project: chemin/vers/le/projet/ci-project-templates
ref: "1.0.0"
file: pipelines/terraform-single-branch.yml
variables:
RIO_TF_IMAGE_VERSION: tf1.0.10
DISABLE_PRD: true # Pas d'environnement de production pour ce projet (pour le moment)
plan-dev:
variables: # Les variables définies ici seront fusionnées avec ceux définies dans le template
TF_VAR_my_super_variable: "${MY_SUPER_VARIABLE_DEV}"
plan-stg:
variables:
TF_VAR_my_super_variable: "${MY_SUPER_VARIABLE_STG}"
Debug et éditeur en ligne
Écrire et tester des pipelines CI/CD pour GitLab peut s’avérer fastidieux et complexe quand on utilise pleinement toutes
les fonctionnalités offertes par GitLab. Pour rendre cela plus facile, GitLab fournit
un éditeur en ligne (disponible dans chaque projet) pour faire du lint
et voir le résultat final du YAML qui sera utilisé. Très utile surtout lorsqu’on utilise des include
et des extends
.
Lorsqu’on déclenche un pipeline avec des include
, les fichiers correspondants sont récupérés lors de l’exécution et
sauvegardés en base de données. Ainsi, pour tester des modifications de templates sur un projet, il faut lancer un
nouveau pipeline pour que les dernières modifications soient prises en compte et non relancer un ancien job.
Dans le projet contenant les templates, nous faisons également un lint automatique des pipelines contenus dans le
dossier pipelines/
via l’API CI Lint de GitLab. Ceci nous permet de valider rapidement la syntaxe de nos templates en
prenant en compte tous les includes ainsi que les extends. Voir le script Python
ci-lint.py .
Templates de projets
GitLab Premium offre la possibilité de définir un groupe comme la source de projets templates. Lors de la création d’un nouveau projet, on peut choisir parmi la liste des projets de ce groupe : GitLab créera un nouveau projet avec tout l’historique Git du projet source.
La configuration se fait dans le menu “General” au niveau du groupe GitLab (groupe parent) qui aura besoin de ces templates. Le groupe utilisé comme groupe de projets templates doit être un sous-groupe direct du groupe parent.
Ensuite on peut sélectionner un de ces projets en faisant New project -> Create from template -> Onglet Group
pour
voir la liste des projets du groupe templates :
Vous aurez peut-être remarqué l’onglet Built-in
. Il s’agit des templates mis à disposition par GitLab. On peut citer
par exemple des templates de projet Hugo/Netlify (frameworks utilisés pour ce blog), Spring et Ruby on Rails. Plus
d’informations ici.
Nos templates Terraform sont inspirés de celui fourni pas GitLab.
Dans la figure précédente, les trois projets Terraform correspondent à des projets “starter” pour différentes typologies de projets Terraform.
Pour terminer
Nous utilisons ces templates sur la majorité de nos projets Terraform (une cinquantaine de projets) et nous en sommes assez satisfaits. Ils fournissent une manière assez pratique de partager des pipelines au sein de GitLab.
Comme tout template, les templates de pipelines GitLab se partagent bien si les projets Terraform suivent une certaine organisation. Par exemple, les templates présentés ici ne supportent pas une approche “monorepo” avec plusieurs stacks Terraform (backend) dans le même projet Git. Ainsi, avant de créer des templates, il est important de déterminer en amont quelles sont les pratiques que nous voulons véhiculer.
Dans une organisation où les équipes de développement ont une certaine autonomie, rien ne garantit pour autant l’usage de ces templates dans les différents projets. Il convient de communiquer auprès des équipes et de les impliquer dans la gestion des templates pour que tout le monde se les approprie. Il n’est pas rare aussi que certaines équipes créer des pipelines qui peuvent servir à d’autres équipes. Dans ces cas, nous les remontons dans le projet central et les rendons plus générique si besoin.
Enfin, dans un autre registre, pour avoir une vision globale de l’usage de nos templates ainsi que de Terraform ( versions, providers et modules) au sein de notre GitLab, nous avons des scripts qui analysent quotidiennement tous les projets Terraform pour générer une page Web récapitulative. Cette page sert notamment à savoir quels projets utilisent quelles versions des providers, versions de nos modules et d’aller directement voir tous les usages d’un module.
tflint est lancé dans le plan car dans certains cas, il a besoin de la valeur de certaines variables Terraform qui ne sont disponibles que dans un workspace. ↩︎
needs
est préféré àdependencies
car il offre plus de possibilités. Les jobs de différentes étapes peuvent s’exécuter en même temps et nous faire gagner beaucoup de temps. Il transforme ainsi le pipeline en un DAG. ↩︎Le merge effectué ici ne fonctionne pas avec tous les paramètres d’un job. On ne peut pas merger des listes par exemples. Si nous voulions par exemple rajouter des commandes au
before_script
ou auscript
, il faudrait réécrire toutes les commandes de ces sections et rajouter les nouvelles ensuite. Une autre manière serait d’utiliser des références. ↩︎