Terraform: Up & Running
Chapitre 4 / 8 · 18 min de lecture

Créer une infrastructure réutilisable avec les modules

Transformer son code en modules réutilisables : entrées, locals, sorties, pièges et versionnement par Git.

À la fin du chapitre précédent, vous aviez déployé une architecture complète sur AWS : un équilibreur de charge (load balancer) ALB devant un groupe de mise à l'échelle automatique (Auto Scaling Group, ASG) de serveurs web, le tout interrogeant une base de données. Cela fait un excellent premier environnement. Mais en pratique, il vous en faut au moins deux : un pour les tests internes de l'équipe (« staging ») et un autre, accessible aux vrais utilisateurs (« production »). Idéalement, les deux sont quasi identiques — quitte à faire tourner des serveurs un peu plus petits ou moins nombreux en staging pour économiser.

Comment ajouter cet environnement de production sans copier-coller tout le code de staging ? Dans un langage généraliste comme Ruby, du code dupliqué se range dans une fonction que l'on réutilise partout. Avec Terraform, l'équivalent est le module : tout dossier de fichiers de configuration que l'on appelle depuis ailleurs. C'est l'ingrédient clé pour écrire du code Terraform réutilisable, maintenable et testable. Une fois que vous y goûtez, vous ne revenez plus en arrière : vous pensez votre infrastructure tout entière comme une collection de modules réutilisables.

Les bases d'un module

Un module Terraform est d'une simplicité désarmante : tout ensemble de fichiers de configuration dans un dossier est un module. Tout ce que vous avez écrit jusqu'ici était donc techniquement un module — mais pas un module très intéressant, puisque vous le déployiez directement. Le module présent dans le répertoire de travail courant porte d'ailleurs un nom : c'est le module racine (root module). Pour découvrir ce dont les modules sont réellement capables, il faut en utiliser un depuis un autre.

Prenons le code de stage/services/webserver-cluster — l'ASG, l'ALB, les groupes de sécurité et les autres ressources — et transformons-le en module réutilisable. On commence par lancer terraform destroy pour nettoyer les ressources existantes, puis on crée un nouveau dossier de premier niveau nommé modules et on y déplace tous les fichiers de stage/services/webserver-cluster vers modules/services/webserver-cluster.

Détail crucial : ouvrez le main.tf du module et retirez la définition du provider. Le provider doit être configuré par l'utilisateur du module, jamais par le module lui-même. La structure obtenue ressemble à ceci.

modules/
└── services/
    └── webserver-cluster/
        ├── main.tf
        ├── variables.tf
        ├── outputs.tf
        └── user-data.sh
stage/
└── services/
    └── webserver-cluster/
        └── main.tf        <- appelle le module ci-dessus

La syntaxe pour utiliser un module repose sur le bloc module et l'argument source :

module "<NAME>" {
  source = "<SOURCE>"

  [CONFIG ...]
}

NAME est un identifiant interne pour référencer ce module dans le reste du code, SOURCE est le chemin où trouver le code du module, et CONFIG rassemble les arguments propres au module. Dans stage/services/webserver-cluster/main.tf, vous appelez donc le module ainsi :

provider "aws" {
  region = "us-east-2"
}

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"
}

Vous réutilisez exactement le même module en production, en créant prod/services/webserver-cluster/main.tf avec un contenu rigoureusement identique (provider plus bloc module pointant sur la même source). Et voilà : la réutilisation de code dans plusieurs environnements, avec un copier-coller réduit au strict minimum.

À retenir

Chaque fois que vous ajoutez un module à votre configuration ou que vous modifiez son paramètre source, vous devez relancer terraform init avant plan ou apply. La commande init télécharge les providers et les modules, et configure les backends — le tout en une seule commande.

$ terraform init
Initializing modules...
- webserver_cluster in ../../../modules/services/webserver-cluster

Initializing the backend...

Initializing provider plugins...

Terraform has been successfully initialized!

Avant de lancer apply, un problème majeur subsiste : tous les noms sont codés en dur (hardcoded). Le nom des groupes de sécurité, de l'ALB et des autres ressources est figé, si bien qu'en utilisant le module deux fois, vous récolteriez des erreurs de conflit de noms. Pire, les détails de la base de données le sont aussi : le terraform_remote_state copié dans le module pointe en dur sur l'environnement de staging. Pour corriger cela, il faut rendre le module configurable.

Les entrées du module

Pour rendre une fonction configurable dans un langage généraliste, on lui ajoute des paramètres d'entrée. Dans Terraform, les modules acceptent aussi des paramètres d'entrée, via un mécanisme que vous connaissez déjà : les variables d'entrée (input variables). Ouvrez modules/services/webserver-cluster/variables.tf et ajoutez trois variables.

variable "cluster_name" {
  description = "The name to use for all the cluster resources"
  type        = string
}

variable "db_remote_state_bucket" {
  description = "The name of the S3 bucket for the database's remote state"
  type        = string
}

variable "db_remote_state_key" {
  description = "The path for the database's remote state in S3"
  type        = string
}

Ensuite, parcourez main.tf et remplacez les noms codés en dur par var.cluster_name. Pour le groupe de sécurité de l'ALB, par exemple, le paramètre name devient une interpolation du nom de cluster.

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Il faut faire la même chose pour l'autre aws_security_group (par exemple en lui donnant le nom instance), pour la ressource aws_alb, et pour la section tag de l'aws_autoscaling_group. Le terraform_remote_state doit lui aussi consommer db_remote_state_bucket et db_remote_state_key comme bucket et key, pour lire le fichier d'état (state) du bon environnement.

data "terraform_remote_state" "db" {
  backend = "s3"

  config = {
    bucket = var.db_remote_state_bucket
    key    = var.db_remote_state_key
    region = "us-east-2"
  }
}

Côté staging, vous renseignez ces variables exactement comme on passe des arguments à une ressource. Les variables d'entrée constituent l'API du module : ce sont elles qui contrôlent son comportement d'un environnement à l'autre.

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-stage"
  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
  db_remote_state_key    = "stage/data-stores/mysql/terraform.tfstate"
}

En production, même chose, mais avec cluster_name = "webservers-prod" et une clé de state qui pointe sur prod/data-stores/mysql/terraform.tfstate.

Note

La base de données de production n'existe pas encore réellement. Comme exercice, à vous de la créer sur le modèle de celle de staging — c'est précisément le genre de duplication que les modules vous apprennent à factoriser ensuite.

Cet exemple ne fait varier que les noms, mais vous voudrez sans doute rendre d'autres paramètres configurables. En staging, on lance un petit cluster pour économiser ; en production, un plus grand, capable d'absorber le trafic. Ajoutez donc trois variables supplémentaires.

variable "instance_type" {
  description = "The type of EC2 Instances to run (e.g. t2.micro)"
  type        = string
}

variable "min_size" {
  description = "The minimum number of EC2 Instances in the ASG"
  type        = number
}

variable "max_size" {
  description = "The maximum number of EC2 Instances in the ASG"
  type        = number
}

La configuration de lancement (launch configuration) utilise alors var.instance_type, et l'ASG var.min_size et var.max_size.

resource "aws_autoscaling_group" "example" {
  launch_configuration = aws_launch_configuration.example.name
  vpc_zone_identifier  = data.aws_subnet_ids.default.ids
  target_group_arns    = [aws_lb_target_group.asg.arn]
  health_check_type    = "ELB"

  min_size = var.min_size
  max_size = var.max_size

  tag {
    key                 = "Name"
    value               = var.cluster_name
    propagate_at_launch = true
  }
}

En staging, on garde le cluster petit et bon marché — instance_type = "t2.micro", min_size et max_size à 2. En production, on monte en gamme : un instance_type plus puissant comme m4.large (attention, ce type n'est pas dans l'offre gratuite d'AWS) et un max_size de 10, pour laisser le cluster croître ou décroître selon la charge — il démarrera tout de même avec deux instances.

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-prod"
  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
  db_remote_state_key    = "prod/data-stores/mysql/terraform.tfstate"

  instance_type = "m4.large"
  min_size      = 2
  max_size      = 10
}

Les locals du module

Les variables d'entrée sont parfaites pour exposer une API, mais que faire lorsque vous avez besoin d'une variable interne — pour un calcul intermédiaire, ou simplement pour rester DRY — sans l'exposer comme entrée configurable ? Dans le module, le load balancer écoute sur le port 80, le port HTTP par défaut. Ce numéro est aujourd'hui copié-collé à plusieurs endroits : le listener du load balancer, mais aussi son groupe de sécurité. De même, le bloc CIDR « toutes les IP » 0.0.0.0/0, la valeur « n'importe quel port » 0 et la valeur « n'importe quel protocole » -1 se répètent partout dans le module.

Ces valeurs magiques disséminées rendent le code difficile à lire et à maintenir. On pourrait les extraire en variables d'entrée — mais les utilisateurs du module pourraient alors les écraser par accident, ce que vous ne voulez surtout pas. La bonne réponse est le bloc locals, qui définit des valeurs locales (local values).

locals {
  http_port    = 80
  any_port     = 0
  any_protocol = "-1"
  tcp_protocol = "tcp"
  all_ips      = ["0.0.0.0/0"]
}

Les valeurs locales attribuent un nom à n'importe quelle expression Terraform, réutilisable dans tout le module. Ces noms ne sont visibles qu'à l'intérieur du module : aucun impact sur les autres modules, et impossibilité de les surcharger depuis l'extérieur. On les lit avec la syntaxe local.<NAME>. Le listener et les groupes de sécurité deviennent alors limpides.

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"

  ingress {
    from_port   = local.http_port
    to_port     = local.http_port
    protocol    = local.tcp_protocol
    cidr_blocks = local.all_ips
  }

  egress {
    from_port   = local.any_port
    to_port     = local.any_port
    protocol    = local.any_protocol
    cidr_blocks = local.all_ips
  }
}

Les locals rendent le code plus lisible et plus facile à maintenir, sans ouvrir de surface de configuration involontaire. Utilisez-les souvent.

Les sorties du module

Une fonctionnalité puissante des ASG est la possibilité d'augmenter ou de réduire le nombre de serveurs en réponse à la charge. Une manière de le faire est l'action planifiée (scheduled action), qui modifie la taille du cluster à une heure donnée. Par exemple, si votre trafic est plus fort aux heures de bureau, vous pouvez monter le nombre de serveurs à 9 h et le redescendre à 17 h.

Définie dans le module, cette action s'appliquerait aux deux environnements ; or vous n'en avez pas besoin en staging. On la définit donc, pour l'instant, directement dans la configuration de production — au chapitre 5, vous verrez comment définir des ressources conditionnellement et la rapatrier dans le module. Ajoutez ces deux ressources aws_autoscaling_schedule à prod/services/webserver-cluster/main.tf.

resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
  scheduled_action_name = "scale-out-during-business-hours"
  min_size              = 2
  max_size              = 10
  desired_capacity      = 10
  recurrence            = "0 9 * * *"
}

resource "aws_autoscaling_schedule" "scale_in_at_night" {
  scheduled_action_name = "scale-in-at-night"
  min_size              = 2
  max_size              = 10
  desired_capacity      = 2
  recurrence            = "0 17 * * *"
}

Le paramètre recurrence utilise la syntaxe cron : "0 9 * * *" signifie « 9 h tous les jours », et "0 17 * * *" « 17 h tous les jours ». Mais ces deux ressources réclament un paramètre obligatoire, autoscaling_group_name, qui désigne le nom de l'ASG. Or l'ASG est défini dans le module : comment accéder à son nom depuis l'extérieur ?

Dans un langage généraliste, une fonction retourne des valeurs. Dans Terraform, un module aussi — via un autre mécanisme déjà connu : les variables de sortie (output variables). Ajoutez le nom de l'ASG comme sortie dans modules/services/webserver-cluster/outputs.tf.

output "asg_name" {
  value       = aws_autoscaling_group.example.name
  description = "The name of the Auto Scaling Group"
}

On accède aux sorties d'un module avec la syntaxe module.<MODULE_NAME>.<OUTPUT_NAME> — ici module.webserver_cluster.asg_name. Vous l'utilisez pour renseigner autoscaling_group_name dans chaque action planifiée.

resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
  scheduled_action_name  = "scale-out-during-business-hours"
  min_size               = 2
  max_size               = 10
  desired_capacity       = 10
  recurrence             = "0 9 * * *"
  autoscaling_group_name = module.webserver_cluster.asg_name
}

Vous voudrez sans doute exposer une autre sortie : le nom DNS de l'ALB, pour connaître l'URL à tester une fois le cluster déployé. Définissez-le dans le outputs.tf du module, puis faites-le remonter (« pass through ») dans les outputs.tf de staging et de production.

# modules/services/webserver-cluster/outputs.tf
output "alb_dns_name" {
  value       = aws_lb.example.dns_name
  description = "The domain name of the load balancer"
}

# stage/services/webserver-cluster/outputs.tf (et prod/…)
output "alb_dns_name" {
  value       = module.webserver_cluster.alb_dns_name
  description = "The domain name of the load balancer"
}

Les pièges des modules

Le cluster est presque prêt. Restent quelques pièges (gotchas) à connaître : les chemins de fichiers et les blocs en ligne (inline blocks).

Les chemins de fichiers

Au chapitre précédent, vous aviez déplacé le script User Data dans un fichier externe, user-data.sh, lu avec la fonction file. Le hic : le chemin passé à file doit être relatif (puisque Terraform peut tourner sur n'importe quelle machine) — mais relatif à quoi ? Par défaut, Terraform l'interprète relativement au répertoire de travail courant. Cela fonctionne quand vous utilisez file dans le module racine, mais pas quand le module est défini dans un autre dossier.

La solution est une expression de référence de chemin (path reference), de la forme path.<TYPE>. Terraform en propose trois : path.module retourne le chemin du module où l'expression est définie ; path.root celui du module racine ; path.cwd celui du répertoire de travail courant (identique à path.root en usage normal). Pour le script User Data, vous voulez un chemin relatif au module lui-même, donc path.module.

data "template_file" "user_data" {
  template = file("${path.module}/user-data.sh")

  vars = {
    server_port = var.server_port
    db_address  = data.terraform_remote_state.db.outputs.address
    db_port     = data.terraform_remote_state.db.outputs.port
  }
}

Attention

Dès qu'un fichier (script User Data, gabarit, certificat) est lu à l'intérieur d'un module, oubliez le chemin relatif nu : il sera résolu depuis le dossier live qui appelle le module, et file échouera. Préfixez systématiquement par path.module.

Les blocs en ligne

La configuration de certaines ressources peut s'écrire soit en blocs en ligne (inline blocks), soit en ressources séparées. Dans un module, vous devez toujours préférer la ressource séparée. La ressource aws_security_group, par exemple, autorise des règles ingress et egress en blocs en ligne, comme vu plus haut. Convertissez-les en ressources aws_security_group_rule distinctes — pour les deux groupes de sécurité du module.

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"
}

resource "aws_security_group_rule" "allow_http_inbound" {
  type              = "ingress"
  security_group_id = aws_security_group.alb.id

  from_port   = local.http_port
  to_port     = local.http_port
  protocol    = local.tcp_protocol
  cidr_blocks = local.all_ips
}

resource "aws_security_group_rule" "allow_all_outbound" {
  type              = "egress"
  security_group_id = aws_security_group.alb.id

  from_port   = local.any_port
  to_port     = local.any_port
  protocol    = local.any_protocol
  cidr_blocks = local.all_ips
}

Pourquoi cette règle ? Si vous mélangez blocs en ligne et ressources séparées, vous obtenez des erreurs : les règles de routage entrent en conflit et s'écrasent mutuellement. C'est l'un ou l'autre, jamais les deux. Surtout, la ressource séparée rend le module plus flexible : un utilisateur peut ajouter ses propres règles depuis l'extérieur. Pour cela, exposez l'ID du groupe de sécurité en sortie.

output "alb_security_group_id" {
  value       = aws_security_group.alb.id
  description = "The ID of the Security Group attached to the load balancer"
}

Imaginez devoir ouvrir un port supplémentaire en staging, juste pour des tests. C'est désormais trivial : ajoutez une aws_security_group_rule dans stage/services/webserver-cluster/main.tf qui se raccroche à l'ID exporté.

resource "aws_security_group_rule" "allow_testing_inbound" {
  type              = "ingress"
  security_group_id = module.webserver_cluster.alb_security_group_id

  from_port   = 12345
  to_port     = 12345
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

Avec un seul bloc en ligne, ce code ne fonctionnerait pas. Le même problème touche d'autres couples de ressources : aws_route_table et aws_route, aws_network_acl et aws_network_acl_rule. La règle est générale.

Piège courant

Les exemples de ce chapitre créent deux environnements isolés dans le code et dotés de leurs propres load balancers, serveurs et bases — mais pas isolés au niveau réseau. Pour rester simples, ils déploient tout dans le même Virtual Private Cloud (VPC), si bien qu'un serveur de staging peut communiquer avec un serveur de production. En conditions réelles, c'est un double risque : une erreur en staging (mauvaise config des tables de routage, port laissé ouvert) peut affecter la production, et un attaquant qui pénètre un environnement accède à l'autre. Hors exemples, faites tourner chaque environnement dans un VPC séparé, voire dans des comptes AWS totalement distincts.

Le versionnement des modules

Si vos environnements de staging et de production pointent tous deux sur le même dossier de module, alors la moindre modification dans ce dossier affectera les deux environnements dès le prochain déploiement. Ce couplage rend impossible de tester un changement en staging sans risquer d'impacter la production. La meilleure approche est de créer des modules versionnés (module versioning), pour utiliser une version en staging (par exemple v0.0.2) et une autre en production (v0.0.1).

Jusqu'ici, source pointait toujours sur un chemin de fichier local. Mais Terraform accepte d'autres types de sources : URL Git, URL Mercurial, URL HTTP arbitraires. Le plus simple pour versionner un module est de placer son code dans un dépôt Git séparé et de faire pointer source sur l'URL de ce dépôt. Votre code Terraform se répartit alors sur (au moins) deux dépôts :

modules/   -> les « plans » : chaque module définit une brique
              réutilisable de l'infrastructure.

live/      -> les « maisons » bâties à partir des plans :
              l'infrastructure réellement en service par environnement
              (stage, prod, mgmt, …).

Pour mettre cela en place, déplacez les dossiers stage, prod et global dans un dossier live, puis configurez live et modules comme deux dépôts Git distincts.

$ cd modules
$ git init
$ git add .
$ git commit -m "Initial commit of modules repo"
$ git remote add origin "(URL OF REMOTE GIT REPOSITORY)"
$ git push origin master

Ajoutez ensuite un tag Git pour servir de numéro de version. Sur GitHub, l'interface de release crée un tag sous le capot ; sinon, la ligne de commande Git suffit.

$ git tag -a "v0.0.1" -m "First release of webserver-cluster module"
$ git push --follow-tags

Vous pouvez maintenant consommer ce module versionné en spécifiant une URL Git dans source, suffixée d'une référence ref=. Si votre dépôt modules est sur github.com/foo/modules, voici à quoi ressemble live/stage/services/webserver-cluster/main.tf — notez que le double slash dans l'URL est obligatoire (il sépare le dépôt du chemin du module à l'intérieur).

module "webserver_cluster" {
  source = "github.com/foo/modules//webserver-cluster?ref=v0.0.1"

  cluster_name           = "webservers-stage"
  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
  db_remote_state_key    = "stage/data-stores/mysql/terraform.tfstate"

  instance_type = "t2.micro"
  min_size      = 2
  max_size      = 2
}

Astuce

Le paramètre ref accepte un hash de commit sha1, un nom de branche ou un tag Git. Privilégiez les tags. Une branche n'est pas stable : vous récupérez toujours le dernier commit, qui peut changer à chaque init. Un hash sha1 est stable mais illisible. Un tag est aussi stable qu'un commit (ce n'en est qu'un pointeur) tout en restant lisible. Adoptez le versionnement sémantique (semantic versioning) MAJOR.MINOR.PATCH : MAJOR pour une rupture d'API incompatible, MINOR pour un ajout rétrocompatible, PATCH pour une correction rétrocompatible. C'est ainsi que vous communiquez aux utilisateurs du module la nature et les implications d'une mise à niveau.

Comme vous avez changé l'URL vers un module versionné, relancez terraform init : cette fois, Terraform télécharge le code depuis Git plutôt que depuis votre disque local.

$ terraform init
Initializing modules...
Downloading git@github.com:brikis98/terraform-up-and-running-code.git?ref=v0.1.0
for webserver_cluster...

(...)

Note

Si votre module est dans un dépôt Git privé, donnez à Terraform un moyen de s'authentifier. Le plus propre est l'authentification SSH : chaque développeur crée une clé SSH, l'associe à son compte Git et l'ajoute à ssh-agent ; Terraform l'utilise automatiquement pour une URL source de la forme git@github.com:<OWNER>/<REPO>.git//<PATH>?ref=<VERSION>. Pour vérifier le format de l'URL, essayez de cloner l'URL de base avec git clone depuis votre terminal : si la commande réussit, Terraform y arrivera aussi.

Faire évoluer un module versionné

Voyons le cycle de modification. Supposons que vous ayez retouché le module webserver-cluster et que vous vouliez tester en staging. Vous commitez d'abord les changements dans le dépôt modules, puis vous créez un nouveau tag.

$ git tag -a "v0.0.2" -m "Second release of webserver-cluster"
$ git push --follow-tags

Vous mettez alors à jour uniquement l'URL source de staging (live/stage/services/webserver-cluster/main.tf) pour pointer sur ?ref=v0.0.2, tandis que la production continue paisiblement de tourner sur v0.0.1, inchangée. Une fois v0.0.2 éprouvée en staging, vous montez la production à son tour. Et si un bug se glisse dans v0.0.2 ? Aucune importance : il n'a aucun effet sur les vrais utilisateurs de production. Vous corrigez, publiez une nouvelle version, et recommencez le cycle jusqu'à obtenir quelque chose d'assez stable pour la production.

À retenir

Les modules versionnés brillent pour déployer dans un environnement partagé (staging, production). Mais quand vous itérez sur votre propre poste, repassez aux chemins de fichiers locaux : vous modifiez le dossier du module et relancez plan ou apply dans le dossier live immédiatement, sans avoir à commiter ni publier une version à chaque essai. Tags Git pour le partagé, chemins locaux pour l'itération rapide.

Conclusion

En définissant l'infrastructure comme du code rangé dans des modules, vous appliquez à votre infrastructure tout l'arsenal du génie logiciel : revue de code pour chaque changement, tests automatisés, releases versionnées sémantiquement, et la possibilité d'essayer différentes versions d'un module dans différents environnements — avec retour arrière en cas de problème. Vous gagnez énormément en vitesse et en fiabilité, car chacun réutilise des briques d'infrastructure éprouvées, testées et documentées. On peut imaginer un module canonique décrivant comment déployer un microservice — cluster, mise à l'échelle, répartition du trafic — que chaque équipe réutilise en quelques lignes.

Pour qu'un tel module serve plusieurs équipes, son code doit être flexible et configurable : une équipe veut une instance unique sans load balancer, une autre une dizaine d'instances derrière un répartiteur. Comment écrire des conditions en Terraform ? Existe-t-il des boucles ? Peut-on déployer des changements sans interruption de service (zero-downtime) ? Ces aspects avancés de la syntaxe sont l'objet du chapitre suivant.

À retenir

  • Tout dossier de fichiers .tf est un module. On en appelle un autre avec le bloc module et l'argument source ; le module du répertoire courant est le module racine. Retirez toujours la définition du provider d'un module : c'est à l'utilisateur de la fournir.
  • Les variables d'entrée (input variables) forment l'API du module et le rendent configurable d'un environnement à l'autre — d'abord pour les noms (éviter les conflits codés en dur), puis pour la taille (instance_type, min_size, max_size). Petit cluster bon marché en staging, grand cluster en production.
  • Les locals (local.xxx) nomment une expression réutilisable, DRY et invisible de l'extérieur ; les sorties (output) sont les valeurs de retour du module, lues via module.<NAME>.<OUTPUT> et « remontées » de proche en proche.
  • Deux pièges à connaître : utilisez path.module pour tout fichier lu dans un module (jamais un chemin relatif nu), et préférez les ressources séparées aux blocs en ligne — sinon les règles entrent en conflit, et le module reste rigide.
  • Versionnez vos modules : source pointant sur une URL Git avec ref=<tag> (double slash obligatoire). Séparez le dépôt modules (les « plans ») du dépôt live (les « maisons »). Privilégiez les tags Git au versionnement sémantique plutôt que branches (instables) ou hashes (illisibles).
  • Le versionnement découple staging et production : v0.0.2 à l'épreuve en staging pendant que la production reste sur v0.0.1 ; un bug en staging n'atteint jamais les vrais utilisateurs. Un changement de source impose toujours un nouveau terraform init.
  • Tags Git en environnement partagé, chemins de fichiers locaux pour itérer sur son poste : on modifie le module et on relance plan/apply aussitôt, sans commit ni publication de version.