Introduction
Quand j’ai pris en charge l’infrastructure d’une grande entreprise française, j’ai découvert un système paralysé. Les déploiements prenaient des mois, parfois une année complète. Des consultants facturés à temps plein passaient leurs journées en réunions. Les documents étaient obsolètes avant même d’être terminés. Et tous les deux ans, les prestataires qui commençaient enfin à maîtriser le système arrivaient en fin de mission légale — emportant leur savoir avec eux.
Le problème n’était pas technique. C’était organisationnel et structurel. L’information était difficile à obtenir, volontairement non partagée (pour “protéger les emplois”), et chaque besoin nécessitait une demande formelle à une équipe dédiée. Un projet simple qui aurait dû prendre deux jours prenait un an. Il fallait payer chaque demande, payer des réunions où rien n’avançait.
J’ai développé une approche différente. Je l’appelle “inventory-driven” — pilotée par l’inventaire. Elle sépare radicalement ce qui change souvent (la liste de ce qu’on veut déployer) de ce qui change rarement (le code qui sait comment déployer). Dans notre cas, les déploiements sont passés de plusieurs mois à quelques jours. Plus important : les équipes projet peuvent désormais provisionner leurs ressources sans solliciter les DevOps.
Cet article explique pourquoi les approches classiques d’Infrastructure as Code ne scalent pas, et comment l’approche inventory-driven résout ce problème.
Pourquoi l’IaC classique ne scale pas
Les patterns habituels et leurs limites
La plupart des équipes qui adoptent Terraform ou Ansible commencent avec de bonnes intentions. Puis la réalité s’impose.
Un repo par application. Chaque équipe crée son propre dépôt Terraform. Au bout de 50 applications, vous avez 50 repos qui divergent, 50 façons différentes de faire la même chose, et personne ne sait ce qui est réellement déployé.
Un module par environnement. Pour gérer dev, staging et production, on duplique le code. Les environnements dérivent lentement. Un correctif appliqué en prod n’est jamais reporté en dev. Six mois plus tard, personne ne comprend pourquoi les environnements se comportent différemment.
Des variables partout. Les fichiers terraform.tfvars prolifèrent. Chaque cas particulier ajoute une variable. Le code devient un enchevêtrement de conditions if/else impossibles à maintenir.
Du code forké sans gouvernance. Les équipes copient-collent des modules trouvés sur internet ou chez le voisin. Sans bonnes pratiques partagées, chaque déploiement devient un cas unique. La dette technique s’accumule.
Le vrai problème
Toutes ces approches font la même erreur : elles mélangent deux choses fondamentalement différentes.
Ce qui décrit l’infrastructure — la liste des applications, leurs configurations, leurs environnements. Ça change souvent : nouvelle app, nouveau serveur, modification d’un paramètre.
Ce qui implémente l’infrastructure — le code Terraform, les rôles Ansible, les pipelines CI/CD. Ça change rarement : une fois que vous savez déployer une application Laravel, le code est le même pour toutes les applications Laravel.
Quand ces deux aspects sont mélangés, chaque ajout d’application ou d’environnement nécessite de toucher au code. Et toucher au code, c’est du risque, du temps, et de l’expertise.
L’idée centrale : séparer l’inventaire de l’implémentation
Une analogie simple
Pensez à un restaurant. Le chef a des recettes (l’implémentation). Le serveur a les commandes des clients (l’inventaire). Le chef ne réécrit pas la recette du risotto à chaque commande — il prend la commande et applique la recette.
L’approche inventory-driven fonctionne pareil :
- L’inventaire décrit ce qu’on veut : quelles applications, sur quels serveurs, avec quels paramètres.
- L’implémentation sait comment faire : provisionner une VM, déployer une app Laravel, configurer un certificat SSL.
Ajouter une application = ajouter une ligne dans l’inventaire. Pas de nouveau code. Pas de nouveau module. Pas de réunion.
Architecture globale
inventory/
├── terraform/
│ ├── dev.yaml → VMs, réseaux, stockage pour l'env dev
│ ├── staging.yaml → VMs, réseaux, stockage pour staging
│ └── prod.yaml → VMs, réseaux, stockage pour prod
└── ansible/
├── dev.yaml → Applications à déployer en dev
├── staging.yaml → Applications à déployer en staging
└── prod.yaml → Applications à déployer en prod
implementation/
├── terraform/ → Modules génériques (VM, réseau, SG...)
└── ansible/ → Rôles par framework (laravel, flask, react...)
L’inventaire est la source de vérité unique. Le code d’implémentation est identique quel que soit l’environnement.
Structure d’un inventaire applicatif (Ansible)
Voici à quoi ressemble un inventaire pour le déploiement d’applications :
# inventory/ansible/dev.yaml
portal:
- name: app-frontend
port_host: 8081
git_url: https://github.com/example/frontend.git
git_version: master
framework: flask
ssl_enabled: false
status: active
- name: app-backend
port_host: 8082
git_url: https://github.com/example/backend.git
git_version: master
framework: laravel
stack: livewire
ssl_enabled: true
app_url: https://app.example.com:8082
status: active
- name: legacy-app
port_host: 8083
git_url: https://github.com/example/legacy.git
framework: php
status: manual # Géré manuellement, Ansible ne touche pas
Les principes de conception
Déclaratif, pas impératif. L’inventaire décrit l’état souhaité, pas les étapes pour y arriver. “Je veux cette application sur ce port” — pas “installe PHP, puis clone le repo, puis configure Apache”.
Valeurs par défaut intelligentes. Le framework détermine automatiquement ce qu’il faut faire. Une app Laravel déclenche composer install, npm install, les migrations. Pas besoin de le spécifier.
Statuts pour le cycle de vie. Trois états possibles :
active: l’application est déployée et maintenueinactive: l’application est désinstallée proprementmanual: Ansible ignore cette entrée (géré par d’autres moyens)
Ce que l’inventaire ne contient PAS. Pas de logique. Pas de conditions. Pas de références à des chemins de fichiers ou des détails d’implémentation. Juste des données.
Structure d’un inventaire infrastructure (Terraform)
Pour le provisioning des ressources cloud, un inventaire séparé :
# inventory/terraform/dev.yaml
environment: "dev"
defaults:
ebs_size: 60 # Disque additionnel par défaut
categories:
- name: IAC_BAS_SPI
description: "Infrastructure as Code - Bac à sable pour tests"
subnet_id: "EXPOSURE"
security_groups: ["SG_BASE_1", "SG_BASE_2"]
cmdb_project_name: "PROJET POC"
instances:
- name: server01
- name: KUBERNETES
description: "Cluster Kubernetes de développement"
subnet_id: "COMPUTE"
security_groups: ["SG_BASE_1", "SG_K8S"]
cmdb_project_name: "Kubernetes Dev"
instances:
- name: k8s-master-01
- name: k8s-worker-01
- name: k8s-worker-02
Organisation par catégories
Les instances sont regroupées par projet ou fonction. Chaque catégorie porte :
- Les métadonnées projet (nom, description, référence CMDB)
- La configuration réseau (subnet, security groups)
- La liste des instances à créer
Cette structure permet de provisionner 3 ou 300 serveurs avec le même code Terraform — seul l’inventaire change.
Implémentation Terraform
Le code Terraform qui consomme l’inventaire :
# main.tf
locals {
config = yamldecode(file("inventory/${var.environment}.yaml"))
# Aplatir les instances avec leur contexte catégorie
instances = flatten([
for cat in local.config.categories : [
for instance in cat.instances : {
name = instance.name
category = cat.name
description = cat.description
subnet_id = cat.subnet_id
security_groups = cat.security_groups
cmdb_project = cat.cmdb_project_name
ebs_size = try(instance.ebs_size, local.config.defaults.ebs_size)
}
]
])
}
module "instance" {
for_each = { for inst in local.instances : inst.name => inst }
source = "./modules/ec2-instance"
name = each.value.name
subnet_id = local.subnets[each.value.subnet_id]
security_groups = [for sg in each.value.security_groups : local.security_groups[sg]]
ebs_size = each.value.ebs_size
tags = {
Project = each.value.category
Description = each.value.description
Environment = var.environment
CMDB = each.value.cmdb_project
}
}
Points clés
yamldecode() pour lire l’inventaire. Terraform parse directement le YAML. Pas de génération de tfvars, pas d’étape intermédiaire.
for_each sur les données aplaties. Une seule ressource module qui s’instancie autant de fois que nécessaire.
Defaults avec try(). Les valeurs par défaut sont dans l’inventaire, appliquées proprement.
State séparé par environnement. Chaque environnement a son propre state backend. Pas de workspaces Terraform — trop de confusion possible.
Implémentation Ansible
Côté déploiement applicatif, un playbook maître qui orchestre :
# deploy.yaml
- name: Deploy applications from inventory
hosts: "{{ target_host }}"
vars:
inventory: "{{ lookup('file', 'inventory/' + env + '.yaml') | from_yaml }}"
tasks:
- name: Process each application
include_role:
name: "deploy-{{ item.framework }}"
vars:
app: "{{ item }}"
loop: "{{ inventory[target_host] }}"
when: item.status == 'active'
- name: Remove inactive applications
include_role:
name: "remove-app"
vars:
app: "{{ item }}"
loop: "{{ inventory[target_host] }}"
when: item.status == 'inactive'
Un rôle par framework
Chaque technologie a son rôle dédié :
roles/
├── deploy-laravel/ → Clone, composer, npm, migrations, Apache/Nginx
├── deploy-flask/ → Clone, venv, pip, Gunicorn
├── deploy-react/ → Clone, npm, build, serveur statique
├── deploy-html/ → Clone, copie fichiers, configuration serveur
└── remove-app/ → Désinstallation propre
Le rôle deploy-laravel sait tout ce qu’il faut pour déployer une application Laravel : installer les dépendances Composer, compiler les assets npm, exécuter les migrations, configurer le serveur web. Ces bonnes pratiques sont codifiées une fois, appliquées partout.
Déclenchement
Deux modes de déclenchement possibles :
Push depuis l’inventaire. Un commit sur le fichier d’inventaire déclenche la pipeline GitLab CI qui exécute Ansible.
Pull depuis l’application. Le repo de l’application contient un hook qui appelle le déployeur avec ses paramètres. L’application “demande” son déploiement.
Orchestration CI/CD
Une pipeline GitLab CI typique :
# .gitlab-ci.yml
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
validate:
stage: validate
script:
- yamllint inventory/
- terraform -chdir=${TF_ROOT} init
- terraform -chdir=${TF_ROOT} validate
rules:
- changes:
- inventory/**/*
- terraform/**/*
plan:
stage: plan
script:
- terraform -chdir=${TF_ROOT} plan -var="environment=${CI_ENVIRONMENT_NAME}" -out=plan.tfplan
artifacts:
paths:
- ${TF_ROOT}/plan.tfplan
environment:
name: ${CI_ENVIRONMENT_NAME}
action: prepare
apply:
stage: apply
script:
- terraform -chdir=${TF_ROOT} apply plan.tfplan
dependencies:
- plan
environment:
name: ${CI_ENVIRONMENT_NAME}
when: manual # Validation humaine requise
rules:
- if: $CI_ENVIRONMENT_NAME == "prod"
Validation de l’inventaire
La première étape valide le YAML avant tout déploiement. Une erreur de syntaxe ou une valeur invalide bloque la pipeline immédiatement — pas de déploiement cassé.
Apply manuel pour la production
Le déploiement en production nécessite une action manuelle. Le plan est visible, reviewable, avant d’être appliqué.
L’interface web : démocratiser l’accès
Remplir un fichier YAML et commit, c’est déjà simple. Mais pour les équipes non techniques, nous avons ajouté une interface web en React.
Fonctionnalités
- Formulaire guidé pour ajouter une application : URL du repo, framework, port, options
- Visualisation de l’inventaire actuel par environnement
- Déclenchement du déploiement en un clic
- Logs de déploiement en temps réel
L’interface génère le YAML, commit dans le repo, et déclenche la pipeline. L’utilisateur n’a pas besoin de connaître Git, YAML, ou Terraform.
Garde-fous
- Validation côté IHM : les champs obligatoires sont vérifiés, les formats validés
- Droits par environnement : seules certaines personnes peuvent toucher à la production
- Audit trail : chaque modification est tracée (qui, quand, quoi)
Documentation auto-générée
Un problème récurrent dans les grandes organisations : la documentation est obsolète. Elle est écrite une fois, jamais mise à jour, et personne ne lui fait confiance.
Notre solution : générer la documentation depuis l’inventaire.
MkDocs intégré
Un site MkDocs est généré automatiquement et expose :
- La liste des applications déployées par environnement
- Les configurations de chaque application
- Les procédures standard (comment ajouter une app, comment debugger)
Édition collaborative
Un bouton “Éditer” sur chaque page permet de modifier la documentation directement. Le clic ouvre l’éditeur GitLab sur le fichier Markdown correspondant. Commit, merge request, déploiement automatique du site.
La documentation reste vivante parce qu’elle est facile à modifier.
Résultats concrets
Avant / Après
| Métrique | Avant | Après |
|---|---|---|
| Temps pour déployer une nouvelle app | 3-12 mois | 1 jour |
| Expertise requise | DevOps senior | Formulaire web |
| Documentation | Obsolète, éparpillée | Générée, centralisée |
| Reproductibilité | Faible (code forké) | Totale (même code partout) |
| Onboarding nouveau DevOps | Semaines | Jours |
Ce qui a vraiment changé
Les DevOps ne codent plus pour chaque projet. Ils maintiennent les rôles et modules génériques. Leur expertise est capitalisée, pas réinventée à chaque mission.
Les équipes projet sont autonomes. Elles remplissent un formulaire ou un YAML, sans dépendre d’une équipe centrale pour chaque action.
L’information est partagée par défaut. L’inventaire est visible, versionné, documenté. Quand un prestataire part, le savoir reste.
Les bonnes pratiques sont appliquées automatiquement. Elles sont dans le code des rôles Ansible, pas dans la tête des gens. Impossible de déployer “sans les bonnes pratiques” — le système ne sait faire qu’avec.
Limites et cas où ne pas utiliser cette approche
Ça fonctionne bien pour
- Beaucoup d’applications similaires. Si vous déployez 50 applications Laravel, l’approche est redoutablement efficace.
- Environnements multiples. Dev, staging, prod avec la même structure — l’inventaire gère les différences.
- Équipes qui veulent de l’autonomie encadrée. Liberté de déployer, dans un cadre qui garantit les bonnes pratiques.
Ça fonctionne moins bien pour
- Infrastructure très hétérogène. Si chaque application est un cas unique avec des besoins spécifiques, le gain est limité.
- Petits projets. Pour 3 applications, l’overhead de mise en place n’est pas justifié.
- Équipes sans culture GitOps. Si personne ne veut versionner ou utiliser des pipelines, l’approche ne prendra pas.
Complexité à prévoir
- Conception initiale de l’inventaire. Le schéma YAML doit être bien pensé dès le départ. Le modifier après coup impacte tous les déploiements.
- Rôles génériques de qualité. Les rôles Ansible doivent couvrir tous les cas d’usage. Ça demande de l’investissement initial.
- Formation des équipes. Même simplifié, le système nécessite une prise en main.
Conclusion
L’approche inventory-driven n’est pas une révolution technique. C’est une discipline : séparer ce qui change de ce qui ne change pas, et traiter l’inventaire comme une donnée de première classe.
Le bénéfice principal n’est pas la vitesse de déploiement — même si passer d’un an à un jour est appréciable. C’est la persistance du savoir. Quand les bonnes pratiques sont dans le code et que l’état du système est dans un fichier YAML versionné, le turnover des équipes ne détruit plus la connaissance.
Dans un contexte où les consultants changent tous les deux ans et où l’information est traditionnellement un pouvoir qu’on garde pour soi, c’est peut-être le changement le plus important.
Ressources
- Code source : Repo GitHub
- Ebook : L’approche Inventory-Driven pour l’Infrastructure as Code
Cet article est basé sur une expérience de mise en œuvre dans un contexte de 400+ applications. Les exemples de code sont simplifiés pour la clarté.