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.
  • 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.

L’ensemble des fichiers YAML présentés dans cet article se trouve dans ce fichier ZIP. Vous pouvez utiliser ce ZIP comme point de départ pour vos propres templates.
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 :

  1. 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.
  2. 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 tags dev et check associés à nos runners privés GitLab : dev pour environnement de développement et check 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 :
      Rapport Terraform dans une MR. Pour chaque environnement, on a le nombre de ressources à ajouter, modifier et supprimer.
      Rapport Terraform dans une MR. Pour chaque environnement, on a le nombre de ressources à ajouter, modifier et supprimer.
    • .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.
  3. 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 tag dev vu plus haut ainsi qu’un nouveau tag stable qui permet de choisir des runners stables (instances EC2 non-spot notamment). Les tags stg et prd 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ême resource_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.
  4. 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 :
Liste des environnements GitLab CI pour ce projet
Liste des environnements GitLab CI pour ce projet
Historique de déploiement sur l'environnement de stg pour ce projet
Historique de déploiement sur l’environnement de stg pour ce projet

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 needs2:

  • 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 :

Choix du template lors de la création d'un nouveau projet
Choix du template lors de la création d’un nouveau projet

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.


  1. 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. ↩︎

  2. 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↩︎

  3. 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 au script, 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↩︎

Mohamed El Mouctar HAIDARA
Mohamed El Mouctar HAIDARA
Senior Platform Engineer
comments powered by Disqus