Architecture en 3 niveaux pour votre IaC avec Terraform et GitHub

Publié par Armel GANNE
Catégorie : Azure / Terraform
05/12/2025

Quand on déploie de l’Infrastructure as Code avec Terraform et GitHub à l’échelle organisationnelle, on tombe souvent dans deux écueils : soit des « grands squelettes partagés » qui bloquent tout le monde, soit un far-west où chaque équipe réinvente la roue dans son propre dépôt. Après avoir vécu cette tension dans plusieurs contextes, nous avons expérimenté une architecture IaC en trois niveaux qui nous a considérablement simplifié la vie.

Cette architecture n’est pas théorique. Elle pilote aujourd’hui des dizaines de services Azure avec Terraform, des équipes autonomes qui déploient plusieurs fois par jour via GitHub Actions, sans sacrifier la gouvernance ni la sécurité. Voici ce que nous avons appris.

 
 

Le cadre en trois niveaux qui nous a simplifié la vie

 

Le problème des architectures « monolithiques »

Nous sommes partis d’une situation classique : une infrastructure centralisée où chaque changement nécessitait une coordination entre équipes, des cycles de déploiement alignés, et une gouvernance lourde. Les équipes produit attendaient des semaines pour obtenir un environnement, mais l’équipe plateforme croulait sous les demandes.

Le diagramme ci-dessous décrit le découpage que nous avons fini par réalisé pour solutionner ce problème.

 
L'architecture iac en 3 niveaux
 

Clarté des responsabilités

Voici une description rapide des responsabilité pour chaque niveaux :

  • Organisation : L’équipe infrastructure déploie et maintient les services partagés (VNet, DNS, Key Vault, Service Bus). Ces composants ont un cycle de vie long et impactent toute l’organisation.
  • Plateforme : L’équipe DevOps maintient les workflows, modules Terraform, et patterns de déploiement. Ces éléments évoluent pour supporter les besoins des équipes produit.
  • Produits : Chaque équipe produit utilise les modules et workflows pour déployer ses services. Elle reste autonome sur sa logique métier et ses cycles de release.

 

Autonomie encadrée

Le plus de ce découpage, c’est qu’il donne aux équipes un périmètre lisible :

  • Imposé par l’organisation : Standards de sécurité, conventions de nommage, architecture réseau
  • Sous leur contrôle direct : Technologie applicative, fréquence de déploiement, configuration métier
  • Influence possible sur : Évolution des standards, adoption de nouvelles pratiques, amélioration des processus

 
Voici un exemple pour une Function App définie par une équipe Produit :

 

  # Une équipe produit utilise un module plateforme
  module "function_app" {
  source = "../modules_shared/function_app"  # ← Fourni par la plateforme


  # Configuration spécifique au produit
  function_app_name = "fun-tiers-enseigne-client-${var.environment}"
  storage_account_name = "sa-tiers-enseigne-client-${var.environment}"


  # Standards appliqués automatiquement par le module
  # → VNet integration, Managed Identity, Key Vault access, monitoring
}

 
 

Pourquoi Terraform s’y prête particulièrement bien

 

Un langage unique pour tous les niveaux

Terraform nous permet de décrire la plateforme et les produits avec le même langage, tout en gardant des états séparés. Ainsi cette cohérence simplifie énormément la collaboration entre équipes.

 

# Niveau Organisation : Infrastructure partagée
resource "azurerm_app_service_environment_v3" "ase" {
  name                         = "ase-network-shared-tiers-${var.env}"
  internal_load_balancing_mode = "Web, Publishing"
  # Configuration enterprise avec isolation réseau complète
}

# Niveau Produit : Application utilisant l'ASE
resource "azurerm_linux_function_app" "app" {
  name                       = "fun-tiers-enseigne-client-${var.environment}"
  app_service_environment_id = data.azurerm_app_service_environment_v3.shared_ase.id
  # L'application hérite automatiquement de l'isolation réseau
}

 

État segmenté par contexte

Un autre des avantages de cette segmentation vient du fait que, maintenant, chaque niveau maintient son propre état Terraform. Par exemple :

  • Organisation : `terraform-state-org-tiers-{env}`
  • Produits : `terraform-state-enseigne-client-{env}`

 
Cette séparation évite les conflits entre équipes et permet des cycles de vie indépendants.
 

Modules qui portent nos conventions

Ainsi, les modules deviennent les véhicules de nos standards, sans engendrer de friction, bien au contraire :

 

# Module Key Vault avec sécurité by design
resource "azurerm_key_vault" "this" {
  name                          = var.keyvaultName
  public_network_access_enabled = false              # ← Sécurité par défaut
  enable_rbac_authorization     = true               # ← RBAC imposé


  # Tags automatiques pour la gouvernance
  tags = merge(var.tags, {
    "cost-center"    = var.cost_center
    "environment"    = var.environment
    "managed-by"     = "terraform"
  })


  lifecycle {
    prevent_destroy = true                           # ← Protection anti-suppression
  }
}


# Private Endpoint automatique
resource "azurerm_private_endpoint" "this" {
  name      = "pe-${azurerm_key_vault.this.name}"
  subnet_id = var.subnet_id


  private_dns_zone_group {
    name                 = "privatelink.vaultcore.azure.net"
    private_dns_zone_ids = [var.private_dns_zone_id]  # ← DNS automatique
  }
}

 
Cela garantit un respect des conventions (de nommage, de sécurité, etc.) facile à suivre, et simple à maintenir. Ce système permet notamment d’imposer les configurations par défaut suivantes :

  • Private isolation : l’accès public aux composants est interdit par défaut
  • Aucun secret : la connectivité entre composants est générée via Managed Identity uniquement
  • Principe de moindre privilège : les permissions minimales sont déjà disponibles pour les ressources à l’aide d’assignation RBAC
  • Observabilité en continue : les diagnostic settings et autres configurations lié au monitoring sont cadrés et déjà calibrés

 

Le petit paramètre de plus et les Golden Paths

Quand on refuse d’ajouter « le petit paramètre de plus » à un module, on protège la vitesse du collectif. En effet, nous considérons qu’il vaut mieux créer un module spécialisé que de complexifier le module générique. C’est évidemment un arbitrage à faire en fonction du paramètre et des besoins.

À l’inverse, nous avons pu identifier quelques scenarios redondants constituant quasiment 80% des projets mis en place. Pour chacun de ces scenarios (ou Golden Path), nous avons défini un template global permettant de n’avoir plus que la configuration à traiter. Cela offre un gain de temps significatif pour la plupart des projets (les 20% restant étant réalisé sur mesure).

 

Les limites à assumer

Il y a cependant certaines limites à garder en tête pour que tout se passe bien :

  • Dette de modules mal pensés : Un module mal conçu impacte tous ses utilisateurs, créant ainsi une dette technique qui se propage à l’échelle de l’organisation. C’est pourquoi nous avons appris à être conservateurs sur les interfaces et à versionner rigoureusement chaque évolution.
  • Discipline de versionnage : Dans cette optique, les changements de module nécessitent une stratégie de migration bien pensée. Par conséquent, nous utilisons un versioning sémantique strict et mettons en place des périodes de support overlap, permettant ainsi aux équipes de migrer à leur rythme sans blocage.

 
 

GitHub Actions comme colonne vertébrale de l’automatisation

 

Même pipeline, contextes différents

L’objectif était ambitieux : que chaque équipe utilise exactement les mêmes workflows, mais avec des contextes différents (environnements, ressources, paramètres).
 

# Workflow produit : délégation totale au workflow plateforme
name: Deploy Infrastructure
on:
  workflow_dispatch:
    inputs:
      env:
        required: true
        type: choice
        options: ['dev', 'int', 'prod']




jobs:
  deploy:
    uses: WORKSPACE_GITHUB/flux-common-ci-config/.github/workflows/deploy-terraform.yaml@1.0.0
    with:
      env: ${{ inputs.env }}
      working-directory-path: ${{ vars.TERRAFORM_WORKING_DIRECTORY_PATH }}
      tfstate-file-name: ${{ vars.TERRAFORM_TFSTATE_FILE_NAME }}
      do-apply: ${{ inputs.do-apply }}

 

Workflows réutilisables et versionnés

Nous centralisons tous les workflows dans un repository dédié (`flux-common-ci-config`), avec un versioning strict :
 

# Workflow plateforme : logique réutilisable
name: Deploy Terraform
on:
  workflow_call:
    inputs:
      env: { required: true, type: string }
      working-directory-path: { type: string }
      tfstate-file-name: { type: string }
      do-apply: { type: boolean, default: false }




jobs:
  deploy-dev:
    if: inputs.env == 'dev'
    runs-on: [self-hosted, ais, dev]           # ← Runner isolé par environnement
    steps:
      - uses: WORKSPACE_GITHUB/flux-common-ci-config/.github/actions/deploy-terraform@1.0.0
        with:
          managed-identity-client-id: ${{ vars.FLUX_ENSEIGNE_MANAGED_IDENTITY_DEPLOYMENT_DEV }}
          storage-account-name: sanetworksharedmaisdev
          tfstate-file-name: ${{ inputs.tfstate-file-name }}

 

 

OIDC pour éliminer les secrets durables

Ce cadre permet également de forcer des mécanismes plus sûrs. Par exemple, fini les secrets stockés dans GitHub, pour les connexions à Azure. Chaque environnement utilise une Managed Identity dédiée :
 

- name: Login with Azure CLI
  run: az login --identity --client-id ${{ inputs.managed-identity-client-id }}


- name: Terraform Init
  run: |
    terraform init \
      -backend-config="storage_account_name=${{ inputs.storage-account-name }}" \
      -backend-config="use_azuread_auth=true"    # ← Authentification Azure AD

 

Traçabilité dans les pipelines

Chaque déploiement est tracé avec son contexte :
 

- name: Tag deployment
  run: |
    az tag create --resource-id ${{ steps.terraform.outputs.function_app_id }} --tags \
      "last-deployed-by=${{ github.actor }}" \
      "last-deployed-at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
      "git-commit=${{ github.sha }}" \
      "workflow-run=${{ github.run_id }}"

 

Séparation Plan / Apply

Nous avons également pu mettre en place un mécanisme lié à la revue. En effet, tous les déploiements passent par deux phases :

  1. Plan obligatoire : Génère le plan, l’affiche pour revue
  2. Apply manuel : Nécessite une validation humaine explicite

 
Cette approche force la revue au sein d’un workflow de déploiement bien rodé. Cela ajoute une latence au moment du déploiement, c’est vrai. Mais cela permet surtout un gain significatif de qualité de façon assez naturelle.
 
 

Commencer petit, mesurer, itérer

 

Ce qui compte vraiment pour nous, c’est de réduire le temps entre une idée et un environnement prêt. Voici par exemple les temps de mise en oeuvre avant et après l’instauration de cette mécanique :

  • Avant : 2-3 semaines pour un nouvel environnement
  • Après : 2-3 heures avec les golden paths

 
Pour arriver à ce résultat, nous apprenons continuellement à l’aide de revues régulières :

  • Revues hebdomadaires des modules les plus utilisés
  • Feedback trimestriel des équipes produit
  • Métriques d’adoption des golden paths

 
Notre but est d’optimiser les indicateur suivants :

  • Lead time : Du commit au déploiement en production
    • Target : < 30 minutes pour une Function App standard
  • Taux de réussite des déploiements :
    • Target : > 95% de réussite au premier essai
  • MTTR (Mean Time To Recovery) :
    • Target : < 15 minutes pour un rollback
  • Part des nouveaux services qui passent par un golden path :
    • Target : > 80% des nouveaux services

 
 

L’architecture qui grandit avec vous

 
Cette approche en trois niveaux nous a appris une chose essentielle : la valeur ne vient pas de la sophistication technique, mais plutôt de la clarté des responsabilités et de la vitesse d’exécution.

Concrètement, cette architecture nous a permis d’obtenir l’autonomie des équipes sans sacrifier la gouvernance. Parallèlement, les standards de sécurité sont appliqués automatiquement, ce qui élimine le risque d’oubli ou de non-conformité. De plus, les cycles de déploiement entre infrastructure et produits sont désormais indépendants, ce qui accélère considérablement les livraisons. Enfin, nous bénéficions d’une boucle de feedback rapide pour faire évoluer la plateforme en fonction des besoins réels du terrain.
 

Les leçons apprises

  1. Commencer par les workflows, pas par les modules : Un pipeline standardisé apporte plus de valeur qu’un module sophistiqué
  2. Optimiser pour le cas d’usage à 80% : Résister à l’envie de couvrir tous les cas « edge »
  3. Mesurer l’adoption, pas la fonctionnalité : Un module non utilisé est un module raté
  4. Documenter par l’exemple : Le code reste la meilleure documentation

 

Références