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.
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.

Voici une description rapide des responsabilité pour chaque niveaux :
Le plus de ce découpage, c’est qu’il donne aux équipes un périmètre lisible :
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
}
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
}
Un autre des avantages de cette segmentation vient du fait que, maintenant, chaque niveau maintient son propre état Terraform. Par exemple :
Cette séparation évite les conflits entre équipes et permet des cycles de vie indépendants.
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 :
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).
Il y a cependant certaines limites à garder en tête pour que tout se passe bien :
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 }}
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 }}
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
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 }}"
Nous avons également pu mettre en place un mécanisme lié à la revue. En effet, tous les déploiements passent par deux phases :
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.
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 :
Pour arriver à ce résultat, nous apprenons continuellement à l’aide de revues régulières :
Notre but est d’optimiser les indicateur suivants :
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.