Aller au contenu
Infrastructure as Code medium

Monorepo vs un repo par stack Terraform

35 min de lecture

logo terraform

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.

  • 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

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èreMonorepoUn dépôt par stack
changement transverse en une revueouinon
droits distincts par périmètredifficileoui
cadences de déploiement indépendantespossibleoui
cohérence module et appelantsautomatiqueà gérer
versionnement du module partagéinutilenécessaire

C'est la dernière ligne qui mène au premier fait mesuré.

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 because
you 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.

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.

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.

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.

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"
}
SymptômeCause probableCorrection
Invalid registry module source addressversion posé à côté d'un source local ou Gitretirer version ; seul un module de registre l'accepte
Unsupported attribute sur un output amontsortie d'un module imbriqué, non ré-exportéeajouter l'output à la racine de la stack amont
Unable to find remote statel'état amont n'existe pas encoreappliquer la stack amont d'abord ; defaults ne couvre pas ce cas
une valeur amont recopiée divergevaleur dupliquée au lieu d'être luepasser par terraform_remote_state
un secret est lisible par une stack avalsortie racine portant le secretle retirer des sorties : sensitive ne protège pas l'état
init très lent en multi-repoclone complet du dépôt de modulesajouter depth=1 à la source Git

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.

  • 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.
  • sensitive masque l'affichage texte, pas output -json, pas show -json, pas l'état.
  • defaults comble un output manquant, jamais un état absent.
  • terraform_remote_state resserre le couplage : les tags ou le DNS le relâchent.
  • depth=1 raccourcit le clone d'un module Git en intégration continue.

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

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn