Refactoriser un Terraform legacy multi-équipes
Cette démarche part d'un dépôt Terraform legacy : un seul module racine, un seul fichier d'état partagé, les ressources de toutes les équipes emmêlées ensemble, et un terraform plan qui prend plusieurs minutes juste pour se rafraîchir. Ensuite, elle découpe cette configuration en modules partagés versionnés et en fichiers d'état séparés par équipe, afin que chaque équipe puisse posséder son infrastructure sans empiéter sur celle des autres.
Avant et après
AVANT APRÈS
───────────────────────────── ─────────────────────────────
infra/ infra/
├── main.tf (2 400 lignes) ├── modules/
├── variables.tf │ ├── networking/
├── outputs.tf │ ├── postgres-service/
└── (un seul fichier d'état │ └── eks-node-pool/
partagé par toutes les équipes) ├── teams/
│ ├── payments/
│ │ ├── main.tf
│ │ └── backend.tf (état propre)
│ ├── platform/
│ │ ├── main.tf
│ │ └── backend.tf (état propre)
│ └── data-eng/
│ ├── main.tf
│ └── backend.tf (état propre)
└── policy/
└── guardrails.regoDeux mouvements suffisent : extraire les primitives partagées et stables (VPC, node pools, un module Postgres) dans des modules versionnés, puis donner à chaque équipe la propriété de son propre fichier d'état. Un apply de l'équipe payments ne peut pas toucher aux ressources de data-eng, pas par convention de nommage, mais parce qu'elles sont dans des fichiers d'état structurellement séparés.
Tracer les frontières de modules par propriété, pas par type de ressource
L'instinct est d'organiser les modules par service AWS/GCP (modules/vpc, modules/rds, modules/iam), bon pour les primitives partagées, mais la mauvaise frontière pour la couche équipes, qui doit être organisée selon qui est d'astreinte dessus :
teams/payments/main.tfmodule "postgres" {
source = "../../modules/postgres-service"
version = "2.3.0"
team = "payments"
instance_size = "db.r6g.xlarge"
multi_az = true
}
module "eks_node_pool" {
source = "../../modules/eks-node-pool"
version = "1.4.0"
team = "payments"
min_size = 3
max_size = 12
instance_types = ["m6i.xlarge"]
}Migrer l'état en toute sécurité avec les blocs moved
L'ancienne méthode pour scinder un fichier d'état monolithique était des commandes terraform state mv manuelles exécutées contre l'état de production (à une faute de frappe de rendre une ressource orpheline). Le bloc moved de Terraform 1.1+ fait le même travail de façon déclarative, dans une PR revue et reproductible :
# Dans teams/payments/main.tf, une fois la ressource
# physiquement déplacée dans cette nouvelle configuration :
moved {
from = module.monolith.aws_db_instance.payments_primary
to = module.postgres.aws_db_instance.primary
}Lancez terraform plan après avoir ajouté un bloc moved : Terraform montre la ressource renommée, pas détruite puis recréée, le feu vert pour lancer apply.
Pour scinder le fichier d'état entre différents backends, terraform state mv -state-out=<new-backend-state> reste nécessaire pour le déplacement physique. Faites-le une fois par groupe de ressources, puis confirmez avec terraform plan que l'ancienne et la nouvelle configuration montrent zéro changement avant de supprimer quoi que ce soit de l'ancien état.
État distant et verrouillage, par équipe
# teams/payments/backend.tf
terraform {
backend "s3" {
bucket = "acme-terraform-state"
key = "teams/payments/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}Un état séparé par équipe, dans le même bucket et la même table de verrouillage : l'isolation sans infrastructure de backend supplémentaire à maintenir.
Garde-fous policy-as-code
Le policy-as-code empêche quiconque, vous y compris, de livrer volontairement quelque chose de dangereux sous la pression d'un deadline. Ceci tourne comme un contrôle CI (conftest test contre le JSON du plan) avant que apply ne soit atteignable :
package terraform.guardrails
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_acl"
resource.change.after.acl == "public-read"
msg := sprintf("Le bucket S3 '%s' ne doit pas être lisible publiquement", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_db_instance"
not resource.change.after.tags.team
msg := sprintf("L'instance RDS '%s' n'a pas le tag 'team' requis", [resource.address])
}# Étape CI, fait échouer le pipeline avant même que apply soit atteignable
- name: Terraform plan → JSON → policy check
run: |
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test --policy policy/ tfplan.jsonDétection de dérive
Une fois que les équipes possèdent leur propre état, la dérive devient un problème que chaque équipe doit détecter tôt, pas un mystère à l'échelle de la plateforme. Refermer cette boucle prend un plan planifié, en lecture seule, contre l'état de chaque équipe, câblé pour alerter sur tout diff non vide :
name: drift-detection
on:
schedule:
- cron: "0 6 * * *"
jobs:
detect:
strategy:
matrix:
team: [payments, platform, data-eng]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: terraform -chdir=teams/${{ matrix.team }} init
- run: terraform -chdir=teams/${{ matrix.team }} plan -detailed-exitcode
# le code de sortie 2 signifie qu'une dérive a été détectée, alerter en conséquenceCe qui change pour les équipes
Un nouvel ingénieur dans l'équipe payments peut lancer terraform apply en toute sécurité dès sa première semaine, parce que le rayon d'impact de son fichier d'état se limite uniquement à ses propres ressources.
Un avant/après représentatif :
Avant : 1 fichier d'état partagé, module racine d'environ 2 400 lignes
terraform plan : 3m40s | 1 seule personne dans l'équipe prête à lancer apply
Après : 6 fichiers d'état détenus par les équipes + 3 modules partagés versionnés
terraform plan : 20-35s par équipe | chaque ingénieur lance ses propres appliesSi l'une de ces étapes (les frontières de modules, une migration d'état sûre, ou câbler le policy-as-code dans la CI) ressemble au mur contre lequel vous butez, c'est précisément à cela que sert une mission de Revue d'architecture.
Envie de faire tourner ça en production ?
Ce tutoriel couvre les concepts et l'architecture. Si vous voulez l'implémenter dans votre propre infrastructure, ou monter en compétence pour posséder ce sujet durablement, je propose du mentorat individuel construit autour de votre environnement réel, pas une formation générique.
Ce tutoriel
- Concepts clés et architecture principale
- Extraits de code illustratifs
- Le raisonnement derrière chaque décision
Mentorat individuel
- Des sessions de travail sur votre propre environnement
- Des réponses directes aux cas particuliers que vous rencontrez
- Un retour sur votre implémentation réelle
- Un accompagnement continu pendant que vous la construisez