Retour aux articles
blog — iac — zsh$ cat iac.md# 21 nov. 2025Arrête de dupliquer tesvaleurs Terraform, utilisela hiérarchie Terragrunt
5 min de lecture

Config root, config account, config environment. Une valeur, définie une fois, dispo partout. Zéro copier-coller.

TerraformTerragruntDevOpsIaC

J’avais 12 dossiers d’environnements. Même région AWS partout. Mêmes tags. Même config KMS

Je change la région? Je dois toucher 12 fichiers. J’ajoute un tag? Encore 12 fichiers. J’en rate un? Cet environnement explose différemment des autres

Terragrunt règle ça avec son système de hiérarchie. Tu définis chaque valeur une fois au bon endroit, elle descend automatiquement partout

Hiérarchie logique

Tes valeurs d’infra, elles rentrent dans des catégories. Y’en a qui valent pour tout. D’autres juste pour un compte AWS. D’autres pour un environnement. D’autres pour une région

root.hcl              → Tout: backend, provider, tags projet
account.hcl           → Compte AWS: ID, rôles IAM, centre de coût
environment.hcl       → Environnement: prod vs staging, domaines
region.hcl            → Région: zones dispo, endpoints

Chaque niveau hérite du dessus. Ton module database dans prod/us-east-1/database/, il récupère automatiquement les quatre niveaux

Zéro duplication. Zéro copier-coller. Une source de vérité par valeur

Config root: ce qui vaut partout

root.hcl à la racine de ton infra. Il contient ce que toutes tes ressources partagent:

# infrastructure/root.hcl
locals {
  project_name = "myapp"
  project_id   = "app123"

  # Ces tags vont sur tout
  root_tags = {
    "project"     = "myapp"
    "managed-by"  = "terraform"
    "cost-center" = "engineering"
  }
}

# Charge les configs enfants
locals {
  account_vars     = read_terragrunt_config(find_in_parent_folders("account.hcl"))
  environment_vars = read_terragrunt_config(find_in_parent_folders("environment.hcl"))
  region_vars      = read_terragrunt_config(find_in_parent_folders("region.hcl"))
}

# Backend unique pour tous les modules
remote_state {
  backend = "s3"
  config = {
    bucket         = "myapp-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

# Provider généré partout
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<-EOF
    provider "aws" {
      region = "${local.region_vars.locals.region}"

      assume_role {
        role_arn = "${local.account_vars.locals.iam_role}"
      }

      default_tags {
        tags = var.default_tags
      }
    }
  EOF
}

# Tout dispo en inputs
inputs = merge(
  local.account_vars.locals,
  local.environment_vars.locals,
  local.region_vars.locals,
  {
    project_name = local.project_name
    default_tags = merge(
      local.root_tags,
      local.account_vars.locals.account_tags,
      local.environment_vars.locals.environment_tags
    )
  }
)

T’écris ton backend une fois. Ton provider une fois. Tes tags root une fois

Tous tes modules récupèrent ça automatiquement. Besoin de changer le bucket S3? Une ligne. Un tag? Une ligne

Config account: le niveau compte AWS

account.hcl contient ce qui est propre à un compte AWS. T’as des comptes séparés prod/non-prod? Un fichier par compte:

# infrastructure/prod-account/account.hcl
locals {
  account_id   = "123456789012"
  account_name = "production"

  # Rôle IAM pour les déploiements
  iam_role = "arn:aws:iam::123456789012:role/terraform-deploy"

  # Tags de coûts niveau compte
  account_tags = {
    "environment" = "production"
    "account-id"  = "123456789012"
    "criticality" = "high"
  }

  # Config prod
  enable_deletion_protection = true
  backup_retention_days      = 90
}
# infrastructure/staging-account/account.hcl
locals {
  account_id   = "987654321098"
  account_name = "staging"

  iam_role = "arn:aws:iam::987654321098:role/terraform-deploy"

  account_tags = {
    "environment" = "staging"
    "account-id"  = "987654321098"
    "criticality" = "medium"
  }

  enable_deletion_protection = false
  backup_retention_days      = 30
}

L’ID du compte, le rôle IAM, les policies: c’est défini une fois. Chaque ressource du compte les récupère

Besoin de changer le rôle IAM prod? Un fichier. Ajouter des tags compte? Un fichier

Config environment: prod vs staging

environment.hcl contient les différences entre prod et staging dans le même compte:

# infrastructure/prod-account/prod/environment.hcl
locals {
  environment = "prod"

  environment_tags = {
    "environment" = "prod"
  }

  # Domaine prod
  domain_name = "myapp.com"

  # Sizing prod
  database_instance_class = "db.r6g.xlarge"
  cache_node_type         = "cache.r6g.large"

  # Scaling prod
  min_instances = 3
  max_instances = 10

  # Monitoring prod
  enable_detailed_monitoring = true
  alarm_email               = "oncall@company.com"
}
# infrastructure/prod-account/staging/environment.hcl
locals {
  environment = "staging"

  environment_tags = {
    "environment" = "staging"
  }

  domain_name = "staging.myapp.com"

  # Staging plus petit
  database_instance_class = "db.t4g.medium"
  cache_node_type         = "cache.t4g.small"

  min_instances = 1
  max_instances = 3

  enable_detailed_monitoring = false
  alarm_email               = "dev-team@company.com"
}

Les différences prod/staging sont explicites. Tailles d’instances, domaines, scaling

Tes ressources prod ont les valeurs prod. Tes ressources staging ont les valeurs staging. Impossible de mélanger

Config region: le géographique

region.hcl contient ce qui change par région:

# infrastructure/prod-account/prod/us-east-1/region.hcl
locals {
  region = "us-east-1"

  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

  # Endpoints régionaux
  regional_domain = "use1.myapp.com"
}
# infrastructure/prod-account/prod/eu-west-1/region.hcl
locals {
  region = "eu-west-1"

  availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]

  regional_domain = "euw1.myapp.com"
}

T’as un fichier par région. Tu veux déployer dans une nouvelle région? Tu dupliques le dossier et tu modifies juste region.hcl

Le reste descend d’en haut. Les configs account, environment, root s’appliquent automatiquement

Config module: le niveau détail

En bas de la pile, chaque module a son terragrunt.hcl:

# infrastructure/prod-account/prod/us-east-1/database/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "${get_repo_root()}/modules/database"
}

dependency "network" {
  config_path = "../network"
}

inputs = {
  # Juste le spécifique au module
  db_name          = "myapp_prod"
  multi_az         = true

  # Le reste vient de la hiérarchie:
  # - region depuis region.hcl
  # - database_instance_class depuis environment.hcl
  # - backup_retention_days depuis account.hcl
  # - default_tags depuis root.hcl

  vpc_id     = dependency.network.outputs.vpc_id
  subnet_ids = dependency.network.outputs.database_subnet_ids
}

La config du module est minuscule. Y’a que ce qui est unique à cette base

Le reste? Déjà défini au-dessus. La hiérarchie le fournit

Comment ça descend

Tu lances terragrunt apply dans prod-account/prod/us-east-1/database/:

  1. Terragrunt trouve root.hcl, charge la config projet
  2. Charge account.hcl pour les valeurs compte
  3. Charge environment.hcl pour les différences prod/staging
  4. Charge region.hcl pour les paramètres région
  5. Merge tout dans inputs

Ton module database reçoit:

  • project_name depuis root
  • account_id, iam_role, backup_retention_days depuis account
  • environment, database_instance_class depuis environment
  • region, availability_zones depuis region
  • db_name, multi_az depuis le module

Une variable, une fois, au bon endroit. Zéro duplication

Stratégie de merge

Le bloc inputs dans root.hcl merge tout:

inputs = merge(
  local.account_vars.locals,
  local.environment_vars.locals,
  local.region_vars.locals,
  {
    project_name = local.project_name
    default_tags = merge(
      local.root_tags,
      local.account_vars.locals.account_tags,
      local.environment_vars.locals.environment_tags
    )
  }
)

Les tags sont mergés. Tags account écrasent tags root. Tags environment écrasent tags account

Tu veux des tags globaux? Mets ça dans root. Tags compte? Dans account. Tags environnement? Dans environment

Structure dossiers

infrastructure/
├── root.hcl
├── modules/
│   ├── database/
│   ├── cache/
│   └── app/
├── prod-account/
│   ├── account.hcl
│   ├── prod/
│   │   ├── environment.hcl
│   │   ├── us-east-1/
│   │   │   ├── region.hcl
│   │   │   ├── database/
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── cache/
│   │   │   │   └── terragrunt.hcl
│   │   │   └── app/
│   │   │       └── terragrunt.hcl
│   │   └── eu-west-1/
│   │       ├── region.hcl
│   │       ├── database/
│   │       └── ...
│   └── staging/
│       ├── environment.hcl
│       └── us-east-1/
│           └── ...
└── staging-account/
    ├── account.hcl
    └── ...

L’hiérarchie est évidente. Tu sais où chercher. Config compte? account.hcl. Config prod? environment.hcl

Fini de fouiller 47 fichiers pour trouver où est définie une valeur

Configs partagées entre modules

Plusieurs modules partagent la même config? Crée _envcommon/:

# infrastructure/_envcommon/database.hcl
terraform {
  source = "${get_repo_root()}/modules/database"
}

locals {
  # Charge environment pour trouver les dépendances réseau
  environment_vars = read_terragrunt_config(
    find_in_parent_folders("environment.hcl")
  )
}

dependency "network" {
  config_path = "../network"
}

inputs = {
  vpc_id     = dependency.network.outputs.vpc_id
  subnet_ids = dependency.network.outputs.database_subnet_ids
}

Chaque instance database l’inclut:

# prod-account/prod/us-east-1/database/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

include "envcommon" {
  path = "${dirname(find_in_parent_folders("root.hcl"))}/_envcommon/database.hcl"
}

inputs = {
  db_name  = "myapp_prod"
  multi_az = true
}

La config commune est partagée. La config spécifique reste locale. Zéro duplication

Exemple concret: nouvelle région

Tu veux déployer dans eu-west-1. Tu dupliques le dossier:

cp -r prod-account/prod/us-east-1 prod-account/prod/eu-west-1

Tu modifies un seul fichier:

# prod-account/prod/eu-west-1/region.hcl
locals {
  region = "eu-west-1"
  availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}

Tu lances:

cd prod-account/prod/eu-west-1
terragrunt run-all apply

Chaque module dans cette région récupère:

  • La nouvelle région automatiquement
  • La config account inchangée
  • La config environment inchangée
  • La config root inchangée

T’as changé une valeur. Tout le reste descend automatiquement

Ce que ça résout

Avant la hiérarchie Terragrunt:

  • Région AWS dans 12 endroits
  • Config backend dupliquée partout
  • Tags copiés dans tous les environnements
  • Rôles IAM éparpillés
  • Aucune idée où mettre quoi

Après:

  • Région dans region.hcl, une fois
  • Backend dans root.hcl, une fois
  • Tags mergés depuis root/account/environment
  • Rôle IAM dans account.hcl, une fois
  • Hiérarchie claire, évident où va chaque valeur

J’ai changé l’ID de notre compte AWS prod. Un fichier. 10 secondes

Avant ça? J’aurais grep dans 47 fichiers, modifié chacun, raté deux, cassé staging, passé une heure à débugger

Patterns utiles

Dépendances entre environnements: La prod qui utilise le VPC de staging pour le VPN:

# prod-account/prod/environment.hcl
locals {
  network_dependency_path = "${get_repo_root()}/infrastructure/staging-account/staging/us-east-1/network"
}

Préfixes dynamiques: Générer les noms de ressources depuis la hiérarchie:

# root.hcl
locals {
  prefix = "${local.project_id}-${local.environment_vars.locals.environment}-${local.region_vars.locals.region}"
}

inputs = {
  resource_prefix = local.prefix
}

Tes ressources dans prod/us-east-1 ont le préfixe app123-prod-us-east-1. Celles dans staging/eu-west-1 ont app123-staging-eu-west-1. Zéro conflits

Config conditionnelle: Features actives seulement dans certains environnements:

# prod-account/prod/environment.hcl
locals {
  enable_waf                 = true
  enable_enhanced_monitoring = true
}

# staging-account/staging/environment.hcl
locals {
  enable_waf                 = false
  enable_enhanced_monitoring = false
}

Quand éviter

Petits projets avec un seul environnement. La hiérarchie c’est overkill. Un seul terragrunt.hcl suffit

Infra complètement différente par environnement. Si prod et staging n’ont rien en commun, la hiérarchie t’apporte pas grand chose

Projets où chaque module est unique. Rien à réutiliser? Rien à faire descendre

Démarrer

Pour ajouter la hiérarchie à ton setup Terragrunt:

  1. Tu déplaces la config backend dans root.hcl
  2. Tu crées account.hcl avec les valeurs compte
  3. Tu crées environment.hcl avec les valeurs environnement
  4. Tu crées region.hcl avec les valeurs région
  5. Tu modifies root.hcl pour charger et merger tout ça

Commence avec une seule valeur. Déplace la région AWS de tous tes modules vers region.hcl. Vérifie que ça marche

Ensuite les tags. Ensuite les rôles IAM. Une valeur à la fois

Une semaine après t’as une config qui tient la route. Valeurs définies une fois. Hiérarchie claire. Zéro duplication

J’ai passé 6 mois à copier-coller de la config Terraform. J’ai passé 2 jours à setup cette hiérarchie. Je reviens jamais en arrière

Vous avez aimé cet article ?

Faites-le savoir ! Un partage, c'est toujours apprécié.

À propos de l'auteur

Sofiane Djerbi

Sofiane Djerbi

Architecte Cloud & Kubernetes, Expert FinOps. J'aide les entreprises à construire des infrastructures scalables, sécurisées et rentables.

Commentaires