From 96235ec6db077c0254b86794520daab21d323dba Mon Sep 17 00:00:00 2001 From: hoaraualderic Date: Thu, 2 Jul 2026 15:45:30 +0200 Subject: [PATCH] Init TP Terraform --- .gitignore | 4 +- README.md | 1109 ++++++++++++++++- starter/terraform/backend.tf | 11 + starter/terraform/main.tf | 99 ++ starter/terraform/modules/app-service/main.tf | 40 + .../terraform/modules/app-service/outputs.tf | 6 + .../modules/app-service/variables.tf | 4 + starter/terraform/modules/container/main.tf | 44 + .../terraform/modules/container/outputs.tf | 5 + .../terraform/modules/container/variables.tf | 4 + .../terraform/modules/function-app/main.tf | 58 + .../terraform/modules/function-app/outputs.tf | 5 + .../modules/function-app/variables.tf | 5 + starter/terraform/modules/network/main.tf | 72 ++ starter/terraform/modules/network/outputs.tf | 13 + .../terraform/modules/network/variables.tf | 4 + starter/terraform/modules/storage/main.tf | 51 + starter/terraform/modules/storage/outputs.tf | 5 + .../terraform/modules/storage/variables.tf | 4 + starter/terraform/outputs.tf | 30 + starter/terraform/providers.tf | 22 + starter/terraform/terraform.tfvars.example | 20 + starter/terraform/variables.tf | 38 + 23 files changed, 1650 insertions(+), 3 deletions(-) create mode 100644 starter/terraform/backend.tf create mode 100644 starter/terraform/main.tf create mode 100644 starter/terraform/modules/app-service/main.tf create mode 100644 starter/terraform/modules/app-service/outputs.tf create mode 100644 starter/terraform/modules/app-service/variables.tf create mode 100644 starter/terraform/modules/container/main.tf create mode 100644 starter/terraform/modules/container/outputs.tf create mode 100644 starter/terraform/modules/container/variables.tf create mode 100644 starter/terraform/modules/function-app/main.tf create mode 100644 starter/terraform/modules/function-app/outputs.tf create mode 100644 starter/terraform/modules/function-app/variables.tf create mode 100644 starter/terraform/modules/network/main.tf create mode 100644 starter/terraform/modules/network/outputs.tf create mode 100644 starter/terraform/modules/network/variables.tf create mode 100644 starter/terraform/modules/storage/main.tf create mode 100644 starter/terraform/modules/storage/outputs.tf create mode 100644 starter/terraform/modules/storage/variables.tf create mode 100644 starter/terraform/outputs.tf create mode 100644 starter/terraform/providers.tf create mode 100644 starter/terraform/terraform.tfvars.example create mode 100644 starter/terraform/variables.tf diff --git a/.gitignore b/.gitignore index 78e7733..a73e082 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ terraform.rc # Optional: ignore plan files saved before destroying Terraform configuration # Uncomment the line below if you want to ignore planout files. -# planout \ No newline at end of file +# planout + +*.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index fc18f8f..b334452 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,1107 @@ -# tp-terraform -TP terraform pour SIMPLON +# TP Terraform — Infrastructure as Code sur Azure + +**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. + +--- + +## Mise en place + +### A — Préparer votre repo `azure-infra-terraform` + +Créez un repo **privé** sur votre GitHub nommé `azure-infra-terraform`, puis clonez-le : + +```bash +git clone https://github.com//azure-infra-terraform.git +cd azure-infra-terraform +git checkout -b feat/terraform-resources +``` + +### B — Copier le starter dans votre repo + +Le formateur met à disposition un dossier `starter/` dans le repo de formation (`tp-terraform`). Copiez son contenu dans votre repo : + +```bash +# 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 `# TODO` au fil du TP. + +### C — Installer Terraform + +**macOS :** +```bash +brew tap hashicorp/tap && brew install hashicorp/tap/terraform +``` + +**Windows :** +```powershell +winget install HashiCorp.Terraform +``` + +**Linux :** +```bash +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 terraform +``` + +```bash +terraform --version # Terraform v1.9.x +``` + +### D — Extensions VS Code + +| Extension | Utilité | +|-----------|---------| +| **HashiCorp Terraform** | Coloration syntaxique, autocomplétion HCL | +| **Azure Terraform** | Intégration Azure | + +### E — Se connecter à Azure + +```bash +az login +az account show --output table +``` + +--- + +## Jour 1 — Les fondamentaux Terraform (6h) + +--- + +## Étape 1 — Comprendre l'Infrastructure as Code (45 min) + +> 🎯 **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. + +### Concept — CLI vs Terraform + +```bash +# 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 apply` deux fois ne crée pas deux fois la ressource + +### Le cycle de vie + +``` +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 +``` + +### La syntaxe HCL + +```hcl +# 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" +} +``` + +--- + +### Exercice 1.1 — Explorer le starter + +Ouvrez `terraform/providers.tf` dans VS Code et répondez : + +1. Quelle version du provider Azure est utilisée ? +2. Que signifie `use_oidc = true` ? +3. À quoi sert le bloc `features {}` ? + +Puis initialisez Terraform : + +```bash +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...). + +
+ +--- + +### Exercice 1.2 — Explorer `variables.tf` et `main.tf` + +Ouvrez `terraform/variables.tf` et `terraform/main.tf`. Repérez : + +1. Quelles variables sont obligatoires (sans valeur par défaut) ? +2. Dans `main.tf`, que font les deux blocs `data` déjà écrits ? +3. Que signifie `local.tags` et 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`. + +
+ +--- + +### Exercice 1.3 — Créer `terraform.tfvars` + +Créez `terraform/terraform.tfvars` (il est dans le `.gitignore` — ne jamais le commiter) : + +```hcl +owner = "prenom-nom" # votre identifiant +resource_group_name = "rg-prenom-nom" # votre resource group +``` + +```bash +terraform 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`. + +
+ +--- + +## Étape 2 — Votre premier module : Storage (1h) + +> 🎯 **Pourquoi des modules ?** +> En Terraform professionnel, on ne met jamais des dizaines de `resource` dans un seul `main.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. + +### Concept — Anatomie d'un module + +``` +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` :** + +```hcl +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 +} +``` + +--- + +### Exercice 2.1 — Lire `modules/storage/variables.tf` + +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. + +
+ +--- + +### Exercice 2.2 — Remplir `modules/storage/main.tf` + +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`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) + +
+💡 Correction + +```hcl +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. + +
+ +--- + +### Exercice 2.3 — Remplir `modules/storage/outputs.tf` + +Exposez le nom du Storage Account pour pouvoir l'afficher dans les outputs racine. + +
+💡 Correction + +```hcl +output "storage_account_name" { + value = azurerm_storage_account.sa.name +} +``` + +
+ +--- + +### Exercice 2.4 — Activer le module dans `main.tf` + +Dans `terraform/main.tf`, décommentez le bloc `module "storage"` et remplacez les `???` par les bonnes valeurs. + +```bash +terraform fmt +terraform validate +terraform plan # doit afficher "3 to add" (SA + 2 containers) +``` + +
+💡 Correction + +```hcl +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) : +```bash +terraform init # enregistre le nouveau module +terraform plan # 3 to add : storage account + api-logs + api-config +terraform apply +``` + +
+ +--- + +## Étape 3 — App Service, Function App, Container (1h30) + +> 🎯 **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. + +--- + +### Exercice 3.1 — `modules/app-service/main.tf` + +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`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app) + +
+💡 Correction + +```hcl +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` :** +```hcl +output "default_hostname" { + value = azurerm_linux_web_app.app.default_hostname +} +``` + +
+ +--- + +### Exercice 3.2 — `modules/function-app/main.tf` + +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`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_function_app) + +
+💡 Correction + +```hcl +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` :** +```hcl +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" }` | + +
+ +--- + +### Exercice 3.3 — `modules/container/main.tf` + +Déployez un container `nginx:latest` accessible publiquement via un FQDN Azure. + +Documentation : [`azurerm_container_group`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_group) + +
+💡 Correction + +```hcl +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` :** +```hcl +output "fqdn" { + value = azurerm_container_group.aci.fqdn +} +``` + +
+ +--- + +### Exercice 3.4 — Activer les 3 modules dans `main.tf` + +Décommentez les blocs `module "app_service"`, `module "function_app"` et `module "container"` dans `terraform/main.tf`. Remplacez les `???`. + +```bash +terraform init # re-init pour enregistrer les nouveaux modules +terraform plan # combien de ressources vont être créées ? +``` + +
+💡 Correction + +```hcl +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. + +
+ +--- + +## Étape 4 — Outputs et premier apply complet (45 min) + +> 🎯 **Pourquoi on fait ça ?** +> Les outputs sont l'équivalent des `echo` à la fin de `provision.sh`. Ils exposent les URLs et FQDNs après le `apply` — sans avoir à fouiller le portail Azure. + +### Exercice 4.1 — Remplir `outputs.tf` + +Dans `terraform/outputs.tf`, décommentez et complétez les 4 outputs en utilisant `module..`. + +
+💡 Correction + +```hcl +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 +} +``` + +
+ +--- + +### Exercice 4.2 — Plan, apply, vérification + +```bash +terraform fmt +terraform validate +terraform plan # vérifiez le récapitulatif avant d'appliquer +terraform apply +``` + +Après l'apply, vous devez voir les 4 outputs avec vos URLs. Testez l'URL du container dans un navigateur. + +--- + +### Exercice 4.3 — Observer le state + +Ouvrez `terraform.tfstate` et répondez : + +1. Que contient ce fichier ? +2. Que se passe-t-il si vous le supprimez et relancez `terraform plan` ? +3. 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). + +
+ +--- + +### Exercice 4.4 — Modifier et détruire + +Changez l'image du container de `nginx:latest` à `nginx:1.25` dans `modules/container/main.tf`. + +1. Lancez `terraform plan`. Est-ce un update ou une recréation (`~` ou `-/+`) ? +2. 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 + +
+ +--- + +## Étape 5 — Exercice fil rouge Jour 1 (45 min) + +> 🎯 Recréez l'infrastructure complète et committez sur votre branche. + +```bash +terraform apply # recréer après le destroy + +terraform fmt +terraform validate +``` + +Vérifiez que vous avez bien les 4 outputs avec vos URLs. + +**Committez :** + +```bash +git add terraform/ +git commit -m "feat(terraform): infrastructure AzureTech — Storage + App Service + Function App + Container" +git push origin feat/terraform-resources +``` + +Ouvrez 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. + +--- + +## Jour 2 — Terraform en équipe (6h) + +--- + +## Étape 6 — Remote State dans Azure Blob Storage (1h30) + +> 🎯 **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. + +### Concept + +``` +State local (problématique) State remote (bonne pratique) +┌─────────────────────────┐ ┌─────────────────────────┐ +│ terraform.tfstate │ │ Azure Blob Storage │ +│ sur votre machine │ ──────▶ │ jean.tfstate │ +│ ❌ perdu si crash │ │ ✅ partagé + verrouillé│ +└─────────────────────────┘ └─────────────────────────┘ +``` + +--- + +### Exercice 6.1 — Créer le storage pour le remote state + +```bash +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" +``` + +--- + +### Exercice 6.2 — Configurer le backend + +`terraform/backend.tf` est déjà préconfiguré pour recevoir les valeurs via `-backend-config`. Initialisez avec migration du state local : + +```bash +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 un `key` différent → pas de conflit +- `-migrate-state` : déplace le state local vers Azure Blob Storage +- Après migration, supprimez `terraform.tfstate` local +- **Verrouillage** : un `apply` simultané depuis une autre machine échoue avec "state locked" + +**Vérifier que le state est en remote :** +```bash +az storage blob list \ + --container-name "tfstate" \ + --account-name "ststate${OWNER//-/}" \ + --output table +``` + +
+ +--- + +## Étape 7 — Réseau avec le module network (1h) + +> 🎯 **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. + +### Exercice 7.1 — Remplir `modules/network/main.tf` + +Complétez les 4 TODO du module network : + +1. VNet `vnet-${var.owner}-tf` avec espace `10.0.0.0/16` +2. `subnet-frontend` (10.0.1.0/24) et `subnet-backend` (10.0.2.0/24) +3. NSG avec les 3 règles : Allow-HTTP (100), Allow-HTTPS (110), Deny-All-Inbound (4000) +4. Association NSG → subnet-frontend + +Documentation : [`azurerm_virtual_network`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network), [`azurerm_network_security_group`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group) + +
+💡 Correction + +```hcl +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` :** +```hcl +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. + +
+ +--- + +### Exercice 7.2 — Activer le module network + +Décommentez `module "network"` dans `main.tf` et appliquez : + +```bash +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`. + +
+ +--- + +## Étape 8 — CI/CD avec GitHub Actions + OIDC (1h30) + +> 🎯 **Pourquoi on fait ça ?** +> Le workflow `.github/workflows/terraform.yml` est déjà dans votre starter. Il fait un `plan` sur chaque PR et un `apply` automatique après le merge — sans jamais stocker de mot de passe. + +### Concept — OIDC + +``` +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 +``` + +--- + +### Exercice 8.1 — Configurer OIDC + +Si vous avez déjà un Service Principal depuis le TP CLI, ajoutez juste une federated credential supplémentaire : + +```bash +APP_ID=$(az ad sp list --display-name "sp-github-" \ + --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:/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 + +```bash +az ad app federated-credential list --id "$APP_ID" \ + --query "[].{Nom:name, Subject:subject}" \ + --output table +``` + +Vous devez voir deux entrées : une pour `azure-infra-cli` et une pour `azure-infra-terraform`. + +
+ +--- + +### Exercice 8.2 — Explorer `.github/workflows/terraform.yml` + +Ouvrez le workflow et répondez : + +1. Quand se déclenche-t-il en mode `plan` ? +2. Quand passe-t-il en mode `apply` ? +3. Comment le plan est-il partagé avec les reviewers ? +4. Comment `destroy` est-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. + +
+ +--- + +### Exercice 8.3 — Déclencher un plan via Pull Request + +Modifiez l'image du container, poussez, ouvrez une PR et observez le commentaire automatique. + +1. Changez `nginx:latest` en `nginx:1.25` dans `modules/container/main.tf` +2. Committez et poussez +3. Ouvrez une PR vers `main` +4. Le plan doit commenter `-/+` sur le container (destroy + recreate) +5. Mergez — l'apply se déclenche automatiquement + +--- + +## Étape 9 — tflint et pre-commit (45 min) + +> 🎯 **Pourquoi on fait ça ?** +> `tflint` va plus loin que `terraform 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. + +### Installation + +```bash +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-packages +``` + +### Exercice 9.1 — Configurer tflint + +Créez `terraform/.tflint.hcl` : + +```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 } +``` + +```bash +tflint --init +tflint --chdir=terraform/ +``` + +### Exercice 9.2 — Hook pre-commit + +```bash +pre-commit install +git add . +git commit -m "test: hooks tflint" +# → Le hook vérifie le format avant chaque commit +``` + +--- + +## Étape 10 — Exercice fil rouge final (45 min) + +### Exercice 10.1 — PR finale + +```bash +terraform 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-resources +``` + +Vérifiez sur la PR que : +- [ ] Le `terraform plan` CI est vert +- [ ] Le plan affiche exactement les ressources attendues +- [ ] Pas de `*.tfstate` ni `.terraform/` commité +- [ ] Les 4 outputs (URLs) sont visibles dans la CI + +### Exercice 10.2 — Revue croisée + +En tant que reviewer, vérifiez : +- [ ] `variables.tf` des modules : toutes les variables ont une `description` +- [ ] `outputs.tf` : URLs App Service, Function App, Container, Storage exposées +- [ ] Tag `managed_by = "terraform"` sur toutes les ressources +- [ ] Pas de `ARM_CLIENT_SECRET` dans le workflow +- [ ] Remote state configuré (pas de `terraform.tfstate` dans le repo) + +--- + +## BONUS — Workspaces Terraform + +```bash +terraform workspace new staging +terraform workspace select staging +terraform plan # state isolé par workspace +``` + +```hcl +resource "azurerm_linux_web_app" "app" { + name = "app-${var.owner}-${terraform.workspace}-tf" +} +``` + +--- + +## Grille d'évaluation (20 pts) + +| 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 :** +> ```bash +> terraform destroy +> # Le Resource Group est conservé — seules les ressources managed_by=terraform sont supprimées +> ``` + +--- + +*Formation DevSecOps Azure — Simplon* diff --git a/starter/terraform/backend.tf b/starter/terraform/backend.tf new file mode 100644 index 0000000..f23ef51 --- /dev/null +++ b/starter/terraform/backend.tf @@ -0,0 +1,11 @@ +# Remote state stored in Azure Blob Storage +# Backend config values are injected at runtime via -backend-config in CI +# or via a backend.hcl file locally (never commit secrets here) +terraform { + backend "azurerm" { + # resource_group_name → injected via -backend-config or TF_BACKEND_RG secret + # storage_account_name → injected via -backend-config or TF_BACKEND_SA secret + # container_name = "tfstate" + # key = ".terraform.tfstate" + } +} diff --git a/starter/terraform/main.tf b/starter/terraform/main.tf new file mode 100644 index 0000000..85dd86b --- /dev/null +++ b/starter/terraform/main.tf @@ -0,0 +1,99 @@ +# ────────────────────────────────────────────────────────────────────────────── +# main.tf — Ressources Azure à provisionner avec Terraform +# +# Ce fichier est votre point d'entrée. Complétez les TODO au fil du TP. +# ────────────────────────────────────────────────────────────────────────────── + +# ── Tags communs à toutes les ressources ────────────────────────────────────── +# Ces tags sont mergés automatiquement dans chaque module via var.tags + +locals { + tags = merge( + { + managed_by = "terraform" + environment = "tp" + owner = var.owner + }, + var.tags + ) +} + +# ── Data sources ────────────────────────────────────────────────────────────── +# Un data source LIT une ressource existante sans la créer. + +# Resource Group pré-créé par le formateur — ne jamais le gérer en Terraform +data "azurerm_resource_group" "rg" { + name = var.resource_group_name +} + +# Plan App Service partagé (dans un Resource Group séparé) +data "azurerm_service_plan" "shared" { + name = var.shared_plan_name + resource_group_name = var.shared_rg_name +} + +# ── Storage (Étape 2) ───────────────────────────────────────────────────────── +# TODO : appeler le module "./modules/storage" +# Paramètres à passer : owner, resource_group_name, location, tags + +# module "storage" { +# source = "./modules/storage" +# +# owner = ??? +# resource_group_name = ??? +# location = ??? +# tags = ??? +# } + +# ── App Service (Étape 3) ───────────────────────────────────────────────────── +# TODO : appeler le module "./modules/app-service" +# Paramètres à passer : owner, resource_group_name, service_plan_id, tags + +# module "app_service" { +# source = "./modules/app-service" +# +# owner = ??? +# resource_group_name = ??? +# service_plan_id = ??? # récupéré depuis data.azurerm_service_plan.shared +# tags = ??? +# } + +# ── Function App (Étape 3) ──────────────────────────────────────────────────── +# TODO : appeler le module "./modules/function-app" +# Paramètres à passer : owner, resource_group_name, location, service_plan_id, tags + +# module "function_app" { +# source = "./modules/function-app" +# +# owner = ??? +# resource_group_name = ??? +# location = ??? +# service_plan_id = ??? +# tags = ??? +# } + +# ── Container Instance (Étape 3) ────────────────────────────────────────────── +# TODO : appeler le module "./modules/container" +# Paramètres à passer : owner, resource_group_name, location, tags + +# module "container" { +# source = "./modules/container" +# +# owner = ??? +# resource_group_name = ??? +# location = ??? +# tags = ??? +# } + +# ── Network (Étape 7) ───────────────────────────────────────────────────────── +# TODO : appeler le module "./modules/network" +# Paramètres à passer : owner, resource_group_name, location, tags + +# module "network" { +# source = "./modules/network" +# +# owner = ??? +# resource_group_name = ??? +# location = ??? +# tags = ??? +# } diff --git a/starter/terraform/modules/app-service/main.tf b/starter/terraform/modules/app-service/main.tf new file mode 100644 index 0000000..0ae900d --- /dev/null +++ b/starter/terraform/modules/app-service/main.tf @@ -0,0 +1,40 @@ +terraform { + required_version = ">= 1.9" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +# TODO : créer un azurerm_linux_web_app +# +# Nom attendu : "app-${var.owner}-tf" +# Plan : var.service_plan_id (récupérer la location via un data source) +# HTTPS only : true +# TLS minimum : "1.2" +# Runtime : Python 3.11 +# Tags : var.tags +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_web_app + +# resource "azurerm_linux_web_app" "app" { +# name = ??? +# resource_group_name = ??? +# location = ??? +# service_plan_id = ??? +# https_only = ??? +# +# site_config { +# minimum_tls_version = ??? +# application_stack { +# python_version = ??? +# } +# } +# +# tags = ??? +# } + +# Indice : pour récupérer la location du plan partagé à partir de son ID, +# utilisez un data source azurerm_service_plan avec split("/", var.service_plan_id) diff --git a/starter/terraform/modules/app-service/outputs.tf b/starter/terraform/modules/app-service/outputs.tf new file mode 100644 index 0000000..958db3e --- /dev/null +++ b/starter/terraform/modules/app-service/outputs.tf @@ -0,0 +1,6 @@ +# TODO : exposer le hostname de l'App Service +# Cet output est utilisé dans outputs.tf racine pour construire l'URL +# +# output "default_hostname" { +# value = azurerm_linux_web_app.app.default_hostname +# } diff --git a/starter/terraform/modules/app-service/variables.tf b/starter/terraform/modules/app-service/variables.tf new file mode 100644 index 0000000..0775106 --- /dev/null +++ b/starter/terraform/modules/app-service/variables.tf @@ -0,0 +1,4 @@ +variable "owner" { type = string } +variable "resource_group_name" { type = string } +variable "service_plan_id" { type = string } +variable "tags" { type = map(string) } diff --git a/starter/terraform/modules/container/main.tf b/starter/terraform/modules/container/main.tf new file mode 100644 index 0000000..f03a20f --- /dev/null +++ b/starter/terraform/modules/container/main.tf @@ -0,0 +1,44 @@ +terraform { + required_version = ">= 1.9" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +# TODO : créer un azurerm_container_group (ACI) +# +# Nom attendu : "aci-${var.owner}-tf" +# Image : "nginx:latest" +# IP : Public +# DNS label : "aci-${var.owner}-tf" +# CPU / Mémoire : 0.5 / 0.5 +# Port : 80 TCP +# OS : Linux +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_group + +# resource "azurerm_container_group" "aci" { +# name = ??? +# resource_group_name = ??? +# location = ??? +# ip_address_type = ??? +# dns_name_label = ??? +# os_type = ??? +# +# container { +# name = "nginx" +# image = ??? +# cpu = ??? +# memory = ??? +# +# ports { +# port = ??? +# protocol = "TCP" +# } +# } +# +# tags = ??? +# } diff --git a/starter/terraform/modules/container/outputs.tf b/starter/terraform/modules/container/outputs.tf new file mode 100644 index 0000000..eca363d --- /dev/null +++ b/starter/terraform/modules/container/outputs.tf @@ -0,0 +1,5 @@ +# TODO : exposer le FQDN public du container +# +# output "fqdn" { +# value = azurerm_container_group.aci.fqdn +# } diff --git a/starter/terraform/modules/container/variables.tf b/starter/terraform/modules/container/variables.tf new file mode 100644 index 0000000..8c6cb88 --- /dev/null +++ b/starter/terraform/modules/container/variables.tf @@ -0,0 +1,4 @@ +variable "owner" { type = string } +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "tags" { type = map(string) } diff --git a/starter/terraform/modules/function-app/main.tf b/starter/terraform/modules/function-app/main.tf new file mode 100644 index 0000000..0773647 --- /dev/null +++ b/starter/terraform/modules/function-app/main.tf @@ -0,0 +1,58 @@ +terraform { + required_version = ">= 1.9" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +# TODO (1/2) : créer un azurerm_storage_account dédié à la Function App +# +# La Function App a besoin d'un storage account propre (obligatoire Azure). +# Ce storage est SÉPARÉ du storage métier du module storage/. +# +# Nom attendu : "stfn${replace(var.owner, "-", "")}" +# Tier : Standard, LRS, TLS 1.2 +# Tags : merge(var.tags, { purpose = "function-storage" }) +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account + +# resource "azurerm_storage_account" "fn_storage" { +# name = ??? +# resource_group_name = ??? +# location = ??? +# account_tier = ??? +# account_replication_type = ??? +# min_tls_version = ??? +# tags = ??? +# } + +# TODO (2/2) : créer un azurerm_linux_function_app +# +# Nom attendu : "fn-${var.owner}-tf" +# Plan : var.service_plan_id +# Storage : azurerm_storage_account.fn_storage.name + primary_access_key +# HTTPS only : true +# Runtime : Python 3.11 +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_function_app + +# resource "azurerm_linux_function_app" "fn" { +# name = ??? +# resource_group_name = ??? +# location = ??? +# service_plan_id = ??? +# storage_account_name = ??? +# storage_account_access_key = ??? +# https_only = ??? +# +# site_config { +# application_stack { +# python_version = ??? +# } +# } +# +# tags = ??? +# } diff --git a/starter/terraform/modules/function-app/outputs.tf b/starter/terraform/modules/function-app/outputs.tf new file mode 100644 index 0000000..200c84f --- /dev/null +++ b/starter/terraform/modules/function-app/outputs.tf @@ -0,0 +1,5 @@ +# TODO : exposer le hostname de la Function App +# +# output "default_hostname" { +# value = azurerm_linux_function_app.fn.default_hostname +# } diff --git a/starter/terraform/modules/function-app/variables.tf b/starter/terraform/modules/function-app/variables.tf new file mode 100644 index 0000000..c7dc842 --- /dev/null +++ b/starter/terraform/modules/function-app/variables.tf @@ -0,0 +1,5 @@ +variable "owner" { type = string } +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "service_plan_id" { type = string } +variable "tags" { type = map(string) } diff --git a/starter/terraform/modules/network/main.tf b/starter/terraform/modules/network/main.tf new file mode 100644 index 0000000..614eef4 --- /dev/null +++ b/starter/terraform/modules/network/main.tf @@ -0,0 +1,72 @@ +terraform { + required_version = ">= 1.9" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +# TODO (1/4) : créer un azurerm_virtual_network +# +# Nom : "vnet-${var.owner}-tf" +# Espace d'adres.: ["10.0.0.0/16"] +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network + +# resource "azurerm_virtual_network" "vnet" { +# name = ??? +# resource_group_name = ??? +# location = ??? +# address_space = ??? +# tags = ??? +# } + +# TODO (2/4) : créer deux subnets dans ce VNet +# +# subnet-frontend : 10.0.1.0/24 +# subnet-backend : 10.0.2.0/24 +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet + +# resource "azurerm_subnet" "frontend" { +# name = "subnet-frontend" +# resource_group_name = ??? +# virtual_network_name = ??? +# address_prefixes = ["10.0.1.0/24"] +# } + +# resource "azurerm_subnet" "backend" { +# name = "subnet-backend" +# resource_group_name = ??? +# virtual_network_name = ??? +# address_prefixes = ["10.0.2.0/24"] +# } + +# TODO (3/4) : créer un NSG avec 3 règles pour subnet-frontend +# +# Nom : "nsg-frontend-${var.owner}-tf" +# Règles : Allow-HTTP (100), Allow-HTTPS (110), Deny-All-Inbound (4000) +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group + +# resource "azurerm_network_security_group" "nsg" { +# name = ??? +# resource_group_name = ??? +# location = ??? +# tags = ??? +# +# # TODO : ajouter Allow-HTTP (port 80, priorité 100, action Allow) +# # TODO : ajouter Allow-HTTPS (port 443, priorité 110, action Allow) +# # TODO : ajouter Deny-All-Inbound (port *, priorité 4000, action Deny) +# } + +# TODO (4/4) : associer le NSG au subnet-frontend +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_network_security_group_association + +# resource "azurerm_subnet_network_security_group_association" "frontend_nsg" { +# subnet_id = ??? +# network_security_group_id = ??? +# } diff --git a/starter/terraform/modules/network/outputs.tf b/starter/terraform/modules/network/outputs.tf new file mode 100644 index 0000000..d8cb03e --- /dev/null +++ b/starter/terraform/modules/network/outputs.tf @@ -0,0 +1,13 @@ +# TODO : exposer l'ID du VNet et les IDs des subnets (utiles si d'autres modules en ont besoin) +# +# output "vnet_id" { +# value = azurerm_virtual_network.vnet.id +# } +# +# output "subnet_frontend_id" { +# value = azurerm_subnet.frontend.id +# } +# +# output "subnet_backend_id" { +# value = azurerm_subnet.backend.id +# } diff --git a/starter/terraform/modules/network/variables.tf b/starter/terraform/modules/network/variables.tf new file mode 100644 index 0000000..8c6cb88 --- /dev/null +++ b/starter/terraform/modules/network/variables.tf @@ -0,0 +1,4 @@ +variable "owner" { type = string } +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "tags" { type = map(string) } diff --git a/starter/terraform/modules/storage/main.tf b/starter/terraform/modules/storage/main.tf new file mode 100644 index 0000000..2ee20cf --- /dev/null +++ b/starter/terraform/modules/storage/main.tf @@ -0,0 +1,51 @@ +terraform { + required_version = ">= 1.9" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +# TODO (1/3) : créer un azurerm_storage_account métier +# +# Nom attendu : "st${replace(var.owner, "-", "")}tf" +# Tier : Standard, LRS, StorageV2, TLS 1.2 +# Accès public blob activé : true (nécessaire pour api-config) +# +# Documentation : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account + +# resource "azurerm_storage_account" "sa" { +# name = ??? +# resource_group_name = ??? +# location = ??? +# account_tier = ??? +# account_replication_type = ??? +# account_kind = ??? +# min_tls_version = ??? +# allow_nested_items_to_be_public = ??? # true pour permettre api-config public +# tags = ??? +# } + +# TODO (2/3) : conteneur privé pour les logs API +# +# Nom : "api-logs" +# container_access_type: "private" + +# resource "azurerm_storage_container" "api_logs" { +# name = ??? +# storage_account_id = ??? +# container_access_type = ??? +# } + +# TODO (3/3) : conteneur public pour la config API +# +# Nom : "api-config" +# container_access_type: "blob" (lecture anonyme des blobs, pas du container) + +# resource "azurerm_storage_container" "api_config" { +# name = ??? +# storage_account_id = ??? +# container_access_type = ??? +# } diff --git a/starter/terraform/modules/storage/outputs.tf b/starter/terraform/modules/storage/outputs.tf new file mode 100644 index 0000000..e094ecb --- /dev/null +++ b/starter/terraform/modules/storage/outputs.tf @@ -0,0 +1,5 @@ +# TODO : exposer le nom du Storage Account (utile pour les outputs racine) +# +# output "storage_account_name" { +# value = azurerm_storage_account.sa.name +# } diff --git a/starter/terraform/modules/storage/variables.tf b/starter/terraform/modules/storage/variables.tf new file mode 100644 index 0000000..8c6cb88 --- /dev/null +++ b/starter/terraform/modules/storage/variables.tf @@ -0,0 +1,4 @@ +variable "owner" { type = string } +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "tags" { type = map(string) } diff --git a/starter/terraform/outputs.tf b/starter/terraform/outputs.tf new file mode 100644 index 0000000..64e9f11 --- /dev/null +++ b/starter/terraform/outputs.tf @@ -0,0 +1,30 @@ +# ────────────────────────────────────────────────────────────────────────────── +# outputs.tf — Valeurs exposées après terraform apply +# +# Les outputs sont l'équivalent des `echo` à la fin de provision.sh. +# Complétez au fur et à mesure que vous créez les ressources. +# ────────────────────────────────────────────────────────────────────────────── + +# TODO : URL de l'App Service +# output "app_service_url" { +# description = "URL publique de l'App Service" +# value = "https://${module.app_service.default_hostname}" +# } + +# TODO : URL de la Function App +# output "function_app_url" { +# description = "URL publique de la Function App" +# value = "https://${module.function_app.default_hostname}" +# } + +# TODO : FQDN du Container ACI +# output "container_fqdn" { +# description = "FQDN public du container nginx" +# value = "http://${module.container.fqdn}" +# } + +# TODO : Nom du Storage Account métier +# output "storage_account_name" { +# description = "Nom du Storage Account" +# value = module.storage.storage_account_name +# } diff --git a/starter/terraform/providers.tf b/starter/terraform/providers.tf new file mode 100644 index 0000000..d468cce --- /dev/null +++ b/starter/terraform/providers.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.9" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +provider "azurerm" { + # Authentication via OIDC — no client secret + # ARM_CLIENT_ID, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID injected by GitHub Actions + use_oidc = true + + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} diff --git a/starter/terraform/terraform.tfvars.example b/starter/terraform/terraform.tfvars.example new file mode 100644 index 0000000..b699659 --- /dev/null +++ b/starter/terraform/terraform.tfvars.example @@ -0,0 +1,20 @@ +# Copy this file to terraform.tfvars and fill in your values +# terraform.tfvars is gitignored — never commit it + +# Your learner identifier (firstname-lastname, lowercase, hyphens only) +owner = "prenom-nom" + +# Resource Group pre-created by the trainer +resource_group_name = "rg-prenom-nom" + +# Azure region — default is francecentral, change only if instructed +# location = "francecentral" + +# Shared infrastructure (pre-created by trainer — do not change) +# shared_rg_name = "rg-shared-prf2026" +# shared_plan_name = "plan-npr-prf2026" + +# Optional extra tags +# tags = { +# project = "tp-azure" +# } diff --git a/starter/terraform/variables.tf b/starter/terraform/variables.tf new file mode 100644 index 0000000..186d9ec --- /dev/null +++ b/starter/terraform/variables.tf @@ -0,0 +1,38 @@ +variable "owner" { + description = "Learner identifier (firstname-lastname, lowercase, hyphens). Ex: john-doe" + type = string + + validation { + condition = can(regex("^[a-z][a-z0-9-]+[a-z0-9]$", var.owner)) + error_message = "owner must be lowercase, letters, digits and hyphens only." + } +} + +variable "resource_group_name" { + description = "Name of the Resource Group pre-created by the trainer. Ex: rg-john-doe" + type = string +} + +variable "location" { + description = "Azure region for resources" + type = string + default = "francecentral" +} + +variable "shared_rg_name" { + description = "Resource Group containing the shared App Service plan" + type = string + default = "rg-shared-prf2026" +} + +variable "shared_plan_name" { + description = "Name of the shared App Service plan" + type = string + default = "plan-npr-prf2026" +} + +variable "tags" { + description = "Additional tags to merge with default tags" + type = map(string) + default = {} +}