
Monorepo ou un dépôt par stack, la question se discute sur des critères d'organisation : revue de code, droits, cadence de déploiement. Mais elle a une conséquence technique qui, elle, ne se discute pas : découper les stacks découpe les états, et une stack ne voit plus rien de l'autre tant qu'on n'a pas publié explicitement ce qu'elle doit partager.
Tout ce qui suit a été exécuté sur Terraform v1.15.4. Trois faits y décident plus que le débat d'organisation, et deux d'entre eux surprennent.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Peser les deux organisations sur des critères concrets
- Pourquoi un module local refuse toute contrainte de version
- Ce qui traverse la frontière entre deux configurations, et ce qui reste
- Lire l'état d'une autre stack sans dupliquer une seule valeur
- Pourquoi un secret ne se publie jamais dans une sortie racine
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
- Savoir appeler un module (module local partagé).
Les deux organisations, en pratique
Section intitulée « Les deux organisations, en pratique »Le monorepo rassemble modules et stacks dans un seul dépôt. Une modification transverse tient dans une revue, le module et ses appelants restent cohérents, et rien ne se désynchronise.
depot/├── modules/reseau/└── stacks/ ├── plateforme/ └── applicatif/Le dépôt par stack sépare les périmètres. Chaque équipe a ses droits, sa cadence et son historique, au prix d'une coordination explicite dès qu'un changement traverse deux dépôts.
| Critère | Monorepo | Un dépôt par stack |
|---|---|---|
| changement transverse en une revue | oui | non |
| droits distincts par périmètre | difficile | oui |
| cadences de déploiement indépendantes | possible | oui |
| cohérence module et appelants | automatique | à gérer |
| versionnement du module partagé | inutile | nécessaire |
C'est la dernière ligne qui mène au premier fait mesuré.
Un module local n'accepte pas version
Section intitulée « Un module local n'accepte pas version »Dans un monorepo, on appelle le module par un chemin relatif, et il n'y a aucune version à gérer. Beaucoup de guides présentent cela comme une commodité qu'on choisit. C'est en réalité un refus de Terraform :
module "reseau" { source = "./modules/reseau" version = "~> 1.0"}Error: Invalid registry module source address
Failed to parse module registry address: can't use local directory"./modules/reseau" as a module registry address.
Terraform assumed that you intended a module registry source address becauseyou also set the argument "version", which applies only to registry modules.L'init sort en 1. La documentation le dit sans ambiguïté : « Modules
sourced from local file paths do not support version because they're loaded
from the same source repository and always share the same version as their
caller. » Le module et son appelant sont donc toujours à la même version.
Découper, c'est couper la circulation des valeurs
Section intitulée « Découper, c'est couper la circulation des valeurs »Le jour où plateforme et applicatif deviennent deux configurations, plus
aucune expression ne traverse. La passerelle officielle est la lecture de
l'état distant, par ses outputs déclarés :
data "terraform_remote_state" "plateforme" { backend = "local"
config = { path = "../plateforme/terraform.tfstate" }}La valeur se lit ensuite par
data.terraform_remote_state.plateforme.outputs.network_cidr.
Seules les sorties de la racine traversent
Section intitulée « Seules les sorties de la racine traversent »Voici le piège numéro un quand on éclate un monorepo, et il est silencieux jusqu'à l'erreur. Une sortie déclarée dans un module imbriqué n'existe pas pour le consommateur. Mesuré, sur une stack amont qui appelle un module et n'en ré-exporte qu'une valeur :
Error: Unsupported attribute
data.terraform_remote_state.amont.outputs is object with 1 attribute "reexporte"This object does not have an attribute named "identifiant_interne".Ce qui doit franchir la frontière se ré-exporte donc explicitement à la racine :
output "network_cidr" { value = module.reseau.network_cidr}C'est une décision d'interface, pas une formalité : la liste des sorties racine est le contrat public de votre stack.
Publier une sortie, c'est publier tout l'état
Section intitulée « Publier une sortie, c'est publier tout l'état »L'avertissement de la documentation est le point le plus important de cette page, et il manque à presque tous les comparatifs :
any user or server which has enough access to read the root module output values will also always have access to the full state snapshot data by direct network requests.
Donner accès aux sorties d'une stack, c'est donner accès au snapshot
complet de son état. Le multi-repo isole les droits sur le dépôt, mais un
couplage par terraform_remote_state contourne cette isolation côté état.
Deux arguments qui évitent des blocages
Section intitulée « Deux arguments qui évitent des blocages »terraform_remote_state accepte defaults, qui comble un output
manquant :
data "terraform_remote_state" "amont" { backend = "local"
config = { path = "../amont/terraform.tfstate" }
defaults = { identifiant = "valeur-de-repli" }}Mesuré : quand l'état existe mais que l'output demandé n'y figure pas, la valeur de repli est bien retenue, et l'apply sort en 0.
Attention toutefois à son périmètre exact. Si l'état amont n'existe pas du
tout, defaults ne sauve rien :
Error: Unable to find remote state
No stored state was found for the given workspace in the given backend.L'argument workspace désigne, lui, le workspace de la stack amont à lire.
Mesuré, sur un backend local dont le path pointe un fichier précis, il
rend la même erreur : cet argument prend son sens sur un backend qui adresse
nativement ses workspaces, pas sur un chemin de fichier codé en dur.
Les pièges mesurés
Section intitulée « Les pièges mesurés »Le couplage a un coût, et la doc le nomme
Section intitulée « Le couplage a un coût, et la doc le nomme »terraform_remote_state « creates a tighter coupling between configurations ».
Chaque lecture d'état lie deux stacks : l'amont ne peut plus renommer une
sortie sans casser l'aval. Les alternatives citées par la documentation
relâchent ce lien, en passant par une convention plutôt que par l'état :
une ressource étiquetée puis retrouvée par ses tags, un enregistrement
DNS à nom prévisible, ou un magasin de paramètres.
Un clone superficiel accélère l'init en multi-repo
Section intitulée « Un clone superficiel accélère l'init en multi-repo »Le modèle multi-repo fait cloner le dépôt de modules à chaque init. Le
paramètre depth ajoute --depth au git clone, et la documentation
recommande « Setting depth to 1 is suitable for most cases ». Sur un dépôt à
historique long, c'est le principal levier de temps d'exécution en
intégration continue.
module "reseau" { source = "git::https://exemple.invalide/infra-modules.git//reseau?ref=v1.3.0&depth=1"}Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Correction |
|---|---|---|
Invalid registry module source address | version posé à côté d'un source local ou Git | retirer version ; seul un module de registre l'accepte |
Unsupported attribute sur un output amont | sortie d'un module imbriqué, non ré-exportée | ajouter l'output à la racine de la stack amont |
Unable to find remote state | l'état amont n'existe pas encore | appliquer la stack amont d'abord ; defaults ne couvre pas ce cas |
| une valeur amont recopiée diverge | valeur dupliquée au lieu d'être lue | passer par terraform_remote_state |
| un secret est lisible par une stack aval | sortie racine portant le secret | le retirer des sorties : sensitive ne protège pas l'état |
init très lent en multi-repo | clone complet du dépôt de modules | ajouter depth=1 à la source Git |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab deux stacks qui se parlent livre un monorepo déjà découpé mais non
branché : un version de trop empêche l'init, les sorties de la plateforme
sont vides, et le bloc data de l'applicatif est troué. Vous ré-exportez ce
qui doit traverser, vous branchez la lecture d'état, et vous laissez le mot
de passe dans l'état amont. Le CIDR étant tiré au sort à l'apply, le recopier
ne tient pas : la validation compare les deux stacks. Il se joue hors ligne.
À retenir
Section intitulée « À retenir »- Découper les stacks découpe les états : plus aucune expression ne traverse.
- Un module local n'accepte pas
version, et l'initéchoue en 1. - Seule la forme registre accepte une contrainte de version ;
git::est un pinning littéral. - Seules les sorties de la racine sont lisibles depuis une autre configuration.
- La liste des sorties racine est le contrat public d'une stack.
- Publier une sortie donne accès au snapshot complet de l'état.
sensitivemasque l'affichage texte, pasoutput -json, passhow -json, pas l'état.defaultscomble un output manquant, jamais un état absent.terraform_remote_stateresserre le couplage : les tags ou le DNS le relâchent.depth=1raccourcit le clone d'un module Git en intégration continue.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui bloque au moment de découper : le
version refusé, la sortie invisible, et le secret qu'on croit protégé.
Mesuré sur 1.15.4
module "reseau" {
source = "./modules/reseau"
version = "~> 1.0"
}
Error: Invalid registry module source address
Terraform assumed that you intended a module registry source address because
you also set the argument "version", which applies only to registry modules.
L'init sort en 1.La règle
« Modules sourced from local file paths do not supportversion because they're loaded from the same source repository and always share the same version as their caller. »Ce n'est donc pas une commodité du monorepo, c'est une impossibilité : dans un monorepo, le module et ses appelants sont par construction à la même version.Deux mécanismes différents
Registre : contrainte résolue par Terraform.module "reseau" {
source = "exemple/reseau/aws"
version = "~> 1.3"
}
Git : pinning littéral, pas de plage.module "reseau" {
source = "git::https://exemple.invalide/modules.git//reseau?ref=v1.3.0"
}
La conséquence pratique
En multi-repo sans registre, vous ne pouvez pas écrire « la dernière 1.3.x » : vous épinglez une référence exacte, et la montée de version devient un changement de code explicite.À noter, un tag Git est déplaçable : seul un SHA-1 est réellement immuable.Le message obtenu
Error: Unsupported attribute
data.terraform_remote_state.amont.outputs is object with 1 attribute "reexporte"
This object does not have an attribute named "identifiant_interne".
La correction
À la racine de la stack amont :output "network_cidr" {
value = module.reseau.network_cidr
}
Pourquoi cela compte
C'est le piège numéro un quand on éclate un monorepo en stacks, et il ne se voit qu'au moment où le consommateur tente la lecture.Prenez-le comme une décision d'interface : la liste des sorties racine est le contrat public de votre stack. Tout ce qui n'y figure pas reste interne.L'avertissement officiel
any user or server which has enough access to read the root module output values will also always have access to the full state snapshot data by direct network requests.
Ce que cela implique
Le multi-repo isole les droits sur le dépôt, mais un couplage parterraform_remote_state contourne cette isolation du côté de l'état.Si la stack amont gère des données que vous considérez sensibles, la documentation recommande de ne pas employer terraform_remote_state du tout, et de publier explicitement les valeurs partagées vers un magasin dédié.Les alternatives citées
Une ressource étiquetée puis retrouvée par une source de données sur ses tags, un enregistrement DNS à nom prévisible, ou un espace de clés partagé. Elles relâchent aussi le couplage entre configurations.Mesuré
Sur une sortie déclaréesensitive = true :| Commande | Valeur en clair ? |
|---|---|
terraform output |
non |
terraform output -json |
oui |
terraform show -json |
oui |
Ce que sensitive fait vraiment
Il masque l'affichage texte, ce qui protège d'une lecture par-dessus l'épaule et de certains logs. Il ne retire rien de l'état, ni des représentations JSON.La règle qui en découle
Un mot de passe reste dans l'état de la stack qui le produit, et n'apparaît dans aucune sortie racine. Sinon, toute stack autorisée à lire ces sorties accède au snapshot complet.Le cas qu'il couvre
data "terraform_remote_state" "amont" {
backend = "local"
config = {
path = "../amont/terraform.tfstate"
}
defaults = {
identifiant = "valeur-de-repli"
}
}
Mesuré : quand l'état existe mais que identifiant n'y figure pas, la valeur de repli est retenue et l'apply sort en 0.Le cas qu'il ne couvre PAS
Si l'état amont n'existe pas du tout :Error: Unable to find remote state
No stored state was found for the given workspace in the given backend.
Code 1. defaults ne remplace donc pas un apply de la stack amont : il gère une interface incomplète, pas une stack absente.Le paramètre
module "reseau" {
source = "git::https://exemple.invalide/infra-modules.git//reseau?ref=v1.3.0&depth=1"
}
« Setting depth to 1 is suitable for most cases. »Pourquoi c'est utile en multi-repo
C'est précisément le modèle qui fait cloner le dépôt de modules à chaqueinit, sur chaque agent de CI. Sur un dépôt à historique long, le clone superficiel est le principal levier de temps d'exécution.Le détail de syntaxe qui va avec
Le double slash// sépare le paquet (le dépôt) de la sous-arborescence (le module dans le dépôt). Un simple slash ne fait pas la même chose.Le comparatif
| Critère | Monorepo | Dépôt par stack |
|---|---|---|
| changement transverse en une revue | oui | non |
| droits distincts par périmètre | difficile | oui |
| cadences indépendantes | possible | oui |
| cohérence module et appelants | automatique | à gérer |
| versionnement du module | inutile | nécessaire |
Le point qui tranche souvent
Les droits. Si deux périmètres doivent avoir des accès réellement distincts, le dépôt par stack s'impose, et il faut alors versionner le module partagé.Ce que le choix ne change pas
Dans les deux cas, dès que les configurations racine sont distinctes, les états le sont aussi. Le partage de valeurs devient un travail explicite, et c'est là que se jouent les vrais problèmes.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Structure standard d'un module : le socle d'un module partagé entre plusieurs stacks.
- Versionner ses modules : publier une version sans casser les consommateurs.
- L'état distant : la référence officielle :
terraform_remote_state, ses arguments et son avertissement de sécurité. - L'argument source : la référence officielle : chemins locaux, Git, registre,
//etdepth.