Ce fichier décide quels journaux on garde, où, combien de temps et qui peut les lire. Il crée aussi une alerte sur les blocages Model Armor. Bases Terraform : 00-bases-terraform.md.
1. À quoi sert ce fichier
Gemini Enterprise produit plusieurs journaux : l'audit (qui a modifié quoi), l'usage (qui a demandé quoi,
quelles sources, quelles réponses ont été bloquées) et, en option, le texte complet des questions et réponses.
Par défaut, Google les range tous dans le bucket _Default du projet, lisible par beaucoup de monde et conservé
30 jours.
Ce fichier range chaque famille dans un bucket dédié en UE, avec une durée de conservation par finalité. Il retire les contenus sensibles du bucket par défaut, réserve la lecture à l'équipe sécurité et alerte en cas de pic de blocages, sans journaliser le contenu filtré. Le guide explique le pourquoi au §10.4 du README.
2. Vue d'ensemble
| Bloc | Type de bloc | Rôle en une phrase |
|---|---|---|
locals (filtres) | locals | Écrit une fois les 3 filtres de journaux réutilisés plus bas |
google_logging_project_bucket_config.security | resource | Bucket « sécurité » en UE, 365 jours, interrogeable en SQL |
google_logging_project_bucket_config.usage | resource | Bucket « usage » en UE, 90 jours, interrogeable en SQL |
google_logging_project_sink.security | resource | Envoie l'audit et les décisions Model Armor vers le bucket sécurité |
google_logging_project_sink.usage | resource | Envoie l'activité des utilisateurs vers le bucket usage |
google_logging_project_exclusion.sensitive_from_default | resource | Retire les contenus sensibles du bucket _Default |
locals (log_buckets) | locals | Petit annuaire des deux buckets |
google_logging_log_view_iam_member.security_reads | resource | Donne la lecture des deux buckets à l'équipe sécurité, et à elle seule |
google_logging_linked_dataset.usage | resource (optionnelle) | Expose le bucket usage dans BigQuery, sans copie |
google_logging_metric.policy_violations | resource | Compte les réponses bloquées par la politique |
google_monitoring_alert_policy.policy_violations | resource | Alerte si plus de 20 blocages en 10 minutes |
Les notions Cloud Logging à connaître :
| Notion | Explication simple |
|---|---|
| Journal (log) | Une suite d'entrées horodatées ; chaque entrée a un logName (son « nom de famille ») |
| Bucket de logs | Un lieu de stockage de journaux, avec une localisation et une durée de conservation. Chaque projet a _Required (audit d'administration, 400 jours, non modifiable) et _Default (le reste, 30 jours) |
| Sink (routeur) | Une règle d'aiguillage : « les entrées qui correspondent à ce filtre vont aussi vers cette destination » |
| Exclusion | Une règle qui empêche certaines entrées d'être stockées dans _Default |
| Vue (log view) | Une « fenêtre » sur un bucket. Chaque bucket a la vue _AllLogs. On donne les droits sur une vue |
| Log Analytics | Permet d'interroger un bucket en SQL, directement dans la console |
| Métrique de logs | Un compteur alimenté par les entrées qui correspondent à un filtre ; on peut alerter dessus |
3. Le code expliqué, bloc par bloc
3.1 Les filtres réutilisables (locals)
locals {
usage_log_filter = <<-EOT
logName="projects/${var.project_id}/logs/discoveryengine.googleapis.com%2Fgemini_enterprise_user_activity"
OR logName=~"projects/${var.project_id}/logs/discoveryengine.googleapis.com%2Fgen_ai\."
EOT<<-EOT … EOT: un texte sur plusieurs lignes, un « heredoc » (voir 00-bases §12.3).- Syntaxe des filtres Cloud Logging :
logName="…": égalité exacte.logName=~"…": expression régulière (« contient ce motif »).OR/ANDcombinent les conditions ; deux conditions sur deux lignes sans opérateur valentAND.
%2Fest le caractère/encodé : le vrai nom estdiscoveryengine.googleapis.com/gemini_enterprise_user_activity.- Le filtre capte le journal d'usage et tous les journaux
gen_ai.*(texte des promptsgen_ai.user.messageet des réponsesgen_ai.choice, produits seulement silog_prompts_responses = true).\.désigne un point littéral dans l'expression régulière. - Avec
project_id = "acme-gemini-enterprise-prd", la 1re ligne devientlogName="projects/acme-gemini-enterprise-prd/logs/discoveryengine.googleapis.com%2Fgemini_enterprise_user_activity".
armor_content_filter = "jsonPayload.@type=\"type.googleapis.com/google.cloud.modelarmor.logging.v1.SanitizeOperationLogEntry\""- Le filtre des décisions Model Armor avec contenu (produites seulement si
log_screened_content = true). \": un guillemet à l'intérieur d'une chaîne Terraform. Le filtre réel estjsonPayload.@type="type.googleapis.com/…SanitizeOperationLogEntry".
security_log_filter = <<-EOT
(logName:"cloudaudit.googleapis.com" AND protoPayload.serviceName=("discoveryengine.googleapis.com" OR "modelarmor.googleapis.com" OR "cloudkms.googleapis.com" OR "secretmanager.googleapis.com" OR "dlp.googleapis.com" OR "cloudresourcemanager.googleapis.com" OR "iam.googleapis.com"))
OR ${local.armor_content_filter}
EOT
}logName:"cloudaudit.googleapis.com"::signifie « contient ». On prend donc tous les journaux d'audit (administration et accès aux données).protoPayload.serviceName=("a" OR "b" …): uniquement les services du socle (Gemini Enterprise, Model Armor, KMS, Secret Manager, DLP, droits du projet, IAM).OR ${local.armor_content_filter}: on réutilise le filtre précédent. Unlocalpeut en utiliser un autre.
3.2 Les deux buckets
resource "google_logging_project_bucket_config" "security" {
project = var.project_id
location = var.ge_location
bucket_id = "ge-security"
description = "Gemini Enterprise : audit et décisions Model Armor."
retention_days = var.security_log_retention_days
locked = var.lock_security_logs
enable_analytics = true
}| Argument | Rôle | Pourquoi |
|---|---|---|
location = var.ge_location | eu : journaux stockés dans l'UE | Même résidence que les données de l'app |
bucket_id = "ge-security" | Nom du bucket | Nom parlant, utilisé dans Logs Explorer |
retention_days | 365 par défaut | Preuve d'audit et enquête sur un an (à valider avec le DPO) |
locked | false par défaut | true = personne ne peut réduire la rétention ni supprimer le bucket. Intégrité des preuves en production. Irréversible |
enable_analytics = true | Active Log Analytics (SQL) | Enquêter sans copier les logs ailleurs. Ne peut plus être désactivé |
resource "google_logging_project_bucket_config" "usage" {
project = var.project_id
location = var.ge_location
bucket_id = "ge-usage"
description = "Gemini Enterprise : activité des utilisateurs (peut contenir des questions)."
retention_days = var.usage_log_retention_days
enable_analytics = true
}- Même principe, 90 jours (minimisation RGPD : ces journaux contiennent des questions de collaborateurs).
- Pas de
locked: on doit pouvoir réduire la durée si le DPO le demande.
3.3 Les sinks (aiguillages)
resource "google_logging_project_sink" "security" {
name = "ge-security-logs"
destination = "logging.googleapis.com/${google_logging_project_bucket_config.security.id}"
filter = local.security_log_filter
}destination: vers où envoyer. Pour un bucket de logs, le format estlogging.googleapis.com/<id du bucket>. L'iddu bucket (projects/…/locations/eu/buckets/ge-security) vient de la ressource créée juste au-dessus. Cette référence crée aussi la dépendance : bucket d'abord, sink ensuite (voir 00-bases §9).filter: seules les entrées qui correspondent sont envoyées.- Un sink copie : les journaux d'audit d'administration restent aussi dans
_Required(règle Google).
resource "google_logging_project_sink" "usage" {
name = "ge-usage-logs"
destination = "logging.googleapis.com/${google_logging_project_bucket_config.usage.id}"
filter = local.usage_log_filter
}Même principe pour l'usage.
3.4 L'exclusion du bucket par défaut
resource "google_logging_project_exclusion" "sensitive_from_default" {
name = "ge-sensitive-content"
description = "Questions/réponses Gemini Enterprise et contenus Model Armor : uniquement dans les buckets dédiés."
filter = "(${trimspace(local.usage_log_filter)}) OR ${local.armor_content_filter}"
}- Une exclusion de projet empêche les entrées correspondantes d'être stockées dans
_Default. Les sinks dédiés les reçoivent quand même : elles ne vont que dans les buckets protégés. trimspace(...)retire les espaces et retours à la ligne en début et en fin du heredoc avant de l'entourer de parenthèses.- Pourquoi :
_Defaultest lisible par de nombreux rôles (logging.viewer,owner,editor…). Les questions des collaborateurs ne doivent pas y traîner.
3.5 Qui peut lire
locals {
log_buckets = {
security = google_logging_project_bucket_config.security.bucket_id
usage = google_logging_project_bucket_config.usage.bucket_id
}
}Un petit annuaire nom court → bucket_id, utilisé dans la boucle suivante et dans outputs.tf.
resource "google_logging_log_view_iam_member" "security_reads" {
for_each = { for pair in setproduct(keys(local.log_buckets), var.security_groups) : "${pair[0]}|${pair[1]}" => pair }
parent = "projects/${var.project_id}"
location = var.ge_location
bucket = local.log_buckets[each.value[0]]
name = "_AllLogs"
role = "roles/logging.viewAccessor"
member = each.value[1]
}setproduct(keys(local.log_buckets), var.security_groups)forme toutes les combinaisons bucket × groupe (voir 00-bases §12.4). Avecsecurity_groups = ["group:securite-ia@acme.example"], cela donne :[["security", "group:securite-ia@acme.example"], ["usage", "group:securite-ia@acme.example"]]- La boucle
fortransforme cette liste en map à clés stables, ce quefor_eachexige :{ "security|group:securite-ia@acme.example" = ["security", "group:securite-ia@acme.example"] "usage|group:securite-ia@acme.example" = ["usage", "group:securite-ia@acme.example"] } each.value[0]="security"ou"usage";local.log_buckets[...]donne le vraibucket_id.name = "_AllLogs": la vue créée automatiquement avec chaque bucket.roles/logging.viewAccessorsur cette vue : lire ce bucket uniquement, sans aucun autre droit sur le projet. C'est le moindre privilège.
3.6 Option : lien BigQuery
resource "google_logging_linked_dataset" "usage" {
count = var.link_logs_to_bigquery ? 1 : 0
link_id = "ge_usage_logs"
parent = "projects/${var.project_id}"
location = var.ge_location
bucket = google_logging_project_bucket_config.usage.bucket_id
description = "Lecture SQL des journaux d'usage Gemini Enterprise."
}count = condition ? 1 : 0: la ressource n'existe que si l'option est activée (voir 00-bases §8.1).- Un dataset lié fait apparaître le bucket dans BigQuery, en lecture seule et sans copie, ce qui permet de
brancher Looker Studio. Il exige
enable_analytics = truesur le bucket (c'est le cas). - Les droits BigQuery sur ce dataset sont à donner explicitement et avec parcimonie.
3.7 L'alerte sur les blocages
resource "google_logging_metric" "policy_violations" {
name = "ge_policy_violations"
filter = "logName=\"projects/${var.project_id}/logs/discoveryengine.googleapis.com%2Fgemini_enterprise_user_activity\" AND \"CUSTOMER_POLICY_VIOLATION\""
metric_descriptor {
metric_kind = "DELTA"
value_type = "INT64"
}
depends_on = [google_project_service.apis]
}- Une métrique de logs compte les entrées qui correspondent au filtre.
"CUSTOMER_POLICY_VIOLATION"seul, entre guillemets, est une recherche plein texte : l'entrée doit contenir ce mot n'importe où. Quand Model Armor (ou une phrase interdite) bloque une réponse, Gemini Enterprise l'indique avec ce motif dans le journal d'usage.DELTA+INT64: un compteur d'événements (combien pendant chaque intervalle).- L'intérêt : on détecte les attaques sans activer la journalisation du contenu Model Armor
(
log_screened_content), que Google déconseille.
resource "google_monitoring_alert_policy" "policy_violations" {
display_name = "Gemini Enterprise : pic de requêtes bloquées par la politique"
combiner = "OR"
conditions {
display_name = "Plus de 20 blocages en 10 minutes"
condition_threshold {
filter = "metric.type=\"logging.googleapis.com/user/${google_logging_metric.policy_violations.name}\""
comparison = "COMPARISON_GT"
threshold_value = 20
duration = "0s"
aggregations {
alignment_period = "600s"
per_series_aligner = "ALIGN_SUM"
cross_series_reducer = "REDUCE_SUM"
}
}
}
notification_channels = google_monitoring_notification_channel.security[*].id
documentation {
mime_type = "text/markdown"
content = "Pic de requêtes bloquées (Model Armor / politique). Enquête : bucket ge-usage, filtre \"CUSTOMER_POLICY_VIOLATION\", regrouper par utilisateur. Voir guide §10.5."
}
}metric.type="logging.googleapis.com/user/ge_policy_violations": les métriques de logs créées par vous sont publiées souslogging.googleapis.com/user/<nom>.aggregations: on additionne (ALIGN_SUM) les blocages sur des fenêtres de 10 minutes (600s), tous utilisateurs confondus (REDUCE_SUM).comparison = "COMPARISON_GT"+threshold_value = 20: alerte si plus de 20.duration = "0s": dès la première fenêtre au-dessus du seuil.google_monitoring_notification_channel.security[*].id: l'expression[*](« splat ») prend l'idde tous les exemplaires. Le canal e-mail est défini dans70_detection.tfaveccount: liste vide sialert_emailest vide (l'alerte existe alors sans e-mail).documentation: le texte affiché dans l'alerte, c'est-à-dire la marche à suivre.
4. Ce que Terraform crée concrètement
Avec l'exemple terraform.tfvars.example (un groupe sécurité, options par défaut) :
| Ressource | Nombre |
|---|---|
Buckets de logs (ge-security, ge-usage) | 2 |
| Sinks | 2 |
| Exclusion | 1 |
| Droits de lecture (2 buckets × 1 groupe) | 2 |
| Dataset BigQuery lié | 0 (option désactivée) |
| Métrique + alerte | 2 |
| Total | 9 |
Dans la console : Logging → Stockage des journaux montre les deux buckets. Logging → Routeur de journaux montre les sinks et l'exclusion. Monitoring → Alertes montre l'alerte.
5. Dépendances avec les autres fichiers
| Vient d'ailleurs | Fichier |
|---|---|
var.project_id, var.ge_location, var.security_groups, var.security_log_retention_days, var.usage_log_retention_days, var.lock_security_logs, var.link_logs_to_bigquery | variables.tf (02-variables.md) |
google_project_service.apis (API Logging et Monitoring) | 00_foundation.tf (03-foundation.md) |
google_monitoring_notification_channel.security (e-mail) | 70_detection.tf (10-detection.md) |
Production des journaux d'usage : observabilité activée par engine_settings (enable_observability, log_prompts_responses) | 50_app.tf (08-app.md) |
Production des journaux d'accès aux données : google_project_iam_audit_config | 70_detection.tf |
Production des contenus Model Armor : log_sanitize_operations = var.log_screened_content | 30_ai_guardrails.tf (06-ai-guardrails.md) |
| Utilisé ailleurs | Fichier |
|---|---|
local.log_buckets → sortie log_buckets | outputs.tf (11-outputs.md) |
À retenir : ce fichier range les journaux, il ne les crée pas. Ce sont les réglages des autres fichiers qui décident s'ils existent.
6. Pour reproduire / adapter
Modèle : isoler un type de journal sensible dans un bucket dédié, lisible par un seul groupe.
# 1. Le bucket (localisation, durée, SQL)
resource "google_logging_project_bucket_config" "mon_bucket" {
project = var.project_id
location = "eu"
bucket_id = "mon-bucket"
retention_days = 90
enable_analytics = true
}
# 2. L'aiguillage : quelles entrées y vont
resource "google_logging_project_sink" "mon_sink" {
name = "mon-sink"
destination = "logging.googleapis.com/${google_logging_project_bucket_config.mon_bucket.id}"
filter = "logName=\"projects/${var.project_id}/logs/MON_JOURNAL\"" # tester le filtre dans Logs Explorer d'abord
}
# 3. Les retirer du bucket par défaut (si sensibles)
resource "google_logging_project_exclusion" "mon_exclusion" {
name = "mon-exclusion"
filter = "logName=\"projects/${var.project_id}/logs/MON_JOURNAL\""
}
# 4. Qui peut lire (sur la vue du bucket uniquement)
resource "google_logging_log_view_iam_member" "lecteurs" {
parent = "projects/${var.project_id}"
location = "eu"
bucket = google_logging_project_bucket_config.mon_bucket.bucket_id
name = "_AllLogs"
role = "roles/logging.viewAccessor"
member = "group:mon-equipe@entreprise.com"
}
# 5. (option) Compter et alerter
resource "google_logging_metric" "mon_compteur" {
name = "mon_compteur"
filter = "logName=\"projects/${var.project_id}/logs/MON_JOURNAL\" AND \"MOT_CLE\""
metric_descriptor {
metric_kind = "DELTA"
value_type = "INT64"
}
}Étapes :
- Trouver le journal dans Logs Explorer et noter son
logName. - Écrire et tester le filtre dans Logs Explorer avant de le mettre dans Terraform.
- Choisir localisation et durée avec le DPO (finalité → durée).
- Ajouter bucket, sink, exclusion et droit de lecture (modèle ci-dessus).
terraform plan, puisapply. Vérifier dans Routeur de journaux que le sink reçoit des entrées.
Documentation des ressources :
- https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_project_bucket_config
- https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_project_sink
- https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_project_exclusion
- https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_log_view_iam
- https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_linked_dataset
- https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_metric
7. Pièges et bonnes pratiques
- Ce fichier ne produit aucun journal : sans
enable_observability = true(ou avecfalse), le bucketge-usagereste vide. Vérifiez la production avant de conclure qu'« il n'y a rien ». enable_analyticsetlockedsont à sens unique : on ne peut pas désactiver Log Analytics, ni déverrouiller un bucket. Activezlock_security_logsseulement en production, une fois la durée validée.- La rétention a un coût : au-delà de 30 jours, le stockage des logs est facturé. Les journaux d'usage peuvent être
volumineux, encore plus avec
log_prompts_responses = true. - Testez l'exclusion : après un premier usage, cherchez
gemini_enterprise_user_activitydans le bucket_Default. Vous ne devez rien trouver. - Les noms de champs (
skipped_reasons,authentication_info…) sont à confirmer sur les premières entrées réelles. La recherche plein texte ("CUSTOMER_POLICY_VIOLATION") fonctionne quelle que soit la structure. Si un blocage de recette n'incrémente pas la métrique, activez temporairementlog_screened_contentpour diagnostiquer. - RGPD : ces journaux sont des données personnelles de salariés. Il faut une finalité écrite, l'information des collaborateurs, le registre des traitements et, si besoin, la consultation du CSE. Ne jamais élargir les lecteurs sans décision.
- Ne donnez pas
roles/logging.viewerà large échelle en pensant protéger les questions : c'est justement pour cela que l'exclusion existe. Gardez les contenus hors de_Default. - Plusieurs projets ? En entreprise, on centralise souvent les journaux de sécurité dans un projet de journalisation dédié, via un sink au niveau dossier ou organisation. Le même modèle s'applique, avec une destination dans l'autre projet.