POC Google Cloud: exposer Gemini avec Cloud Run, Azure APIM et Workload Identity Federation

19 May 2026

L'objectif de cette POC est de deployer une API Python minimale sur Google Cloud Run, d'appeler Gemini via Vertex AI, puis de l'exposer a travers Azure API Management sans rendre Cloud Run public.

La version finale validee utilise une federation d'identite entre Azure et Google Cloud: APIM utilise sa managed identity, Google STS accepte cette identite via Workload Identity Federation, puis APIM genere un ID token Google pour appeler Cloud Run. L'API peut aussi exiger un token Entra ID client, par exemple emis pour un service principal autorise a consommer un modele Gemini.

Architecture

flowchart LR
  Client[Client HTTP<br/>SP Entra ID optionnel] --> APIM[Azure API Management<br/>Consumption]
  Client --> ClientToken[Entra ID<br/>client_credentials]
  ClientToken --> APIM
  APIM --> Entra[Microsoft Entra ID<br/>Managed Identity token]
  APIM --> STS[Google STS<br/>Workload Identity Federation]
  APIM --> IAMCreds[Google IAM Credentials<br/>generateIdToken]
  APIM --> Run[Cloud Run prive<br/>FastAPI]
  Run --> Gemini[Vertex AI<br/>Gemini allowlist]

  TerraformGCP[Terraform GCP] --> WIF[Workload Identity Pool<br/>OIDC Azure provider]
  TerraformGCP --> Run
  TerraformGCP --> Registry[Artifact Registry]
  TerraformGCP --> Build[Cloud Build]
  TerraformAzure[Terraform Azure] --> APIM
  TerraformAzure --> Policy[APIM policy<br/>MI + STS + ID token]

  WIF --> GSA[Google service account<br/>APIM invoker]
  GSA --> Run

Ressources deployeees

Cote GCP, Terraform cree ou configure:

  • un projet GCP dedie lorsque create_project=true;
  • les APIs Cloud Run, Cloud Build, Artifact Registry, Vertex AI, IAM, IAM Credentials, STS et Service Usage;
  • un repository Docker Artifact Registry;
  • une image applicative construite par Cloud Build;
  • un service Cloud Run prive avec enforcement IAM;
  • un service account Cloud Run avec roles/aiplatform.user;
  • un service account Google dedie a APIM avec roles/run.invoker sur Cloud Run;
  • un Workload Identity Pool et un provider OIDC Azure;
  • une liaison roles/iam.workloadIdentityUser entre la managed identity APIM et le service account Google.

Cote Azure, Terraform cree:

  • un Resource Group;
  • une instance Azure API Management en SKU Consumption_0;
  • une managed identity system-assigned sur APIM, ou une user-assigned managed identity optionnelle pour stabiliser l'identite WIF;
  • une API APIM importee depuis une definition OpenAPI minimale;
  • une policy APIM qui peut valider un JWT client Entra ID, puis orchestre l'authentification vers Google Cloud.

Workflow d'authentification

Le chemin d'appel valide est le suivant:

  1. le client peut obtenir un token Entra ID avec un service principal et le flow client_credentials;
  2. le client appelle l'URL APIM avec Authorization: Bearer <token-client>;
  3. si l'authentification client est activee, APIM valide le JWT client, son audience et ses roles ou son App ID;
  4. APIM recupere un token Microsoft Entra ID avec sa managed identity;
  5. APIM envoie ce token a Google Security Token Service;
  6. Google STS valide le token via Workload Identity Federation et retourne un access token Google court;
  7. APIM appelle IAM Credentials generateIdToken pour le service account Google autorise;
  8. APIM appelle Cloud Run avec Authorization: Bearer <id-token-google>;
  9. Cloud Run appelle Vertex AI Gemini avec son propre service account.

Ce flux evite un secret long terme entre APIM et Cloud Run. La frontiere d'acces est geree par IAM et par des tokens courts.

Pour l'authentification client, le mode recommande est d'exposer un app role sur l'App Registration representant l'API APIM, par exemple Gemini.Invoke, puis d'assigner ce role aux service principals clients autorises. APIM valide alors le claim roles. Une variante plus simple consiste a autoriser directement des App IDs clients via le claim azp, mais elle est moins expressive qu'un role applicatif.

API exposee

L'application FastAPI expose:

GET  /healthz
GET  /status
POST /generate

/healthz sert de sonde Cloud Run. /status et /generate sont exposes via APIM.

Payload de generation:

{
  "prompt": "Reponds en francais en une phrase: que valide cette POC ?",
  "model": "gemini-2.5-flash-lite"
}

Le champ model est optionnel pour compatibilite. S'il est absent, l'application utilise GEMINI_MODEL. S'il est present, il doit appartenir a l'allowlist GEMINI_MODELS.

Modeles declares dans cette version:

gemini-3.5-flash
gemini-2.5-flash
gemini-3.1-flash
gemini-2.5-flash-lite
gemini-3-pro
gemini-2.5-pro
gemini-3.1-pro
gemini-3-flash

Validation

Les tests se lancent depuis le repertoire de la POC:

cd /home/marc/poc-gcloud-gemini

Les endpoints APIM de cette instance sont:

https://apim-poc-gemini-k6hh7b.azure-api.net/gemini/status
https://apim-poc-gemini-k6hh7b.azure-api.net/gemini/generate

L'appel direct a Cloud Run sans identite Google est refuse:

URL="$(terraform -chdir=terraform output -raw service_url)"

curl -sS -o /tmp/direct -w '%{http_code}\n' \
  -H "Content-Type: application/json" \
  -d '{"prompt":"direct","model":"gemini-2.5-flash-lite"}' \
  "$URL/generate"

Le resultat attendu est 403.

L'appel via APIM fonctionne:

curl -sS "$(terraform -chdir=terraform-azure-apim output -raw gemini_status_url)" | jq .
{
  "status": "ok",
  "model": "gemini-2.5-flash-lite",
  "models": [
    "gemini-3.5-flash",
    "gemini-2.5-flash",
    "gemini-3.1-flash",
    "gemini-2.5-flash-lite",
    "gemini-3-pro",
    "gemini-2.5-pro",
    "gemini-3.1-pro",
    "gemini-3-flash"
  ],
  "location": "global"
}

Pour tester la generation:

./scripts/call_apim.sh \
  "Reponds en francais en une phrase: valide le chemin APIM Managed Identity vers Cloud Run." \
  "gemini-2.5-flash-lite"

Equivalent manuel:

APIM_URL="$(terraform -chdir=terraform-azure-apim output -raw gemini_generate_url)"

curl -sS \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Reponds en francais en une phrase: valide le workflow APIM vers Cloud Run prive via WIF.","model":"gemini-2.5-flash-lite"}' \
  "$APIM_URL" | jq .

POST /gemini/generate via APIM retourne 200 OK avec un JSON contenant model, location et text.

La selection dynamique d'un autre modele autorise se teste de la meme maniere:

./scripts/call_apim.sh \
  "Reponds en francais en une phrase: valide le choix dynamique du modele." \
  "gemini-2.5-flash"

Workflow de deploiement

Le deploiement est volontairement separe en deux states Terraform: un pour GCP, un pour Azure. Cela permet de supprimer APIM sans toucher aux ressources GCP, ou inversement.

terraform -chdir=terraform init
terraform -chdir=terraform apply

terraform -chdir=terraform-azure-apim init
terraform -chdir=terraform-azure-apim apply \
  -var='backend_auth_mode=wif' \
  -var="google_sts_audience=$(terraform -chdir=terraform output -raw azure_wif_provider_audience)" \
  -var="google_service_account_email=$(terraform -chdir=terraform output -raw apim_invoker_service_account)"

En pratique, l'identite managée APIM est connue apres creation d'APIM. On la reporte ensuite cote GCP pour creer le provider WIF et fermer Cloud Run au public.

Pour eviter de devoir changer l'object ID cote GCP a chaque recreation d'APIM, le module Azure peut creer une user-assigned managed identity stable:

create_user_assigned_identity = true

Dans ce mode, la sortie apim_principal_id correspond a l'object ID de cette identite stable; c'est cette valeur qu'il faut reporter cote GCP dans azure_apim_principal_id.

Authentifier les clients APIM avec un service principal

Le controle client peut etre fait directement dans APIM avec validate-jwt. Le client obtient un token Entra ID en client_credentials, puis appelle APIM avec ce token. APIM valide le token avant d'executer la federation vers Google Cloud.

Configuration Terraform recommandee:

enable_client_sp_auth     = true
client_auth_tenant_id     = "<tenant-id>"
client_auth_audience      = "api://<app-registration-api-id>"
client_auth_allowed_roles = ["Gemini.Invoke"]

Exemple d'appel client:

TOKEN="$(curl -sS -X POST "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=<client-app-id>" \
  -d "client_secret=<client-secret>" \
  -d "grant_type=client_credentials" \
  -d "scope=api://<app-registration-api-id>/.default" | jq -r .access_token)"

curl -sS \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Reponds en une phrase.","model":"gemini-2.5-flash-lite"}' \
  "https://<apim-name>.azure-api.net/gemini/generate" | jq .

Ce controle est independant de l'authentification APIM vers Cloud Run. Le token client n'est pas transmis a Cloud Run; APIM le remplace par un ID token Google genere pour le service account autorise.

Variante Cloud Run seul avec Artifactory

Le depot contient aussi un module terraform-cloud-run-only/ pour un socle GCP deja livre. Ce module ne cree ni projet, ni interconnect, ni load balancer. Le workflow GitHub Actions construit l'image Docker, la pousse dans Artifactory, puis Cloud Run lit l'image via un repository Artifact Registry remote.

Le pipeline manuel .github/workflows/deploy-cloud-run.yml utilise une cle JSON de service account Terraform stockee dans le secret GitHub GCP_TERRAFORM_SA_KEY, puis applique Terraform avec un backend GCS.

Disponibiliser a plusieurs clients

Pour exposer l'API a plusieurs clients, APIM doit devenir le point de gestion des consommateurs:

  • creer un produit APIM par usage: sandbox, partenaire, interne ou production;
  • exiger un token Entra ID client avec un role applicatif comme Gemini.Invoke lorsque l'appelant est une application machine-to-machine;
  • activer subscription_required=true pour identifier chaque client;
  • creer une subscription par client ou par application cliente;
  • appliquer des quotas et du rate limiting par produit ou par subscription;
  • journaliser l'identifiant de subscription dans les logs;
  • versionner les routes, par exemple /gemini/v1/generate;
  • revoquer un client en suspendant ou supprimant sa subscription APIM;
  • garder Cloud Run prive et ne jamais distribuer son URL comme endpoint officiel.

Le flux d'onboarding d'un client devient simple: creer une subscription APIM, l'associer a un produit, appliquer les limites attendues, fournir l'URL APIM et la cle de subscription, puis suivre la consommation dans APIM et Cloud Logging.

Bonnes pratiques retenues

  • pas de cle de service account locale;
  • pas de secret long terme entre APIM et Cloud Run en mode WIF;
  • Cloud Run prive avec enforcement IAM;
  • service account Google dedie pour APIM avec uniquement roles/run.invoker;
  • service account applicatif distinct pour Vertex AI;
  • tokens courts generes a la demande;
  • authentification client optionnelle par service principal Entra ID;
  • user-assigned managed identity possible pour stabiliser l'identite APIM dans Google WIF;
  • states Terraform separes pour GCP et Azure;
  • min_instance_count=0 pour limiter les couts idle.

Desinstallation

terraform -chdir=terraform-azure-apim destroy
terraform -chdir=terraform destroy

Si create_project=true, la destruction Terraform cote GCP supprime le projet dedie et les ressources associees.