---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 06 — Terraform : modules et organisation avancée

Une fois les fondations maîtrisées, la productivité de Terraform repose sur la **modularisation** : encapsuler des patterns d'infrastructure réutilisables, organiser les dépendances entre stacks, et industrialiser les ressources multiples. Ce chapitre couvre les modules, l'organisation de dépôt, les métaarguments `count`/`for_each`, les blocs dynamiques, les fonctions avancées et les outils de débogage.

## Modules : structure et utilisation

Un module Terraform est simplement un répertoire contenant des fichiers `.tf`. Le répertoire racine où vous lancez Terraform est le **module racine** ; il peut appeler des **modules enfants**.

### Structure d'un module

```text
modules/
└── eks-cluster/
    ├── main.tf          # ressources principales
    ├── variables.tf     # inputs du module
    ├── outputs.tf       # outputs exposés
    ├── versions.tf      # required_providers
    └── README.md        # documentation (inputs, outputs, exemple)
```

### Appel d'un module

```hcl
# Depuis le module racine
module "eks" {
  source  = "./modules/eks-cluster"        # local
  # source = "terraform-aws-modules/eks/aws"  # Terraform Registry
  # source = "git::https://github.com/org/repo.git//modules/eks?ref=v2.1.0"  # Git

  version = "~> 19.0"   # uniquement pour les sources registry

  # Inputs du module
  cluster_name    = "prod-eks"
  cluster_version = "1.29"
  vpc_id          = module.vpc.vpc_id
  subnet_ids      = module.vpc.private_subnet_ids

  node_groups = {
    general = {
      desired_size   = 3
      min_size       = 2
      max_size       = 10
      instance_types = ["t3.large"]
    }
  }
}

# Consommer les outputs du module
output "cluster_endpoint" {
  value = module.eks.cluster_endpoint
}
```

## Terraform Registry

Le [Terraform Registry](https://registry.terraform.io) héberge des modules officiels (préfixe `hashicorp/`) et communautaires. Les modules officiels pour les trois grands clouds suivent la convention de nommage `terraform-<provider>-<nom>`.

**Modules particulièrement utiles :**

- `terraform-aws-modules/vpc/aws` — VPC complet avec subnets, NAT, IGW
- `terraform-aws-modules/eks/aws` — EKS avec node groups et addons
- `terraform-google-modules/network/google` — VPC GCP
- `Azure/aks/azurerm` — AKS

```hcl
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.1"

  name = "${local.name_prefix}-vpc"
  cidr = "10.0.0.0/16"

  azs             = data.aws_availability_zones.available.names
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = !local.is_production
  enable_dns_hostnames = true

  tags = local.common_tags
}
```

## Organisation d'un dépôt Terraform

### Structure recommandée (mono-repo)

```text
infrastructure/
├── modules/                   # modules réutilisables
│   ├── vpc/
│   ├── eks/
│   ├── rds/
│   └── iam-role/
├── envs/                      # configurations par environnement
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   └── prod/
├── global/                    # ressources partagées (DNS, IAM global)
│   ├── route53/
│   └── iam/
└── .github/
    └── workflows/
        └── terraform.yml
```

### Mono-repo vs poly-repo

**Mono-repo :** toute l'infrastructure dans un seul dépôt.

- Avantages : refactoring atomique, visibilité globale, CI unifiée.
- Inconvénients : blast radius plus large, permissions moins granulaires.

**Poly-repo :** un dépôt par équipe ou domaine.

- Avantages : isolation forte, ownership clair, pipelines indépendants.
- Inconvénients : dépendances inter-dépôts difficiles à synchroniser.

## Remote state et dépendances inter-stacks

Pour partager des outputs entre projets Terraform indépendants, on utilise `terraform_remote_state`.

```hcl
# Stack "network" — output exposé
output "private_subnet_ids" {
  value = module.vpc.private_subnet_ids
}

# Stack "application" — consommation du state réseau
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-terraform-states"
    key    = "network/prod/terraform.tfstate"
    region = "eu-west-1"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
  # ...
}
```

:::{admonition} Alternative : SSM Parameter Store ou outputs explicites
:class: tip
Plutôt que de coupler directement deux stacks via `terraform_remote_state`, certaines équipes préfèrent écrire les valeurs importantes dans AWS SSM Parameter Store ou dans des data sources dédiées. Cela réduit le couplage et permet à des équipes non-Terraform de consommer les valeurs.
:::

## `count` et `for_each`

Ces métaarguments permettent de créer plusieurs instances d'une même ressource.

```hcl
# count — basé sur un entier
resource "aws_subnet" "private" {
  count             = length(var.private_cidr_blocks)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_cidr_blocks[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = { Name = "private-subnet-${count.index + 1}" }
}

# for_each — basé sur une map ou un set (plus robuste)
resource "aws_security_group_rule" "ingress" {
  for_each = {
    http  = { port = 80,  protocol = "tcp" }
    https = { port = 443, protocol = "tcp" }
    ssh   = { port = 22,  protocol = "tcp", cidr = ["10.0.0.0/8"] }
  }

  type              = "ingress"
  security_group_id = aws_security_group.app.id
  from_port         = each.value.port
  to_port           = each.value.port
  protocol          = each.value.protocol
  cidr_blocks       = lookup(each.value, "cidr", ["0.0.0.0/0"])
  description       = "Règle ${each.key}"
}
```

La clé de `for_each` devient l'identifiant de la ressource dans le state : `aws_security_group_rule.ingress["https"]`. Avec `count`, ce serait `aws_security_group_rule.ingress[1]` — si on insère un élément en début de liste, tous les index bougent et Terraform va recréer toutes les ressources suivantes.

## Dynamic blocks

Les blocs dynamiques génèrent des blocs imbriqués répétitifs à partir d'une collection.

```hcl
variable "ingress_rules" {
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
}

resource "aws_security_group" "app" {
  name   = "${local.name_prefix}-sg"
  vpc_id = aws_vpc.main.id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

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

## Fonctions avancées

```hcl
# templatefile() : rend un template avec des variables
resource "aws_instance" "app" {
  user_data = templatefile("${path.module}/templates/cloud-init.yaml.tpl", {
    hostname    = "app-${var.environment}"
    packages    = ["nginx", "certbot", "fail2ban"]
    ssh_pub_key = var.ssh_public_key
  })
}

# jsonencode() / yamlencode() : sérialisation inline
resource "aws_iam_policy" "s3_read" {
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:ListBucket"]
      Resource = ["arn:aws:s3:::${var.bucket_name}", "arn:aws:s3:::${var.bucket_name}/*"]
    }]
  })
}

# Autres fonctions utiles
locals {
  # Fusionner des maps
  merged_tags = merge(var.default_tags, var.extra_tags)

  # Extraire des clés d'une map
  subnet_names = keys(var.subnets_config)

  # Aplatir une liste de listes
  all_cidrs = flatten([for vpc in var.vpcs : vpc.subnet_cidrs])

  # Encoder en base64
  encoded_script = base64encode(file("${path.module}/scripts/init.sh"))
}
```

## Lifecycle rules avancées

```hcl
resource "aws_db_instance" "main" {
  identifier = "${local.name_prefix}-rds"
  # ...

  lifecycle {
    # Éviter la suppression accidentelle de la DB de production
    prevent_destroy = true

    # Ignorer les changements de mot de passe (géré par rotation externe)
    ignore_changes = [password, snapshot_identifier]

    # Créer le remplacement avant de supprimer (zéro downtime)
    create_before_destroy = true

    # Hook avant destruction (ex: snapshot final)
    replace_triggered_by = [aws_db_parameter_group.main]
  }
}
```

## Import de ressources existantes

Terraform 1.5+ introduit le bloc `import` déclaratif, plus pratique que la commande `terraform import`.

```hcl
# Import déclaratif (TF 1.5+)
import {
  to = aws_s3_bucket.legacy
  id = "my-existing-bucket-name"
}

resource "aws_s3_bucket" "legacy" {
  bucket = "my-existing-bucket-name"
  # Terraform génèrera la configuration avec `terraform plan -generate-config-out=generated.tf`
}
```

```bash
# Ancienne méthode (toujours valide)
terraform import aws_s3_bucket.legacy my-existing-bucket-name

# Générer la configuration depuis les ressources importées
terraform plan -generate-config-out=imported_resources.tf
```

## Refactoring avec `moved`

Le bloc `moved` permet de renommer des ressources dans le state sans les recréer.

```hcl
# Renommer une ressource
moved {
  from = aws_instance.app
  to   = aws_instance.web_server
}

# Déplacer une ressource vers un module
moved {
  from = aws_security_group.main
  to   = module.network.aws_security_group.main
}

# Convertir count → for_each
moved {
  from = aws_subnet.private[0]
  to   = aws_subnet.private["eu-west-1a"]
}
```

## Débogage

```bash
# Niveaux de log : TRACE, DEBUG, INFO, WARN, ERROR
export TF_LOG=DEBUG
export TF_LOG_PATH=/tmp/terraform-debug.log
terraform apply

# Console interactive — évaluer des expressions HCL
terraform console
> cidrsubnets("10.0.0.0/16", 8, 8, 8)
> jsondecode(file("config.json"))
> length(var.subnet_ids)

# Afficher le graphe de dépendances
terraform graph | dot -Tsvg > graph.svg
```

---

## Visualisations

```{code-cell} python3
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import numpy as np
import networkx as nx
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

```{code-cell} python3
# Visualisation 1 : Graphe de dépendances entre modules Terraform

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

G = nx.DiGraph()

# Nœuds : modules
modules = {
    "root": {"layer": 0, "color": "#4dabf7"},
    "vpc":  {"layer": 1, "color": "#74c0fc"},
    "iam":  {"layer": 1, "color": "#74c0fc"},
    "eks":  {"layer": 2, "color": "#51cf66"},
    "rds":  {"layer": 2, "color": "#51cf66"},
    "alb":  {"layer": 2, "color": "#51cf66"},
    "monitoring": {"layer": 3, "color": "#ffd43b"},
    "dns":        {"layer": 3, "color": "#ffd43b"},
}

for node, attrs in modules.items():
    G.add_node(node, **attrs)

# Arêtes : dépendances (A → B signifie A dépend de B)
edges = [
    ("root", "vpc"), ("root", "iam"),
    ("eks",  "vpc"), ("eks", "iam"),
    ("rds",  "vpc"),
    ("alb",  "vpc"), ("alb", "eks"),
    ("monitoring", "eks"), ("monitoring", "rds"),
    ("dns",  "alb"),
    ("root", "eks"), ("root", "rds"), ("root", "alb"),
    ("root", "monitoring"), ("root", "dns"),
]
G.add_edges_from(edges)

# Layout hiérarchique manuel
pos = {
    "root":       (4.0,  4.0),
    "vpc":        (2.0,  3.0),
    "iam":        (6.0,  3.0),
    "eks":        (2.0,  2.0),
    "rds":        (4.0,  2.0),
    "alb":        (6.0,  2.0),
    "monitoring": (3.0,  1.0),
    "dns":        (5.5,  1.0),
}

fig, ax = plt.subplots(figsize=(11, 7))
ax.set_title("Graphe de dépendances entre modules Terraform", fontsize=13, fontweight="bold", pad=15)

node_colors = [modules[n]["color"] for n in G.nodes()]
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=1800, alpha=0.92, ax=ax)
nx.draw_networkx_labels(G, pos, font_size=9, font_weight="bold", ax=ax)
nx.draw_networkx_edges(G, pos, arrows=True, arrowsize=18, width=1.8,
                       edge_color="#495057", alpha=0.7,
                       connectionstyle="arc3,rad=0.07", ax=ax)

layer_labels = {0: "Module racine", 1: "Infrastructure de base", 2: "Services", 3: "Transversal"}
legend_colors = ["#4dabf7", "#74c0fc", "#51cf66", "#ffd43b"]
patches = [mpatches.Patch(color=c, label=l) for c, l in zip(legend_colors, layer_labels.values())]
ax.legend(handles=patches, loc="lower left", fontsize=9)
ax.axis("off")

plt.savefig("terraform_modules_dag.png", dpi=120, bbox_inches="tight")
plt.show()
```

```{code-cell} python3
# Visualisation 2 : Mono-repo vs poly-repo — complexité vs isolation

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

np.random.seed(7)

strategies = {
    "Mono-repo\n(petite équipe)":  {"x": 2.5, "y": 3.5, "size": 180, "color": "#51cf66"},
    "Mono-repo\n(grande équipe)":  {"x": 7.5, "y": 2.5, "size": 280, "color": "#ffd43b"},
    "Poly-repo\n(par domaine)":    {"x": 5.0, "y": 7.5, "size": 240, "color": "#74c0fc"},
    "Poly-repo\n(par service)":    {"x": 8.5, "y": 8.0, "size": 200, "color": "#ff922b"},
    "Terragrunt\nmono-repo":       {"x": 4.0, "y": 6.5, "size": 260, "color": "#cc5de8"},
}

fig, ax = plt.subplots(figsize=(10, 8))

for label, props in strategies.items():
    ax.scatter(props["x"], props["y"], s=props["size"] * 3,
               color=props["color"], alpha=0.78, edgecolors="white", linewidths=2, zorder=3)
    ax.annotate(label, (props["x"], props["y"]),
                textcoords="offset points", xytext=(12, 5),
                fontsize=9.5, fontweight="bold", color="#333")

ax.set_xlabel("Complexité de gestion  →", fontsize=11)
ax.set_ylabel("Isolation entre équipes  →", fontsize=11)
ax.set_title("Stratégies d'organisation de dépôt Terraform\nComplexité vs isolation", fontsize=13, fontweight="bold")
ax.set_xlim(0, 11)
ax.set_ylim(0, 11)

# Quadrants
ax.axvline(x=5.5, color="#ced4da", linestyle="--", linewidth=1.2, alpha=0.7)
ax.axhline(y=5.5, color="#ced4da", linestyle="--", linewidth=1.2, alpha=0.7)
ax.text(1.5, 9.5, "Simple + Isolé\n(idéal)", fontsize=8.5, color="#2f9e44", alpha=0.6, style="italic")
ax.text(7.0, 9.5, "Complexe + Isolé\n(acceptable à grande échelle)", fontsize=8, color="#e67700", alpha=0.6, style="italic")
ax.text(1.5, 1.0, "Simple + Couplé\n(petites équipes)", fontsize=8.5, color="#1971c2", alpha=0.6, style="italic")

plt.savefig("terraform_repo_strategies.png", dpi=120, bbox_inches="tight")
plt.show()
```

```{code-cell} python3
# Visualisation 3 : Tableau comparatif count vs for_each

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 5))
ax.axis("off")
ax.set_title("Comparaison : count vs for_each en Terraform", fontsize=13, fontweight="bold", pad=20)

columns = ["Critère", "count", "for_each"]
rows = [
    ["Type de collection",   "Entier (0..n-1)",         "Map ou set de strings"],
    ["Référence dans state", "resource[0], resource[1]", 'resource["clé"]'],
    ["Ajout en milieu",      "Décale tous les index\n→ recreate en cascade", "Seule la nouvelle clé est ajoutée"],
    ["Suppression",          "Décale les index restants", "Supprime uniquement la clé retirée"],
    ["Use case typique",     "N instances identiques", "Ressources différenciées par une clé"],
    ["Lisibilité du plan",   "Index numériques peu lisibles", "Clés sémantiques explicites"],
    ["Recommandation",       "Simples répliques homogènes", "Tout ce qui a une identité propre"],
]

colors_header = ["#4dabf7"] * 3
colors_rows = []
for i, _ in enumerate(rows):
    if i % 2 == 0:
        colors_rows.append(["#f8f9fa", "#e8f4fd", "#e8f7ee"])
    else:
        colors_rows.append(["#ffffff", "#ffffff", "#ffffff"])

table = ax.table(
    cellText=rows,
    colLabels=columns,
    cellLoc="center",
    loc="center",
    cellColours=colors_rows,
    colColours=colors_header,
)
table.auto_set_font_size(False)
table.set_fontsize(9.5)
table.scale(1.0, 2.2)

for (row, col), cell in table.get_celld().items():
    cell.set_edgecolor("#dee2e6")
    if row == 0:
        cell.set_text_props(fontweight="bold", color="white")
    if col == 0 and row > 0:
        cell.set_text_props(fontweight="bold")

plt.savefig("count_vs_foreach.png", dpi=120, bbox_inches="tight")
plt.show()
```

## Résumé

1. Un **module** Terraform est tout répertoire contenant des fichiers `.tf` ; il encapsule des ressources avec des inputs et outputs clairement définis, favorisant la réutilisation et l'abstraction.
2. Le **Terraform Registry** fournit des modules maintenus et éprouvés pour les providers majeurs ; les préférer à des modules maison réduit la maintenance.
3. Le **remote state** via `terraform_remote_state` ou des data sources dédiées permet aux stacks indépendantes de partager des valeurs sans fusionner leurs configurations.
4. `for_each` est presque toujours préférable à `count` pour les ressources qui ont une identité propre : les identifiants sémantiques dans le state évitent les recréations en cascade lors des modifications de liste.
5. Les **blocs dynamiques** éliminent la duplication de blocs imbriqués répétitifs tout en conservant la lisibilité grâce au nommage explicite de l'itérateur.
6. `templatefile()`, `jsonencode()` et `yamlencode()` sont les fonctions de sérialisation incontournables pour générer des configurations complexes (cloud-init, policies IAM, fichiers de configuration).
7. Le bloc `moved` permet de refactoriser le state sans recréer les ressources, rendant les migrations de code Terraform non destructives.
8. L'import déclaratif (TF 1.5+) avec génération de configuration (`-generate-config-out`) facilite l'adoption de Terraform sur une infrastructure existante.
9. `TF_LOG=DEBUG` et `terraform console` sont les outils de premier recours pour diagnostiquer les comportements inattendus des expressions et des providers.
10. La stratégie mono-repo avec Terragrunt est souvent le meilleur compromis à l'échelle d'une organisation : visibilité globale, DRY, et isolation suffisante par environnement.
