Composer une infrastructure en couches
Pourquoi
Après les trois premières leçons, signalements-iac a des modules propres et versionnés, mais il reste un seul état par environnement. Un terraform apply sur la production relit et peut modifier le réseau, la base de données, le bucket, les instances et le répartiteur d'un seul bloc. Cela pose trois problèmes qui grandissent avec le projet.
Le rayon d'impact. Une faute de frappe dans la configuration d'une instance applicative passe par le même plan, la même clé d'API et le même état que la base de données. Le jour où un plan propose de remplacer la base, il est noyé dans les soixante autres lignes de la revue.
La vitesse. Chaque plan rafraîchit toutes les ressources : une trentaine d'appels d'API aujourd'hui, plusieurs centaines demain. Un plan de plusieurs minutes pour changer l'étiquette d'une image est une taxe quotidienne.
Les cycles de vie. Le réseau change une fois par an, la base deux fois par an, l'application à chaque déploiement. Les mélanger fait peser sur le réseau le rythme de l'application : on applique tous les jours des changements qui peuvent, par accident, toucher ce qui ne doit presque jamais bouger.
La réponse est de découper l'infrastructure en couches : des ensembles de ressources qui changent à la même cadence, avec chacun son état. Cette leçon explique comment choisir les couches, comment elles se transmettent des informations, et ce que ce découpage coûte. La leçon 8 traite de la mécanique de découpage d'un état existant ; celle-ci traite de la conception.
Les concepts
Qu'est-ce qu'une couche
Une couche d'infrastructure est un ensemble de ressources déployées ensemble, avec un état propre, un cycle de vie homogène et une responsabilité identifiable. Morris parle de « pile » (stack) : l'unité qu'on déploie, détruit et reconstruit d'un bloc. Dans Terraform, une couche est un module racine : un répertoire avec son backend, son fournisseur, ses variables et ses sorties. Le mot « couche » insiste sur l'empilement : les couches hautes dépendent des basses, jamais l'inverse.
Pour Signalements, trois couches s'imposent :
| Couche | Contenu | Rythme | Dépend de |
|---|---|---|---|
reseau | VPC, réseau privé, adresses IPAM, passerelle, bastion | rare | rien |
donnees | base PostgreSQL, bucket, secret du mot de passe | modéré | reseau |
application | instances, groupes de sécurité, répartiteur | fréquent | reseau, donnees |
Chaque couche tire sa légitimité d'un critère de découpage. Les trois qui comptent, par ordre d'importance :
- le cycle de vie : ce qui change ensemble reste ensemble ;
- le rayon d'impact : ce dont l'erreur coûte cher (la base, le réseau) est isolé de ce qui change souvent ;
- la propriété : si une équipe réseau et une équipe applicative existent, leurs couches sont distinctes, avec des droits distincts.
Une couche par environnement
Les environnements (preprod, prod) multiplient les couches : trois couches par deux environnements font six états, six répertoires à planifier et appliquer. Le premier cours a posé le principe : un état par environnement, jamais partagé. Le découpage en couches le prolonge : un état par couche et par environnement.
preprod/reseau.tfstate prod/reseau.tfstate
preprod/donnees.tfstate prod/donnees.tfstate
preprod/application.tfstate prod/application.tfstateLa transmission d'informations
La couche application a besoin de l'identifiant du réseau privé, créé par reseau. Comme leurs états sont séparés, Terraform ne peut pas lui passer la référence module.reseau.private_network_id. Trois mécanismes existent.
1. Les sources de données (data sources). La couche haute retrouve l'objet par une requête à l'API du fournisseur, avec un identifiant stable comme le nom :
data "scaleway_vpc_private_network" "app" {
name = "pn-signalements"
}C'est le mécanisme le plus découplé : la couche application ne sait rien de la façon dont reseau a été créé (par Terraform, à la main, ou par un autre outil) et ne lit aucun état. Elle dépend d'une convention de nommage et de l'existence de l'objet.
2. terraform_remote_state. Cette source de données lit les sorties de l'état d'une autre couche :
data "terraform_remote_state" "reseau" {
backend = "s3"
config = {
bucket = "signalements-tfstate-<suffixe>"
key = "preprod/reseau.tfstate"
# ... mêmes paramètres que le backend
}
}La valeur s'utilise ensuite par data.terraform_remote_state.reseau.outputs.private_network_id. Le mécanisme est pratique et il a un défaut sérieux que la documentation signale elle-même : pour lire les sorties, le consommateur doit avoir accès à tout l'état, secrets compris. Plus loin, une expérience le montre.
3. Des sorties publiées ailleurs. La couche basse écrit ses sorties dans un endroit lisible (un secret ou un paramètre du fournisseur, un fichier JSON dans un bucket, un enregistrement dans un outil de configuration), et la couche haute les lit par une source de données. Le contrat est explicite (ce qui est publié est ce qui est promis), les droits aussi (on publie seulement ce qui peut être lu), et l'état n'est jamais exposé. C'est le plus rigoureux et le plus lourd.
En pratique, on choisit comme suit : sources de données par défaut pour ce qui se retrouve par un nom ; terraform_remote_state pour ce qui n'a pas de requête naturelle, quand tout le monde qui lit l'état de la couche basse en a de toute façon le droit ; sorties publiées quand les équipes ou les niveaux de confiance diffèrent.
Le sens des dépendances
Les couches forment un graphe acyclique : reseau ne lit rien, donnees lit reseau, application lit reseau et donnees. Une dépendance circulaire (la couche réseau qui lit une valeur de la couche application) est le signe d'un mauvais découpage : l'objet lu est dans la mauvaise couche. L'ordre d'application suit le graphe (reseau, puis donnees, puis application) et celui de destruction l'inverse. Terraform ne connaît pas cet ordre entre états ; c'est le rôle de l'outil d'orchestration (leçon 9) ou du pipeline.
En pratique
L'arborescence du dépôt
Le dépôt signalements-iac devient une arborescence où chaque répertoire feuille est un module racine :
signalements-iac/
├── environnements/
│ ├── preprod/
│ │ ├── reseau/
│ │ │ ├── main.tf appel du module reseau
│ │ │ ├── backend.tf état preprod/reseau.tfstate
│ │ │ ├── providers.tf
│ │ │ ├── variables.tf
│ │ │ ├── terraform.tfvars valeurs de la préproduction
│ │ │ └── outputs.tf
│ │ ├── donnees/
│ │ └── application/
│ └── prod/
│ ├── reseau/
│ ├── donnees/
│ └── application/
└── modules/ ou modules tirés de lyneko-modules (leçon 3)
├── reseau/
├── base/
└── app/Chaque feuille est mince : elle appelle un module avec des valeurs, et ne contient presque aucune ressource. La logique est dans les modules ; les répertoires d'environnement ne portent que ce qui est propre à ce couple environnement-couche (état, valeurs, projet Scaleway).
La feuille environnements/preprod/reseau/main.tf ressemble à :
module "reseau" {
source = "git::https://github.com/lyneko-formation/lyneko-modules.git//modules/reseau?ref=reseau-v1.0.0"
environnement = "preprod"
adresses_instances = { "sig-app-1" = "172.16.20.11", "sig-app-2" = "172.16.20.12" }
etiquettes = ["app=signalements", "env=preprod", "gere-par=terraform"]
passerelle = {
plages_autorisees = var.plages_bastion
}
}
output "private_network_id" {
value = module.reseau.private_network_id
}et son backend.tf suit le modèle du cours précédent, avec une clé propre à la couche :
terraform {
backend "s3" {
bucket = "signalements-tfstate-<suffixe>"
key = "preprod/reseau.tfstate"
region = "fr-par"
use_lockfile = true
endpoints = { s3 = "https://s3.fr-par.scw.cloud" }
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
}
}La clé de l'état (preprod/reseau.tfstate) fait de chaque couche un état distinct, avec son propre verrou : deux équipes peuvent appliquer application et donnees en même temps sans se bloquer.
Lire la couche basse par une source de données
La couche application retrouve le réseau et la base par leur nom :
# environnements/preprod/application/main.tf
data "scaleway_vpc_private_network" "app" {
name = "pn-signalements"
}
data "scaleway_rdb_instance" "principale" {
name = "sig-db-${var.environnement}"
}Ces deux blocs ont été vérifiés avec terraform init et terraform validate contre le fournisseur Scaleway 2.84.0 (jamais planifiés). Le module app reçoit ensuite data.scaleway_vpc_private_network.app.id comme entrée. Deux conséquences :
- les données sont lues à chaque plan, donc la couche haute voit toujours l'état courant de la couche basse, sans attendre une mise à jour d'état ;
- si l'objet n'existe pas encore (la couche basse n'a pas été appliquée), le plan échoue avec une erreur claire du fournisseur : c'est le bon comportement, il signale que l'ordre d'application n'a pas été respecté.
Pour que name = "pn-signalements" soit non ambigu, le nom doit être unique dans le projet Scaleway, donc un projet par environnement (déjà le cas dans signalements-iac) ou un nom qui porte l'environnement.
Ce que terraform_remote_state expose
Pour mesurer le défaut de terraform_remote_state, une expérience en local avec des ressources factices. La couche reseau publie deux sorties, dont une porte un secret (par imprudence, comme dans beaucoup de dépôts) :
# environnements/preprod/reseau/main.tf
terraform {
backend "local" { path = "../../../etat/preprod-reseau.tfstate" }
}
resource "terraform_data" "reseau" {
input = "pn-signalements"
}
output "private_network_id" { value = terraform_data.reseau.id }
output "secret_interne" { value = "mot-de-passe-de-demo" }La couche application ne lit que private_network_id :
# environnements/preprod/application/main.tf
terraform {
backend "local" { path = "../../../etat/preprod-application.tfstate" }
}
data "terraform_remote_state" "reseau" {
backend = "local"
config = { path = "../../../etat/preprod-reseau.tfstate" }
}
resource "terraform_data" "instance" {
input = data.terraform_remote_state.reseau.outputs.private_network_id
}Après application des deux couches, on regarde l'état de la couche application (vérifié avec Terraform 1.16.1) :
$ grep -n "mot-de-passe" ../../etat/preprod-application.tfstate
33: "secret_interne": "mot-de-passe-de-demo"
La couche application n'utilise jamais secret_interne, et pourtant elle l'a dans son propre état : la source de données terraform_remote_state y enregistre toutes les sorties de la couche lue. Avec un backend distant, deux conséquences s'ajoutent : le compte qui exécute application doit pouvoir lire l'état complet de reseau (et pas seulement ses sorties), et tout secret qui y figure est recopié dans un second fichier, avec des droits potentiellement différents.
Règles qui en découlent :
- Ne mettez jamais de secret dans les sorties d'une couche lue par
terraform_remote_state, même marquésensitive = true: la marque masque l'affichage, pas l'état. - Préférez des sources de données du fournisseur quand elles existent.
- Si vous gardez
terraform_remote_state, limitez les sorties de la couche basse à ce que les couches hautes doivent consommer, et traitez le droit de lecture de l'état comme un droit sensible.
Publier ses sorties
Pour les informations qui n'ont pas de source de données naturelle, on peut publier les sorties comme des objets d'un service tiers : par exemple un secret du gestionnaire de secrets de Scaleway qui contient un JSON non sensible (sous-réseaux, identifiants), écrit par la couche basse avec une ressource, et lu par la haute avec une source de données. Le contrat devient une interface publiée : ce que la couche basse y met est ce qu'elle promet, avec des droits d'accès propres. C'est plus de code (deux ressources de plus), mais cela permet à une autre équipe, ou à un autre outil (un script, un chart Helm), de consommer les mêmes valeurs sans accès aux états. Vous choisirez cette voie quand les couches appartiennent à des équipes différentes, pas pour trois couches dans un seul dépôt.
Répéter par environnement : duplication ou factorisation
Il reste une question de style : environnements/preprod/reseau/ et environnements/prod/reseau/ sont presque identiques. Trois approches.
1. Dupliquer les feuilles (approche retenue ici). Chaque couple environnement-couche a son répertoire, avec son main.tf qui appelle le module. La duplication se limite à de courts fichiers (quinze lignes), la logique est dans les modules. Avantages : tout est explicite ; ce qui diffère entre préproduction et production se voit à l'œil et dans un diff -r ; un changement peut être testé en préproduction, puis reporté à la production dans une demande de fusion distincte. Défaut : on peut oublier de reporter un changement ; un diff en CI sur les feuilles comparables compense.
2. Un seul répertoire et un fichier de valeurs par environnement (l'approche du premier cours) : terraform apply -var-file=environnements/prod.tfvars, avec un backend en configuration partielle. Concis, mais l'environnement est choisi à l'exécution : une faute de frappe applique le mauvais fichier sur le bon état, ou l'inverse. Il suffit pour deux environnements simples.
3. Les espaces de travail (workspaces). terraform workspace new prod crée un état distinct dans le même backend et la même configuration, avec la valeur terraform.workspace pour distinguer. La documentation les présente comme utiles pour des copies temporaires (une pile de test jetable, un environnement par branche) et les déconseille pour séparer des environnements de production et de préproduction : même code, mêmes identifiants, même backend, et rien dans le répertoire ne dit quel espace est actif. Le coût d'une erreur est celui d'un apply sur la production avec le mauvais espace sélectionné.
Notre choix. L'explicite : des feuilles minces et dupliquées, qui diffèrent par leurs valeurs et leur version de module. Le coût est modéré, parce que la logique est dans les modules. Les outils de génération (Terragrunt, Terraform Stacks, ou des gabarits) réduisent encore la duplication ; ils ajoutent leur propre couche d'abstraction à comprendre, et font l'objet de la leçon 9.
Tip
La préproduction doit pouvoir devancer la production d'une version de module : feuilles distinctes, ref distincts. C'est ce qui fait de la préproduction un banc d'essai de la montée de version, au lieu d'un clone qui bouge toujours en même temps.
Sous le capot
Un état, un graphe, un verrou. Chaque racine a son état, son graphe de ressources et son verrou. Terraform ne voit pas les autres couches : pour lui, data.scaleway_vpc_private_network.app est une source de données comme une autre, qui interroge l'API. Le découpage se fait donc hors de Terraform, par la façon dont on organise les répertoires et par l'ordre dans lequel on les exécute.
terraform_remote_state lit un instantané. La source de données ouvre le backend indiqué, charge l'état entier de l'autre configuration, et extrait ses sorties racine. Elle est évaluée au moment du plan. Ses résultats sont enregistrés dans l'état du consommateur (c'est ce que l'expérience a montré). Elle ne verrouille pas l'état lu : si la couche basse est en cours d'application pendant que la haute planifie, le résultat peut être périmé ou incohérent.
Les sources de données lisent l'état réel, pas l'état Terraform. Une source de données du fournisseur demande à l'API ce qui existe vraiment, y compris des objets créés hors de Terraform. C'est un avantage (aucune dépendance à l'outil qui a créé l'objet) et un risque (un objet recréé à la main avec le même nom est accepté sans discussion). Un filtre trop large (un nom porté par plusieurs objets) peut être refusé par le fournisseur ou, selon la source de données, retenir un résultat arbitraire : lisez la documentation de la source avant de vous y fier.
Pas de dépendances automatiques entre états. Le plan de la couche application ne sait pas qu'un apply de reseau est en cours ou nécessaire. Il lit ce qui est là. Si vous modifiez reseau (par exemple une nouvelle adresse) et que vous appliquez application avant, elle ne voit pas le changement. L'ordre est une discipline du pipeline.
Pièges courants
Un découpage trop fin. Quinze couches de trois ressources donnent quinze états, quinze plans, quinze init et des dépendances croisées sans fin. Le critère reste le cycle de vie et le rayon d'impact, pas l'élégance : trois à cinq couches suffisent à la plupart des applications.
Des couches qui dépendent les unes des autres dans les deux sens. Si reseau doit connaître une valeur de application, l'objet est mal rangé. Déplacez-le, ou remplacez-le par une convention (une plage réservée, un nom fixé d'avance).
Mettre un secret dans une sortie lue en terraform_remote_state. Voir plus haut : il est copié dans un second état.
Chercher par un nom non unique. name = "pn-signalements" dans un projet qui contient deux réseaux de ce nom : le fournisseur échoue. Inclure l'environnement dans le nom, ou un projet par environnement.
Appliquer dans le mauvais ordre. application avant donnees : le plan échoue sur la source de données ; c'est un bon signal, mais c'est aussi la raison pour laquelle un pipeline qui applique tout doit connaître le graphe.
Dupliquer sans reporter. Un correctif appliqué en préproduction et oublié en production n'est visible que par comparaison. Mettez en place un contrôle (diff des feuilles, test de conformité) plutôt que la mémoire de l'équipe.
Tout rendre commun à tous les environnements. Une couche partagée entre préproduction et production (par exemple un réseau commun) lie le sort des deux : la préproduction n'est plus un banc d'essai. Partagez avec prudence, et seulement ce qui ne change pas.
Sécurité
- Des droits par couche. Le principal avantage de sécurité du découpage : la clé d'API qui applique
applicationn'a pas besoin de modifier le réseau ni de supprimer la base. Une clé par couche, avec les permissions Scaleway strictement nécessaires, réduit ce qu'une compromission du pipeline applicatif peut faire. C'est impossible avec un état unique. - Séparer les états sensibles. L'état de
donneescontient des mots de passe et des identifiants de base ; celui deapplicationn'en a pas besoin. Mettez-les dans des buckets ou des préfixes distincts, avec des droits de lecture distincts, et ne les relisez pas depuis l'application. terraform_remote_stateélargit le droit de lecture. Quiconque exécute la couche haute lit l'état complet de la basse : traitez ce droit comme celui de lire les secrets de la basse.- Les noms sont un contrat. Les sources de données par nom dépendent d'une convention : quiconque peut créer un objet de même nom dans le projet peut détourner la lecture (un faux réseau privé du même nom qui attire les instances). Un projet Scaleway par environnement, avec des droits de création restreints, limite le risque.
- La production a ses propres identifiants. Les feuilles
produtilisent un projet, une clé d'API et un backend à part ; la préproduction ne peut pas écrire dans l'état de la production, même par erreur de configuration.
En production
- Commencez par deux couches. Séparez d'abord ce qui est dangereux ou rare (réseau et données) de ce qui change tous les jours (application). Une troisième couche vient quand le besoin se fait sentir, pas avant.
- Un pipeline qui connaît l'ordre. La leçon 12 du premier cours a posé un pipeline pour un état. Avec plusieurs couches, la forme la plus simple est un travail par couche, enchaînés par dépendance (
needs), avec plan sur demande de fusion et apply après fusion. La leçon 9 présente les outils qui automatisent ce graphe. - Changer une couche basse est un événement. Un changement du réseau déclenche des relectures des couches qui en dépendent : prévoyez un plan sur les couches hautes après chaque modification d'une basse, pour vérifier que leurs sources de données lisent toujours ce qu'elles attendent.
- Mesurez le coût. Plus de couches, c'est plus de répertoires, de pipelines, de backends, de clés. Si l'équipe ne peut pas les opérer, le découpage est trop fin.
- Migrer un état existant (un état unique vers trois) est une opération délicate, traitée en détail dans la leçon 8 : blocs
movedentre états,import,removed, et un plan à zéro changement comme critère de réussite. - OpenTofu se comporte de même pour les sources de données,
terraform_remote_stateet les espaces de travail. Il offre en plus le chiffrement natif de l'état, qui réduit l'impact de l'exposition de l'état entre couches : à étudier dans la leçon sur les secrets.
Exercices
Exercice 1 : choisir les couches
Signalements ajoute un bucket de journaux, partagé par tous les environnements d'un même client, et un enregistrement DNS pour chaque environnement. Dans quelles couches les placez-vous, ou faut-il en créer d'autres ?
Solution
L'enregistrement DNS suit le répartiteur : couche application (même cycle de vie, il change avec l'adresse du répartiteur). Le bucket de journaux, partagé entre environnements, a un cycle de vie propre, et sa destruction ne doit jamais être une conséquence d'un changement d'environnement : couche dédiée (transverse, par exemple), avec son propre état hors de preprod et prod. Les couches application des deux environnements le retrouvent par une source de données (son nom est fixe). Le piège serait de le mettre dans la couche donnees de la production : la préproduction dépendrait alors de l'état de la production.
Exercice 2 : reproduire l'exposition
Dans un répertoire de test, créez les deux couches de la section « Ce que terraform_remote_state expose » avec un backend local. Appliquez-les, puis vérifiez que secret_interne apparaît dans l'état de application. Retirez la sortie secret_interne de reseau, réappliquez les deux couches, et vérifiez qu'elle disparaît de l'état de application après un nouveau plan et apply.
Solution
Après l'apply initial, grep secret_interne sur le fichier d'état de application trouve la valeur en clair. Après suppression de la sortie dans reseau et apply de reseau, un terraform apply dans application relit l'état de reseau, où la sortie n'existe plus, et réécrit le sien : le secret disparaît de l'état courant. Il reste en revanche dans les anciennes versions de l'état (versionnement du bucket) et dans les sauvegardes locales : un secret sorti par erreur doit être changé, pas seulement retiré.
Exercice 3 : peser un découpage
Une équipe de deux personnes propose cinq couches (reseau, securite, donnees, calcul, dns) pour une application et deux environnements. Quels arguments pour et contre ?
Solution
Pour : les droits (une clé d'API plus étroite par couche), les cycles de vie (le DNS et la sécurité changent peu), les plans plus rapides. Contre : dix états à opérer pour deux personnes, des dépendances croisées probables (securite et reseau sont souvent liés, dns dépend du répartiteur de calcul), un pipeline à écrire et maintenir. Une alternative raisonnable : trois couches (reseau, donnees, application) avec la sécurité dans reseau et le DNS dans application, quitte à scinder plus tard si un besoin apparaît (droits distincts, équipes distinctes).
Récapitulatif
- Une couche est un module racine avec son état, son cycle de vie et son rayon d'impact :
reseau,donnees,applicationpour Signalements. Les dépendances vont toujours du haut vers le bas. - On découpe selon le cycle de vie, le rayon d'impact et la propriété, pas par goût de l'élégance.
- Un état par couche et par environnement (
preprod/reseau.tfstate, etc.), chacun avec son verrou. - Entre couches, l'information passe par des sources de données (par défaut),
terraform_remote_state(avec prudence) ou des sorties publiées. terraform_remote_statecopie toutes les sorties de la couche lue dans l'état du consommateur et exige la lecture de l'état entier : jamais de secret dans ces sorties.- Pour les environnements : feuilles minces dupliquées (explicite) plutôt que espaces de travail (déconseillés pour séparer production et préproduction) ; la logique vit dans les modules.
- L'ordre d'application entre couches n'est pas géré par Terraform : c'est une discipline du pipeline, outillée à la leçon 9.
Pour aller plus loin
- La documentation de
terraform_remote_stateet sa mise en garde sur l'accès à l'état. - Kief Morris, Infrastructure as Code, chapitres sur la décomposition des piles : critères de découpage et couplage.
- La leçon 8 pour migrer un état unique vers plusieurs couches, et la leçon 5 pour tester les modules que ces couches appellent.
- La leçon 7 pour garder les secrets hors des états que les couches se lisent.
Sources
- HashiCorp, Terraform : la source de données terraform_remote_state
- HashiCorp, Terraform : sources de données
- HashiCorp, Terraform : espaces de travail (CLI)
- Fournisseur Scaleway : data sources scaleway_vpc_private_network et scaleway_rdb_instance
- Kief Morris, Infrastructure as Code (3e éd., O'Reilly, 2025), chapitres sur la décomposition des piles et des environnements
- Yevgeniy Brikman, Terraform: Up & Running (3e éd., O'Reilly, 2022), chapitre 3 : isolation de l'état