Fiche pour débutants. Les bases (resource, variable,
count,for_each,dynamic) sont dans 00-bases-terraform.md. Pour chaque fichier : à quoi il sert, le code expliqué, comment reproduire quelque chose de semblable.
1. Le motif qu'on retrouve partout : « option activée par une variable »
Presque tout ce que le projet 08 ajoute est optionnel. Le motif est toujours le même :
resource "google_pubsub_topic" "siem" {
count = var.enable_siem_export ? 1 : 0 # 1 exemplaire si l'option est vraie, 0 sinon
name = "ge-security-siem"
}condition ? A : B: si la condition est vraie, A, sinon B.count = 0: la ressource n'existe pas.count = 1: elle existe, et on la désigne avec[0]:google_pubsub_topic.siem[0].id.[*](« splat ») : la liste de tous les exemplaires, vide sicount = 0. Pratique pour les arguments qui acceptent une liste :notification_channels = google_monitoring_notification_channel.security[*].id.
Pour un bloc optionnel à l'intérieur d'une ressource, on utilise dynamic :
dynamic "ai_platform_floor_setting" {
for_each = var.armor_agent_platform_mode == "off" ? [] : [1] # liste vide = pas de bloc
content {
inspect_only = var.armor_agent_platform_mode == "inspect"
}
}2. 30_ai_guardrails.tf (modifié) : le plancher couvre aussi les agents
integrated_services = var.armor_agent_platform_mode == "off" ? [] : ["AI_PLATFORM"]
dynamic "ai_platform_floor_setting" {
for_each = var.armor_agent_platform_mode == "off" ? [] : [1]
content {
inspect_only = var.armor_agent_platform_mode == "inspect"
inspect_and_block = var.armor_agent_platform_mode == "block"
enable_cloud_logging = true
}
}| Ligne | Explication |
|---|---|
integrated_services = ["AI_PLATFORM"] | Le plancher ne sert plus seulement de minimum pour les templates : il filtre chaque appel Gemini fait dans le projet (agents, notebooks) |
inspect_only / inspect_and_block | Deux booléens, un seul vrai. On compare la variable à un texte : == renvoie true ou false |
enable_cloud_logging = true | Sans journaux, le mode « inspect » ne sert à rien : on ne verrait pas ce qui aurait été bloqué |
La variable et sa validation (variables.tf, section 9) : validation refuse toute valeur autre que
off, inspect, block avant d'appeler Google. Une faute de frappe est arrêtée au plan.
3. 60_advanced.tf (modifié) : périmètre élargi et niveau d'accès
resource "google_access_context_manager_access_level" "corporate" {
count = var.vpc_sc_access_policy != "" && length(var.corporate_ip_ranges) > 0 ? 1 : 0
parent = "accessPolicies/${var.vpc_sc_access_policy}"
name = "accessPolicies/${var.vpc_sc_access_policy}/accessLevels/acme_corporate_${replace(var.project_id, "-", "_")}"
title = "acme-corporate-${var.project_id}"
basic {
conditions {
ip_subnetworks = var.corporate_ip_ranges
}
}
}&&: « et ». Le niveau d'accès n'existe que si les deux informations sont fournies.length(liste): nombre d'éléments.replace(texte, "-", "_"): les noms de niveaux d'accès n'acceptent pas le tiret."${...}": insérer une valeur dans un texte (interpolation).
Dans le périmètre : access_levels = google_access_context_manager_access_level.corporate[*].name. Liste vide
si pas de niveau d'accès : le code marche dans les deux cas, sans if.
4. 75_shadow_ai.tf (nouveau) : empêcher et détecter
Politiques d'organisation construites dans une map :
locals {
org_policies = var.enforce_org_policies ? merge(
{ "run.allowedIngress" = ["internal-and-cloud-load-balancing"], "gcp.resourceLocations" = ["in:eu-locations"] },
var.workspace_customer_id != "" ? { "iam.allowedPolicyMemberDomains" = [var.workspace_customer_id] } : {},
) : {}
}
resource "google_org_policy_policy" "guardrails" {
for_each = local.org_policies # une politique par clé de la map
name = "projects/${var.project_id}/policies/${each.key}"
...
allowed_values = each.valuemerge(map1, map2): fusionne deux maps. On ajoute la 3e politique seulement si l'ID client est connu.for_eachsur une map :each.key= nom de la contrainte,each.value= valeurs autorisées.- Reproduire : ajouter une contrainte = ajouter une ligne dans la map. Aucune autre ressource à écrire.
Détection : une métrique basée sur les journaux (google_logging_metric) compte les entrées qui
correspondent à un filtre ; une règle d'alerte (google_monitoring_alert_policy) envoie un e-mail dès que
le compteur dépasse 0 sur 5 minutes. Même motif que l'alerte « garde-fou modifié » du socle
(10-detection.md).
5. 85_siem_access.tf (nouveau) : SIEM et Access Approval
La chaîne SIEM en 4 ressources :
journaux ──(sink: filtre)──▶ topic Pub/Sub ──▶ abonnement ──▶ connecteur du SIEM
│ ▲
└─ writer_identity ──┘ roles/pubsub.publisher
resource "google_logging_project_sink" "siem" {
destination = "pubsub.googleapis.com/${google_pubsub_topic.siem[0].id}"
filter = local.security_log_filter # le MÊME filtre que le bucket ge-security
unique_writer_identity = true # Google crée un compte dédié à ce sink
}
resource "google_pubsub_topic_iam_member" "siem_writer" {
role = "roles/pubsub.publisher"
member = google_logging_project_sink.siem[0].writer_identity # attribut calculé par Google
}Point clé pour débutant : writer_identity n'est connu qu'après la création du sink. Terraform le
comprend tout seul grâce à la référence : il crée le sink, lit l'identité, puis donne le droit. Pas besoin de
depends_on.
Réutiliser un filtre : local.security_log_filter est défini une fois dans 80_logging.tf et utilisé deux
fois. Tous les fichiers d'un dossier forment un seul module : un local est visible partout.
Access Approval :
resource "google_project_access_approval_settings" "this" {
count = length(var.access_approval_emails) > 0 ? 1 : 0
project_id = var.project_id
notification_emails = var.access_approval_emails
enrolled_services {
cloud_product = "all"
}
}La liste vide sert d'interrupteur : pas d'e-mail, pas de ressource.
6. 90_agents_registry.tf (nouveau) : agents et catalogue
| Bloc | Rôle |
|---|---|
google_project_iam_member.registry_viewers / registry_editors | Lecture du catalogue pour les utilisateurs, publication pour les développeurs (for_each sur les groupes) |
google_service_account.secure_agent | Identité propre à l'agent |
google_project_iam_member.secure_agent | 5 rôles minimum (Gemini, Model Armor, journaux, métriques, traces) |
data.external.agent_source | Lance scripts/pack.py qui compresse agent/ de façon déterministe : même code = mêmes octets = pas de redéploiement inutile |
google_vertex_ai_reasoning_engine.secure_agent | L'agent sur Agent Runtime, avec les templates Model Armor en variables d'environnement |
dynamic "agent_gateway_config" | Si Agent Gateway est activé, la sortie de l'agent passe par lui |
terraform_data.register_agent | Publie l'agent dans l'app GE par l'API (pas encore de ressource Terraform) ; le retire au destroy |
Les deux local-exec de terraform_data : le premier s'exécute à la création, le second (when = destroy)
à la suppression. self.input permet au provisioner de destruction de retrouver les valeurs : il ne peut pas lire
les variables, seulement ce qui a été mémorisé dans l'état.
Le provider external est déclaré dans versions.tf (hashicorp/external). Il permet d'appeler un
programme local qui lit du JSON sur l'entrée standard et écrit du JSON sur la sortie.
7. 95_secure_web_proxy.tf (nouveau) : SWP optionnel
Une variable objet (var.swp) regroupe tous les paramètres. null = désactivé :
count = var.swp == null ? 0 : 1
network = var.swp.network| Ressource | Rôle |
|---|---|
google_compute_subnetwork.swp_proxy_only | Sous-réseau réservé au proxy (purpose = "REGIONAL_MANAGED_PROXY"), exigé par Google |
google_network_security_url_lists.allowed | La liste blanche (domaines, *. autorisé) |
google_network_security_gateway_security_policy | La politique, refus par défaut |
..._policy_rule.allow_listed | La règle : inUrlList(host(), 'LISTE') → ALLOW (expression CEL) |
google_network_services_gateway.swp | Le proxy lui-même (type = "SECURE_WEB_GATEWAY", mode explicite : les charges utilisent HTTPS_PROXY) |
Reproduire pour un autre besoin : ajouter un domaine = l'ajouter dans allowed_hosts (PR validée par la
sécurité). Une règle plus fine (méthode HTTP, chemin) demande l'inspection TLS : voir le commentaire ponytail
dans le fichier.
8. imports.tf.example : adopter ce qui existe
import {
to = google_model_armor_template.shield["ge-prompt-shield"] # adresse dans le code
id = "projects/PROJECT_ID/locations/eu/templates/ge-prompt-shield" # ressource chez Google
}Le plan affiche alors « 1 to import » au lieu de « 1 to add ». Le format de id de chaque type est donné en
bas de sa page dans la documentation du provider (section Import). Avec for_each, un seul bloc importe
plusieurs ressources (voir les journaux d'audit dans le fichier).