Leçon 24 — Le code Terraform, couche par couche

08. `50_app.tf` : l'application Gemini Enterprise et son assistant

18 min de lecture

Ce fichier crée l'app Gemini Enterprise, son assistant protégé par Model Armor, et applique par l'API REST les réglages que le provider ne sait pas encore gérer. Bases Terraform : 00-bases-terraform.md.


1. À quoi sert ce fichier

C'est le cœur visible du projet : l'application que les collaborateurs ouvrent dans leur navigateur. Le fichier crée l'app (le « moteur » de recherche et de chat), lui rattache les sources de données des connecteurs, puis crée son assistant (le chat IA) en le branchant sur les filtres Model Armor. Enfin, il applique trois réglages absents du provider Terraform (durée de conservation des conversations, observabilité, Agent Gateway) par un appel direct à l'API Google.


2. Vue d'ensemble

Bloc (adresse)Type de blocRôle en une phrase
google_discovery_engine_search_engine.appresourceL'app Gemini Enterprise elle-même, avec ses sources et ses fonctionnalités.
google_discovery_engine_assistant.defaultresourceL'assistant (chat) de l'app : langue, consignes, filtres Model Armor, phrases interdites, recherche web.
local.engine_settingslocalsLe contenu JSON des réglages non gérés par le provider.
terraform_data.engine_settingsresourceEnvoie ces réglages à l'API REST avec curl, à chaque changement.

3. Le code expliqué, bloc par bloc

3.1 L'app : google_discovery_engine_search_engine.app

Vocabulaire. Dans l'API Google, Gemini Enterprise s'appelle encore « Discovery Engine » (ancien nom : Agentspace). Une app y est un engine (moteur). Les sources de données y sont des data stores (magasins de données). Les apps sont rangées dans une collection ; la collection par défaut s'appelle default_collection.

resource "google_discovery_engine_search_engine" "app" {
  location          = var.ge_location
  collection_id     = "default_collection"
  engine_id         = var.app_id
  display_name      = var.app_display_name
  industry_vertical = "GENERIC"
  app_type          = "APP_TYPE_INTRANET" # = une app Gemini Enterprise
LigneCe qu'elle faitD'où vient la valeur / pourquoi
location = var.ge_locationMulti-région de l'app.Variable, défaut "eu" : données en Union européenne (guide du socle (docs/00) §9.1). Immuable : la changer recrée l'app.
collection_id = "default_collection"Collection où ranger l'app.Valeur fixe de Google.
engine_id = var.app_idIdentifiant technique de l'app (apparaît dans les URL).Variable, défaut "assistant-ia".
display_name = var.app_display_nameNom affiché aux utilisateurs.Variable, défaut "Assistant IA".
industry_vertical = "GENERIC"Secteur métier de l'app.GENERIC = usage général (pas « santé » ni « média »).
app_type = "APP_TYPE_INTRANET"Type d'app.C'est cette valeur qui fait de l'engine une app Gemini Enterprise (et pas un simple moteur de recherche de site web).
  data_store_ids    = concat(local.connector_data_store_ids, var.extra_data_store_ids)
  kms_key_name      = var.enable_cmek ? google_kms_crypto_key.ge[0].id : null
  features          = var.features
  • data_store_ids : la liste des sources rattachées à l'app. On la construit avec concat, qui colle deux listes bout à bout (00-bases §12.4) :

    • local.connector_data_store_ids : les data stores créés par les connecteurs, calculés dans 40_connectors.tf (voir 07-connectors.md) ;
    • var.extra_data_store_ids : des data stores créés à la main dans la console (défaut []).

    Exemple concret : si les connecteurs produisent ["sharepoint_file"] et que extra_data_store_ids vaut ["intranet-rh"], alors concat(...) donne ["sharepoint_file", "intranet-rh"]. Avec le terraform.tfvars.example, extra_data_store_ids n'est pas renseigné, donc vaut [].

    Comme cette ligne utilise un attribut des connecteurs, Terraform crée les connecteurs avant l'app (dépendance implicite, 00-bases §9).

  • kms_key_name : la clé de chiffrement CMEK (Customer-Managed Encryption Key : une clé que Acme possède dans Cloud KMS, au lieu d'une clé gérée par Google). C'est une condition ternaire (00-bases §12.1) :

    • si var.enable_cmek vaut true (défaut), on prend google_kms_crypto_key.ge[0].id (le [0] car la clé est créée avec count, voir 05-data-protection.md) ;
    • sinon null, ce qui veut dire « ne pas renseigner cet argument » (Google chiffre alors avec ses propres clés).
  • features : une map « nom de fonctionnalité → état ». La valeur vient de var.features (variables.tf). Par défaut :

    "disable-agent-sharing"                = "FEATURE_STATE_ON"  # pas de partage d'agents maison
    "skill-sharing-without-admin-approval" = "FEATURE_STATE_OFF" # partage de skills = validation admin
    "session-sharing"                      = "FEATURE_STATE_OFF" # pas de partage de conversations
    "feedback"                             = "FEATURE_STATE_ON"  # retours utilisateurs

    Pourquoi (guide du socle (docs/00) §9.3) : un agent ou une conversation partagés peuvent diffuser des consignes inadaptées ou des informations que le destinataire n'a pas le droit de voir. On rouvre le partage quand la gouvernance est définie.

  common_config {
    company_name = var.company_name
  }
  • common_config est un sous-bloc (00-bases §5). company_name donne le nom de l'entreprise à l'IA, pour contextualiser ses réponses. Variable, défaut "Acme".
  search_engine_config {
    search_tier                = "SEARCH_TIER_ENTERPRISE"
    search_add_ons             = ["SEARCH_ADD_ON_LLM"]
    required_subscription_tier = "SUBSCRIPTION_TIER_ENTERPRISE"
  }
  • search_tier = "SEARCH_TIER_ENTERPRISE" : niveau « entreprise » de la recherche (fonctions avancées).
  • search_add_ons = ["SEARCH_ADD_ON_LLM"] : active l'option « LLM » (grand modèle de langage), c'est-à-dire les réponses générées par l'IA et pas seulement une liste de résultats. C'est une liste, même avec un seul élément.
  • required_subscription_tier = "SUBSCRIPTION_TIER_ENTERPRISE" : l'édition de licence Gemini Enterprise exigée des utilisateurs (ici, « Enterprise »). Elle doit correspondre aux licences achetées.
  depends_on = [google_discovery_engine_cmek_config.ge]
}
  • depends_on (00-bases §9) : force Terraform à créer l'enregistrement CMEK (google_discovery_engine_cmek_config.ge, dans 20_data_protection.tf) avant l'app. Rien dans les arguments ne le laisse deviner, mais Google l'exige : une app créée avant l'enregistrement de la clé reste chiffrée par Google, définitivement.
  • On peut écrire depends_on vers une ressource qui a count = 0 (CMEK désactivé) : la liste est alors simplement vide, sans erreur.

3.2 L'assistant : google_discovery_engine_assistant.default

L'assistant est la partie « chat » de l'app : il reçoit les questions, interroge les sources et rédige les réponses. Chaque app en a un, nommé default_assistant.

resource "google_discovery_engine_assistant" "default" {
  location           = var.ge_location
  collection_id      = "default_collection"
  engine_id          = google_discovery_engine_search_engine.app.engine_id
  assistant_id       = "default_assistant"
  display_name       = "${var.app_display_name} - assistant"
  web_grounding_type = var.web_grounding
  • location et collection_id : les mêmes que l'app (l'assistant vit dans l'app).
  • engine_id = google_discovery_engine_search_engine.app.engine_id : une référence à l'app (00-bases §7). Résultat : "assistant-ia". Comme on utilise un attribut de l'app, Terraform crée l'app d'abord (pas besoin de depends_on).
  • assistant_id = "default_assistant" : identifiant fixe attendu par Gemini Enterprise.
  • display_name = "${var.app_display_name} - assistant" : interpolation (${...} insère une valeur dans un texte). Avec la valeur par défaut : "Assistant IA - assistant".
  • web_grounding_type = var.web_grounding : la recherche web (« grounding » = appuyer les réponses sur des sources). Défaut "WEB_GROUNDING_TYPE_DISABLED" : pas de recherche web publique. Pourquoi (guide du socle (docs/00) §9.2) : des fragments de questions confidentielles partiraient vers un moteur public, et la recherche Google est incompatible avec la résidence UE et CMEK. Si le métier en a besoin : WEB_GROUNDING_TYPE_ENTERPRISE_WEB_SEARCH.
  generation_config {
    default_language = "fr"
    system_instruction {
      additional_system_instruction = "Réponds à partir des données de l'entreprise et cite tes sources. Ne demande jamais de mot de passe, de numéro de sécurité sociale ou de coordonnées bancaires."
    }
  }
  • generation_config : comment l'IA rédige.
  • default_language = "fr" : langue par défaut des réponses.
  • system_instruction → additional_system_instruction : une consigne système, ajoutée à celles de Google, que l'IA lit avant chaque question. Ici : citer ses sources et ne jamais réclamer de données sensibles. C'est une défense en complément de Model Armor, pas un remplacement (une consigne peut être contournée, un filtre non).
  customer_policy {
    model_armor_config {
      user_prompt_template = google_model_armor_template.shield["ge-prompt-shield"].id
      response_template    = google_model_armor_template.shield["ge-response-shield"].id
      failure_mode         = "FAIL_CLOSED" # Model Armor indisponible => refus, jamais de réponse non filtrée
    }
  • customer_policy : la politique de sécurité propre à l'entreprise.
  • model_armor_config branche les templates Model Armor (Model Armor = le service Google qui filtre les questions et réponses : injection de prompt, données sensibles, liens malveillants…) créés dans 30_ai_guardrails.tf (voir 06-ai-guardrails.md).
    • user_prompt_template : le template appliqué aux questions. La notation shield["ge-prompt-shield"] vient du for_each de la ressource shield (00-bases §8.2).
    • response_template : le template appliqué aux réponses.
    • failure_mode = "FAIL_CLOSED" : si Model Armor ne répond pas (panne, droit manquant), l'assistant refuse au lieu de répondre sans filtre. L'autre valeur (FAIL_OPEN) privilégierait la disponibilité. Choix assumé (guide du socle (docs/00) §8.5 et §15) : la sécurité passe avant la disponibilité.
    dynamic "banned_phrases" {
      for_each = var.banned_phrases
      content {
        phrase            = banned_phrases.value
        match_type        = "WORD_BOUNDARY_STRING_MATCH"
        ignore_diacritics = true
      }
    }
  }
  • dynamic "banned_phrases" génère un sous-bloc banned_phrases par élément de la liste var.banned_phrases (00-bases §10). Dans content, l'élément en cours s'appelle banned_phrases.value (même nom que le bloc).
  • phrase : l'expression que l'assistant ne doit jamais produire (par exemple un nom de code de projet secret).
  • match_type = "WORD_BOUNDARY_STRING_MATCH" : on cherche l'expression comme mot entier. la phrase « Atlas » bloquera « projet Atlas » mais pas « Atlassian ».
  • ignore_diacritics = true : les accents sont ignorés. « élève » et « eleve » sont traités pareil.

Exemple concret. Avec banned_phrases = ["nom de code projet confidentiel"], Terraform génère :

banned_phrases {
  phrase            = "nom de code projet confidentiel"
  match_type        = "WORD_BOUNDARY_STRING_MATCH"
  ignore_diacritics = true
}

Avec le terraform.tfvars.example, la ligne banned_phrases est en commentaire : la liste vaut [] (défaut), donc aucun bloc n'est généré. C'est tout l'intérêt de dynamic : un bloc écrit à la main ne pourrait pas « disparaître ».

  depends_on = [google_project_iam_member.ge_uses_armor]
}
  • Force la création du droit « Gemini Enterprise peut appeler Model Armor » (roles/modelarmor.user, dans 30_ai_guardrails.tf) avant l'assistant. Sans ce droit, et avec FAIL_CLOSED, l'assistant refuserait tout.

3.3 Les réglages calculés : locals { engine_settings }

Le problème. Trois réglages de l'app existent dans l'API Google, mais pas encore dans le provider Terraform (version ~> 8.4) :

  • la durée de conservation des conversations (guide du socle (docs/00) §6.3 : minimisation RGPD, 30 jours au lieu de 60) ;
  • l'observabilité (traces techniques, sans le contenu des questions, guide du socle (docs/00) §9.4) ;
  • le rattachement à l'Agent Gateway (option, voir 09-advanced.md).

On prépare donc leur contenu JSON dans une valeur locale (00-bases §6.4), puis on l'envoie à l'API (§3.4).

locals {
  engine_settings = merge(
    {
      sessionConfig       = { sessionTtl = { days = var.session_ttl_days } }
      observabilityConfig = { observabilityEnabled = var.enable_observability, sensitiveLoggingEnabled = var.enable_observability && var.log_prompts_responses }
    },
    var.enable_agent_gateway ? {
      agentGatewaySetting = { defaultEgressAgentGateway = { name = "projects/${local.project_number}/locations/${var.region}/agentGateways/${google_network_services_agent_gateway.egress[0].name}" } }
    } : {},
  )
}
  • merge(map1, map2) fusionne deux maps en une seule (00-bases §12.4).
  • Première map (toujours présente) :
    • sessionConfig = { sessionTtl = { days = var.session_ttl_days } } : conserver les conversations session_ttl_days jours (défaut 30).
    • observabilityConfig = { observabilityEnabled = var.enable_observability, sensitiveLoggingEnabled = var.enable_observability && var.log_prompts_responses } : traces et journaux d'usage activés (défaut true). Le texte complet des questions et réponses (sensitiveLoggingEnabled) n'est activé que si les deux variables valent true : && est le « ET » logique, car la journalisation sensible n'a pas de sens sans l'observabilité. Par défaut log_prompts_responses = false. Où vont ces journaux : 13-logging.md.
    • Les noms en camelCase (sessionConfig, sessionTtl…) sont ceux de l'API REST Google (ressource Engine), pas des noms Terraform. C'est pour ça qu'ils ne suivent pas le style snake_case habituel.
  • Deuxième map (conditionnelle) : var.enable_agent_gateway ? { agentGatewaySetting = … } : {}.
    • Si l'Agent Gateway est activé : une map avec la clé agentGatewaySetting, qui donne le nom complet de la passerelle. Ce nom est construit par interpolation à partir du numéro de projet (local.project_number, dans 00_foundation.tf), de la région et du nom de la passerelle (egress[0], car créée avec count).
    • Sinon : {}, une map vide. Fusionner une map vide n'ajoute rien.
    • C'est la technique classique pour ajouter une clé sous condition. Terraform n'évalue la référence egress[0] que dans la branche choisie : pas d'erreur quand la passerelle n'existe pas.

Exemple concret avec le terraform.tfvars.example (donc les défauts session_ttl_days = 30, enable_observability = true, enable_agent_gateway = false) :

local.engine_settings = {
  observabilityConfig = { observabilityEnabled = true, sensitiveLoggingEnabled = false }
  sessionConfig       = { sessionTtl = { days = 30 } }
}

Avec enable_agent_gateway = true, une troisième clé s'ajoute : agentGatewaySetting = { defaultEgressAgentGateway = { name = "projects/123456789012/locations/europe-west1/agentGateways/assistant-ia-egress" } } (le numéro de projet est un exemple).

3.4 L'appel à l'API : terraform_data.engine_settings

Principe général : 00-bases §14.

resource "terraform_data" "engine_settings" {
  triggers_replace = [jsonencode(local.engine_settings), google_discovery_engine_search_engine.app.id]
  • terraform_data : une ressource « vide », fournie par Terraform lui-même (pas par Google). Elle ne crée rien chez Google ; elle sert de support à une commande.
  • triggers_replace : une liste de valeurs surveillées. Si l'une change, Terraform recrée la ressource, et la commande est donc relancée. Ici on surveille :
    • jsonencode(local.engine_settings) : le texte JSON des réglages. jsonencode transforme une valeur Terraform en texte JSON. Si vous passez session_ttl_days de 30 à 15, le texte change, donc l'appel est rejoué ;
    • google_discovery_engine_search_engine.app.id : l'identifiant de l'app. Si l'app est recréée, ses réglages sont perdus : il faut les réappliquer.
  provisioner "local-exec" {
    command = <<-EOT
      curl -fsS -X PATCH \
        -H "Authorization: Bearer $(gcloud auth print-access-token)" \
        -H "Content-Type: application/json" -H "X-Goog-User-Project: ${var.project_id}" \
        "https://${local.de_host}/v1/${google_discovery_engine_search_engine.app.name}?updateMask=${join(",", keys(local.engine_settings))}" \
        -d '${jsonencode(local.engine_settings)}' >/dev/null && echo "engine settings applied"
    EOT
  }
}
  • provisioner "local-exec" : exécute une commande sur la machine qui lance terraform apply (votre poste ou la CI), une seule fois, à la création de la ressource.
  • <<-EOT … EOT : un heredoc, c'est-à-dire un texte sur plusieurs lignes (00-bases §12.3). Le - après << retire l'indentation commune, pour que le code reste lisible.
  • Les \ en fin de ligne sont pour le shell : « la commande continue à la ligne suivante ».

Ligne par ligne :

MorceauSignification
curl -fsS -X PATCHcurl envoie une requête HTTP. -X PATCH = « modifier une partie » de la ressource. -f : échouer si Google renvoie une erreur (donc apply échoue aussi). -s : silencieux. -S : mais afficher les erreurs.
-H "Authorization: Bearer $(gcloud auth print-access-token)"S'authentifier. $(...) est une syntaxe du shell (pas de Terraform) : elle exécute gcloud auth print-access-token et insère le jeton obtenu. Terraform ne touche qu'à ${...}, il laisse $(...) intact.
-H "Content-Type: application/json"Le corps de la requête est du JSON.
-H "X-Goog-User-Project: ${var.project_id}"Le projet à qui facturer l'appel et décompter les quotas. Requis quand on appelle ces API avec un compte utilisateur.
"https://${local.de_host}/v1/${…app.name}?updateMask=…"L'URL. local.de_host vaut "eu-discoveryengine.googleapis.com" (point d'accès régional, dans 00_foundation.tf). app.name est le nom complet de l'app chez Google.
?updateMask=${join(",", keys(local.engine_settings))}Le masque de mise à jour : la liste des champs à modifier. Sans lui, un PATCH pourrait remettre à zéro les autres champs de l'app. keys(map) donne la liste des clés ; join(",", liste) les colle avec des virgules.
-d '${jsonencode(local.engine_settings)}'Le corps de la requête : les réglages en JSON, entre apostrophes pour le shell.
>/dev/nullJeter la réponse de Google (longue et inutile ici).
&& echo "engine settings applied"Si curl a réussi, afficher une confirmation dans la sortie de apply.

Exemple concret avec session_ttl_days = 30, enable_observability = true, enable_agent_gateway = false, project_id = "acme-gemini-enterprise-prd", app_id = "assistant-ia" :

  • keys(local.engine_settings) → ["observabilityConfig", "sessionConfig"] (Terraform trie les clés par ordre alphabétique) ;
  • join(",", …) → "observabilityConfig,sessionConfig" ;
  • jsonencode(…) → {"observabilityConfig":{"observabilityEnabled":true,"sensitiveLoggingEnabled":false},"sessionConfig":{"sessionTtl":{"days":30}}}.

La commande réellement exécutée ressemble donc à ceci (le numéro de projet 123456789012 est un exemple ; le format exact de app.name, numéro ou ID de projet, est à vérifier dans terraform state show) :

curl -fsS -X PATCH \
  -H "Authorization: Bearer ya29.xxxxx" \
  -H "Content-Type: application/json" -H "X-Goog-User-Project: acme-gemini-enterprise-prd" \
  "https://eu-discoveryengine.googleapis.com/v1/projects/123456789012/locations/eu/collections/default_collection/engines/assistant-ia?updateMask=observabilityConfig,sessionConfig" \
  -d '{"observabilityConfig":{"observabilityEnabled":true,"sensitiveLoggingEnabled":false},"sessionConfig":{"sessionTtl":{"days":30}}}' >/dev/null && echo "engine settings applied"

Avec enable_agent_gateway = true, le masque devient agentGatewaySetting,observabilityConfig,sessionConfig et le JSON contient en plus la clé agentGatewaySetting.

Pourquoi ce contournement, et ses limites. C'est la seule façon d'appliquer ces réglages depuis le code tant que le provider ne les gère pas. Mais Terraform ne sait pas lire leur valeur réelle :

  • si quelqu'un change la durée de conservation dans la console, terraform plan ne le verra pas (pas de détection de dérive) ;
  • supprimer ce bloc ne remet pas les réglages à leur valeur Google : il ne se passe rien chez Google ;
  • la commande exige curl et gcloud (connecté) sur la machine qui lance apply.

Dès que le provider ajoutera ces arguments à google_discovery_engine_search_engine, il faudra les y déplacer et supprimer ce bloc.


4. Ce que Terraform crée concrètement

Avec le terraform.tfvars.example, 3 ressources :

  1. une app Gemini Enterprise assistant-ia, en UE, chiffrée par la clé CMEK, avec les data stores des connecteurs et les 4 réglages de fonctionnalités ;
  2. un assistant default_assistant, en français, branché sur les deux templates Model Armor en FAIL_CLOSED, sans recherche web et sans phrase interdite (liste vide) ;
  3. une ressource terraform_data, qui ne crée rien chez Google mais lance une fois l'appel curl : conservation 30 jours, observabilité sans contenu.

5. Dépendances avec les autres fichiers

Ce fichier utilise……défini dansFiche
var.ge_location, var.app_id, var.features, var.web_grounding, var.banned_phrases, var.session_ttl_days…variables.tf02-variables.md
local.project_number, local.de_host00_foundation.tf03-foundation.md
google_kms_crypto_key.ge, google_discovery_engine_cmek_config.ge20_data_protection.tf05-data-protection.md
google_model_armor_template.shield, google_project_iam_member.ge_uses_armor30_ai_guardrails.tf06-ai-guardrails.md
local.connector_data_store_ids40_connectors.tf07-connectors.md
google_network_services_agent_gateway.egress60_advanced.tf09-advanced.md

Dans l'autre sens : 70_detection.tf surveille les modifications de l'assistant (10-detection.md) et outputs.tf affiche data_store_ids (11-outputs.md).


6. Pour reproduire / adapter

Besoin type : appliquer un réglage d'API que le provider Terraform ne supporte pas encore.

# 1. Le corps de la requête, avec les noms de champs de l'API REST (camelCase).
locals {
  mes_reglages = merge(
    { champToujoursPresent = { sousChamp = var.ma_valeur } },   # toujours envoyé
    var.mon_option ? { champOptionnel = { actif = true } } : {}, # ajouté seulement si l'option est vraie
  )
}

# 2. Une ressource "support" qui relance la commande quand les réglages changent.
resource "terraform_data" "mes_reglages" {
  triggers_replace = [
    jsonencode(local.mes_reglages),    # rejouer si le contenu change
    google_xxx.ma_ressource.id,        # rejouer si la ressource cible est recréée
  ]

  provisioner "local-exec" {
    command = <<-EOT
      curl -fsS -X PATCH \
        -H "Authorization: Bearer $(gcloud auth print-access-token)" \
        -H "Content-Type: application/json" -H "X-Goog-User-Project: ${var.project_id}" \
        "https://<service>.googleapis.com/v1/${google_xxx.ma_ressource.name}?updateMask=${join(",", keys(local.mes_reglages))}" \
        -d '${jsonencode(local.mes_reglages)}' >/dev/null && echo "réglages appliqués"
    EOT
  }
}
  1. Vérifier d'abord que l'argument n'existe vraiment pas dans le provider : page de la ressource, par exemple https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/discovery_engine_search_engine (et https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/discovery_engine_assistant). Si l'argument existe, utilisez-le : c'est toujours mieux.
  2. Trouver dans la référence REST de l'API Google le nom exact des champs (camelCase) et le verbe (PATCH
    • updateMask pour une mise à jour partielle).
  3. Tester l'appel curl à la main une fois, dans un terminal, avant de le mettre dans Terraform.
  4. Mettre le corps dans un locals, la commande dans un terraform_data avec triggers_replace.
  5. terraform plan doit montrer terraform_data.mes_reglages à créer ; après apply, vérifier le réglage dans la console ou avec un curl en GET.
  6. Ajouter un commentaire « à supprimer quand le provider supportera X ».

7. Pièges et bonnes pratiques

  • Modifier data_store_ids peut recréer l'app (bug connu du provider, guide du socle (docs/00) §16). Ajouter un connecteur ajoute un data store, donc change cette liste. Lisez le plan : si l'app est en -/+ (détruite puis recréée), arrêtez. Idéalement, déclarez les connecteurs avant le lancement aux utilisateurs.
  • location et engine_id sont immuables : les changer recrée l'app (conversations et réglages perdus).
  • local-exec exige gcloud et curl sur la machine qui lance apply, et un gcloud auth login valide. Le jeton vient du compte gcloud, qui peut être différent du compte utilisé par Terraform (ADC, gcloud auth application-default login) : en CI, les deux doivent être configurés.
  • Pas de détection de dérive sur les réglages envoyés par curl : une modification dans la console reste invisible pour Terraform. Pour forcer une réapplication : terraform apply -replace=terraform_data.engine_settings.
  • Si curl échoue, l'apply échoue (grâce à -f) : lisez le message d'erreur HTTP (droit manquant, champ inconnu…). Si l'API renomme un champ, l'appel cassera : c'est voulu, mieux vaut une erreur qu'un réglage ignoré.
  • Apostrophes : le JSON est entouré de '…' pour le shell. Une valeur contenant une apostrophe casserait la commande. Ici les valeurs sont des nombres et des booléens, donc sans risque.
  • FAIL_CLOSED + droit IAM manquant = tout est bloqué. Juste après apply, attendez 1 à 2 minutes (propagation IAM) avant de conclure à une panne.
  • banned_phrases n'est pas un secret : la liste est en clair dans le code et dans l'état Terraform. N'y mettez pas d'information plus sensible que le nom de code lui-même.
  • Toute modification de l'assistant déclenche l'alerte « garde-fou modifié » (10-detection.md) : c'est normal et voulu.
Suivant