guest@6admin.io:~$

GitOps, le mot après la pratique : gitflow, GitLab CI, Terraform et Ansible

Le mot se répand depuis un an, GitOps, posé par Weaveworks en 2017 autour de Kubernetes et d’une idée simple, git comme source de vérité de l’infrastructure et une boucle qui fait converger le réel vers ce que décrit le dépôt. Je l’ai lu comme j’avais lu DevOps quelques années plus tôt, non pas en découvrant une méthode mais en reconnaissant la mienne (DevOps, un mot sur ce que je bricolais déjà ), à l’époque avec git et des hooks bash. En 2018, le bricolage a grandi et il a pris la forme d’un gitflow adapté, de conventions et d’un pipeline qui fait tout le travail.

Le dépôt comme source de vérité

Le principe n’a pas changé depuis mes hooks de 2010, seul l’outillage s’est étoffé. Toute l’infrastructure est décrite dans git, le Terraform qui crée les machines et le réseau, l’Ansible qui les configure, les variables, les inventaires et plus rien ne se fait à la main sur une console, sauf à devoir ensuite justifier pourquoi le dépôt ment. Le dépôt n’est pas une sauvegarde de la vérité, il est la vérité et la production n’en est que le reflet appliqué. La conséquence pratique est que git log devient le journal de qui a changé quoi dans l’infra et qu’un retour en arrière est un revert suivi d’un nouvel apply, pas une archéologie de mémoire.

Le gitflow, adapté : une branche crée son infra

Un gitflow d’école, pensé pour du logiciel applicatif, ne tombe pas juste sur de l’infra, donc je l’ai adapté plutôt que subi. Le cœur de mon adaptation tenait en une règle, la branche est l’environnement. Créer une branche ne préparait pas un déploiement futur, cela provisionnait l’infrastructure correspondante et la supprimer la détruisait. Le cycle de vie de la branche gouvernait le cycle de vie de l’infra, exactement, sans décalage à gérer à la main.

Concrètement, le nom de la branche devenait la clef de tout, un workspace Terraform dédié d’un côté, un environnement GitLab dynamique de l’autre, les deux indexés sur $CI_COMMIT_REF_SLUG. Pousser feature/metrics faisait naître un terraform workspace du même nom, avec son propre état, donc ses propres machines, son réseau, ses enregistrements DNS, une réplique isolée de la prod le temps du travail. La branche principale valait la production, les branches de travail valaient autant d’environnements éphémères et la revue en merge request portait sur une infra réelle, pas sur une promesse.

Le pipeline, Terraform puis Ansible

Le cœur, c’est GitLab CI qui orchestre deux outils dans leurs rôles respectifs. Terraform crée, Ansible configure et tenir cette frontière évite les hacks des deux côtés, ce que j’ai détaillé ailleurs (Terraform crée, Ansible configure ). La bascule branche = infra se lit directement dans le pipeline, le plan puis l’apply sur toute branche qui crée son workspace et le destroy accroché à la fermeture de l’environnement. GitLab n’offre pour l’instant que only et except pour dire quand un job tourne, ce qui oblige à quelques contournements pas très élégants pour faire tenir la logique et le YAML ci-dessous en montre l’ossature plutôt que toutes les rustines.

stages: [plan, apply, configure]

.workspace: &workspace
  before_script:
    - terraform init
    - terraform workspace select "$CI_COMMIT_REF_SLUG" || terraform workspace new "$CI_COMMIT_REF_SLUG"

terraform-plan:
  <<: *workspace
  stage: plan
  script:
    - terraform plan -out=tfplan
  artifacts:
    paths: [tfplan]
    expire_in: 1 week
  only: [branches]

terraform-apply:
  <<: *workspace
  stage: apply
  script:
    - terraform apply -input=false tfplan
  environment:
    name: "$CI_COMMIT_REF_SLUG"
    on_stop: terraform-destroy
  only: [branches]

ansible-configure:
  stage: configure
  script:
    - ansible-playbook -i "inventories/$CI_COMMIT_REF_SLUG" site.yml
  only: [branches]

terraform-destroy:
  <<: *workspace
  stage: apply
  script:
    - terraform destroy -input=false -auto-approve
    - terraform workspace select default
    - terraform workspace delete "$CI_COMMIT_REF_SLUG"
  environment:
    name: "$CI_COMMIT_REF_SLUG"
    action: stop
  when: manual
  only: [branches]

Chaque push déclenche le plan, publié en artefact et c’est lui qu’on relit dans la merge request comme on relit une diff de code, parce que c’en est une, celle de l’état du monde à venir. L’apply suit sur le workspace de la branche puis la configuration et GitLab enregistre l’environnement comme actif. Quand la branche fusionne ou disparaît, l’on_stop déclenche le destroy, l’environnement se ferme et l’infra éphémère rend ses ressources, sans qu’un humain ait à se souvenir de nettoyer. Les secrets vivent dans les variables protégées du CI, l’état Terraform dans un backend distant et verrouillé, par workspace et l’inventaire Ansible se lit parfois dans le DNS plutôt que dans un fichier (inventaire piloté par DNS ).

La part honnête, ce que ce n’est pas tout à fait

Je joue cartes sur table, voici ce que mon montage n’est pas. Le GitOps au sens strict, celui de Weaveworks, repose sur une boucle de réconciliation, un agent qui vit dans le cluster, observe en continu l’écart entre le dépôt et le réel et corrige de lui-même, le tout pensé pour Kubernetes. Ce que je fais est en push, piloté par la CI, c’est de l’IaC déclenchée par git, pas un contrôleur qui réconcilie en pull. Le principe est le même, git est la source de vérité et tout se rejoue depuis lui, mais le mécanisme diffère et la différence n’est pas cosmétique. Mon pipeline ne voit pas une dérive, si quelqu’un modifie une machine à la main entre deux apply, rien ne le rattrape avant le prochain passage, là où une boucle de réconciliation le corrigerait toute seule. J’appelle donc cela du GitOps au sens large, parce que l’esprit y est et je nomme le pari plutôt que de le maquiller.

Où atterrir

L’étiquette compte moins que la discipline qu’elle recouvre, l’infrastructure décrite dans git, appliquée par un pipeline et rien à la main. Le gitflow adapté et les conventions ne sont pas de la décoration, ils sont ce qui rend l’automate fiable et l’historique lisible. Terraform crée, Ansible configure, la CI enchaîne, git arbitre et le jour où j’ajouterai une vraie boucle de réconciliation, ce sera une marche de plus sur le même escalier, pas un reniement.

[edit: 2019] Les contournements n’ont pas survécu longtemps, puisque l’arrivée de rules dans GitLab CI, avec la version 12.3, a enfin permis d’écrire la logique telle qu’elle était pensée. Le plan tourne désormais sur la merge request, où il se relit et sur chaque push de branche, l’apply reste automatique pour les environnements éphémères mais devient manuel sur la branche principale et le destroy ne peut plus être déclenché sur la production par mégarde. C’est cette version-là qui a tourné pendant des années sans que j’aie à y retoucher:

stages: [plan, apply, configure]

.workspace: &workspace
  before_script:
    - terraform init
    - terraform workspace select "$CI_COMMIT_REF_SLUG" || terraform workspace new "$CI_COMMIT_REF_SLUG"

terraform-plan:
  <<: *workspace
  stage: plan
  script:
    - terraform plan -out=tfplan
  artifacts:
    paths: [tfplan]
    expire_in: 1 week
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_PIPELINE_SOURCE == "push"'

terraform-apply:
  <<: *workspace
  stage: apply
  script:
    - terraform apply -input=false tfplan
  environment:
    name: "$CI_COMMIT_REF_SLUG"
    on_stop: terraform-destroy
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "master"'
      when: manual
    - if: '$CI_PIPELINE_SOURCE == "push"'

ansible-configure:
  stage: configure
  script:
    - ansible-playbook -i "inventories/$CI_COMMIT_REF_SLUG" site.yml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'

terraform-destroy:
  <<: *workspace
  stage: apply
  script:
    - terraform destroy -input=false -auto-approve
    - terraform workspace select default
    - terraform workspace delete "$CI_COMMIT_REF_SLUG"
  environment:
    name: "$CI_COMMIT_REF_SLUG"
    action: stop
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME != "master"'
      when: manual

[edit: 2026] Le GitOps au sens strict, celui de la boucle de réconciliation que je n’avais pas en 2018, a gagné sur Kubernetes, porté par Argo CD et Flux, tous deux diplômés de la CNCF fin 2022. Weaveworks, qui avait forgé le mot, a fermé début 2024 et Flux ne s’est pas arrêté pour autant, la communauté ayant repris le projet, ce qui dit assez bien qu’une pratique survit à l’entreprise qui l’a nommée. Côté IaC, le paysage a bougé aussi, Terraform étant passé sous licence BSL en 2023 et OpenTofu étant né du fork qui a suivi et l’on sait désormais faire réconcilier du Terraform ou de l’OpenTofu en pull, avec des contrôleurs comme tofu-controller dans l’écosystème Flux ou décrire l’infrastructure cloud comme des ressources Kubernetes avec Crossplane. GitLab lui-même a fini par s’appuyer sur Flux pour le GitOps de son agent Kubernetes. Hors Kubernetes, en revanche, le push piloté par la CI reste la norme et le montage de ce billet, branche comprise, n’a rien perdu de sa pertinence pour une infra de VM ou de bare-metal.

[edit: 2026] L’implémentation Ansible de tout ceci: le rôle gitlab-runner .