Leçon 7 — Évaluer et déployer

Déployer sur Cloud Run privé

9 min de lecture

La démonstration a plu, la direction veut « l'URL pour l'équipe ». Le chemin le plus court est un gcloud run deploy --allow-unauthenticated avec le compte de service par défaut. Ce compte a le rôle Éditeur sur tout le projet : n'importe qui sur Internet pourrait alors lancer des requêtes BigQuery facturées à l'entreprise, sous une identité capable de supprimer des tables. Cette leçon déploie l'assistant en service privé, sous une identité qui ne sait faire que deux choses, puis supprime tout et le prouve.

Le mécanisme : trois barrières, une étiquette

flowchart LR
  U["Utilisateur authentifié<br/>(jeton d'identité Google)"] -->|"proxy gcloud ou en-tête Bearer"| IAM{"IAM Cloud Run<br/>roles/run.invoker"}
  X["Requête anonyme"] -->|"403"| IAM
  IAM --> S["Service lab-assistant-data<br/>max 1 instance, 1 Gio, délai 120 s"]
  S -->|"compte de service lab-assistant-data"| V["Vertex AI<br/>roles/aiplatform.user"]
  S -->|"même compte"| B["BigQuery jobs<br/>roles/bigquery.jobUser"]
  B --> D[("bigquery-public-data<br/>lecture publique")]
  AR["Artifact Registry<br/>dépôt lab-assistant-data"] -.->|"image"| S
BarrièreCe qu'elle empêcheOù elle est posée
--no-allow-unauthenticatedtout appel sans jeton d'identité Google : 403 avant le codedeployer.sh
compte de service dédié à 2 rôlesécrire, supprimer, lire d'autres données du projetdeployer.sh, étape 2
garde-fous applicatifsrequêtes hors liste blanche ou au-delà de 1 Gocode, inchangé depuis le module 2
--max-instances 1une facture qui monte avec un trafic imprévudeployer.sh, étape 5

roles/bigquery.jobUser permet de lancer des requêtes facturées au projet ; il ne donne accès à aucune table. Les données lues sont publiques (bigquery-public-data), donc aucun rôle de lecture n'est nécessaire. Sur vos propres données, ajoutez roles/bigquery.dataViewer au niveau du jeu de données, jamais du projet.

Chaque ressource porte le nom lab-assistant-data et, quand le service le permet, le label formation-lab=assistant-data. C'est ce qui rend le nettoyage vérifiable.

La mise en œuvre

L'image

Le Dockerfile part de python:3.12-slim, retire pytest et httpx des dépendances, copie seulement semantique.yaml, assistant/ et web/, et lance uvicorn sous un utilisateur sans privilège (uid 10001). Le .dockerignore exclut .env : aucune clé n'entre dans l'image. Les identifiants viennent du compte de service attaché au service, par le serveur de métadonnées.

Le déploiement

infra/deployer.sh est idempotent : chaque création est précédée d'un describe. L'étape qui compte :

files/infra/deployer.sh (extrait)
gcloud run deploy "$NOM" --image "$IMAGE" --region "$REGION" "${G[@]}" \
  --service-account "$SA" --no-allow-unauthenticated \
  --max-instances 1 --memory 1Gi --cpu 1 --timeout 120 --concurrency 20 \
  --labels "formation-lab=${SLUG}" \
  --set-env-vars "GOOGLE_CLOUD_PROJECT=${PROJECT},GOOGLE_CLOUD_LOCATION=${REGION},GOOGLE_GENAI_USE_VERTEXAI=TRUE,MODEL=${MODEL:-gemini-2.5-flash},MAX_OCTETS=${MAX_OCTETS:-1000000000}"

--max-instances 1 n'est pas qu'une limite de coût : les conversations vivent en mémoire du processus (InMemoryRunner). Avec deux instances, la deuxième question d'une conversation pourrait arriver sur une instance qui ne connaît pas la première. La route de santé est /health : /healthz est réservé par Cloud Run.

make cloud-up            # PROJECT lu dans .env, ou : make cloud-up PROJECT=mon-projet

Résultat obtenu le 2026-10-16 (identifiant de projet masqué) :

1/5 API
2/5 Compte de service dédié : Gemini (Vertex AI) + jobs BigQuery, rien d'autre
Created service account [lab-assistant-data].
Updated IAM policy for project [PROJET].
Updated IAM policy for project [PROJET].
3/5 Dépôt d'images dédié (supprimé en entier par cloud-down)
Created repository [lab-assistant-data].
4/5 Image
europe-west1-docker.pkg.dev/PROJET/lab-assistant-data/app:20261016-141357
5/5 Service Cloud Run privé
Service [lab-assistant-data] revision [lab-assistant-data-00001-bnt] has been deployed and is serving 100 percent of traffic.
--- Contrôle d'accès ---
sans jeton    : HTTP 403 (attendu 403)
avec jeton    : {"statut":"ok","modele":"gemini-2.5-flash"}

Tester le service privé

Deux façons d'appeler un service privé avec votre propre identité :

# 1. Interface complète dans le navigateur : le proxy ajoute le jeton à chaque requête
gcloud run services proxy lab-assistant-data --region europe-west1 --port 8081   # puis http://localhost:8081

# 2. Appel d'API direct
curl -s -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  -H 'Content-Type: application/json' -d '{"question":"Panier moyen par tranche d'\''âge en 2025"}' \
  "$URL/api/question"

Résultat de l'appel direct, le 2026-10-16 :

En 2025, le panier moyen le plus élevé a été observé chez les clients de 65 ans et plus, atteignant environ 88,11.
Les tranches d'âge 50-64 ans et 25-34 ans suivent avec des paniers moyens respectifs de 87,90 et 87,42. [...]
barres 5 lignes 20971520 octets 0.000119 $ 45896 ms
limites ['Les montants sont exprimés sans devise.', 'Le panier moyen est calculé sur les articles ni annulés ni retournés.']

46 secondes pour une question qui en prend 5 en local : les journaux du service expliquent l'écart, voir la section suivante. Par le proxy, l'interface est identique à la version locale (captures du 2026-10-15) :

L'assistant sur Cloud Run privé : indicateur, SQL déplié, 31,46 Mo facturés

Supprimer, puis prouver

infra/nettoyer.sh supprime dans l'ordre inverse : service, rôles IAM du compte de service (avant le compte, sinon il reste des liaisons orphelines deleted:serviceAccount:...), compte de service, dépôt d'images entier, images locales. Puis il appelle infra/statut.sh --doit-etre-vide, qui interroge chaque type de ressource par nom et sort en erreur s'il en reste une.

make cloud-down

Résultat obtenu le 2026-10-16 :

service Cloud Run
rôles IAM du compte de service
compte de service
dépôt d'images (et images)
images Docker locales
--- Vérification ---
Aucune ressource lab-assistant-data.

Ce qui casse en production

SymptômeCauseParade
46 s pour une question, puis réponse correctequatre 429 RESOURCE_EXHAUSTED de Vertex AI, réessayés après 2,8 s, 4,6 s, 8,5 s et 16,2 s (journaux du 2026-10-16)attendu avec HttpRetryOptions ; en production, quota dédié ou débit provisionné, et message d'attente côté interface
Réponse 503 « momentanément indisponible »réessais épuisés ou délai Cloud Run de 120 s dépasséjournaux échec question ; ne pas allonger le délai sans borne, réduire max_llm_calls
403 Forbidden pour un collègueil n'a pas roles/run.invoker sur le servicegcloud run services add-iam-policy-binding ... --member user:... --role roles/run.invoker ; pour une équipe, Identity-Aware Proxy
« Access Denied: User does not have bigquery.jobs.create »rôle oublié ou retiré du compte de servicemake cloud-status liste les rôles ; relancer make cloud-up, idempotent
La conversation « oublie » la question précédenteredémarrage d'instance ou plusieurs instances--max-instances 1 ici ; au-delà, sessions persistantes (DatabaseSessionService)
Facture Artifact Registry qui court après le testimages conservées après suppression du servicedépôt dédié supprimé en entier par cloud-down

La règle de décision

  • Privé par défaut. Un service public ne se justifie qu'avec une authentification applicative et un quota par utilisateur ; pour une équipe interne, run.invoker ou Identity-Aware Proxy suffit.
  • Un compte de service par service, rôles minimaux, jamais le compte par défaut.
  • Cloud Run convient tant que le trafic est intermittent : sans instance minimale, le service ne coûte rien au repos. Passez à --min-instances 1 seulement si le démarrage à froid gêne réellement les utilisateurs, et mesurez le coût mensuel avant.
  • Surveillez les octets facturés par jour (label formation-lab sur les jobs BigQuery) et le taux de 429 côté Vertex AI.

Pratique

Un collègue propose de remplacer roles/bigquery.jobUser par roles/bigquery.user « pour éviter les erreurs de droits ». Que répondez-vous ?

Après make cloud-down, make cloud-status affiche « rôle IAM : roles/aiplatform.user » et sort en erreur. Le compte de service n'existe plus. Que s'est-il passé et que faites-vous ?

Exercice. Votre équipe de 15 analystes doit utiliser l'assistant sans installer gcloud. Proposez la modification minimale du déploiement, et dites ce qui change dans deployer.sh et nettoyer.sh.

Voir la solution

Placez le service derrière Identity-Aware Proxy (IAP) : l'utilisateur se connecte avec son compte Google dans le navigateur, sans gcloud. Cloud Run prend en charge IAP directement :

gcloud run deploy "$NOM" ... --no-allow-unauthenticated --iap
gcloud iap web add-iam-policy-binding --resource-type cloud-run --service "$NOM" --region "$REGION" \
  --member "group:analystes@exemple.fr" --role roles/iap.httpsResourceAccessor

Donnez l'accès à un groupe, pas à 15 personnes une par une. Dans nettoyer.sh, rien à ajouter pour le service (la suppression emporte la configuration IAP), mais statut.sh doit vérifier que la liaison IAP a disparu si elle a été posée au niveau du projet. Gardez --max-instances 1 tant que les sessions sont en mémoire ; 15 utilisateurs intermittents tiennent sur une instance avec --concurrency 20, à vérifier par la latence médiane.

  • Je sais déployer un service Cloud Run privé sous un compte de service à rôles minimaux
  • Je sais appeler un service privé avec le proxy gcloud ou un jeton d'identité
  • Je sais lire dans les journaux pourquoi une question a pris 46 secondes
  • J'ai lancé make cloud-down et vu « Aucune ressource lab-assistant-data. »