Cloud Run privé derrière un load balancer et IAP
Le chemin d'une requête
IAP (Identity-Aware Proxy) est un proxy géré par Google qui vérifie l'identité de l'utilisateur (compte Google Workspace ou Cloud Identity) et son droit roles/iap.httpsResourceAccessor avant de transmettre la requête. L'application n'a pas à coder d'authentification.
sequenceDiagram participant U as "Salarié" participant LB as "Load balancer HTTPS (IP publique)" participant IAP as "IAP" participant CR as "Cloud Run (ingress interne + LB)" U->>LB: "GET https://assistant.exemple.fr" LB->>IAP: "requête" IAP-->>U: "302 vers la connexion Google (si pas de session)" U->>LB: "requête + cookie IAP" IAP->>IAP: "groupe assistant-utilisateurs ? oui" IAP->>CR: "requête + en-tête x-goog-iap-jwt-assertion (agent IAP = invocateur)" CR-->>U: "réponse de l'assistant" U->>CR: "GET https://...run.app (accès direct)" CR-->>U: "404 / refus : ingress internal-and-cloud-load-balancing"
Trois verrous
| Verrou | Paramètre | Effet |
|---|---|---|
| Entrée réseau | ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" | l'URL run.app refuse le trafic internet direct |
| Identité utilisateur | iap { enabled = true } sur le backend | connexion Google obligatoire |
| Autorisation | roles/iap.httpsResourceAccessor à un groupe | seuls les membres passent |
| Appel de Cloud Run | roles/run.invoker à l'agent IAP | Cloud Run n'accepte que les requêtes relayées par IAP |
resource "google_cloud_run_v2_service" "app" {
name = "${var.prefix}-assistant"
location = var.region
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
template {
service_account = google_service_account.app.email
scaling { max_instance_count = 5 } # plafonne aussi la dépense
# vpc_access { egress = "ALL_TRAFFIC" ... } (leçon 2.1)
containers {
image = var.app_image
env {
name = "MODEL_ARMOR_TEMPLATE"
value = google_model_armor_template.assistant.id
}
}
}
}
resource "google_cloud_run_v2_service_iam_member" "iap_invoker" {
name = google_cloud_run_v2_service.app.name
role = "roles/run.invoker"
member = local.iap_agent # service-NUMERO@gcp-sa-iap.iam.gserviceaccount.com
}Le load balancer se compose de six ressources, créées seulement si app_domain est renseigné :
resource "google_compute_region_network_endpoint_group" "app" {
network_endpoint_type = "SERVERLESS" # un NEG sans serveur pointe vers Cloud Run
cloud_run { service = google_cloud_run_v2_service.app.name }
}
resource "google_compute_backend_service" "app" {
load_balancing_scheme = "EXTERNAL_MANAGED"
backend { group = google_compute_region_network_endpoint_group.app[0].id }
iap { enabled = true } # client OAuth géré par Google
log_config { enable = true }
}
# + url_map -> target_https_proxy (certificat géré) -> forwarding_rule :443 sur une IP globaleVous oubliez de donner roles/run.invoker à l'agent IAP. Que voit le salarié autorisé après s'être connecté ?
IAP authentifie l'utilisateur puis appelle Cloud Run avec sa propre identité (l'agent de service IAP). Sans le rôle invocateur, Cloud Run répond 403. La boucle de connexion signale plutôt un problème de cookie ou de domaine.
Le certificat et le DNS
Le certificat géré par Google (google_compute_managed_ssl_certificate) n'est émis qu'une fois que l'enregistrement DNS A de app_domain pointe vers l'IP du load balancer (sortie lb_ip). Comptez de quelques minutes à une heure ; en attendant, le HTTPS échoue, c'est normal.
Pourquoi accorder roles/iap.httpsResourceAccessor à un groupe plutôt qu'à des utilisateurs un par un ?
IAP accepte les utilisateurs, mais gérer l'accès par groupe découple les mouvements de personnel de l'infrastructure. Un groupe n'a pas plus de droits qu'un utilisateur.
Exercice
L'application veut connaître l'adresse e-mail de l'utilisateur pour personnaliser les réponses. Comment l'obtenir de façon fiable derrière IAP ?
Voir la solution
IAP ajoute l'en-tête signé x-goog-iap-jwt-assertion. L'application vérifie ce JWT (signature par les clés publiques d'IAP, audience /projects/NUMERO/global/backendServices/ID_BACKEND) puis lit la revendication email :
from google.auth.transport import requests
from google.oauth2 import id_token
def utilisateur(request_headers, audience):
jwt = request_headers["x-goog-iap-jwt-assertion"]
claims = id_token.verify_token(jwt, requests.Request(), audience=audience,
certs_url="https://www.gstatic.com/iap/verify/public_key")
return claims["email"]L'en-tête non signé x-goog-authenticated-user-email existe aussi, mais il ne faut pas s'y fier seul : la vérification du JWT garantit que la requête vient bien d'IAP.
Résultat obtenu
terraform plan réel le 2026-10-15 avec app_domain=assistant.exemple.fr (domaine fictif ; extrait) :
# google_cloud_run_v2_service.app will be created
+ resource "google_cloud_run_v2_service" "app" {
+ deletion_protection = false
+ effective_labels = {
+ "formation-lab" = "architecture-ia"
+ "goog-terraform-provisioned" = "true"
}
+ ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
# google_cloud_run_v2_service_iam_member.iap_invoker[0] will be created
# google_compute_backend_service.app[0] will be created
# google_compute_global_address.lb[0] will be created
# google_compute_global_forwarding_rule.https[0] will be created
# google_compute_managed_ssl_certificate.app[0] will be created
# google_compute_region_network_endpoint_group.app[0] will be created
# google_compute_target_https_proxy.app[0] will be created
# google_compute_url_map.app[0] will be created
# google_iap_web_backend_service_iam_member.users["group:assistant@exemple.fr"] will be createdJe sais faire
- Fermer l'accès direct à Cloud Run avec le bon paramètre d'ingress
- Assembler un load balancer HTTPS avec NEG sans serveur et certificat géré
- Activer IAP et donner les deux rôles nécessaires (utilisateurs, agent IAP)
- Vérifier l'identité de l'utilisateur dans l'application avec le JWT IAP