Durée : 2 jours (12h)
Niveau : Débutant-Intermédiaire — avoir terminé les TPs CLI Azure (Module 3 Stockage + Module 4 Réseau)
Prérequis : Git, VS Code, Azure CLI, compte Azure actif
💡 Lien avec le TP CLI : vous avez déjà créé un App Service, une Function App et un Container avec
az CLI. Dans ce TP, vous allez faire exactement la même chose, mais en décrivant ces ressources dans des fichiers Terraform. La différence : votre infrastructure devient du code versionné, reproductible, et déployable automatiquement.
Créez un repo privé sur votre GitHub nommé azure-infra-terraform, puis clonez-le :
git clone https://github.com/<votre-username>/azure-infra-terraform.git
cd azure-infra-terraform
git checkout -b feat/terraform-resourcesLe formateur met à disposition un dossier starter/ dans le repo de formation (tp-terraform). Copiez son contenu dans votre repo :
# Depuis le dossier tp-terraform du formateur
cp -r starter/.gitignore ../azure-infra-terraform/
cp -r starter/.github ../azure-infra-terraform/
cp -r starter/terraform ../azure-infra-terraform/
cd ../azure-infra-terraform
ls terraform/
# backend.tf main.tf modules/ outputs.tf providers.tf variables.tfℹ️ Le starter vous donne la structure et le boilerplate. Vous allez compléter les
# TODOau fil du TP.
macOS :
brew tap hashicorp/tap && brew install hashicorp/tap/terraformWindows :
winget install HashiCorp.TerraformLinux :
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraformterraform --version # Terraform v1.9.x| Extension | Utilité |
|---|---|
| HashiCorp Terraform | Coloration syntaxique, autocomplétion HCL |
| Azure Terraform | Intégration Azure |
az login
az account show --output table🎯 Pourquoi on fait ça ? Jusqu'ici vous avez créé des ressources Azure avec
az. C'est rapide pour tester, mais personne ne sait exactement ce qui existe, vous ne pouvez pas recréer l'environnement facilement, et les changements ne sont pas tracés dans Git. Terraform résout tout ça : l'infrastructure est décrite dans des fichiers texte versionnés, reproductibles et auditables.
# CLI — impératif : vous dites COMMENT faire
az webapp create --name "app-jean-cli" --plan "$APP_PLAN"
# Terraform — déclaratif : vous décrivez CE QUE vous voulez
resource "azurerm_linux_web_app" "app" {
name = "app-jean-tf"
service_plan_id = data.azurerm_service_plan.shared.id
}Les avantages clés :
- Reproductible : le même code crée exactement la même infrastructure
- Versionné : les changements d'infra sont tracés dans Git comme du code
- Idempotent : relancer
terraform applydeux fois ne crée pas deux fois la ressource
terraform init → télécharge les providers
terraform plan → compare état actuel vs code → affiche les différences
terraform apply → applique les changements
terraform destroy → supprime toutes les ressources
# resource : CRÉE une ressource Azure
resource "azurerm_storage_account" "sa" {
name = "stjeandupont"
resource_group_name = "rg-jean-dupont"
location = "francecentral"
}
# data : LIT une ressource existante (sans la créer)
data "azurerm_resource_group" "rg" {
name = "rg-jean-dupont"
}Ouvrez terraform/providers.tf dans VS Code et répondez :
- Quelle version du provider Azure est utilisée ?
- Que signifie
use_oidc = true? - À quoi sert le bloc
features {}?
Puis initialisez Terraform :
cd terraform/
terraform init💡 Correction
1. ~> 4.0 : version 4.x (compatible mineur, refuse 5.0).
2. use_oidc = true : Terraform s'authentifie à Azure via un token JWT signé par GitHub — pas de CLIENT_SECRET stocké en dur.
3. features {} : bloc obligatoire pour le provider Azure, même vide. Il permet de configurer des comportements avancés (soft delete, purge protection...).
Ouvrez terraform/variables.tf et terraform/main.tf. Repérez :
- Quelles variables sont obligatoires (sans valeur par défaut) ?
- Dans
main.tf, que font les deux blocsdatadéjà écrits ? - Que signifie
local.tagset d'où vient-il ?
💡 Correction
1. owner et resource_group_name sont sans default → Terraform les demandera interactivement.
2. Les deux data sources lisent sans créer :
data.azurerm_resource_group.rg: lit votre resource group (créé par le formateur)data.azurerm_service_plan.shared: lit le plan App Service partagé
3. locals dans main.tf construit local.tags en fusionnant des tags communs avec var.tags. Toutes les ressources utilisent tags = local.tags pour être taggées managed_by=terraform.
Créez terraform/terraform.tfvars (il est dans le .gitignore — ne jamais le commiter) :
owner = "prenom-nom" # votre identifiant
resource_group_name = "rg-prenom-nom" # votre resource groupterraform plan # doit afficher "0 to add" — les data sources ne créent rien💡 Correction
terraform plan avec seulement des data sources affiche :
No changes. Your infrastructure matches the configuration.
C'est attendu : un data source lit sans modifier. Vous verrez des changements dès que vous ajouterez des resource.
🎯 Pourquoi des modules ? En Terraform professionnel, on ne met jamais des dizaines de
resourcedans un seulmain.tf. On organise le code en modules — des dossiers réutilisables, chacun responsable d'une partie de l'infrastructure. Dans votre starter, chaque ressource Azure a son module :modules/storage/,modules/app-service/, etc.
modules/storage/
├── variables.tf ← paramètres d'entrée (owner, location, tags...)
├── main.tf ← les ressources du module (storage account, containers...)
└── outputs.tf ← valeurs exposées après création (nom, ID...)
Appeler un module depuis main.tf :
module "storage" {
source = "./modules/storage"
owner = var.owner # passage des variables
resource_group_name = data.azurerm_resource_group.rg.name
location = var.location
tags = local.tags
}Ouvrez le fichier. Quelles variables le module attend-il ? Pourquoi ne sont-elles pas déclarées dans le variables.tf racine ?
💡 Correction
Le module attend : owner, resource_group_name, location, tags.
Ces variables sont locales au module — elles ne remontent pas au variables.tf racine. Quand main.tf appelle le module, il passe les valeurs depuis le contexte racine (var.owner, data.azurerm_resource_group.rg.name...). C'est le principe d'encapsulation : chaque module déclare ce dont il a besoin.
Ouvrez modules/storage/main.tf. Vous y trouvez 3 TODO. Complétez-les :
TODO 1 — Storage Account métier :
- Nom :
"st${replace(var.owner, "-", "")}tf" - Tier : Standard, LRS, StorageV2, TLS 1.2
allow_nested_items_to_be_public = true(nécessaire pour api-config)
TODO 2 — Conteneur privé api-logs (container_access_type = "private")
TODO 3 — Conteneur public api-config (container_access_type = "blob")
Documentation : azurerm_storage_account
💡 Correction
resource "azurerm_storage_account" "sa" {
name = "st${replace(var.owner, "-", "")}tf"
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = true
tags = var.tags
}
resource "azurerm_storage_container" "api_logs" {
name = "api-logs"
storage_account_id = azurerm_storage_account.sa.id
container_access_type = "private"
}
resource "azurerm_storage_container" "api_config" {
name = "api-config"
storage_account_id = azurerm_storage_account.sa.id
container_access_type = "blob"
}Pourquoi storage_account_id et pas storage_account_name ?
Le provider Azure 4.x préfère les IDs (plus stables que les noms). azurerm_storage_account.sa.id référence l'objet créé juste au-dessus — Terraform résout automatiquement la dépendance.
Exposez le nom du Storage Account pour pouvoir l'afficher dans les outputs racine.
💡 Correction
output "storage_account_name" {
value = azurerm_storage_account.sa.name
}Dans terraform/main.tf, décommentez le bloc module "storage" et remplacez les ??? par les bonnes valeurs.
terraform fmt
terraform validate
terraform plan # doit afficher "3 to add" (SA + 2 containers)💡 Correction
module "storage" {
source = "./modules/storage"
owner = var.owner
resource_group_name = data.azurerm_resource_group.rg.name
location = var.location
tags = local.tags
}Après terraform init (obligatoire quand on ajoute un module) :
terraform init # enregistre le nouveau module
terraform plan # 3 to add : storage account + api-logs + api-config
terraform apply🎯 Pourquoi on fait ça ? Vous avez déjà créé ces trois ressources en CLI. Ici, vous écrivez les mêmes ressources en HCL dans leurs modules respectifs. La logique est identique — seule la syntaxe change.
Complétez le module App Service. Le plan partagé est passé via var.service_plan_id.
Contraintes : Python 3.11, HTTPS only, TLS 1.2.
Documentation : azurerm_linux_web_app
💡 Correction
resource "azurerm_linux_web_app" "app" {
name = "app-${var.owner}-tf"
resource_group_name = var.resource_group_name
location = data.azurerm_service_plan.plan.location
service_plan_id = var.service_plan_id
https_only = true
site_config {
minimum_tls_version = "1.2"
application_stack {
python_version = "3.11"
}
}
tags = var.tags
}
# Data source pour récupérer la location du plan depuis son ID
data "azurerm_service_plan" "plan" {
name = split("/", var.service_plan_id)[8]
resource_group_name = split("/", var.service_plan_id)[4]
}Dans outputs.tf :
output "default_hostname" {
value = azurerm_linux_web_app.app.default_hostname
}La Function App a besoin d'un storage account dédié (obligatoire Azure, séparé du storage métier).
Créez les deux ressources : azurerm_storage_account (nommé stfn${replace(var.owner, "-", "")}) puis azurerm_linux_function_app.
Documentation : azurerm_linux_function_app
💡 Correction
resource "azurerm_storage_account" "fn_storage" {
name = "stfn${replace(var.owner, "-", "")}"
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
tags = merge(var.tags, { purpose = "function-storage" })
}
resource "azurerm_linux_function_app" "fn" {
name = "fn-${var.owner}-tf"
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = var.service_plan_id
storage_account_name = azurerm_storage_account.fn_storage.name
storage_account_access_key = azurerm_storage_account.fn_storage.primary_access_key
https_only = true
site_config {
application_stack {
python_version = "3.11"
}
}
tags = var.tags
}Dans outputs.tf :
output "default_hostname" {
value = azurerm_linux_function_app.fn.default_hostname
}Correspondance CLI → Terraform :
az functionapp create |
azurerm_linux_function_app |
|---|---|
--storage-account |
storage_account_name |
--plan |
service_plan_id |
--runtime python --runtime-version 3.11 |
application_stack { python_version = "3.11" } |
Déployez un container nginx:latest accessible publiquement via un FQDN Azure.
Documentation : azurerm_container_group
💡 Correction
resource "azurerm_container_group" "aci" {
name = "aci-${var.owner}-tf"
resource_group_name = var.resource_group_name
location = var.location
ip_address_type = "Public"
dns_name_label = "aci-${var.owner}-tf"
os_type = "Linux"
container {
name = "nginx"
image = "nginx:latest"
cpu = 0.5
memory = 0.5
ports {
port = 80
protocol = "TCP"
}
}
tags = var.tags
}Dans outputs.tf :
output "fqdn" {
value = azurerm_container_group.aci.fqdn
}Décommentez les blocs module "app_service", module "function_app" et module "container" dans terraform/main.tf. Remplacez les ???.
terraform init # re-init pour enregistrer les nouveaux modules
terraform plan # combien de ressources vont être créées ?💡 Correction
module "app_service" {
source = "./modules/app-service"
owner = var.owner
resource_group_name = data.azurerm_resource_group.rg.name
service_plan_id = data.azurerm_service_plan.shared.id
tags = local.tags
}
module "function_app" {
source = "./modules/function-app"
owner = var.owner
resource_group_name = data.azurerm_resource_group.rg.name
location = var.location
service_plan_id = data.azurerm_service_plan.shared.id
tags = local.tags
}
module "container" {
source = "./modules/container"
owner = var.owner
resource_group_name = data.azurerm_resource_group.rg.name
location = var.location
tags = local.tags
}Le plan doit afficher 6 to add : App Service + Function App + son storage + Container + le data source du plan.
🎯 Pourquoi on fait ça ? Les outputs sont l'équivalent des
echoà la fin deprovision.sh. Ils exposent les URLs et FQDNs après leapply— sans avoir à fouiller le portail Azure.
Dans terraform/outputs.tf, décommentez et complétez les 4 outputs en utilisant module.<nom>.<output>.
💡 Correction
output "app_service_url" {
description = "URL de l'App Service"
value = "https://${module.app_service.default_hostname}"
}
output "function_app_url" {
description = "URL de la Function App"
value = "https://${module.function_app.default_hostname}"
}
output "container_fqdn" {
description = "FQDN du container nginx"
value = "http://${module.container.fqdn}"
}
output "storage_account_name" {
description = "Nom du Storage Account"
value = module.storage.storage_account_name
}terraform fmt
terraform validate
terraform plan # vérifiez le récapitulatif avant d'appliquer
terraform applyAprès l'apply, vous devez voir les 4 outputs avec vos URLs. Testez l'URL du container dans un navigateur.
Ouvrez terraform.tfstate et répondez :
- Que contient ce fichier ?
- Que se passe-t-il si vous le supprimez et relancez
terraform plan? - Pourquoi ne faut-il jamais le commiter ? (regardez s'il contient des valeurs sensibles)
💡 Correction
1. terraform.tfstate est un JSON avec l'état exact de toutes les ressources gérées : ID Azure, attributs, dépendances. C'est la "mémoire" de Terraform.
2. Terraform croit que rien n'existe → affiche 6 to add → tente de recréer → erreur Azure "resource already exists".
3. Le state peut contenir des clés d'accès en clair (comme primary_access_key du storage de la Function App). Solution : le remote state dans Azure Blob Storage (Étape 6).
Changez l'image du container de nginx:latest à nginx:1.25 dans modules/container/main.tf.
- Lancez
terraform plan. Est-ce un update ou une recréation (~ou-/+) ? - Lancez
terraform destroy.
💡 Correction
-/+ azurerm_container_group.aci must be replaced
~ container[0].image = "nginx:latest" → "nginx:1.25"
Un changement d'image ACI est un destroy + recreate (-/+) — Azure ne peut pas modifier l'image d'un container en cours d'exécution.
Les symboles du plan :
+create,-destroy,~update in-place,-/+destroy and recreate
🎯 Recréez l'infrastructure complète et committez sur votre branche.
terraform apply # recréer après le destroy
terraform fmt
terraform validateVérifiez que vous avez bien les 4 outputs avec vos URLs.
Committez :
git add terraform/
git commit -m "feat(terraform): infrastructure AzureTech — Storage + App Service + Function App + Container"
git push origin feat/terraform-resourcesOuvrez une Pull Request sur votre repo azure-infra-terraform. Le workflow .github/workflows/terraform.yml déclenchera un terraform plan automatique commenté sur la PR.
🎯 Pourquoi on fait ça ? Le state local pose un problème fondamental : si vous travaillez depuis deux machines, vos states divergent. Le remote state stocke le fichier dans Azure Blob Storage — partagé, sécurisé, avec verrouillage automatique.
State local (problématique) State remote (bonne pratique)
┌─────────────────────────┐ ┌─────────────────────────┐
│ terraform.tfstate │ │ Azure Blob Storage │
│ sur votre machine │ ──────▶ │ jean.tfstate │
│ ❌ perdu si crash │ │ ✅ partagé + verrouillé│
└─────────────────────────┘ └─────────────────────────┘
export OWNER="prenom-nom"
export RG_BACKEND="rg-tfstate-${OWNER}"
export SA_BACKEND="ststate${OWNER//-/}"
az group create --name "$RG_BACKEND" --location "francecentral"
az storage account create \
--name "$SA_BACKEND" \
--resource-group "$RG_BACKEND" \
--location "francecentral" \
--sku Standard_LRS
az storage container create \
--name "tfstate" \
--account-name "$SA_BACKEND"terraform/backend.tf est déjà préconfiguré pour recevoir les valeurs via -backend-config. Initialisez avec migration du state local :
terraform init \
-backend-config="resource_group_name=rg-tfstate-${OWNER}" \
-backend-config="storage_account_name=ststate${OWNER//-/}" \
-backend-config="container_name=tfstate" \
-backend-config="key=${OWNER}.terraform.tfstate" \
-migrate-state💡 Correction
Points clés :
key: nom du blob — chaque étudiant a unkeydifférent → pas de conflit-migrate-state: déplace le state local vers Azure Blob Storage- Après migration, supprimez
terraform.tfstatelocal - Verrouillage : un
applysimultané depuis une autre machine échoue avec "state locked"
Vérifier que le state est en remote :
az storage blob list \
--container-name "tfstate" \
--account-name "ststate${OWNER//-/}" \
--output table🎯 Pourquoi on fait ça ? Vous avez créé VNet, subnets et NSG manuellement en CLI dans le TP Module 4. Ici, vous les décrivez dans
modules/network/main.tf. L'avantage Terraform : si vous avez besoin du même réseau en staging et en production, vous appelez deux fois le même module avec des paramètres différents.
Complétez les 4 TODO du module network :
- VNet
vnet-${var.owner}-tfavec espace10.0.0.0/16 subnet-frontend(10.0.1.0/24) etsubnet-backend(10.0.2.0/24)- NSG avec les 3 règles : Allow-HTTP (100), Allow-HTTPS (110), Deny-All-Inbound (4000)
- Association NSG → subnet-frontend
Documentation : azurerm_virtual_network, azurerm_network_security_group
💡 Correction
resource "azurerm_virtual_network" "vnet" {
name = "vnet-${var.owner}-tf"
resource_group_name = var.resource_group_name
location = var.location
address_space = ["10.0.0.0/16"]
tags = var.tags
}
resource "azurerm_subnet" "frontend" {
name = "subnet-frontend"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_subnet" "backend" {
name = "subnet-backend"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.2.0/24"]
}
resource "azurerm_network_security_group" "nsg" {
name = "nsg-frontend-${var.owner}-tf"
resource_group_name = var.resource_group_name
location = var.location
tags = var.tags
security_rule {
name = "Allow-HTTP"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "Allow-HTTPS"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "Deny-All-Inbound"
priority = 4000
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_subnet_network_security_group_association" "frontend_nsg" {
subnet_id = azurerm_subnet.frontend.id
network_security_group_id = azurerm_network_security_group.nsg.id
}Dans outputs.tf :
output "vnet_id" {
value = azurerm_virtual_network.vnet.id
}
output "subnet_frontend_id" {
value = azurerm_subnet.frontend.id
}Différence CLI vs Terraform pour le NSG :
En CLI, vous deviez désassocier le NSG du subnet avant de le supprimer. En Terraform, la ressource azurerm_subnet_network_security_group_association gère cela automatiquement — terraform destroy supprime l'association avant le NSG.
Décommentez module "network" dans main.tf et appliquez :
terraform init
terraform plan # combien de ressources réseau ?
terraform apply💡 Correction
Le plan doit afficher 5 to add : VNet, 2 subnets, NSG, association NSG-subnet.
En CLI, vous avez créé ces ressources avec 7 commandes az. En Terraform : un terraform apply.
🎯 Pourquoi on fait ça ? Le workflow
.github/workflows/terraform.ymlest déjà dans votre starter. Il fait unplansur chaque PR et unapplyautomatique après le merge — sans jamais stocker de mot de passe.
Classique (à éviter) OIDC (recommandé)
ARM_CLIENT_SECRET = mot de passe → Token JWT GitHub (5 min, lié à votre repo)
❌ à stocker dans les secrets ✅ pas de secret
❌ à renouveler tous les 90 jours ✅ généré à chaque run
Si vous avez déjà un Service Principal depuis le TP CLI, ajoutez juste une federated credential supplémentaire :
APP_ID=$(az ad sp list --display-name "sp-github-<votre-username>" \
--query "[0].appId" -o tsv)
az ad app federated-credential create \
--id "$APP_ID" \
--parameters "{
\"name\": \"github-azure-infra-terraform\",
\"issuer\": \"https://token.actions.githubusercontent.com\",
\"subject\": \"repo:<votre-username>/azure-infra-terraform:ref:refs/heads/main\",
\"audiences\": [\"api://AzureADTokenExchange\"]
}"Secrets GitHub (Settings → Secrets and variables → Actions de votre repo) :
| Secret | Valeur |
|---|---|
AZURE_CLIENT_ID |
App ID du Service Principal |
AZURE_TENANT_ID |
Tenant ID Azure AD |
AZURE_SUBSCRIPTION_ID |
ID de votre subscription |
AZURE_OWNER |
votre prenom-nom |
TF_BACKEND_RG |
rg-tfstate-prenom-nom |
TF_BACKEND_SA |
ststateprenomnom |
ℹ️ Pas de
AZURE_CLIENT_SECRET— c'est tout l'intérêt de l'OIDC.
💡 Vérifier la federated credential
az ad app federated-credential list --id "$APP_ID" \
--query "[].{Nom:name, Subject:subject}" \
--output tableVous devez voir deux entrées : une pour azure-infra-cli et une pour azure-infra-terraform.
Ouvrez le workflow et répondez :
- Quand se déclenche-t-il en mode
plan? - Quand passe-t-il en mode
apply? - Comment le plan est-il partagé avec les reviewers ?
- Comment
destroyest-il déclenché ?
💡 Correction
1. Sur chaque Pull Request ciblant main qui touche terraform/**
2. Sur chaque push sur main (après merge de PR)
3. Le workflow commente la PR avec le texte du terraform plan via actions/github-script. Les reviewers voient exactement ce qui va changer avant d'approuver.
4. Uniquement via workflow_dispatch avec action: destroy — jamais automatiquement. Protection contre les destructions accidentelles.
Modifiez l'image du container, poussez, ouvrez une PR et observez le commentaire automatique.
- Changez
nginx:latestennginx:1.25dansmodules/container/main.tf - Committez et poussez
- Ouvrez une PR vers
main - Le plan doit commenter
-/+sur le container (destroy + recreate) - Mergez — l'apply se déclenche automatiquement
🎯 Pourquoi on fait ça ?
tflintva plus loin queterraform validate: il détecte les variables non utilisées, les types incorrects pour Azure, les conventions de nommage. Associé à un hook pre-commit, il bloque le commit si le code ne passe pas.
brew install tflint # macOS
# Linux : curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
pip install pre-commit --break-system-packagesCréez terraform/.tflint.hcl :
plugin "azurerm" {
enabled = true
version = "0.26.0"
source = "github.com/terraform-linters/tflint-ruleset-azurerm"
}
rule "terraform_naming_convention" { enabled = true }
rule "terraform_unused_declarations" { enabled = true }
rule "terraform_documented_variables" { enabled = true }
rule "terraform_documented_outputs" { enabled = true }tflint --init
tflint --chdir=terraform/pre-commit install
git add .
git commit -m "test: hooks tflint"
# → Le hook vérifie le format avant chaque committerraform fmt
terraform validate
tflint --chdir=terraform/
git add terraform/
git commit -m "feat(terraform): infrastructure AzureTech complète — réseau + remote state + CI/CD"
git push origin feat/terraform-resourcesVérifiez sur la PR que :
- Le
terraform planCI est vert - Le plan affiche exactement les ressources attendues
- Pas de
*.tfstateni.terraform/commité - Les 4 outputs (URLs) sont visibles dans la CI
En tant que reviewer, vérifiez :
-
variables.tfdes modules : toutes les variables ont unedescription -
outputs.tf: URLs App Service, Function App, Container, Storage exposées - Tag
managed_by = "terraform"sur toutes les ressources - Pas de
ARM_CLIENT_SECRETdans le workflow - Remote state configuré (pas de
terraform.tfstatedans le repo)
terraform workspace new staging
terraform workspace select staging
terraform plan # state isolé par workspaceresource "azurerm_linux_web_app" "app" {
name = "app-${var.owner}-${terraform.workspace}-tf"
}| Critère | Points |
|---|---|
modules/storage/main.tf complété (SA + 2 containers) |
2 |
modules/app-service/main.tf complété (Python 3.11, HTTPS) |
2 |
modules/function-app/main.tf complété (storage dédié) |
2 |
modules/container/main.tf complété (nginx, IP publique) |
2 |
modules/network/main.tf complété (VNet + NSG + association) |
2 |
Modules activés dans main.tf avec les bons paramètres |
2 |
| Outputs racine (4 URLs) | 1 |
| Remote state configuré dans Azure Blob Storage | 2 |
PR avec terraform plan commenté automatiquement (OIDC) |
2 |
Pas de secret dans le code (ARM_CLIENT_SECRET absent) |
1 |
| PR mergée après revue croisée | 1 |
| BONUS — Workspaces staging/production | 1 |
💶 Pensez à détruire vos ressources à la fin du TP :
terraform destroy # Le Resource Group est conservé — seules les ressources managed_by=terraform sont supprimées
Formation DevSecOps Azure — Simplon