Ce fichier transforme chaque entrée de la variable
connectors(Google Drive, SharePoint, GitHub, Jira…) en une source de données de Gemini Enterprise, avec secrets référencés, CMEK si besoin, et IP fixes. Notions de base : 00-bases-terraform.md.
1. À quoi sert ce fichier
Un connecteur relie Gemini Enterprise à une source de données de l'entreprise (Drive, SharePoint, Teams, GitHub,
Jira…). Ce fichier contient un seul modèle générique : pour ajouter une source, on n'écrit pas de nouveau code, on
ajoute une entrée dans terraform.tfvars. Par défaut, les sources sont en mode fédéré : la question est envoyée
en direct à la source, avec le jeton de l'utilisateur, donc aucune copie chez Google et les droits de la
source s'appliquent. Le mode ingestion (copie indexée) se choisit au cas par cas ; ses données sont alors
chiffrées avec la clé CMEK du client. Le « pourquoi » complet : guide du socle (docs/00) §7.
Vocabulaire Google Cloud :
| Terme | En clair |
|---|---|
| Connecteur (data connector) | La configuration qui relie Gemini Enterprise à une source. |
| Collection | Le « dossier » qui regroupe un connecteur et ses data stores. Son identifiant est ici la clé de l'entrée (sharepoint). |
| Data store | Un magasin de données que l'app interroge. Un connecteur en crée un par entité (type d'objet : fichier, ticket…). |
| Fédéré / ingestion | Fédéré : interroger la source en direct. Ingestion : copier et indexer chez Google. Voir guide du socle (docs/00) §7.2. |
| Secret Manager | Le coffre-fort à secrets de Google Cloud. Les identifiants OAuth des sources y sont rangés. |
| CMEK | Chiffrement avec la clé du client (voir 05-data-protection.md). |
| IP statiques | Adresses IP fixes depuis lesquelles Gemini Enterprise appelle la source ; on les autorise côté SaaS. |
2. Vue d'ensemble
| Bloc | Type de bloc | Rôle en une phrase |
|---|---|---|
google_discovery_engine_data_connector.src | resource (avec for_each) | Un connecteur par entrée de var.connectors. |
locals (connector_data_store_ids) | locals | Liste à plat des data stores créés par les connecteurs, pour les rattacher à l'app. |
3. Le code expliqué, bloc par bloc
3.0 Les données d'entrée
La variable connectors est déclarée dans variables.tf avec un type précis :
type = map(object({
data_source = string # google_drive, sharepoint, github, jira, bigquery...
display_name = string
modes = optional(list(string), ["FEDERATED"]) # FEDERATED = pas de copie, droits de la source
params = optional(map(string), {}) # paramètres non secrets
secret_params = optional(map(string), {}) # clé du paramètre => ID du secret Secret Manager
entities = optional(list(object({ name = string, params = optional(string) })), [])
refresh_interval = optional(string, "86400s")
static_ip = optional(bool, false) # IP sortantes fixes à autoriser côté SaaS (IMMUABLE)
}))optional(type, défaut) : si le champ manque dans terraform.tfvars, Terraform met la valeur par défaut. Voir
00-bases §6.3. Avec terraform.tfvars.example, Terraform voit donc,
pour sharepoint :
sharepoint = {
data_source = "sharepoint"
display_name = "SharePoint"
modes = ["FEDERATED"] # défaut
params = { instance_uri = "https://acme.sharepoint.com", tenant_id = "00000000-0000-0000-0000-000000000000" }
secret_params = { client_id = "sharepoint-client-id", client_secret = "sharepoint-client-secret" }
entities = [{ name = "file", params = null }]
refresh_interval = "86400s" # défaut (24 h)
static_ip = true
}Et pour gdrive : seulement data_source = "google_drive" et display_name = "Google Drive", tout le reste à sa
valeur par défaut (params = {}, secret_params = {}, entities = [], static_ip = false…).
3.1 Le connecteur générique : identité et mode
# CONNECTEURS - un modèle générique : chaque entrée de var.connectors devient une source.
# Par défaut en FEDERATED : la question est envoyée en direct à la source avec le
# jeton de l'utilisateur => aucune copie chez Google, les droits de la source s'appliquent.
resource "google_discovery_engine_data_connector" "src" {
for_each = var.connectors
location = var.ge_location
collection_id = each.key
collection_display_name = each.value.display_name
data_source = each.value.data_source
connector_modes = each.value.modes
refresh_interval = each.value.refresh_interval
static_ip_enabled = each.value.static_ipfor_each = var.connectors: un connecteur par entrée de la map. Avec l'exemple, 5 connecteurs :gcalendar,gdrive,github,sharepoint,teams. Adresses :google_discovery_engine_data_connector.src["sharepoint"], etc.each.key= la clé de l'entrée ("sharepoint") ;each.value= l'objet complet ci-dessus. Voir 00-bases §8.2.location = var.ge_location:"eu", comme l'app.collection_id = each.key: la clé de l'entrée devient l'identifiant de la collection ("sharepoint").collection_display_name = each.value.display_name: nom affiché ("SharePoint").data_source = each.value.data_source: le type de source, nom fixé par Google ("google_drive","sharepoint","github"…). Pour SharePoint,sharepointousharepoint_federated_searchest à vérifier (guide du socle (docs/00) §16).connector_modes = each.value.modes:["FEDERATED"]par défaut ;["DATA_INGESTION"]pour copier/indexer.refresh_interval = each.value.refresh_interval: fréquence de synchronisation,"86400s"(24 h) par défaut.static_ip_enabled = each.value.static_ip:truepour SharePoint, Teams et GitHub dans l'exemple. Gemini Enterprise appellera la source depuis des IP fixes, à autoriser côté SaaS (guide du socle (docs/00) §7.5). Immuable : on ne peut pas l'activer après coup sans recréer le connecteur.
3.2 Les paramètres : valeurs normales + références de secrets
# Paramètres : les valeurs secrètes sont des RÉFÉRENCES Secret Manager, jamais des valeurs.
json_params = jsonencode(merge(
each.value.params,
{ for k, s in each.value.secret_params : k => "${data.google_secret_manager_secret.connector[s].name}/versions/latest" },
))Chaque source attend des paramètres (URL de l'instance, tenant, identifiants OAuth…). On les passe sous forme d'un
texte JSON dans json_params. Construction, de l'intérieur vers l'extérieur :
-
{ for k, s in each.value.secret_params : k => "..." }: expressionforsur une map.kreçoit le nom du paramètre (client_id),sle nom du secret ("sharepoint-client-id"). Pour chacun, on fabrique la référence du secret :data.google_secret_manager_secret.connector[s]: le secret lu dans20_data_protection.tf;.name: son nom complet, de la formeprojects/<numéro du projet>/secrets/sharepoint-client-id;- on ajoute
/versions/latest: « la dernière version », résolue par Gemini Enterprise au moment de l'appel.
Voir 00-bases §12.2.
-
merge(each.value.params, {...}): fusionne les paramètres normaux et les références de secrets en une seule map. (Si un même nom existait dans les deux, la map de droite gagnerait.) -
jsonencode(...): transforme la map en texte JSON. Les clés sortent triées par ordre alphabétique.
Résultat pour sharepoint (numéro de projet fictif 123456789012) :
{"client_id":"projects/123456789012/secrets/sharepoint-client-id/versions/latest","client_secret":"projects/123456789012/secrets/sharepoint-client-secret/versions/latest","instance_uri":"https://acme.sharepoint.com","tenant_id":"00000000-0000-0000-0000-000000000000"}Pour gdrive : params = {} et secret_params = {}, donc json_params = "{}".
Pourquoi des références : la valeur du secret n'apparaît ni dans le code, ni dans l'état Terraform, ni dans le
plan. Une rotation (nouvelle version du secret) est prise en compte sans redéploiement
(guide du socle (docs/00) §6.2).
Les noms des paramètres (instance_uri, tenant_id, organization…) dépendent de chaque source et sont
à confirmer dans la page du connecteur de la doc Google (marqués [à confirmer] dans terraform.tfvars.example).
3.3 La clé CMEK, seulement pour l'ingestion
# CMEK : seules les données COPIÉES (ingestion) sont chiffrées avec la clé du client.
kms_key_name = var.enable_cmek && contains(each.value.modes, "DATA_INGESTION") ? google_kms_crypto_key.ge[0].id : null- Ternaire
condition ? A : B(00-bases §12.1), avec une condition composée par&&(« et ») :var.enable_cmek: CMEK est activé ;contains(each.value.modes, "DATA_INGESTION"):truesi la liste des modes contient"DATA_INGESTION".
- Si les deux sont vrais : la clé principale
google_kms_crypto_key.ge[0].id(créée dans20_data_protection.tf). - Sinon :
null, ce qui veut dire « ne pas renseigner l'argument ». En fédéré, rien n'est stocké, il n'y a rien à chiffrer. - Avec l'exemple : les 5 connecteurs sont en
["FEDERATED"], donckms_key_name = nullpartout. Pour lejiracommenté de l'exemple (modes = ["DATA_INGESTION"]) :projects/acme-gemini-enterprise-prd/locations/europe/keyRings/gemini-enterprise/cryptoKeys/gemini-enterprise-data. - Les clés régionales (exigées pour les connecteurs tiers en ingestion) ne sont pas citées ici : elles sont
enregistrées dans la configuration CMEK de Gemini Enterprise (
single_region_keys, voir 05-data-protection.md).
3.4 Les entités (un bloc par type d'objet)
dynamic "entities" {
for_each = each.value.entities
content {
entity_name = entities.value.name
params = entities.value.params
}
}dynamic "entities": génère un sous-blocentities { ... }par élément de la listeeach.value.entities. Danscontent, l'élément en cours s'appelle comme le bloc :entities.value. Voir 00-bases §10.- Attention à ne pas confondre :
eachdésigne le connecteur en cours (boucle de la ressource),entitiesdésigne l'entité en cours (boucle du bloc dynamique). entity_name = entities.value.name: le type d'objet ("file"pour SharePoint ;"project","issue","comment"pour le Jira d'exemple).params = entities.value.params: paramètres propres à l'entité (texte),nullsi absent (l'argument est alors omis).- Avec l'exemple :
sharepointproduit un bloc ;gdrive,gcalendar,teams,githubn'ont pas d'entities(liste vide) : zéro bloc.
# Équivalent écrit à la main pour sharepoint :
entities {
entity_name = "file"
}3.5 L'ordre de création
depends_on = [
google_discovery_engine_acl_config.identity,
google_discovery_engine_cmek_config.ge, # la clé doit être enregistrée AVANT
google_secret_manager_secret_iam_member.ge_reads,
]
}depends_on force des dépendances qui n'apparaissent dans aucun attribut (00-bases §9) :
google_discovery_engine_acl_config.identity(10_identity_access.tf, 04-identity-access.md) : la configuration d'identité (qui sont les utilisateurs, quel fournisseur d'identité) doit exister avant les connecteurs, qui s'appuient dessus pour appliquer les droits.google_discovery_engine_cmek_config.ge(20_data_protection.tf) : la clé doit être enregistrée avant, sinon les données ingérées seraient chiffrées par Google.google_secret_manager_secret_iam_member.ge_reads(20_data_protection.tf) : Gemini Enterprise doit pouvoir lire les secrets au moment où il crée le connecteur.
3.6 La liste des data stores créés
locals {
# Chaque entité d'un connecteur produit un data store "<collection>_<entité>".
connector_data_store_ids = flatten([
for c in google_discovery_engine_data_connector.src : [
for e in c.entities : basename(e.data_store) if try(e.data_store != null && e.data_store != "", false)
]
])
}L'app (50_app.tf) doit connaître les identifiants courts des data stores à interroger. Google
les crée pendant la création du connecteur et les renvoie dans l'attribut data_store de chaque entité. Ce bloc les
récupère.
for c in google_discovery_engine_data_connector.src: boucle sur les connecteurs créés (une ressource avecfor_eachest une map ; avec une seule variable,creçoit chaque ressource).for e in c.entities : ... if ...: boucle imbriquée sur les entités de ce connecteur, avec un filtreifqui ne garde que les entités qui ont un data store.try(e.data_store != null && e.data_store != "", false): vauttrueseulement si l'entité a un data store (ninull, ni vide). Si évaluer l'attribut provoque une erreur (attribut absent),tryrenvoiefalse. Ce filtre garantit quebasenamene reçoit jamaisnull(ce qui ferait échouer leplan).basename(e.data_store): garde la dernière partie du chemin. Exemple :basename("projects/123456789012/locations/eu/collections/sharepoint/dataStores/sharepoint_file")→"sharepoint_file".- On obtient une liste de listes (une par connecteur) ;
flattenl'aplatit.
Avec l'exemple :
# avant flatten (ordre alphabétique des connecteurs) :
[ [], [], [], ["sharepoint_file"], [] ] # gcalendar, gdrive, github, sharepoint, teams
# après flatten :
local.connector_data_store_ids = ["sharepoint_file"]Le format exact du chemin et du nom <collection>_<entité> est celui annoncé par le commentaire du code ; il est
à vérifier au premier déploiement. Les connecteurs sans entités (Workspace, Teams, GitHub dans l'exemple) ne
fournissent aucun ID ici : s'ils créent quand même des data stores, il faut ajouter leurs IDs dans
extra_data_store_ids (guide du socle (docs/00) §16).
4. Ce que Terraform crée concrètement
Avec terraform.tfvars.example : 5 connecteurs (5 ressources), donc 5 collections dans Gemini Enterprise
(eu) : Google Drive, Google Agenda, SharePoint, Microsoft Teams, GitHub. Tous en fédéré, sans clé CMEK. Trois
ont des IP statiques (SharePoint, Teams, GitHub). SharePoint crée un data store pour l'entité file.
Dans la console : Gemini Enterprise → Data stores / Connecteurs. Après apply :
terraform output connector_static_ips: les IP à autoriser côté Microsoft et GitHub ;terraform output connector_states: l'état de chaque connecteur ;ALLOWLIST_STATIC_IPtant que les IP ne sont pas autorisées ;- il reste à faire l'autorisation OAuth initiale dans la console (guide du socle (docs/00) §7.6).
Le bloc locals ne crée rien : il calcule une liste.
5. Dépendances avec les autres fichiers
Ce fichier utilise :
| Valeur | Vient de |
|---|---|
var.connectors, var.ge_location, var.enable_cmek | variables.tf (02-variables.md) et terraform.tfvars (12-tfvars.md) |
google_discovery_engine_acl_config.identity | 10_identity_access.tf (04-identity-access.md) |
data.google_secret_manager_secret.connector, google_secret_manager_secret_iam_member.ge_reads, google_kms_crypto_key.ge, google_discovery_engine_cmek_config.ge | 20_data_protection.tf (05-data-protection.md) |
À noter : 20_data_protection.tf lit lui aussi var.connectors pour trouver la liste des secrets (local.secret_ids).
Ce fichier est utilisé par :
| Valeur | Utilisée dans |
|---|---|
local.connector_data_store_ids | data_store_ids de l'app : concat(local.connector_data_store_ids, var.extra_data_store_ids) (50_app.tf, 08-app.md) |
google_discovery_engine_data_connector.src | sorties connector_static_ips et connector_states (outputs.tf, 11-outputs.md) |
6. Pour reproduire / adapter
Cas 1 (le plus courant) : ajouter une source dans CE projet. Pas de code : une entrée dans terraform.tfvars.
Exemple, un connecteur Jira en ingestion (repris de l'exemple commenté) :
connectors = {
# ... entrées existantes ...
jira = { # clé = ID de collection (ne plus la changer ensuite)
data_source = "jira"
display_name = "Jira"
modes = ["DATA_INGESTION"] # copie indexée => chiffrée avec la clé CMEK
params = { # paramètres NON secrets [à confirmer dans la doc du connecteur]
instance_uri = "https://acme.atlassian.net"
instance_id = "..."
auth_type = "OAUTH"
}
secret_params = { # paramètre => NOM du secret (créé avant par la sécurité)
client_id = "jira-client-id"
client_secret = "jira-client-secret"
refresh_token = "jira-refresh-token"
}
entities = [{ name = "project" }, { name = "issue" }, { name = "comment" }]
static_ip = true # à décider dès la création (immuable)
}
}Cas 2 : reproduire le modèle dans un autre projet Terraform. Squelette commenté :
variable "connectors" {
type = map(object({
data_source = string
display_name = string
modes = optional(list(string), ["FEDERATED"])
params = optional(map(string), {})
secret_params = optional(map(string), {}) # param => nom du secret Secret Manager
entities = optional(list(object({ name = string, params = optional(string) })), [])
static_ip = optional(bool, false)
}))
default = {}
}
# Les secrets cités (lus, jamais leurs valeurs).
locals {
secret_ids = toset(flatten([for c in var.connectors : values(c.secret_params)]))
}
data "google_secret_manager_secret" "s" {
for_each = local.secret_ids
secret_id = each.value
}
resource "google_discovery_engine_data_connector" "src" {
for_each = var.connectors
location = "eu"
collection_id = each.key
collection_display_name = each.value.display_name
data_source = each.value.data_source
connector_modes = each.value.modes
refresh_interval = "86400s"
static_ip_enabled = each.value.static_ip
# Paramètres normaux + RÉFÉRENCES de secrets, dans un seul JSON.
json_params = jsonencode(merge(
each.value.params,
{ for k, s in each.value.secret_params : k => "${data.google_secret_manager_secret.s[s].name}/versions/latest" },
))
# Clé CMEK uniquement si des données sont copiées.
kms_key_name = contains(each.value.modes, "DATA_INGESTION") ? "projects/.../cryptoKeys/ma-cle" : null
dynamic "entities" {
for_each = each.value.entities
content {
entity_name = entities.value.name
params = entities.value.params
}
}
# Ajouter ici : la config d'identité, la config CMEK, les droits de lecture des secrets.
# depends_on = [ ... ]
}Étapes :
- Côté source : créer l'application OAuth (Entra ID, GitHub App, Atlassian…) avec les permissions minimales en
lecture et l'URL de redirection
https://vertexaisearch.cloud.google.com/oauth-redirect. - Sécurité : créer les secrets dans Secret Manager (UE) avant le
plan(gcloud secrets create ...puisgcloud secrets versions add ..., voirterraform.tfvars.example). - Code : ajouter l'entrée dans
connectors; trouver les noms exacts dedata_source, des paramètres et des entités dans la page du connecteur de la doc Google et dans https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/discovery_engine_data_connector - Décider fédéré ou ingestion (guide du socle (docs/00) §7.2),
et
static_ip(immuable). terraform plan: vérifier qu'aucun connecteur existant n'est recréé (-/+), ni l'app.- Après
apply: autoriser les IP (terraform output connector_static_ips), faire l'autorisation OAuth dans la console, puis tester qu'un utilisateur sans droit ne voit pas un document de test.
7. Pièges et bonnes pratiques
- La clé CMEK doit être enregistrée avant les connecteurs. Le
depends_onsurgoogle_discovery_engine_cmek_config.gele garantit. Un connecteur en ingestion créé avant reste chiffré par Google. static_ipest immuable : le changer recrée le connecteur (et oblige à refaire l'autorisation OAuth et l'allowlist). Activez-le dès la création pour les sources tierces.- La clé de l'entrée (
sharepoint,jira…) devientcollection_id: la renommer dansterraform.tfvars= détruire puis recréer le connecteur. Choisissez une clé définitive. json_paramsplutôt queparams: la ressource propose aussi un argumentparams(map de textes). Le projet n'utilise quejson_params, construit avecjsonencode. N'utilisez qu'un des deux, jamais les deux.- Jamais de valeur secrète dans
params:paramsfinit en clair dans le code, l'état et leplan. Tout ce qui est secret va danssecret_params(des noms de secrets). - Les secrets doivent exister avant
plan, sinon la lecturedata.google_secret_manager_secretéchoue. - Noms de paramètres et de
data_sourceà confirmer pour certaines sources (Teams, GitHub, SharePoint fédéré, BigQuery en ingestion) : en cas de doute, créer une fois le connecteur dans la console et comparer (guide du socle (docs/00) §16). - Ajouter un connecteur modifie
data_store_idsde l'app, ce qui peut recréer l'app (bug connu du provider). Lisez leplanet ajoutez les connecteurs de préférence avant le lancement (guide du socle (docs/00) §16). - Sources sans entités (Workspace, Teams, GitHub) : leurs data stores n'apparaissent pas dans
connector_data_store_ids; les ajouter à la main dansextra_data_store_idssi besoin. - Toujours filtrer avant d'appeler une fonction sur une valeur « connue après apply » :
basename(null)fait échouer leplan. Le filtretry(e.data_store != null && e.data_store != "", false)écartenull, chaîne vide et attribut absent. - BigQuery en ingestion perd les droits BigQuery : tout utilisateur de l'app voit toutes les lignes indexées. N'indexer qu'un dataset curé, ou préférer le fédéré (guide du socle (docs/00) §7.3).
- En fédéré, la question part chez l'éditeur de la source (Microsoft, GitHub…) et relève de ses conditions.